Sunday, November 14, 2004

A little JNI

For an integration project at work, I found myself needing to implement portions of a Java class in C++ to access a custom shared library another group wrote. JNI, of course. So I looked around and remembered SWIG for auto-generating things. The problem with SWIG, however, is that it is designed to go C++->Java, and I need to go the other direction.

After some searching, I decided to just go it alone and see what happened. About a half-day later, I had a decent looking setup. First, the GNUmakefile (for some hypothetical project named Pants). Just as important as tests are an easy build (yes, this is for Cygwin):

MAKEFILE = $(word 1,$(MAKEFILE_LIST))

TARGET = Pants

FLAGS = -g3 # -Os -W -Wall

CC = cc
CFLAGS = $(FLAGS)
CPPFLAGS = -I. -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/win32"
CXX = c++
CXXFLAGS = $(FLAGS) -fabi-version=0 # -Weffc++
JAR = $(JAVA_HOME)/bin/jar
JARFLAGS =
JAVAC = $(JAVA_HOME)/bin/javac
JAVACFLAGS = -g -Xlint:all
JAVAH = $(JAVA_HOME)/bin/javah
JAVAHFLAGS =
LDFLAGS = -L. -L"$(JAVA_HOME)/bin" -L"$(JAVA_HOME)/lib"
TARGET_ARCH = -mno-cygwin # turn off Cygwin-specific dependencies

COMPILE.java = $(JAVAC) $(JAVACFLAGS)
LINK.o = $(CXX) $(LDFLAGS) $(TARGET_ARCH)

%.d: %.cpp $(MAKEFILE)
	@$(CXX) -MM $(CPPFLAGS) $< > $@.$$$$; 	  sed 's,\($*\)\.o[ :]*,\1.o $@ : $(MAKEFILE) ,g' < $@.$$$$ > \ $@;
	  rm -f $@.$$$$

%.class: %.java $(MAKEFILE)
	$(COMPILE.java) $<

%.h: %.class
	$(JAVAH) $(JAVAHFLAGS) -jni $(patsubst %.class,%,$^)
	@touch $@ # javah doesn't update the timestamp

SRCS = $(wildcard *.cpp)

all: $(TARGET).jar

-include $(SRCS:.cpp=.d)

# Teach make to generate the header when compiling the source
$(TARGET).o: $(TARGET).h

$(TARGET).dll: $(SRCS:.cpp=.o)
	$(LINK.cpp) -shared -o $@ $^ $(LDLIBS)

$(TARGET).jar: $(TARGET).class $(TARGET).dll
	[ -e $@ ] 	  && $(JAR) uf $@ $^
	  || $(JAR) cf $@ $^

clean:
	$(RM) *~ *.d *.o
	$(RM) $(TARGET).h $(TARGET).class $(TARGET).dll $(TARGET).jar

Ok, then. What is this all for?

You start with a directory listing such as:

  1. GNUmakefile
  2. Pants.cpp
  3. Pants.java

And running make compiles Pants.class, magically creates Pants.h containing the JNI bindings for any native methods in Pants.class, uses Pants.cpp to implement the methods, links Pants.dll, and finally combines Pants.class and Pants.dll into Pants.jar. Easy, peasy. The output is:

Pants.cpp:1:19: Pants.h: No such file or directory
javac -g -Xlint:all Pants.java
javah  -jni Pants
c++ -g3  -fabi-version=0  -I. -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/win32" -mno-cygwin -c -o Pants.o Pants.cpp
c++ -g3  -fabi-version=0  -I. -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/win32" -L. -L"$JAVA_HOME/bin" -L"$JAVA_HOME/lib" -mno-cygwin -shared -o Pants.dll
 Pants.o
[ -e Pants.jar ]   && jar uf Pants.jar Pants.class Pants.dll   || jar cf Pants.jar Pants.class Pants.dll

(The warning in the first line only happens once, and is a side-effect of auto-generating header dependencies. A fix would be very welcome. And the nuisome check for Pants.jar existence is because jar isn't very smart.)

Take a trivial Pants.java:

public class Pants {
    public native void wear();
}

And a simple-minded implementation:

#include "Pants.h"

#include <iostream>

using namespace std;

/*
 * Class:     Pants
 * Method:    wear
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_Pants_wear(JNIEnv* env, jobject self)
{
  cout << "One leg at a time." << endl;
}

Now I find it easier to work with more natural-looking C++ methods than with Java_Pants_wear(JNIEnv*, jobject). Here is my solution. Rather than trying a full-blown peer wrapper (such as JNI++ or Jace), I did the simplest thing that could possibly work. First, a hand-coded matching C++ peer class to the Java class, JPants.h:

// Emacs, this is -*- c++ -*- code.
#ifndef J_PANTS_H_
#define J_PANTS_H_

#include 
#include 

class JPants
{
  JNIEnv* env;
  jobject self;

public:
  JPants(JNIEnv* env, jobject self);
  void wear();
};

#endif // J_PANTS_H_

Then I moved the implementation code from Pants.cpp to JPants.cpp:

#include "JPants.h"

#include <iostream>

using namespace std;

JPants::JPants(JNIEnv* env, jobject self)
  : env(env), self(self)
{
}

void
JPants::wear()
{
  cout << "One leg at a time." << endl;
}

Lastly, I updated Pants.cpp to be a purely forwarding implementation:

#include "Pants.h"
#include "JPants.h"

/*
 * Class:     Pants
 * Method:    wear
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_Pants_wear(JNIEnv* env, jobject self)
{
  JPants(env, self).wear();
}

The only thing left is to have Pants actually do something interesting. If I were to formalize this, I'd write a simple wrapper generator to write the forwarding code and peer class, and provide some helpers such as a std::string factory for jstring. But after a while, I'd be rewriting those other packages I mentioned up front that I was avoiding. It is always so tempting to over-generalize and write meta-code instead of delivering functionality.

UPDATE: There is a definitely gotcha with using Cygwin: the DLL is fine except that Sun's JVM cannot find the symbols in it. The symptom is an java.lang.UnsatisfiedLinkError exception when using the DLL. The reason is esoteric, but the solution is straight-forward. Fix GNUmakefile and replace:

$(TARGET).dll: $(SRCS:.cpp=.o)
	$(LINK.cpp) -shared -o $@ $^ $(LDLIBS)

with:

$(TARGET).dll: $(SRCS:.cpp=.o)
	$(LINK.cpp) -shared -Wl,--add-stdcall-alias -o $@ $^ $(LDLIBS)

UPDATE: I failed to include a very imporant bit of code in Pants.java:

static {
    System.loadLibrary("Pants");
}

Otherwise, the JVM will never find your native methods and you'll see the abysmal java.lang.UnsatisfiedLinkError error. Also I've uploaded a sample ZIP file of the code in this posting along with a simple unit test: http://binkley.fastmail.fm/Pants.zip.

No comments: