Your first Ruby native extension: Java

In my last post, I covered how to get started writing code in C and wiring it up to your Ruby code. While that code technically will work in JRuby, it’s preferred to write native JRuby extensions in Java rather than in C. In this article we’ll add JRuby support to our gem.

Let’s start with the Java code itself. Just like we did in C, we need to define the business logic itself as a Java method, and some glue code to set up modules and expose the methods to the Ruby runtime. The JRuby APIs are fairly easy to google, but getting things wired up correctly is quite challenging if you’re new to Java. The slightly obtuse-looking wiring here mostly concerns getting a reference to the current JRuby runtime, which parts of the API need (e.g. see RubyArray.newArray()).

Here’s the required code, which we put in ext/faye_websocket/FayeWebSocketService.java:

package com.jcoglan.faye;

import java.lang.Long;
import java.io.IOException;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.BasicLibraryService;

public class FayeWebSocketService implements BasicLibraryService {
  private Ruby runtime;
  
  // Initial setup function. Takes a reference to the current JRuby runtime and
  // sets up our modules. For JRuby, we will define mask() as an instance method
  // on a specially created class, Faye::WebSocketMask.
  public boolean basicLoad(Ruby runtime) throws IOException {
    this.runtime = runtime;
    RubyModule faye = runtime.defineModule("Faye");
    
    // Create the WebSocketMask class. defineClassUnder() takes a name, a
    // reference to the superclass -- runtime.getObject() gets you the Object
    // class for the current runtime -- and an allocator function that says
    // which Java object to constuct when you call new() on the class.
    RubyClass webSocket = faye.defineClassUnder("WebSocketMask", runtime.getObject(), new ObjectAllocator() {
      public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) {
        return new WebSocket(runtime, rubyClass);
      }
    });
    
    webSocket.defineAnnotatedMethods(WebSocket.class);
    return true;
  }
  
  // The Java class that backs the Ruby class Faye::WebSocketMask. Its methods
  // annotated with @JRubyMethod become exposed as instance methods on the Ruby
  // class through the call to defineAnnotatedMethods() above.
  public class WebSocket extends RubyObject {
    public WebSocket(final Ruby runtime, RubyClass rubyClass) {
      super(runtime, rubyClass);
    }
    
    @JRubyMethod
    public IRubyObject mask(ThreadContext context, IRubyObject payload, IRubyObject mask) {
      int n = ((RubyArray)payload).getLength(), i;
      long p, m;
      RubyArray unmasked = RubyArray.newArray(runtime, n);
      
      long[] maskArray = {
        (Long)((RubyArray)mask).get(0),
        (Long)((RubyArray)mask).get(1),
        (Long)((RubyArray)mask).get(2),
        (Long)((RubyArray)mask).get(3)
      };
      
      for (i = 0; i < n; i++) {
        p = (Long)((RubyArray)payload).get(i);
        m = maskArray[i % 4];
        unmasked.set(i, p ^ m);
      }
      return unmasked;
    }
  }
}

There’s another strategy you can use for JRuby, which is more like FFI: you define the logic you want in pure Java, and then tell JRuby to expose that to Ruby, mapping data types between the two runtimes. I tried that for this problem and it ended up being slower, so I went with the approach above which uses the JRuby APIs directly.

Next thing we need is code to compile it. Find the Rakefile from the C example and modify it like so: we need to define a different compiler task based on which runtime we’re using.

# Rakefile

spec = Gem::Specification.load('faye-websocket.gemspec')

if RUBY_PLATFORM =~ /java/
  require 'rake/javaextensiontask'
  Rake::JavaExtensionTask.new('faye_websocket', spec)
else
  require 'rake/extensiontask'
  Rake::ExtensionTask.new('faye_websocket', spec)
end

You should be able to run rake compile on JRuby and it should just work. It will create a new file in lib called faye_websocket.jar, which is the compiled Java bytecode package.

We also need a little more glue to load this code on JRuby, and we need some additional logic to map our intended single method WebSocket.mask() onto the Java method WebSocketMask#mask(). Create the file lib/faye.rb with this content:

# lib/faye.rb

# This loads either faye_websocket.so, faye_websocket.bundle or
# faye_websocket.jar, depending on your Ruby platform and OS
require File.expand_path('../faye_websocket', __FILE__)

module Faye
  module WebSocket
    
    if RUBY_PLATFORM =~ /java/
      require 'jruby'
      com.jcoglan.faye.FayeWebSocketService.new.basicLoad(JRuby.runtime)
      
      def self.mask(*args)
        @mask ||= WebSocketMask.new
        @mask.mask(*args)
      end
    end
    
  end
end

As you can see, we need to use the JRuby API to load the extension when running on JRuby. This code will load the native code and then add any glue we need to make everything work correctly. If you run rake compile && irb -r ./lib/faye on either MRI or JRuby you’ll find that the Faye::WebSocket.mask() method works as expected.

Finally there’s the question of packaging. Unlike compiled C code, compiled Java code is portable to any JVM, so JRuby extensions are not compiled on site. Instead, you put the .jar file in your gem. Your gemspec needs to tell Rubygems to compile on site for MRI, but include the .jar for JRuby.

# faye-webosocket.gemspec

Gem::Specification.new do |s|
  s.name    = "faye-websocket"
  s.version = "0.4.0"
  s.summary = "WebSockets for Ruby"
  s.author  = "James Coglan"
  
  files = Dir.glob("ext/**/*.{c,java,rb}") +
          Dir.glob("lib/**/*.rb")
  
  if RUBY_PLATFORM =~ /java/
    s.platform = "java"
    files << "lib/faye_websocket.jar"
  else
    s.extensions << "ext/faye_websocket/extconf.rb"
  end
  
  s.files = files
  
  s.add_development_dependency "rake-compiler"
end

I’ve put "lib/faye_websocket.jar" as an explicit addition rather than including it in the Dir.glob for two reasons: you don’t want it in the MRI gem, and you want gem build to fail if the file is missing when building on JRuby, which can easily happen if you forget.

Now the final part, which isn’t obvious the first time you do this: you need to release two gems, one generic one and one for JRuby. The s.platform = "java" line means you’ll get a different gem file when building on JRuby. All you need to do is build the gem on two different platforms, and remember to compile before building the JRuby gems.

$ rbenv shell 1.9.3-p194 
$ gem build faye-websocket.gemspec 
  Successfully built RubyGem
  Name: faye-websocket
  Version: 0.4.0
  File: faye-websocket-0.4.0.gem

$ rbenv shell jruby-1.6.7 
$ rake compile
install -c tmp/java/faye_websocket/faye_websocket.jar lib/faye_websocket.jar
$ gem build faye-websocket.gemspec 
  Successfully built RubyGem
  Name: faye-websocket
  Version: 0.4.0
  File: faye-websocket-0.4.0-java.gem

Just push those two gem files to Rubygems.org, and you’re done.