Including C code in a Java Web Start application
The problem
Some days ago I was given an interesting problem by my current employer. We have some legacy code written in C, and we would like to be able to use that code in a Java app that the user would run from their browser, using Java Web Start. You basically have your JAR deployed on a server, and the user just clicks a link to a JNLP (Java Network Launching Protocol) file that downloads the JAR to the client and runs the app.
To use C libraries with Java one must use the Java Native Interface (JNI) framework. Usually you use JNI to call a library that exists on the client's LD_LIBRARY_PATH
, but since we want our app to be self-contained in order to deploy with Java Web Start, we would like to ship those C libraries inside our app's JAR file, and call them from there.
In this post I'll make a small temperature conversion app, in which the conversion is handled by a small C library I created.
Our temperature conversion library in C
Let's have the following code with two functions: one converting from Celsius to Fahrenheit and another one to convert the other way around:
#include "temperature.h"
#include <math.h>
float to_fahrenheit(float celsius) {
float fahrenheit;
fahrenheit = roundf( ( (celsius * (9.00f/5.00f)) + 32.00f ) * 100.00f ) / 100.00f;
return fahrenheit;
}
float to_celsius(float fahrenheit) {
float celsius;
celsius = roundf( ( (fahrenheit - 32.00f) * (5.00f/9.00f) ) * 100.00f ) / 100.00f;
return celsius;
}
Simple, there's not much to be said about this. Here's a Gist with temperature.c
and its header file temperature.h
. If we compile it into a static library we get libtemperature.a
.
Loading our C library in our Java app
We first need to create a Java class with the native methods that will call our C functions. Like this:
package com.marcastr0.temperature;
public class Converter {
public native float toCelsius(float fahrenheit);
public native float toFahrenheit(float celsius);
...
}
As you can see the toCelsius
function's signature resembles the signature for to_celsius
in temperature.c
, and same thing goes for toFahrenheit
and to_fahrenheit
. We now need to implement the native toCelsius
and toFahrenheit
methods in C (using our libtemperature library from before) before using them. After compiling the Converter
class, we can generate a C header file with the javah
command like this:
cd <path to compiled Java classes>
javah -jni com.marcastr0.temperature.Converter
This will generate a file named com_marcastr0_temperature_Converter.h
that will look something like this:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_marcastr0_temperature_Converter */
#ifndef _Included_com_marcastr0_temperature_Converter
#define _Included_com_marcastr0_temperature_Converter
#ifdef __cplusplus
extern "C" {
#endif
...
/*
* Class: com_marcastr0_temperature_Converter
* Method: toCelsius
* Signature: (F)F
*/
JNIEXPORT jfloat JNICALL Java_com_marcastr0_temperature_Converter_toCelsius
(JNIEnv *, jobject, jfloat);
/*
* Class: com_marcastr0_temperature_Converter
* Method: toFahrenheit
* Signature: (F)F
*/
JNIEXPORT jfloat JNICALL Java_com_marcastr0_temperature_Converter_toFahrenheit
(JNIEnv *, jobject, jfloat);
#ifdef __cplusplus
}
#endif
#endif
Using this header file, we now implement the native methods in com_marcastr0_temperature_Converter.c
using our libtemperature
library. Having that, we'll then be able to compile this code into a library that we can load with JNI in our Java code. com_marcastr0_temperature_Converter.c
will look something like this:
#include "temperature.h"
#include "com_marcastr0_temperature_Converter.h"
JNIEXPORT jfloat JNICALL Java_com_marcastr0_temperature_Converter_toCelsius(JNIEnv *env, jobject obj, jfloat fahrenheit) {
return to_celsius(fahrenheit);
}
JNIEXPORT jfloat JNICALL Java_com_marcastr0_temperature_Converter_toFahrenheit(JNIEnv *env, jobject obj, jfloat celsius) {
return to_fahrenheit(celsius);
}
We can now compile and generate the library that will be loaded by our Java application. The compilation is system dependent. That means, that if you plan to ship the app you must have versions for different systems. In my case, for this example I'm using macOS:
gcc -o libTemperatureConverter.jnilib \
-lc -shared -I/System/Library/Frameworks/JavaVM.framework/Headers \
-I<path to where temperature.h is located> com_marcastr0_temperature_Converter.c \
<path to where libtemperature.a is located> -Wall
Good! We now have libTemperatureConverter.jnilib
that we can now load from our Converter
class.
Loading our generated library
The usual method of loading our generated libTemperatureConverter
(the extension depends on the system, .so
on Linux, .dll
on Windows and .jnilib
on macOS) is the following. Let's get back to our Converter.java
:
package com.marcastr0.temperature;
public class Converter {
public native float toCelsius(float fahrenheit);
public native float toFahrenheit(float celsius);
static { System.loadLibrary("TemperatureConverter"); }
...
}
For this to work we need to have the path to our libTemperatureConverter
as part or our LD_LIBRARY_PATH
. Since I wanted to ship a JAR with the library included and not have the user place it somewhere and set its own LD_LIBRARY_PATH
I had to look for a way to package libTemperatureConverter
and load it from there. A bit of searching around Google and I found this very useful library class with which I can now load my library from within the JAR. Let's assume we place our libTemperatureConverter
as a resource:
package com.marcastr0.temperature;
import cz.adamh.NativeUtils;
public class Converter {
public native float toCelsius(float fahrenheit);
public native float toFahrenheit(float celsius);
static {
try {
NativeUtils.loadLibraryFromJar("/resources/libTemperatureConverter.jnilib");
} catch (IOException e) {
// This is probably not the best way to handle exception :-)
e.printStackTrace();
}
}
...
}
Putting it all together
I've put together a GUI using Swing and AWT. I'll describe the relevant parts of our modified Converter.java
(The full code is on my GitHub repo):
package com.marcastr0.temperature;
import cz.adamh.utils.NativeUtils;
...
public class Converter extends JPanel implements ActionListener{
public native float toCelsius(float fahrenheit);
public native float toFahrenheit(float celsius);
static {
try {
NativeUtils.loadLibraryFromJar("/resources/libTemperatureConverter.jnilib");
} catch (IOException e) {
// This is probably not the best way to handle exception :-)
e.printStackTrace();
}
}
private static String TO_CELSIUS = "temperatureField";
private static String TO_FAHRENHEIT = "fahrenheit";
private JFrame controllingFrame;
private JTextField temperatureField;
private JFormattedTextField resultField;
public Converter(JFrame f) {
// Use the default FlowLayout.
controllingFrame = f;
// Create everything.
temperatureField = new JTextField(10);
JLabel celsiusLabel = new JLabel("Temperature: ");
celsiusLabel.setLabelFor(temperatureField);
JComponent buttonPane = createButtonPanel();
resultField = new JFormattedTextField("");
resultField.setEditable(false);
JLabel resultLabel = new JLabel("Result: ");
// Lay out everything.
JPanel textPane = new JPanel(new GridLayout(2, 2));
textPane.add(celsiusLabel);
textPane.add(temperatureField);
textPane.add(resultLabel);
textPane.add(resultField);
add(textPane);
add(buttonPane);
}
protected JComponent createButtonPanel() {
JPanel p = new JPanel(new GridLayout(2,1));
JButton toCelsiusButton = new JButton("Convert to Celsius");
JButton toFahrenheitButton = new JButton("Convert to Fahrenheit");
toCelsiusButton.setActionCommand(TO_CELSIUS);
toCelsiusButton.addActionListener(this);
toFahrenheitButton.setActionCommand(TO_FAHRENHEIT);
toFahrenheitButton.addActionListener(this);
p.add(toCelsiusButton);
p.add(toFahrenheitButton);
return p;
}
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (TO_CELSIUS.equals(cmd)) {
String input = temperatureField.getText();
if (!input.equals("")) {
float fahrenheit = Float.parseFloat(input);
float celsius = toCelsius(fahrenheit);
resultField.setValue(String.valueOf(celsius) + " °C");
} else {
JOptionPane.showMessageDialog(controllingFrame, "Please enter a temperature", "Error message", JOptionPane.ERROR_MESSAGE);
}
temperatureField.selectAll();
resetFocus();
} else {
String input = temperatureField.getText();
if (!input.equals("")) {
float celsius = Float.parseFloat(input);
float fahrenheit = toFahrenheit(celsius);
resultField.setValue(String.valueOf(fahrenheit) + " °F");
} else {
JOptionPane.showMessageDialog(controllingFrame, "Please enter a temperature", "Error message", JOptionPane.ERROR_MESSAGE);
}
temperatureField.selectAll();
resetFocus();
}
}
...
}
What is relevant here is that when we construct the object we define our text fields and assign actions to our buttons. When ActionPerformed
is called we call our native methods toCelsius
or toFahrenheit
depending on which button was clicked. These functiones were implemented separately and were loaded from libTemperatureConverter
. That way we've successfully reused C code inside a Java application!
Our final GUI looks something like this:
Deploying as a Java Web Start app
In order to deploy as a Java Web Start application we need to sign the JAR with the application and write a JNLP file to launch the application. A JNLP file for this app looks like this:
<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0+" codebase="http://temperature.marcastr0.com/">
<information>
<title>Temperature Converter</title>
<vendor>Mario Castro Squella</vendor>
<homepage href="http://temperature.marcastr0.com/"></homepage>
<description>Java Web Start app for converting temperatures</description>
</information>
<security>
<all-permissions></all-permissions>
</security>
<resources>
<j2se version="1.6+"></j2se>
<jar href="jni-jnlp-poc-1.0-SNAPSHOT-macosx-x64.jar"></jar>
</resources>
<application-desc main-class="com.marcastr0.temperature.Converter"></application-desc>
</jnlp>
I followed the steps in this tutorial for signing the JAR.
Here are the following links to the app builds for Linux (x64) and macOS (Windows coming soon!):
You might have some security related problems launching the app because the JARs are self-signed. To get around that you might need to add a site exception to my site temperature.marcastr0.com.