Rolling a JRuby desktop application
- Work on our application as developers
- Distribute our application as a single jar file to our users
- Compile the Ruby source files into Java class files
- Integrate third-party Ruby and Java libraries
This article includes an example project that demonstrates all of the above.
The example is our old friend Hello World. A tarball and zip file are available here:This article will read best if you can download the example and follow along, but I will include some code snippets so that you can gather the main ideas even without the example.
I’ll start by discussing the project layout. It is meant to be similar to a normal Ruby project—there’s a Rakefile at the top level, a lib directory for Ruby files, and a vendor directory for third-party libraries (though I think having a vendor directory may be a Railsism, not necessarily a Ruby thing). In this case, I’ve got a couple of Ruby files in the lib directory, a Java library and a Ruby library in vendor, and all of JRuby 1.1.2 in vendor as well.
The Java library I’ve chosen to use is SwingX from SwingLabs. SwingX includes some nice Swing widgets that don’t ship with Java. In the sample application, I’m going to use the JXDatePicker widget. The swingx-0.9.2.jar is hanging out in the top level of the vendor directory.
Constructor is a Ruby library we use at Atomic for simplifying the declaration of Ruby class constructors. The constructor gem is unpacked into the vendor directory, so it lives in vendor/constructor-1.0.0 1.
I’ve laid the project out like this for several reasons. The first is that it is a typical Ruby project layout. The second is that all of the libraries this application needs are distributed with the application itself; thus, the only dependence on the host system is a working Java 1.5+ installation. Err the blog has an excellent write-up about why distributing an application like this is a good idea. His discussion is in the context of Rails, but it is still relevant here.
The third reason is that this layout allows me to work as a developer or roll everything up for a user. As a developer, I don’t want to build a new jar every time I run the tests, since that is too slow. Users want to run a jar instead of trying to navigate some files splattered all over the filesystem. Using some tricks with JRuby’s $LOAD_PATH and $CLASSPATH globals, I will make the application work transparently as either a developer working against the filesystem or a user working with the jar.
Now let’s take a peek at the Rakefile. There are two namespaces: dist and java. The dist namespace defines tasks for building, cleaning, and running the single jar file. The tasks in the java namespace are used by the dist tasks to do the actual building.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
namespace :java do output_directory = "classes" desc "Compile java executable stub class" task :stub => 'java:clean' do mkdir_p output_directory sh %+javac -target 1.5 -d #{output_directory} -classpath vendor/jruby-complete-#{JRUBY_VERSION}.jar lib/Main.java+ end desc "Compile the Ruby files into class files" task :rb => "java:clean" do prefix = "com/atomicobject/hello_world" sh %+#{jruby} #{jrubyc} -p #{prefix} -t #{output_directory} lib+ end desc "Compile all source files into class files" task :compile => ["java:stub", "java:rb"] CLEAN.include output_directory end |
The truly interesting bits of the Rakefile are the java:stub and java:rb tasks, which compile the Java and Ruby files, respectively. The Java stub file is a small Java class that fires up the JRuby interpreter; we’ll look at that next. The java:rb task uses the JRuby compiler to translate all of the Ruby .rb files in the lib directory into Java .class files. The java:rb task also passes the ‘com/atomicobject/hello_world’ argument to the JRuby compiler, which in turn will use that for assigning a package to the each compiled class. After the compiler has run against a .rb file, the corresponding .class file will have the package ‘com.atomicobject.hello_world.lib’—a combination of the prefix and directory we pointed the compiler at.
After building the Java classes, the dist:build task concludes by running ant. An ant task will build the final jar for us using JarJar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.atomicobject.hello_world.lib;
import org.jruby.Ruby;
import org.jruby.RubyRuntimeAdapter;
import org.jruby.javasupport.JavaEmbedUtils;
import java.util.ArrayList;
// This technique for starting JRuby is taken from
// http://wiki.jruby.org/wiki/Direct_JRuby_Embedding
public class Main {
public static void main(String[] args) {
Ruby runtime = JavaEmbedUtils.initialize(new ArrayList());
RubyRuntimeAdapter evaler = JavaEmbedUtils.newRuntimeAdapter();
evaler.eval(runtime, "require 'com/atomicobject/hello_world/lib/application_bootstrap'");
JavaEmbedUtils.terminate(runtime);
}
} |
The next thing worth looking at is the Main Java class in ‘lib/Main.java’, which is the class that is executed when Java is pointed at the jar. The main method is using one of the JRuby APIs for firing up the interpreter. We’ve got it requiring the ‘lib/application_bootstrap.rb’ file, which is a classless script that takes over the responsibility of starting the application.
The Main.java file distributed with the example includes an alternative method (which is not shown here) of starting JRuby. This method allows you to start the JRuby interpreter by giving it JRuby parameters just like you would on the command line. The downside to this technique is that it is not considered part of the public JRuby embedding API, which means it may be brittle with respect to JRuby versioning.
Now we can move along to the application_bootstrap.rb file.1 2 3 4 5 6 7 8 9 10 11 |
include Java $LOAD_PATH << "lib" # running from filesystem $LOAD_PATH << "com/atomicobject/hello_world/lib" # running from jar $LOAD_PATH << "vendor/constructor-1.0.0/lib" $CLASSPATH << "vendor/swingx-0.9.2.jar" # running from filesystem require "view_builder" require "hello_world" hello_world = HelloWorld.new(:view_builder => ViewBuilder.new) hello_world.run |
The first line of application_bootstrap includes the Java module, which brings in JRuby’s Java integration support. The second and third lines make the source files in the lib directory available to the require lines farther down. The fourth line and fifth lines make the Ruby constructor and Java SwingX libraries available, respectively. Note that the jar is added to $CLASSPATH instead of $LOAD_PATH; $CLASSPATH is a global made available via Java integration and is useful for adding things to Java’s classpath during runtime 2. My applications generally loop over directory globs when setting up the LOAD_PATH and CLASSPATH, but in this example I think it is more instructive to be explicit. Once the environment is setup, we require and build a couple of Ruby classses and then start the application using HelloWorld’s run method.
Rather than pasting in the entirety of ViewBuilder and HelloWorld, I will simply discuss the interesting bits.
view_builder.rb contains the line
import org.jdesktop.swingx.JXDatePicker |
date_picker = JXDatePicker.new |
This is how the application can access the Java library.
Similarly, hello_world.rb contains
constructor :view_builder |
which uses the Ruby library. Thanks to the work done by application_bootstrap.rb, accessing both the Java and Ruby libraries is transparent to the application, whether it is running from the filesystem or within a jar.
Finally, here are the guts of the build.xml Ant file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<project basedir="." default="dist" name="Hello World"> <description>Combine Ruby and Java source with the jruby-complete jar</description> <target name="dist" description="Create the deliverable jar"> <taskdef name="jarjar" classname="com.tonicsystems.jarjar.JarJarTask" classpath="vendor/jarjar-1.0rc7.jar"/> <mkdir dir="pkg"/> <jarjar destfile="pkg/HelloWorld.jar"> <manifest> <attribute name="Main-Class" value="com.atomicobject.hello_world.lib.Main"/> </manifest> <fileset dir="classes"/> <zipfileset src="vendor/jruby-complete-1.1.1.jar"/> <zipfileset src="vendor/swingx-0.9.2.jar"/> <zipfileset dir="vendor/constructor-1.0.0" prefix="vendor/constructor-1.0.0"/> </jarjar> </target> </project> |
The Ant file is pretty small; it exists only to make use of JarJar for bundling the application. The
If your application is dependent on any RubyGems then I suggest throwing them them into the vendor directory as well. Rolling them in with jruby-complete is not easy, and the same arguments from the previously linked Err the Blog post are relevant. His technique for vendoring gems is good and I recommend using it (Constructor was, in fact, a gem that I unpacked).
I have run into a problem when using JarJar to roll signed jars into our single jar file. In my case, I got exceptions coming out of the signed Bouncy Castle library I use for OpenSSL encryption. My problem likely stems from the fact that JarJar does some bytecode manipulation, which in turn means the bouncycastle files no longer pass signature verification. I do not entirely understand what JarJar does, so I’m not totally sure about this. Either way, my solution was to distribute the bouncycastle library alongside the application’s jar file—the standard way of distributing a Java application. It isn’t as clean as I wanted, but it works.
One thing I’m not particularly happy about with this process is with how many places the com.atomicobject.hello_world package name shows up—it appears in one form or another in almost every file we’ve discussed. Each case is slightly different and exists in a different part of the process, so solving this problem is not easy. I have considered having the main Rakefile generate Main.java, build.xml, and application_bootstrap.rb. This could not only solve the problem of the com.atomicobject.hello_world duplication, but also make it so that less files need to be touched when we add new Ruby and Java libraries to the project. But this solution is not particularly easy to implement and I do not have a great sampling of different project with which to base my abstractions.
David Koontz has a RubyGem named Rawr that helps you turn your project into a single jar. The last time I tried it (a while ago) I could not get it to work the way I wanted; then when I looked again I couldn’t find the website anymore. The above linked page is now active, but I have not tried it lately. So I cannot speak to whether it works or not. And I cannot speak to whether is better or worse than what I’ve described in this article.
I’ve written this post from the first-person perspective, but Atomic’s jar rolling process is hardly my own effort: Micah Alles, David Crosby, Shawn Crowley, Karlin Fox, and Andrew Witte all helped figure this out a year ago and evolve it since then.
-
My goal here is not to promote the Constructor or SwingX libraries. I chose them simply because they are representative of both Ruby and Java libraries and were easy for me to integrate into the example. ↩
-
I believe there is an outstanding JRuby JIRA issue for bringing $LOAD_PATH and $CLASSPATH closer to synonymous. ↩
Chris Says:
July 2nd, 2008 at 09:25 PMThat's brilliant! I love anything to get ruby on the desktop :) Can you provide any screenshots of it in action? Also, does it take the typical ~60 seconds lag time to open the application that most Java apps have?
Ronen Says:
July 7th, 2008 at 04:24 PMHey, iv used another approach which is to package the entire project as a gem and distribute it with Jruby's complete jar (to see it in action http://code.google.com/p/gookup/).
David Koontz Says:
July 8th, 2008 at 05:05 AMHey Matt, it's David Koontz of the aforementioned Rawr project. Rawr is indeed alive and well, although it's never gotten the attention in terms of a nice home page and such that our other project Monkeybars gets. Rawr won't give you the nice JarJar style single jar file for distribution, instead we pack all of your project's files into a jar and then bundle the other jars you need into a sub-directory and set up your app's manifest file to auto-load them on the classpath. Rawr also gives you the ability to generate .exe and .app launchers for Windows and OSX so your app feels a bit more native. I like what you've done with using the ant task to get access to JarJar though, we've discussed that ourselves. Maybe we can incorporate some of that into a future Rawr release. Thanks for the article!
Matt Fletcher Says:
July 10th, 2008 at 09:25 AMChris: In a highly unscientific experiment on my Macbook Pro (the first model with Core2Dou) with Java 1.5, it takes 3-5 seconds to launch the HelloWorld application from either the filesystem or the jar. It definitely takes longer to launch our real applications, but that's because they are real applications that do a lot of work at startup time.
Matt Fletcher Says:
July 10th, 2008 at 09:25 AMDavid: I regret not examining Rawr again before I published this article because it sounds like you have some pretty nice features in it. We've been using the Java izpack tool for creating a .exe and .app after the .jar is produced. I really like JarJar for rolling everything up into one jar; in fact, outside of JRuby, I consider it the key feature here. But as I noted, I ran into some trouble combining JarJar and the Bouncycastle signed library. Your solution sounds like an interesting way to avoid that problem.
Sebastian Wenzlaff Says:
July 17th, 2008 at 01:06 PMHi Matt, really an awesome approach and tutorial, thank you! I will use this approach for an application which I have to develop for my master thesis. But I have on problem... I have a directory "foo" inside the "lib" directory. In this directory are X .rb files which I want to load at the beginning. I thought it would be enough to put something like: Dir['lib/foo'].each { |file| require file } into "application_bootstrap.rb", but it doesn't work when calling the .jar file directly (i.e. cd pkg; java -jar MyApp.jar). I tried nearly all possible paths but it seems to me that it is not possible to get all files of a directory *inside* the jar file. Du you have an idea? Thank you very much in advance!