Saturday, 9 October 2010

Improving JarClassLoader

Providing a Java application easily runnable and platform independent can still turn out to be a challenge. I recently wrote a graphical console application for a students' lab experiment. What made it kind of special is that it requires access to the serial interface and therefore native libraries. Yet it has to run in the labs environment and on the students' PCs. So I have to support at least 32-bit and 64-bit Linux and Windows (XP and 7).

Obviously, I chose Java Web Start for a start and happily found that it supports embedding native libraries. You can even choose the libraries to link depending on the operating system (system property "os.name") and architecture ("os.arch"). But then additional requirements came up. For one experiment it turned out that the application must be started with varying arguments from a batch file. Of course there is javaws, but it does not support passing arbitrary command line arguments to the application. And writing JNLP files with embedded arguments for all required combinations soon becomes a maintenance problem. This problem is aggravated because we have one lab room without a network connection. This requires another set of JNLP files using local files instead of network resources.

The solution would be to package everything in a single JAR that can be used both for Web Start and for command line start with "java -jar ...". The latter way to start an application has the old and well known problem that you cannot embed jarred libraries in the JAR. You have to unpack them and this may lead to unexpected effects if the libraries use information from META-INF. Worse, you cannot simply pack native libraries in the JAR. But there is help: the JarClassLoader. While there are other utilities for the job, this is the only one that claims to support loading native libraries from the JAR. It does not claim to support Web Start, though. And that's where the first issue came up.

The JarClassLoader needs the location of the JAR in order to search through it. It uses this code to find the location:
    pd = getClass().getProtectionDomain();
    CodeSource cs = pd.getCodeSource();
    URL urlTopJAR = cs.getLocation();
Provided that the JarClassLoader.class is packed in the JAR, this returns the URL of the JAR. JarClassLoader assumes this to be a file and proceeds accordingly. But with Web Start it is the network resource (something like "http://some.host/my.jar"). (With older versions of Web Start [pre JDK6] it can also be the URL of the cached file as described here.) So I changed the way this URL is handled to:
    if (!urlTopJAR.getProtocol().equals("jar:")) {
        urlTopJAR = new URL("jar:" + urlTopJAR.toString() + "!/");
    }
    ...
    loadJar(((JarURLConnection)urlTopJAR.openConnection()).getJarFile());
Now JarClassLoader works for both for Web Start and command line start.

Another issue is loading the native libraries. JarClassLoader keeps its promise, the libraries load fine. But it cannot handle different architectures. Take Linux as an example. A dynamic library is usually named "libMyCode.so" no matter if it is compiled for 32-bit or 64-bit. If both libraries have to coexist on the system, they are kept in different directories (typically "/usr/lib/" and /usr/lib64"). When loading such a library from Java, you do it with "System.load("MyCode")". This eventually calls the classloader's findLibrary method. JarClassLoader returns the path to the first entry that matches the library name, it does not consider the architecture.

The solution I implemented is to put the native libraries in subdirectories within the JAR and extend the JarClassLoader's findLibrary method to first find all entries matching the library name and then score them using os.name and os.arch. Finally the best match is returned. If you're interested, download the complete modified JarClassLoader. I have also submitted the modifications to the maintainer, so maybe they'll become "official" one day.

Probably I have missed some resource on the web and the problem has been solved before. But anyway, I now have a solution that fits my needs perfectly. And it shows that the promise "Write once, run anywhere" can be kept — although it requires some efforts from time to time.