libjava
Embed a JVM in a Nim process and call Java from it. Thin JNI bindings plus a
layer that makes the common cases pleasant.
import libjava
let jvm = startJVM(classPath = ["build/classes"], options = ["-Xmx256m"])
defer: jvm.destroy()
let env = jvm.env
let math = env.findClass("java/lang/Math")
echo env.callStatic[:JDouble](math, "sqrt", "(D)D", 2.0)
Build
The JDK is located at compile time: -d:javaHome=/path/to/jdk, else
JAVA_HOME, else by following javac on PATH. The include flags and
-ljvm (with an rpath, so no LD_LIBRARY_PATH needed) are added for you.
nimble test
nimble demo
There is also a justfile wrapping these. just build produces a release
binary of the example at out/demo; just check, just docs, just clean
and just jdk (prints the JDK that will be linked) round it out. just -l
lists them; the nimble tasks stay the source of truth.
Pass -d:libjavaNoLink if your process already has the JVM symbols — when
this code is itself a shared library loaded by a JVM.
What's here
| Module | |
|---|---|
libjava/config |
compile-time JDK discovery, C/link flags |
libjava/jni |
raw JNI bindings — the two function tables, JNI_CreateJavaVM |
libjava/core |
strings, exceptions, typed calls, signatures |
libjava/vm |
JVM lifecycle, per-thread JNIEnv |
| Example | |
|---|---|
examples/demo.nim |
Java: statics, objects, fields, exceptions, collections |
examples/clojure_demo.nim |
Clojure via clojure.java.api.Clojure |
Other JVM languages
Anything that compiles to JVM bytecode is reachable without a line of
language-specific support — it's just jars on the classpath.
examples/clojure_demo.nim calls Clojure through its documented Java API,
which is two members wide:
# clojure.java.api.Clojure/var -> a clojure.lang.IFn; then IFn/invoke
let greet = env.callStatic[:JObjectRef](clojure, "var",
"(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;", "demo.core", "greet")
discard env.call[:JObjectRef](greet, ifn, "invoke",
"(Ljava/lang/Object;)Ljava/lang/Object;", arg)
Requiring a namespace, reading a keyword and walking a Clojure map are all that
same pair pointed at clojure.core. Run it with just clojure, which needs the
clojure CLI on PATH; deps.edn pins the dependency and just clojure-cp
resolves it into build/clojure-classpath. Note that IFn/invoke takes
Object, so primitives must be boxed — java.lang.Long.valueOf in the example.
Calling
Method signatures are JVM descriptors. javap -s SomeClass prints the exact
string for anything you're unsure about; signature(["int", "String"], "void")
builds simple ones in code.
# static
env.callStatic[:string](cls, "shout", "(Ljava/lang/String;)Ljava/lang/String;", "hi")
env.callStatic[:void](cls, "run", "()V")
# construct + instance call
let obj = env.newObject(cls, "(Ljava/lang/String;)V", "world")
echo env.call[:string](obj, cls, "greet", "(I)Ljava/lang/String;", 2)
# omit the class and it resolves against the object's runtime class
echo env.call[:string](obj, "greet", "(I)Ljava/lang/String;", 2)
The type parameter picks the JNI call variant, so it has to agree with the
descriptor's return type. string is sugar for an object call plus a
conversion. Nim's int maps to Java int; use asLong(x) for long, and
asByte / asShort / asChar / asFloat for the other narrow types.
Three things JNI will bite you with
Exceptions don't unwind C. A Java throw just sets a flag on the thread, and
nearly every subsequent JNI call is undefined behaviour while it's set. Every
call here ends in checkException, which clears the throwable and raises
JavaError with the class name, message, and Java stack trace attached. If you
drop to the raw layer, call env.checkException() yourself.
Local references accumulate. They're only freed when the native frame
returns, so a loop that creates them will exhaust the reference table long
before that. Wrap the body:
for i in 0 ..< 100_000:
env.withLocalFrame(8):
let s = env.newJString("item " & $i)
discard env.toNimString(s)
To keep a reference past the frame, promote it: env.newGlobalRef(obj), and
deleteGlobalRef when done.
JNIEnv is per-thread. Only the JavaVM handle may cross threads. A
thread the JVM didn't start must attach before its first JNI call and detach
before it exits, or the VM won't shut down:
proc worker(vm: JavaVM) {.thread.} =
let env = envOf(vm) # attaches on first use
defer: detachOf(vm)
...
createThread(t, worker, jvm.vm)
Strings
newJString / toNimString go through UTF-16, not GetStringUTFChars. JNI's
"UTF-8" is modified UTF-8: supplementary characters become two 3-byte
surrogates and NUL becomes 0xC0 0x80, neither of which is valid UTF-8. Going
through UTF-16 is the only way emoji and other non-BMP text survive the trip.
One VM per process
The JNI spec has never supported creating a second VM, and a destroyed one
can't be restarted. startJVM called twice returns the existing VM. If a JVM
already exists — because your library was loaded by one — use
attachExistingJVM(), or adoptJVM(vm) with the handle from JNI_OnLoad.