I’d been playing around with starting up a JVM from C++ code on Windows. I was futzing around between MSVC and cygwin/mingw and it all worked well. Then I decided to do the same under Mac OS X.
Firstly, if you’re using the OS X VM, when you try to compile some simple code under clang, you’re going to see this awesome warning:
warning: 'JNI_CreateJavaVM' is deprecated [-Wdeprecated-declarations]
Apple really, really don’t want you using the JavaVM framework; in general if you want to use their framework, you have to ignore the warnings. and soldier on.
Secondly, if you want a GUI, you can’t instantiate the VM on the main thread. If you do that then your process will deadlock, and you won’t be able to see any of the GUI.
With that in mind, we hive-off the creation of the VM and main class to a separate thread. With some crufty code, we make a struct of the VM initialization arguments (this code is not clean it contains strdups and news and there’s no cleanup on free):
struct start_args { JavaVMInitArgs vm_args; const char *launch_class; start_args(const char **args, const char *classname) { vm_args.version = JNI_VERSION_1_6; vm_args.ignoreUnrecognized = JNI_TRUE; int arg_count = 0; const char **atarg = args; while (*atarg++) arg_count++; JavaVMOption *options = new JavaVMOption[arg_count]; vm_args.nOptions = arg_count; vm_args.options = options; while (*args) { options->optionString = strdup(*args); options++; args++; } launch_class = strdup(classname); } };
Next we have the thread function that launches the VM. This is a standard posix thread routine, so there’s no magic there.
void * start_java(void *start_args) { struct start_args *args = (struct start_args *)start_args; int res; JavaVM *jvm; JNIEnv *env; res = JNI_CreateJavaVM(&jvm, (void**)&env, &args->vm_args); if (res < 0) exit(1); /* load the launch class */ jclass main_class; jmethodID main_method_id; main_class = env->FindClass(args->launch_class); if (main_class == NULL) { jvm->DestroyJavaVM(); exit(1); } /* get main method */ main_method_id = env->GetStaticMethodID(main_class, "main", "([Ljava/lang/String;)V"); if (main_method_id == NULL) { jvm->DestroyJavaVM(); exit(1); } /* make the initial argument */ jobject empty_args = env->NewObjectArray(0, env->FindClass("java/lang/String"), NULL); /* call the method */ env->CallStaticVoidMethod(main_class, main_method_id, empty_args); /* Don't forget to destroy the JVM at the end */ jvm->DestroyJavaVM(); return (0); }
What this code does is Create the Java VM (short piece at the start). Then it finds and invokes the public static void main(String args[])
of the class that’s passed in. At the end, it destroys that Java VM. You’re supposed to do that; for memory allocation’s sake.
Next we have the main routine, which creates the thread and invokes the run loop
int main(int argc, char **argv) { const char *vm_arglist[] = { "-Djava.class.path=.", 0 }; struct start_args args(vm_arglist, "launch"); pthread_t thr; pthread_create(&thr, NULL, start_java, &args); CFRunLoopRun(); }
The trick is the CFRunLoopRun()
at the end. What this does is triggers the CoreFoundation main application run-loop, which allows the application to pump messages for all the other run-loops that are created by the java UI framework.
The next thing is an example java file that creates a window.
public class launch extends JFrame { JLabel emptyLabel; public launch() { super("FrameDemo"); emptyLabel = new JLabel("Hello World"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); getContentPane().add(emptyLabel, BorderLayout.CENTER); pack(); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { launch l = new launch(); l.setVisible(true); } }); } }
Because it’s got a EXIT_ON_CLOSE
option, when you close the window the application will terminate.
Gluing this into a makefile involves the following; it’s got conditional sections for building with either the 1.6 or 1.7 VM, and there are a couple of changes that are needed in the cpp code to get this to work:
JAVA_VERSION?=6 JHOME:=$(shell /usr/libexec/java_home -v 1.$(JAVA_VERSION)) SYSTEM:=$(shell uname -s) CPPFLAGS += -DJAVA_VERSION=$(JAVA_VERSION) CXXFLAGS += -g ifeq ($(JAVA_VERSION),7) VM_DIR=$(JHOME)/jre/lib LDFLAGS += -L$(VM_DIR)/server -Wl,-rpath,$(VM_DIR) -Wl,-rpath,$(VM_DIR)/server CXXFLAGS += -I$(JHOME)/include -I$(JHOME)/include/$(SYSTEM) LDLIBS += -ljvm else CXXFLAGS += -framework JavaVM endif CXXFLAGS += -framework CoreFoundation all: launch launch.class launch.class: launch.java /usr/libexec/java_home -v 1.$(JAVA_VERSION) --exec javac launch.java clean: rm -rf launch *.class *.dSYM
For the C++
, you need to change the include path depending on whether you’re building for the 1.6 or 1.7 VM.
#if JAVA_VERSION == 6 #include <JavaVM/jni.h> #else #include <jni.h> #endif
.
You can’t really use the code that was compiled against the 1.6 VM on the 1.7 environment, as it uses the JavaVM
framework in 1.6, vs. the libjni.dylib
on the 1.7 VM, but I would consider this a given across all JVM variants.
This source can be cloned from the Repository on BitBucket.