-
-
Notifications
You must be signed in to change notification settings - Fork 923
Method Signatures and Annotations in JRuby extensions
Or how to create ruby methods using java, well at least some guidance, for a working library jar see jruby-examples.
# in ruby
def active?
end
// java, note active_p follows the naming convention for a query
@JRubyMethod(name = "active?")
public IRubyObject active_p(ThreadContext context) {
}
# in ruby we might have vector normalize
def normalize
end
// java, norm we will return a normalized copy leaving original unchanged
@JRubyMethod(name = "normalize")
public IRubyObject norm(ThreadContext context) {
}
# in ruby we might have vector normalize, where we change the original
def normalize!
end
// in java, the normalize_bang follows the naming convention for a hard change
// eg in vector normalize the original is changed
@JRubyMethod(name = "normalize!")
public IRubyObject norm_bang(ThreadContext context) {
}
If you are creating your own objects it not a bad idea to create a human-readable String representation, that can be used by inspect for example (regular ruby will do something sensible for this) eg:-
/**
* @param context ThreadContext
* @return IRubyObject to_s
*/
@JRubyMethod(name = {"to_s", "inspect"})
public IRubyObject to_s(ThreadContext context) {
return context.getRuntime().newString(String.format("Vec3D(x = %4.4f, y = %4.4f, z = %4.4f)", jx, jy, jz));
}
In the above case had we not been aliasing the to_s
to inspect
, we could have omitted name from the @JRubyMethod annotation and the ruby method name would be taken from the name of the java method. In general less is better for the annotations, there are some obscure annotations that are probably well left alone or for the experts.
Implementing equals, here is how you might implement ==
for a custom Fraction Class (in this case based on the Apache Commons Math Fraction class, which has its own equals method) note: the convention for naming the java method involving a operator, stick to it and people will find it easier to read your code. Also featured is the creation of a RubyBoolean from a java boolean.
@JRubyMethod(name = "==")
@Override
public IRubyObject op_equal(ThreadContext context, IRubyObject other) {
boolean result = false;
if (other instanceof RubyFraction) {
Fraction other_fraction = ((RubyFraction) other).getJFraction();
result = j_fraction.equals(other_fraction);
}
return RubyBoolean.newBoolean(context.getRuntime(), result);
}
Here is how you might implement eql?
for a custom Fraction Class (based Apache Commons Math) note: the convention for naming the java method for a query, stick to it and people will find it easier to read your code.
@JRubyMethod(name = "eql?")
@Override
public IRubyObject equal_p(ThreadContext context, IRubyObject other) {
boolean result = false;
if (other instanceof RubyFraction) {
Fraction other_fraction = ((RubyFraction) other).getJFraction();
result = j_fraction.equals(other_fraction);
}
return RubyBoolean.newBoolean(context.getRuntime(), result);
}
# ruby
def initialize(*args)
end
// java
@JRubyMethod(name = "initialize", rest = true)
public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
}
# ruby
def get(key, marshal=true)
end
// java (the default value for required and optional is 0)
@JRubyMethod(name = "get", required = 1, optional = 1)
public IRubyObject get(ThreadContext context, IRubyObject[] args) {
Ruby ruby = context.getRuntime();
RubyString key = (RubyString) args[0];
RubyBoolean marshal = ruby.getTrue();
if (args.length > 1) {
marshal = args[1];
}
}
# ruby
attr_reader :x, :y
def initialize(*args)
if args.length == 2
@x = args[0]
@y = args[1]
else
@x = 0
@y = 0
end
end
Note the new
method should be a class method, so it is a static method in Java. Also it is meta
method that should be defined on the metaclass (hence meta = true
in the @JRubyMethod annotation). The convention is to name the java method rbNew
, this is perhaps one the trickiest methods to implement correctly, where you will most likely create some helper methods as we have below. Note we can possibly should do an arity check on args, klazz is the class we are creating (as with module methods below static methods expect a receiver).
// java where meta = true mean this method should be defined on the metaclass
/**
*
* @param context ThreadContext
* @param klazz IRubyObject
* @param args optional (no args jx = 0, jy = 0)
* @return new Vec2 object (ruby)
*/
@JRubyMethod(name = "new", meta = true, rest = true)
public static final IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
vec2.init(context, args);
return vec2;
}
/**
*
* @param runtime Ruby
* @param klass RubyClass
*/
public Vec2(Ruby runtime, RubyClass klass) {
super(runtime, klass);
}
void init(ThreadContext context, IRubyObject[] args) {
if (Arity.checkArgumentCount(context.getRuntime(), args, Arity.OPTIONAL.getValue(), 2) == 2) {
jx = (Double) args[0].toJava(Double.class);
jy = (Double) args[1].toJava(Double.class);
}
}
module Foo
def build_string
return 'This is a new String'
end
alias_method :build_string :new_string
end
Note in java the module method is a static method, and requires a receiver, also illustrated below is how to create an alias (there is an alias option in jruby annotations but it is not required just name
the method as below
/**
*
* @param context ThreadContext
* @param recv the receiver
* @return A RubyString.
*/
@JRubyMethod(name = {"build_string", "new_string"}, module = true)
public static IRubyObject buildString(ThreadContext context, IRubyObject recv) {
Ruby runtime = context.getRuntime();
return runtime.newString("This is a new String");
}
Should you wish to create a module class method (as below)
module Foo
def self.build_string
return 'This is a new String'
end
end
We create the class method using the @JRubyMethod annotation meta = true
which directs that this method should be defined on the metaclass.
/**
*
* @param context ThreadContext
* @param recv the receiver
* @return A RubyString.
*/
@JRubyMethod(name = {"build_string", "new_string"}, module = true, meta = true)
public static IRubyObject buildString(ThreadContext context, IRubyObject recv) {
Ruby runtime = context.getRuntime();
return runtime.newString("This is a new String");
}