Let’s assume you work at big enterprise company X. Your company has assigned a new project to you, and one of its only requirements is to make sure you use this library… You have no control over the source code in the library, but you know you have to use it, because it contains some very valuable data to your users. You’ve got a ton of logic wrapped up in that library, and the company you work for already has some logic built around using that library. It is key to some department’s success. This means a couple things: (1) there’s great potential for you to provide great value to your users. It also means something else: (2) it’s probably written in C, and in general, C sucks for programmer happiness. That’s why most people programmers abandoned it — but the people who wrote that library haven’t, and it isn’t being replaced by java or ruby any day soon.
Now, before you search hotjobs or the 37signals job board for a new gig, realize that you still have some level of freedom in how you choose to go about satisfying your core requirement. This freedom includes building a user interface in a more agile-friendly language or framework. Let’s, for the sake of argument, pick some of our favorite tools to use from our development warchest. Is it possible? Could we be happy performing this task that includes a cryptic old C library? I think so. Let’s pick JRuby.
So how could you go about using JRuby to hit that old C library? You could try JRuby => C through FFI, but if the structures are quite complex and nested, you might end up debugging a lot of nasty mapping issues. JRuby can communicate with Java classes quite easily, and Java already has a way of communicating with native C code that’s well established and robust. Good old JNI. So how could we build a JRuby => Java => JNI => C connection?
It’s easier than you think. For the sake of simplicity, let’s send all data from JRuby to Java in hashes or arrays, and all data coming back from the JNI layer should be hash maps.
Wouldn’t it be nice to have a JRuby process that is able to do the following?
require 'proprietary_api' Proprietary::API.some_calculation( some, arguments, for, calculation )
It’s quite possible, and it’s not too difficult to achieve, if you know the proper steps involved.
You’ll need:
a jar (for convenience), which contains…
a java class to declare some native methods, which will be implemented in C…
and those C methods will call the library functions you want to use.
The Java Class
import java.util.*;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.RubyString;
public class ProprietaryJavaAPI
{
private IRubyObject rubyAPI;
public static void init( String library ) {
try {
System.load( library );
} catch ( Exception e ) {
System.out.println( "Attempted to load " + library + ", but failed" );
}
}
public ProprietaryJavaAPI(IRubyObject rubyAPI) {
this.rubyAPI = rubyAPI;
}
public native Collection some_calculation( String some, String arguments, String to_the, String calculation );
}
The C code in some_calculation.c
#include
#include
#include "string.h"
#include "ProprietaryJavaAPI.h"
#include "proprietary_c_lib.h"
JNIEXPORT jobject JNICALL Java_ProprietaryJavaAPI_some_calculation(JNIEnv * env, jobject this, jstring some, jstring arguments, jstring to_the, jstring calculation )
{
// here's where you call the c library and return the results...
}
The JRuby Code that uses the Java => C layer
require 'java'
# The jar file containing java class ProprietaryJavaAPI
require File.join( File.expand_path( File.dirname(__FILE__) ), 'proprietary_c_library_java_support' )
# The Java class containing definitions for native methods, like proprietary_query
include_class 'ProprietaryJavaAPI'
# Calling the static init method on ProprietaryJavaAPI to load the native shared object library, containing C code.
ProprietaryJavaAPI.init( File.join( File.expand_path( File.dirname(__FILE__) ), "libProprietaryAPI.so" ) )
module Proprietary
class API
class << self
JAVA_API = ProprietaryJavaAPI.new( Proprietary::API )
def some_calculation( *args )
JAVA_API.some_calculation( *args )
end
end
end
Putting it all together
Compiling Java
javac -target 1.6 -cp #{ ENV['JRUBY_HOME'] }/lib/jruby.jar #{Dir.glob("*.java").join(" ")}
javah -jni ProprietaryJavaAPI
jar cvf proprietary_c_library_java_support.jar #{Dir.glob("*.class").join(" ")}
Compiling C (Makefile)
libProprietaryJavaAPI : some_calculation.c
gcc -m32 -fPIC -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux -I.. --shared -o libProprietaryJavaAPI.so -L. -lproprietary_c ProprietaryJavaAPI.c some_calculation.c
clean :
rm libProprietaryJavaAPI.so