nandi/libjavapublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/libjava.git
git clone ssh://git@rickub.com/nandi/libjava.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md

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.