Example JRuby Extension in Java
JRuby is a Ruby implementation that runs on the Java Virtual Machine. JRuby supports extensions written in Java so that a Java class is made available from within Ruby code. This example shows how to get a simple extension up and running.
Example was tested on Linux with JRuby 1.4.0. The Rakefile will likely only work on a Unix-like system.
Download example code: jruby-ext.tgz
The example file structure looks like this:
- Rakefile
- README
- ext/
- example/
- MyExtService.java loads Java classes into JRuby
- MyObj.java simple class
- MySubObj.java extends MyObj
- example/
- lib/
- example/
- my_ext.jar compiled jar file to be loaded by JRuby
- example/
- test/
- test.rb unit tests as examples
The plan is to create a Ruby library that can be used like require 'example/my_ext'. With the libary loading technique that will be used (BasicLibraryService), the Java package and class names must follow a convention in order to be loaded by JRuby. In this example, all Java classes will be in the example package, and the name of the library loader must be example.MyExtService.
The library service will define the desired Ruby modules and classes, and it will load Java classes that extend RubyObject and define methods annotated with @JRubyMethod. These Java classes will then be callable from Ruby code.
ext/example/MyExtService.java
package example;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.BasicLibraryService;
/**
* Implement a library service for making Java classes available from
* Ruby.
*
* Note that for JRuby to run #basicLoad(), the packages and module
* names must obey the JRuby naming conventions:
*
* The name of the jar file must match the package and class name of
* the library service. In this example, we are providing the 'MyExt'
* module. The Java package is 'example', and thus the full class name
* of the library service must be 'example.MyExtService'. The jar
* file of the library must be 'example/my_ext.jar', and Ruby code
* will 'require example/my_ext'.
*
* JRuby plans to provide a method to load classes with names that do
* not follow the above convention. See
* 'src/org/jruby/runtime/load/LoadService.java' in the JRuby source.
*
* @author Patrick Mahoney <pat@polycrystal.org>
*/
public class MyExtService implements BasicLibraryService {
public boolean basicLoad(Ruby runtime) {
RubyModule myExt = runtime.defineModule("MyExt");
RubyModule myObj =
myExt.defineClassUnder("MyObj", // class name
runtime.getObject(), // superclass
MY_OBJ_ALLOCATOR);
// Any method annotated with @JRubyMethod will be available from Ruby
myObj.defineAnnotatedMethods(MyObj.class);
RubyModule mySubObj =
myExt.defineClassUnder("SubObj", // class name need not
// match Java class name
(RubyClass) myObj,
SUB_OBJ_ALLOCATOR);
myExt.defineAnnotatedMethods(MySubObj.class);
return true;
}
private static ObjectAllocator MY_OBJ_ALLOCATOR =
new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new MyObj(runtime, klass);
}
};
private static ObjectAllocator SUB_OBJ_ALLOCATOR =
new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new MySubObj(runtime, klass);
}
};
}
ext/example/MySubObj.java
package example;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
public class MySubObj extends MyObj {
String title;
public MySubObj(Ruby runtime, RubyClass klass) {
super(runtime, klass);
}
// If MyObj uses its first attempt initializer, this initialize is
// never called; MyObj.initialize is called, and fails with a
// wrong # of arguments error.
/*
@JRubyMethod(visibility=Visibility.PRIVATE)
public IRubyObject initialize(ThreadContext context,
IRubyObject title,
IRubyObject name) {
super.initialize(context, name);
this.title = title.toString();
return context.getRuntime().getNil();
}
*/
@Override
@JRubyMethod(visibility=Visibility.PRIVATE, required=2, rest=true)
public IRubyObject initialize(ThreadContext context,
IRubyObject[] args,
Block block) {
// Must check args length because we 'required=2' has no
// effect when overriding
if (args.length != 2)
throw context.getRuntime().newArgumentError(args.length, 2);
title = args[0].toString();
IRubyObject[] superArgs = new IRubyObject[] {args[1]};
return super.initialize(context, superArgs, block);
}
@Override
@JRubyMethod
public IRubyObject format_name(ThreadContext context) {
return context.getRuntime()
.newString(title + ". " + super.format_name(context));
}
}
A set of unit tests gives an example of how to use the Java library from within JRuby. It also shows that the Java class may be extended from within Ruby.
test/test.rb
require 'test/unit'
require 'example/my_ext'
class SubSub < MyExt::SubObj
def initialize(title, name, suffix)
super(title, name)
@suffix = suffix
end
def format_name
super + ", " + @suffix
end
end
class MyObjTest < Test::Unit::TestCase
def test_create_object
obj = MyExt::MyObj.new("JRuby")
assert obj
assert_equal "hello JRuby", obj.greet()
end
def test_sub_object
sub = MyExt::SubObj.new("Mr", "JRuby")
assert sub
assert_equal "hello Mr. JRuby", sub.greet()
end
def test_subclass_from_ruby
obj = SubSub.new("Mr", "JRuby", "PhD")
assert obj
assert_instance_of SubSub, obj
assert_equal "hello Mr. JRuby, PhD", obj.greet()
end
end
Finally, a simple Rakefile defines build tasks. The default task can be run with jruby -S rake.
Rakefile
require 'rake/testtask'
# These are all relative to the 'ext' directory
JAVA_EXT = "../lib/example/my_ext.jar"
JAVA_SOURCES = FileList['example/**/*.java']
JAVA_OBJS = FileList['example/**/*.class']
JRUBY_HOME = Config::CONFIG['prefix']
CLASSPATH = "#{JRUBY_HOME}/lib/jruby.jar"
desc "Removes all generated during compilation .class files."
task :clean_classes do
Dir.chdir('ext') do
JAVA_OBJS.to_a.each do |file|
File.delete file if File.exists?(file)
end
end
end
desc "Removes the generated .jar"
task :clean_jar do
File.delete JAVA_EXT if File.exists?(JAVA_EXT)
end
desc "Same as :clean_classes and :clean_jar"
task :clean_all => [:clean_classes, :clean_jar]
# Escape a filelist by quoting each file in single quotes and joining
# them with spaces suitable for passing as file arguments on in a
# shell. Required to escape e.g. MyClass$1.class for inner classes.
def quote_file_list(list)
(list.to_a.map { |f| "'#{f}'" }).join(' ')
end
desc "Build Java library"
task :build do
# It is critical that the paths in the jar file begin with 'example'
# and not 'ext/example'. This is why we cd into the 'ext' before
# building.
Dir.chdir('ext') do
sh "javac -g -cp #{CLASSPATH} #{quote_file_list(JAVA_SOURCES)}"
sh "jar cf #{JAVA_EXT} #{quote_file_list(JAVA_OBJS)}"
end
end
desc "Run the unit tests"
Rake::TestTask.new do |t|
t.test_files = FileList['test/**/test*.rb']
t.verbose = true
end
task :clean => [:clean_jar, :clean_classes]
task :default => [:build, :test]
