<< Notes on Video Encoding | Home | Deploying Sonar to JBoss AS7 >>

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
  • lib/
    • example/
      • my_ext.jar compiled jar file to be loaded by JRuby
  • 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]
Tags : , ,



Add a comment Send a TrackBack