nandi/libjavapublic Fork 0
963f309
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.

Initial commit: JNI bindings and a JVM embedding layer for Nim

Starts a JVM in-process via JNI_CreateJavaVM and calls Java from Nim.

The two JNI function tables are bound as {.importc.} structs with only the
fields we use declared, so Nim emits `(*env)->FindClass(env, ...)` verbatim
and the real jni.h decides the layout -- no hand-counted offsets, and the
bindings cannot drift from the JDK they compile against. Only the ...A call
variants are bound; the variadic forms lose type safety for nothing.

On top of that:

- Java throwables surface as JavaError, carrying class name, message and the
  Java stack trace. The pending exception is always cleared first, since
  nearly every JNI call is undefined behaviour while one is set.
- Strings convert through UTF-16 rather than GetStringUTFChars, whose
  "modified UTF-8" mangles supplementary characters and NUL.
- envOf/detachOf take the raw JavaVM handle, the one JNI pointer that may
  legally cross threads.
- The JDK is located at compile time via -d:javaHome, JAVA_HOME, or javac on
  PATH, and the include/link flags are added automatically.

30 tests pass against JDK 17, covering non-BMP string round-trips, exception
propagation, local-frame reference management, and four worker threads
attaching to the VM concurrently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T14:17:33-07:00 Browse files
963f309
added .gitignore +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+# Build outputs
2+build/
3+nimcache/
4+*.class
5+
6+# Compiled test/example binaries (nim c -r drops these next to the source)
7+/tests/tlibjava
8+/examples/demo
new file mode 100644
@@ -0,0 +1,8 @@
1+# Build outputs
2+build/
3+nimcache/
4+*.class
5+
6+# Compiled test/example binaries (nim c -r drops these next to the source)
7+/tests/tlibjava
8+/examples/demo
added LICENSE +21 -0
new file mode 100644
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 nandi
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
new file mode 100644
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 nandi
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
added README.md +110 -0
new file mode 100644
@@ -0,0 +1,110 @@
1+# libjava
2+
3+Embed a JVM in a Nim process and call Java from it. Thin JNI bindings plus a
4+layer that makes the common cases pleasant.
5+
6+```nim
7+import libjava
8+
9+let jvm = startJVM(classPath = ["build/classes"], options = ["-Xmx256m"])
10+defer: jvm.destroy()
11+let env = jvm.env
12+
13+let math = env.findClass("java/lang/Math")
14+echo env.callStatic[:JDouble](math, "sqrt", "(D)D", 2.0)
15+```
16+
17+## Build
18+
19+The JDK is located at compile time: `-d:javaHome=/path/to/jdk`, else
20+`JAVA_HOME`, else by following `javac` on `PATH`. The include flags and
21+`-ljvm` (with an rpath, so no `LD_LIBRARY_PATH` needed) are added for you.
22+
23+```bash
24+nimble test
25+nimble demo
26+```
27+
28+Pass `-d:libjavaNoLink` if your process already has the JVM symbols — when
29+this code is itself a shared library loaded *by* a JVM.
30+
31+## What's here
32+
33+| Module | |
34+|---|---|
35+| `libjava/config` | compile-time JDK discovery, C/link flags |
36+| `libjava/jni` | raw JNI bindings — the two function tables, `JNI_CreateJavaVM` |
37+| `libjava/core` | strings, exceptions, typed calls, signatures |
38+| `libjava/vm` | JVM lifecycle, per-thread `JNIEnv` |
39+
40+## Calling
41+
42+Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
43+string for anything you're unsure about; `signature(["int", "String"], "void")`
44+builds simple ones in code.
45+
46+```nim
47+# static
48+env.callStatic[:string](cls, "shout", "(Ljava/lang/String;)Ljava/lang/String;", "hi")
49+env.callStatic[:void](cls, "run", "()V")
50+
51+# construct + instance call
52+let obj = env.newObject(cls, "(Ljava/lang/String;)V", "world")
53+echo env.call[:string](obj, cls, "greet", "(I)Ljava/lang/String;", 2)
54+
55+# omit the class and it resolves against the object's runtime class
56+echo env.call[:string](obj, "greet", "(I)Ljava/lang/String;", 2)
57+```
58+
59+The type parameter picks the JNI call variant, so it has to agree with the
60+descriptor's return type. `string` is sugar for an object call plus a
61+conversion. Nim's `int` maps to Java `int`; use `asLong(x)` for `long`, and
62+`asByte` / `asShort` / `asChar` / `asFloat` for the other narrow types.
63+
64+## Three things JNI will bite you with
65+
66+**Exceptions don't unwind C.** A Java throw just sets a flag on the thread, and
67+nearly every subsequent JNI call is undefined behaviour while it's set. Every
68+call here ends in `checkException`, which clears the throwable and raises
69+`JavaError` with the class name, message, and Java stack trace attached. If you
70+drop to the raw layer, call `env.checkException()` yourself.
71+
72+**Local references accumulate.** They're only freed when the native frame
73+returns, so a loop that creates them will exhaust the reference table long
74+before that. Wrap the body:
75+
76+```nim
77+for i in 0 ..< 100_000:
78+ env.withLocalFrame(8):
79+ let s = env.newJString("item " & $i)
80+ discard env.toNimString(s)
81+```
82+
83+To keep a reference past the frame, promote it: `env.newGlobalRef(obj)`, and
84+`deleteGlobalRef` when done.
85+
86+**`JNIEnv` is per-thread.** Only the `JavaVM` handle may cross threads. A
87+thread the JVM didn't start must attach before its first JNI call and detach
88+before it exits, or the VM won't shut down:
89+
90+```nim
91+proc worker(vm: JavaVM) {.thread.} =
92+ let env = envOf(vm) # attaches on first use
93+ defer: detachOf(vm)
94+ ...
95+createThread(t, worker, jvm.vm)
96+```
97+
98+## Strings
99+
100+`newJString` / `toNimString` go through UTF-16, not `GetStringUTFChars`. JNI's
101+"UTF-8" is *modified* UTF-8: supplementary characters become two 3-byte
102+surrogates and NUL becomes `0xC0 0x80`, neither of which is valid UTF-8. Going
103+through UTF-16 is the only way emoji and other non-BMP text survive the trip.
104+
105+## One VM per process
106+
107+The JNI spec has never supported creating a second VM, and a destroyed one
108+can't be restarted. `startJVM` called twice returns the existing VM. If a JVM
109+already exists — because your library was loaded by one — use
110+`attachExistingJVM()`, or `adoptJVM(vm)` with the handle from `JNI_OnLoad`.
new file mode 100644
@@ -0,0 +1,110 @@
1+# libjava
2+
3+Embed a JVM in a Nim process and call Java from it. Thin JNI bindings plus a
4+layer that makes the common cases pleasant.
5+
6+```nim
7+import libjava
8+
9+let jvm = startJVM(classPath = ["build/classes"], options = ["-Xmx256m"])
10+defer: jvm.destroy()
11+let env = jvm.env
12+
13+let math = env.findClass("java/lang/Math")
14+echo env.callStatic[:JDouble](math, "sqrt", "(D)D", 2.0)
15+```
16+
17+## Build
18+
19+The JDK is located at compile time: `-d:javaHome=/path/to/jdk`, else
20+`JAVA_HOME`, else by following `javac` on `PATH`. The include flags and
21+`-ljvm` (with an rpath, so no `LD_LIBRARY_PATH` needed) are added for you.
22+
23+```bash
24+nimble test
25+nimble demo
26+```
27+
28+Pass `-d:libjavaNoLink` if your process already has the JVM symbols — when
29+this code is itself a shared library loaded *by* a JVM.
30+
31+## What's here
32+
33+| Module | |
34+|---|---|
35+| `libjava/config` | compile-time JDK discovery, C/link flags |
36+| `libjava/jni` | raw JNI bindings — the two function tables, `JNI_CreateJavaVM` |
37+| `libjava/core` | strings, exceptions, typed calls, signatures |
38+| `libjava/vm` | JVM lifecycle, per-thread `JNIEnv` |
39+
40+## Calling
41+
42+Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
43+string for anything you're unsure about; `signature(["int", "String"], "void")`
44+builds simple ones in code.
45+
46+```nim
47+# static
48+env.callStatic[:string](cls, "shout", "(Ljava/lang/String;)Ljava/lang/String;", "hi")
49+env.callStatic[:void](cls, "run", "()V")
50+
51+# construct + instance call
52+let obj = env.newObject(cls, "(Ljava/lang/String;)V", "world")
53+echo env.call[:string](obj, cls, "greet", "(I)Ljava/lang/String;", 2)
54+
55+# omit the class and it resolves against the object's runtime class
56+echo env.call[:string](obj, "greet", "(I)Ljava/lang/String;", 2)
57+```
58+
59+The type parameter picks the JNI call variant, so it has to agree with the
60+descriptor's return type. `string` is sugar for an object call plus a
61+conversion. Nim's `int` maps to Java `int`; use `asLong(x)` for `long`, and
62+`asByte` / `asShort` / `asChar` / `asFloat` for the other narrow types.
63+
64+## Three things JNI will bite you with
65+
66+**Exceptions don't unwind C.** A Java throw just sets a flag on the thread, and
67+nearly every subsequent JNI call is undefined behaviour while it's set. Every
68+call here ends in `checkException`, which clears the throwable and raises
69+`JavaError` with the class name, message, and Java stack trace attached. If you
70+drop to the raw layer, call `env.checkException()` yourself.
71+
72+**Local references accumulate.** They're only freed when the native frame
73+returns, so a loop that creates them will exhaust the reference table long
74+before that. Wrap the body:
75+
76+```nim
77+for i in 0 ..< 100_000:
78+ env.withLocalFrame(8):
79+ let s = env.newJString("item " & $i)
80+ discard env.toNimString(s)
81+```
82+
83+To keep a reference past the frame, promote it: `env.newGlobalRef(obj)`, and
84+`deleteGlobalRef` when done.
85+
86+**`JNIEnv` is per-thread.** Only the `JavaVM` handle may cross threads. A
87+thread the JVM didn't start must attach before its first JNI call and detach
88+before it exits, or the VM won't shut down:
89+
90+```nim
91+proc worker(vm: JavaVM) {.thread.} =
92+ let env = envOf(vm) # attaches on first use
93+ defer: detachOf(vm)
94+ ...
95+createThread(t, worker, jvm.vm)
96+```
97+
98+## Strings
99+
100+`newJString` / `toNimString` go through UTF-16, not `GetStringUTFChars`. JNI's
101+"UTF-8" is *modified* UTF-8: supplementary characters become two 3-byte
102+surrogates and NUL becomes `0xC0 0x80`, neither of which is valid UTF-8. Going
103+through UTF-16 is the only way emoji and other non-BMP text survive the trip.
104+
105+## One VM per process
106+
107+The JNI spec has never supported creating a second VM, and a destroyed one
108+can't be restarted. `startJVM` called twice returns the existing VM. If a JVM
109+already exists — because your library was loaded by one — use
110+`attachExistingJVM()`, or `adoptJVM(vm)` with the handle from `JNI_OnLoad`.
added examples/demo.nim +65 -0
new file mode 100644
@@ -0,0 +1,65 @@
1+## Run with: nim c -r --path:src examples/demo.nim
2+import std/[strformat, strutils]
3+import libjava
4+
5+proc main() =
6+
7+ let jvm = startJVM(
8+ classPath = ["build/classes"],
9+ options = ["-Xmx256m", "-Djava.awt.headless=true"])
10+ defer: jvm.destroy()
11+
12+ let env = jvm.env
13+ echo &"JVM up, JNI version 0x{env[].GetVersion(env):08X}"
14+
15+ # --- static calls on the JDK itself ---------------------------------------
16+ block:
17+ let system = env.findClass("java/lang/System")
18+ let version = env.callStatic[:string](system, "getProperty",
19+ "(Ljava/lang/String;)Ljava/lang/String;", "java.version")
20+ echo &"java.version = {version}"
21+ env.deleteLocalRef(system)
22+
23+ # --- static calls on our own class ----------------------------------------
24+ let greeter = env.findClass("demo/Greeter")
25+
26+ echo "shout = ", env.callStatic[:string](greeter, "shout",
27+ "(Ljava/lang/String;)Ljava/lang/String;", "hello from nim")
28+ echo "mean = ", env.callStatic[:JDouble](greeter, "mean", "(DD)D", 3.0, 4.0)
29+ echo "fib(90) = ", env.callStatic[:JLong](greeter, "fib", "(J)J", asLong(90))
30+
31+ # --- constructing an object and calling a method on it --------------------
32+ block:
33+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "world")
34+ defer: env.deleteLocalRef(g)
35+ echo "greet(2) = ", env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 2)
36+
37+ let counter = env.staticFieldID(greeter, "callCount", "I")
38+ echo "callCount = ", env[].GetStaticIntField(env, greeter, counter)
39+
40+ # --- signatures built rather than hand-written ----------------------------
41+ echo "signature = ", signature(["int", "String", "double[]"], "String")
42+
43+ # --- a Java exception arriving as a Nim exception -------------------------
44+ try:
45+ env.callStatic[:void](greeter, "explode", "()V")
46+ except JavaError as e:
47+ echo &"caught {e.className}: {e.javaMessage}"
48+ echo " first trace line: ", e.stackTrace.splitLines()[0]
49+
50+ # --- iterating a java.util.List, with local refs properly scoped ----------
51+ block:
52+ let list = env.callStatic[:JObjectRef](greeter, "words", "()Ljava/util/List;")
53+ defer: env.deleteLocalRef(list)
54+ let listCls = env.findClass("java/util/List")
55+ defer: env.deleteLocalRef(listCls)
56+
57+ let n = env.call[:JInt](list, listCls, "size", "()I")
58+ for i in 0 ..< n:
59+ env.withLocalFrame(8):
60+ let item = env.call[:JObjectRef](list, listCls, "get", "(I)Ljava/lang/Object;", i)
61+ echo &" words[{i}] = ", env.toStringOf(item)
62+
63+ echo "done"
64+
65+main()
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,65 @@
1+## Run with: nim c -r --path:src examples/demo.nim
2+import std/[strformat, strutils]
3+import libjava
4+
5+proc main() =
6+
7+ let jvm = startJVM(
8+ classPath = ["build/classes"],
9+ options = ["-Xmx256m", "-Djava.awt.headless=true"])
10+ defer: jvm.destroy()
11+
12+ let env = jvm.env
13+ echo &"JVM up, JNI version 0x{env[].GetVersion(env):08X}"
14+
15+ # --- static calls on the JDK itself ---------------------------------------
16+ block:
17+ let system = env.findClass("java/lang/System")
18+ let version = env.callStatic[:string](system, "getProperty",
19+ "(Ljava/lang/String;)Ljava/lang/String;", "java.version")
20+ echo &"java.version = {version}"
21+ env.deleteLocalRef(system)
22+
23+ # --- static calls on our own class ----------------------------------------
24+ let greeter = env.findClass("demo/Greeter")
25+
26+ echo "shout = ", env.callStatic[:string](greeter, "shout",
27+ "(Ljava/lang/String;)Ljava/lang/String;", "hello from nim")
28+ echo "mean = ", env.callStatic[:JDouble](greeter, "mean", "(DD)D", 3.0, 4.0)
29+ echo "fib(90) = ", env.callStatic[:JLong](greeter, "fib", "(J)J", asLong(90))
30+
31+ # --- constructing an object and calling a method on it --------------------
32+ block:
33+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "world")
34+ defer: env.deleteLocalRef(g)
35+ echo "greet(2) = ", env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 2)
36+
37+ let counter = env.staticFieldID(greeter, "callCount", "I")
38+ echo "callCount = ", env[].GetStaticIntField(env, greeter, counter)
39+
40+ # --- signatures built rather than hand-written ----------------------------
41+ echo "signature = ", signature(["int", "String", "double[]"], "String")
42+
43+ # --- a Java exception arriving as a Nim exception -------------------------
44+ try:
45+ env.callStatic[:void](greeter, "explode", "()V")
46+ except JavaError as e:
47+ echo &"caught {e.className}: {e.javaMessage}"
48+ echo " first trace line: ", e.stackTrace.splitLines()[0]
49+
50+ # --- iterating a java.util.List, with local refs properly scoped ----------
51+ block:
52+ let list = env.callStatic[:JObjectRef](greeter, "words", "()Ljava/util/List;")
53+ defer: env.deleteLocalRef(list)
54+ let listCls = env.findClass("java/util/List")
55+ defer: env.deleteLocalRef(listCls)
56+
57+ let n = env.call[:JInt](list, listCls, "size", "()I")
58+ for i in 0 ..< n:
59+ env.withLocalFrame(8):
60+ let item = env.call[:JObjectRef](list, listCls, "get", "(I)Ljava/lang/Object;", i)
61+ echo &" words[{i}] = ", env.toStringOf(item)
62+
63+ echo "done"
64+
65+main()
\ No newline at end of file\ No newline at end of file
added examples/java/demo/Greeter.java +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+package demo;
2+
3+import java.util.ArrayList;
4+import java.util.List;
5+
6+public class Greeter {
7+ public static int callCount = 0;
8+
9+ private final String name;
10+
11+ public Greeter(String name) {
12+ this.name = name;
13+ }
14+
15+ public String greet(int times) {
16+ callCount++;
17+ StringBuilder sb = new StringBuilder();
18+ for (int i = 0; i < times; i++) {
19+ if (i > 0) sb.append(" ");
20+ sb.append("Hello, ").append(name).append("!");
21+ }
22+ return sb.toString();
23+ }
24+
25+ public static String shout(String s) {
26+ return s.toUpperCase() + " — 🚀";
27+ }
28+
29+ public static double mean(double a, double b) {
30+ return (a + b) / 2.0;
31+ }
32+
33+ public static long fib(long n) {
34+ long a = 0, b = 1;
35+ for (long i = 0; i < n; i++) {
36+ long t = a + b;
37+ a = b;
38+ b = t;
39+ }
40+ return a;
41+ }
42+
43+ public static void explode() {
44+ throw new IllegalStateException("deliberate failure from Java");
45+ }
46+
47+ public static List<String> words() {
48+ List<String> out = new ArrayList<>();
49+ out.add("alpha");
50+ out.add("beta");
51+ return out;
52+ }
53+}
new file mode 100644
@@ -0,0 +1,53 @@
1+package demo;
2+
3+import java.util.ArrayList;
4+import java.util.List;
5+
6+public class Greeter {
7+ public static int callCount = 0;
8+
9+ private final String name;
10+
11+ public Greeter(String name) {
12+ this.name = name;
13+ }
14+
15+ public String greet(int times) {
16+ callCount++;
17+ StringBuilder sb = new StringBuilder();
18+ for (int i = 0; i < times; i++) {
19+ if (i > 0) sb.append(" ");
20+ sb.append("Hello, ").append(name).append("!");
21+ }
22+ return sb.toString();
23+ }
24+
25+ public static String shout(String s) {
26+ return s.toUpperCase() + " — 🚀";
27+ }
28+
29+ public static double mean(double a, double b) {
30+ return (a + b) / 2.0;
31+ }
32+
33+ public static long fib(long n) {
34+ long a = 0, b = 1;
35+ for (long i = 0; i < n; i++) {
36+ long t = a + b;
37+ a = b;
38+ b = t;
39+ }
40+ return a;
41+ }
42+
43+ public static void explode() {
44+ throw new IllegalStateException("deliberate failure from Java");
45+ }
46+
47+ public static List<String> words() {
48+ List<String> out = new ArrayList<>();
49+ out.add("alpha");
50+ out.add("beta");
51+ return out;
52+ }
53+}
added libjava.nimble +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+version = "0.1.0"
2+author = "nandi"
3+description = "Embed a JVM in a Nim process and call Java from it, via JNI"
4+license = "MIT"
5+srcDir = "src"
6+
7+requires "nim >= 2.0.0"
8+
9+task java, "Compile the example Java classes":
10+ exec "javac -d build/classes examples/java/demo/Greeter.java"
11+
12+task test, "Run the test suite":
13+ javaTask()
14+ exec "nim c -r --path:src tests/tlibjava.nim"
15+
16+task demo, "Run the example":
17+ javaTask()
18+ exec "nim c -r --path:src examples/demo.nim"
new file mode 100644
@@ -0,0 +1,18 @@
1+version = "0.1.0"
2+author = "nandi"
3+description = "Embed a JVM in a Nim process and call Java from it, via JNI"
4+license = "MIT"
5+srcDir = "src"
6+
7+requires "nim >= 2.0.0"
8+
9+task java, "Compile the example Java classes":
10+ exec "javac -d build/classes examples/java/demo/Greeter.java"
11+
12+task test, "Run the test suite":
13+ javaTask()
14+ exec "nim c -r --path:src tests/tlibjava.nim"
15+
16+task demo, "Run the example":
17+ javaTask()
18+ exec "nim c -r --path:src examples/demo.nim"
added src/libjava.nim +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+## libjava -- embed a JVM in a Nim process and call Java from it.
2+##
3+## ```nim
4+## import libjava
5+##
6+## let jvm = startJVM(classPath = ["build/classes"])
7+## defer: jvm.destroy()
8+## let env = jvm.env
9+##
10+## let cls = env.findClass("java/lang/Math")
11+## echo env.callStatic[:JDouble](cls, "sqrt", "(D)D", 2.0)
12+##
13+## let props = env.findClass("java/lang/System")
14+## echo env.callStatic[:string](props, "getProperty",
15+## "(Ljava/lang/String;)Ljava/lang/String;",
16+## "java.version")
17+## ```
18+##
19+## Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
20+## string for every method; `signature(["int", "String"], "void")` builds simple
21+## ones by hand.
22+
23+import ./libjava/[jni, core, vm]
24+export jni, core, vm
new file mode 100644
@@ -0,0 +1,24 @@
1+## libjava -- embed a JVM in a Nim process and call Java from it.
2+##
3+## ```nim
4+## import libjava
5+##
6+## let jvm = startJVM(classPath = ["build/classes"])
7+## defer: jvm.destroy()
8+## let env = jvm.env
9+##
10+## let cls = env.findClass("java/lang/Math")
11+## echo env.callStatic[:JDouble](cls, "sqrt", "(D)D", 2.0)
12+##
13+## let props = env.findClass("java/lang/System")
14+## echo env.callStatic[:string](props, "getProperty",
15+## "(Ljava/lang/String;)Ljava/lang/String;",
16+## "java.version")
17+## ```
18+##
19+## Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
20+## string for every method; `signature(["int", "String"], "void")` builds simple
21+## ones by hand.
22+
23+import ./libjava/[jni, core, vm]
24+export jni, core, vm
added src/libjava/config.nim +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+## Compile-time discovery of the JDK, and the C flags needed to link libjvm.
2+##
3+## Resolution order for the JDK home:
4+## 1. `-d:javaHome=/path/to/jdk` passed to the Nim compiler
5+## 2. the `JAVA_HOME` environment variable at compile time
6+## 3. following `javac` on PATH back through its symlinks
7+
8+import std/[os, strutils, macros]
9+
10+const javaHomeDefine {.strdefine: "javaHome".} = ""
11+
12+proc discoverJavaHome(): string {.compileTime.} =
13+ if javaHomeDefine.len > 0:
14+ return javaHomeDefine
15+
16+ let fromEnv = getEnv("JAVA_HOME")
17+ if fromEnv.len > 0:
18+ return fromEnv
19+
20+ # `javac` usually lives at <home>/bin/javac, often behind a chain of symlinks.
21+ let javac = staticExec("readlink -f \"$(command -v javac)\" 2>/dev/null").strip()
22+ if javac.len > 0:
23+ return javac.parentDir.parentDir
24+
25+ error("libjava: cannot locate a JDK. Set JAVA_HOME, or pass -d:javaHome=/path/to/jdk")
26+
27+const
28+ javaHome* = discoverJavaHome()
29+
30+ osIncludeDir* =
31+ when defined(windows): "win32"
32+ elif defined(macosx): "darwin"
33+ elif defined(freebsd): "freebsd"
34+ elif defined(openbsd): "openbsd"
35+ else: "linux"
36+
37+ ## Where libjvm lives inside the JDK. Modern JDKs (9+) use lib/server;
38+ ## JDK 8 buried it one level deeper under jre/.
39+ jvmLibDir* =
40+ when defined(macosx):
41+ javaHome & "/lib/server"
42+ elif defined(windows):
43+ javaHome & "/bin/server"
44+ else:
45+ javaHome & "/lib/server"
46+
47+{.passC: "-I" & javaHome & "/include".}
48+{.passC: "-I" & javaHome & "/include/" & osIncludeDir.}
49+
50+when defined(libjavaNoLink):
51+ # For callers that load libjvm themselves, or that are themselves loaded
52+ # *by* a JVM (JNI_OnLoad style) and so already have the symbols.
53+ discard
54+else:
55+ {.passL: "-L" & jvmLibDir & " -ljvm".}
56+ when not defined(windows):
57+ # Let the binary find libjvm.so without LD_LIBRARY_PATH.
58+ {.passL: "-Wl,-rpath," & jvmLibDir.}
new file mode 100644
@@ -0,0 +1,58 @@
1+## Compile-time discovery of the JDK, and the C flags needed to link libjvm.
2+##
3+## Resolution order for the JDK home:
4+## 1. `-d:javaHome=/path/to/jdk` passed to the Nim compiler
5+## 2. the `JAVA_HOME` environment variable at compile time
6+## 3. following `javac` on PATH back through its symlinks
7+
8+import std/[os, strutils, macros]
9+
10+const javaHomeDefine {.strdefine: "javaHome".} = ""
11+
12+proc discoverJavaHome(): string {.compileTime.} =
13+ if javaHomeDefine.len > 0:
14+ return javaHomeDefine
15+
16+ let fromEnv = getEnv("JAVA_HOME")
17+ if fromEnv.len > 0:
18+ return fromEnv
19+
20+ # `javac` usually lives at <home>/bin/javac, often behind a chain of symlinks.
21+ let javac = staticExec("readlink -f \"$(command -v javac)\" 2>/dev/null").strip()
22+ if javac.len > 0:
23+ return javac.parentDir.parentDir
24+
25+ error("libjava: cannot locate a JDK. Set JAVA_HOME, or pass -d:javaHome=/path/to/jdk")
26+
27+const
28+ javaHome* = discoverJavaHome()
29+
30+ osIncludeDir* =
31+ when defined(windows): "win32"
32+ elif defined(macosx): "darwin"
33+ elif defined(freebsd): "freebsd"
34+ elif defined(openbsd): "openbsd"
35+ else: "linux"
36+
37+ ## Where libjvm lives inside the JDK. Modern JDKs (9+) use lib/server;
38+ ## JDK 8 buried it one level deeper under jre/.
39+ jvmLibDir* =
40+ when defined(macosx):
41+ javaHome & "/lib/server"
42+ elif defined(windows):
43+ javaHome & "/bin/server"
44+ else:
45+ javaHome & "/lib/server"
46+
47+{.passC: "-I" & javaHome & "/include".}
48+{.passC: "-I" & javaHome & "/include/" & osIncludeDir.}
49+
50+when defined(libjavaNoLink):
51+ # For callers that load libjvm themselves, or that are themselves loaded
52+ # *by* a JVM (JNI_OnLoad style) and so already have the symbols.
53+ discard
54+else:
55+ {.passL: "-L" & jvmLibDir & " -ljvm".}
56+ when not defined(windows):
57+ # Let the binary find libjvm.so without LD_LIBRARY_PATH.
58+ {.passL: "-Wl,-rpath," & jvmLibDir.}
added src/libjava/core.nim +418 -0
new file mode 100644
@@ -0,0 +1,418 @@
1+## Ergonomics on top of the raw JNI bindings: UTF-16-correct strings, Java
2+## exceptions surfaced as Nim exceptions, and typed method dispatch.
3+
4+import std/[strutils]
5+import ./jni
6+
7+type
8+ JavaError* = object of CatchableError
9+ ## A Java throwable that crossed the boundary. The Java-side exception is
10+ ## always cleared before this is raised -- a pending exception makes almost
11+ ## every subsequent JNI call undefined behaviour.
12+ className*: string
13+ javaMessage*: string
14+ stackTrace*: string
15+
16+ JavaVMError* = object of CatchableError
17+ ## The JVM itself misbehaved: failed to start, bad version, attach failed.
18+
19+# --------------------------------------------------------------------------
20+# UTF-16 <-> UTF-8
21+#
22+# Deliberately *not* using GetStringUTFChars: that hands back "modified UTF-8",
23+# which encodes supplementary characters as two 3-byte surrogates and NUL as
24+# 0xC0 0x80. Neither is valid UTF-8, so an emoji round-tripped through it comes
25+# out mangled. Converting from UTF-16 ourselves is the only correct route.
26+# --------------------------------------------------------------------------
27+
28+proc utf16ToUtf8(src: ptr UncheckedArray[JChar], n: int): string =
29+ result = newStringOfCap(n * 3)
30+ var i = 0
31+ while i < n:
32+ var cp = uint32(src[i])
33+ inc i
34+ if cp >= 0xD800'u32 and cp <= 0xDBFF'u32 and i < n:
35+ let lo = uint32(src[i])
36+ if lo >= 0xDC00'u32 and lo <= 0xDFFF'u32:
37+ cp = 0x10000'u32 + ((cp - 0xD800'u32) shl 10) + (lo - 0xDC00'u32)
38+ inc i
39+ if cp < 0x80'u32:
40+ result.add char(cp)
41+ elif cp < 0x800'u32:
42+ result.add char(0xC0'u32 or (cp shr 6))
43+ result.add char(0x80'u32 or (cp and 0x3F'u32))
44+ elif cp < 0x10000'u32:
45+ result.add char(0xE0'u32 or (cp shr 12))
46+ result.add char(0x80'u32 or ((cp shr 6) and 0x3F'u32))
47+ result.add char(0x80'u32 or (cp and 0x3F'u32))
48+ else:
49+ result.add char(0xF0'u32 or (cp shr 18))
50+ result.add char(0x80'u32 or ((cp shr 12) and 0x3F'u32))
51+ result.add char(0x80'u32 or ((cp shr 6) and 0x3F'u32))
52+ result.add char(0x80'u32 or (cp and 0x3F'u32))
53+
54+proc utf8ToUtf16(s: string): seq[JChar] =
55+ result = newSeqOfCap[JChar](s.len)
56+ var i = 0
57+ while i < s.len:
58+ let b0 = uint32(uint8(s[i]))
59+ var cp: uint32
60+ var width: int
61+ if b0 < 0x80'u32: (cp, width) = (b0, 1)
62+ elif b0 < 0xE0'u32: (cp, width) = (b0 and 0x1F'u32, 2)
63+ elif b0 < 0xF0'u32: (cp, width) = (b0 and 0x0F'u32, 3)
64+ else: (cp, width) = (b0 and 0x07'u32, 4)
65+ if i + width > s.len:
66+ cp = 0xFFFD'u32
67+ width = s.len - i
68+ else:
69+ for k in 1 ..< width:
70+ cp = (cp shl 6) or (uint32(uint8(s[i + k])) and 0x3F'u32)
71+ i += width
72+ if cp >= 0x10000'u32:
73+ let v = cp - 0x10000'u32
74+ result.add JChar(0xD800'u32 + (v shr 10))
75+ result.add JChar(0xDC00'u32 + (v and 0x3FF'u32))
76+ else:
77+ result.add JChar(cp)
78+
79+# --------------------------------------------------------------------------
80+# Strings
81+# --------------------------------------------------------------------------
82+
83+proc newJString*(env: JNIEnv, s: string): JStringRef =
84+ ## Builds a java.lang.String. Returns a *local* reference.
85+ var u16 = utf8ToUtf16(s)
86+ if u16.len == 0:
87+ result = env[].NewString(env, nil, 0)
88+ else:
89+ result = env[].NewString(env, addr u16[0], JSize(u16.len))
90+
91+proc toNimString*(env: JNIEnv, s: JStringRef): string =
92+ if s == nil: return ""
93+ let n = int env[].GetStringLength(env, s)
94+ let chars = env[].GetStringChars(env, s, nil)
95+ if chars == nil: return ""
96+ result = utf16ToUtf8(cast[ptr UncheckedArray[JChar]](chars), n)
97+ env[].ReleaseStringChars(env, s, chars)
98+
99+# --------------------------------------------------------------------------
100+# References
101+# --------------------------------------------------------------------------
102+
103+proc newGlobalRef*(env: JNIEnv, obj: JObjectRef): JObjectRef {.inline.} =
104+ env[].NewGlobalRef(env, obj)
105+
106+proc deleteGlobalRef*(env: JNIEnv, obj: JObjectRef) {.inline.} =
107+ if obj != nil: env[].DeleteGlobalRef(env, obj)
108+
109+proc deleteLocalRef*(env: JNIEnv, obj: JObjectRef) {.inline.} =
110+ if obj != nil: env[].DeleteLocalRef(env, obj)
111+
112+template withLocalFrame*(env: JNIEnv, capacity: int, body: untyped) =
113+ ## Every local reference created inside `body` is released on exit. Use this
114+ ## around loops -- local refs are *not* freed until the native frame returns,
115+ ## so a long-running loop without one will exhaust the local ref table.
116+ if env[].PushLocalFrame(env, JInt(capacity)) != JNI_OK:
117+ raise newException(JavaVMError, "PushLocalFrame failed (out of memory?)")
118+ try:
119+ body
120+ finally:
121+ discard env[].PopLocalFrame(env, nil)
122+
123+# --------------------------------------------------------------------------
124+# Exceptions
125+# --------------------------------------------------------------------------
126+
127+proc callStringMethod(env: JNIEnv, obj: JObjectRef, cls: JClassRef,
128+ name: string): string =
129+ ## Minimal no-exception-checking helper, used only while building an error
130+ ## report. Anything that goes wrong here yields "" rather than recursing.
131+ let m = env[].GetMethodID(env, cls, name.cstring, "()Ljava/lang/String;")
132+ if m == nil:
133+ env[].ExceptionClear(env)
134+ return ""
135+ let s = env[].CallObjectMethodA(env, obj, m, nil)
136+ if env[].ExceptionCheck(env) != JNI_FALSE:
137+ env[].ExceptionClear(env)
138+ return ""
139+ result = env.toNimString(s)
140+ env.deleteLocalRef(s)
141+
142+proc stackTraceOf(env: JNIEnv, throwable: JThrowableRef): string =
143+ ## throwable.printStackTrace(new PrintWriter(new StringWriter())).toString()
144+ let swCls = env[].FindClass(env, "java/io/StringWriter")
145+ let pwCls = env[].FindClass(env, "java/io/PrintWriter")
146+ let thCls = env[].FindClass(env, "java/lang/Throwable")
147+ if swCls == nil or pwCls == nil or thCls == nil:
148+ env[].ExceptionClear(env)
149+ return ""
150+
151+ let swInit = env[].GetMethodID(env, swCls, "<init>", "()V")
152+ let pwInit = env[].GetMethodID(env, pwCls, "<init>", "(Ljava/io/Writer;)V")
153+ let pst = env[].GetMethodID(env, thCls, "printStackTrace", "(Ljava/io/PrintWriter;)V")
154+ if swInit == nil or pwInit == nil or pst == nil:
155+ env[].ExceptionClear(env)
156+ return ""
157+
158+ let sw = env[].NewObjectA(env, swCls, swInit, nil)
159+ var a: array[1, JValue]
160+ a[0].l = sw
161+ let pw = env[].NewObjectA(env, pwCls, pwInit, addr a[0])
162+ a[0].l = pw
163+ env[].CallVoidMethodA(env, throwable, pst, addr a[0])
164+ if env[].ExceptionCheck(env) != JNI_FALSE:
165+ env[].ExceptionClear(env)
166+ else:
167+ result = env.callStringMethod(sw, swCls, "toString").strip(leading = false)
168+
169+ env.deleteLocalRef(pw)
170+ env.deleteLocalRef(sw)
171+
172+proc checkException*(env: JNIEnv) =
173+ ## Raises `JavaError` if a Java exception is pending, clearing it first.
174+ ## Call this after *every* JNI operation that can throw -- a Java exception
175+ ## does not unwind the C stack, it just sits there poisoning the thread.
176+ if env[].ExceptionCheck(env) == JNI_FALSE:
177+ return
178+
179+ let throwable = env[].ExceptionOccurred(env)
180+ env[].ExceptionClear(env)
181+ if throwable == nil:
182+ raise newException(JavaError, "unknown Java exception")
183+
184+ let
185+ thCls = env[].GetObjectClass(env, throwable)
186+ clsCls = env[].FindClass(env, "java/lang/Class")
187+ name = if clsCls != nil: env.callStringMethod(thCls, clsCls, "getName") else: ""
188+ msg = env.callStringMethod(throwable, thCls, "getMessage")
189+ trace = env.stackTraceOf(throwable)
190+
191+ var e = newException(JavaError,
192+ (if name.len > 0: name else: "java.lang.Throwable") &
193+ (if msg.len > 0: ": " & msg else: ""))
194+ e.className = name
195+ e.javaMessage = msg
196+ e.stackTrace = trace
197+
198+ env.deleteLocalRef(clsCls)
199+ env.deleteLocalRef(thCls)
200+ env.deleteLocalRef(throwable)
201+ raise e
202+
203+proc throwJava*(env: JNIEnv, class, message: string) =
204+ ## Raise an exception on the *Java* side. Useful from a native method
205+ ## registered with `RegisterNatives`.
206+ let cls = env[].FindClass(env, class.cstring)
207+ if cls == nil:
208+ env[].ExceptionClear(env)
209+ env[].FatalError(env, ("libjava: no such exception class: " & class).cstring)
210+ discard env[].ThrowNew(env, cls, message.cstring)
211+
212+# --------------------------------------------------------------------------
213+# Arguments
214+# --------------------------------------------------------------------------
215+
216+type
217+ JArgKind* = enum
218+ akValue, akString
219+
220+ JArg* = object
221+ ## An argument on its way to Java. Strings are kept as Nim strings and
222+ ## only turned into java.lang.String at call time, when an env is in hand.
223+ case kind*: JArgKind
224+ of akValue: val*: JValue
225+ of akString: str*: string
226+
227+proc toJArg*(x: bool): JArg =
228+ JArg(kind: akValue, val: JValue(z: if x: JNI_TRUE else: JNI_FALSE))
229+proc toJArg*(x: int8): JArg = JArg(kind: akValue, val: JValue(b: x))
230+proc toJArg*(x: int16): JArg = JArg(kind: akValue, val: JValue(s: x))
231+proc toJArg*(x: int32): JArg = JArg(kind: akValue, val: JValue(i: x))
232+proc toJArg*(x: int64): JArg = JArg(kind: akValue, val: JValue(j: x))
233+proc toJArg*(x: int): JArg = JArg(kind: akValue, val: JValue(i: JInt(x)))
234+ ## Nim's default int maps to Java `int`; use an explicit `int64` for `long`.
235+proc toJArg*(x: uint16): JArg = JArg(kind: akValue, val: JValue(c: x))
236+proc toJArg*(x: float32): JArg = JArg(kind: akValue, val: JValue(f: x))
237+proc toJArg*(x: float64): JArg = JArg(kind: akValue, val: JValue(d: x))
238+proc toJArg*(x: string): JArg = JArg(kind: akString, str: x)
239+proc toJArg*(x: JObjectRef): JArg = JArg(kind: akValue, val: JValue(l: x))
240+proc toJArg*(x: JArg): JArg = x
241+
242+proc asLong*(x: int64): JArg = JArg(kind: akValue, val: JValue(j: x))
243+proc asByte*(x: int): JArg = JArg(kind: akValue, val: JValue(b: JByte(x)))
244+proc asShort*(x: int): JArg = JArg(kind: akValue, val: JValue(s: JShort(x)))
245+proc asChar*(x: char): JArg = JArg(kind: akValue, val: JValue(c: JChar(ord x)))
246+proc asFloat*(x: float): JArg = JArg(kind: akValue, val: JValue(f: JFloat(x)))
247+
248+proc materialize(env: JNIEnv, args: openArray[JArg],
249+ temps: var seq[JObjectRef]): seq[JValue] =
250+ result = newSeq[JValue](args.len)
251+ for i, a in args:
252+ case a.kind
253+ of akValue:
254+ result[i] = a.val
255+ of akString:
256+ let s = env.newJString(a.str)
257+ temps.add s
258+ result[i] = JValue(l: s)
259+
260+template argPtr(vals: seq[JValue]): ptr JValue =
261+ if vals.len == 0: nil else: unsafeAddr vals[0]
262+
263+# --------------------------------------------------------------------------
264+# Lookup
265+# --------------------------------------------------------------------------
266+
267+proc findClass*(env: JNIEnv, name: string): JClassRef =
268+ ## `name` is a JVM binary name: "java/lang/String", "com/x/Outer$Inner".
269+ ## Returns a local reference; wrap in a global ref to keep it across calls.
270+ result = env[].FindClass(env, name.replace('.', '/').cstring)
271+ env.checkException()
272+ if result == nil:
273+ raise newException(JavaError, "class not found: " & name)
274+
275+proc methodID*(env: JNIEnv, cls: JClassRef, name, sig: string): JMethodID =
276+ result = env[].GetMethodID(env, cls, name.cstring, sig.cstring)
277+ env.checkException()
278+ if result == nil:
279+ raise newException(JavaError, "no such method: " & name & sig)
280+
281+proc staticMethodID*(env: JNIEnv, cls: JClassRef, name, sig: string): JMethodID =
282+ result = env[].GetStaticMethodID(env, cls, name.cstring, sig.cstring)
283+ env.checkException()
284+ if result == nil:
285+ raise newException(JavaError, "no such static method: " & name & sig)
286+
287+proc fieldID*(env: JNIEnv, cls: JClassRef, name, sig: string): JFieldID =
288+ result = env[].GetFieldID(env, cls, name.cstring, sig.cstring)
289+ env.checkException()
290+ if result == nil:
291+ raise newException(JavaError, "no such field: " & name & ":" & sig)
292+
293+proc staticFieldID*(env: JNIEnv, cls: JClassRef, name, sig: string): JFieldID =
294+ result = env[].GetStaticFieldID(env, cls, name.cstring, sig.cstring)
295+ env.checkException()
296+ if result == nil:
297+ raise newException(JavaError, "no such static field: " & name & ":" & sig)
298+
299+# --------------------------------------------------------------------------
300+# Invocation
301+#
302+# `T` selects the JNI call variant, so it must match the signature's return
303+# type. `string` is sugar for an object call plus a conversion.
304+# --------------------------------------------------------------------------
305+
306+proc callStatic*[T](env: JNIEnv, cls: JClassRef, name, sig: string,
307+ args: varargs[JArg, toJArg]): T =
308+ let m = env.staticMethodID(cls, name, sig)
309+ var temps: seq[JObjectRef]
310+ let vals = env.materialize(args, temps)
311+ let p = argPtr(vals)
312+ try:
313+ when T is void: env[].CallStaticVoidMethodA(env, cls, m, p)
314+ elif T is bool: result = env[].CallStaticBooleanMethodA(env, cls, m, p) != JNI_FALSE
315+ elif T is JByte: result = env[].CallStaticByteMethodA(env, cls, m, p)
316+ elif T is JChar: result = env[].CallStaticCharMethodA(env, cls, m, p)
317+ elif T is JShort: result = env[].CallStaticShortMethodA(env, cls, m, p)
318+ elif T is JInt: result = env[].CallStaticIntMethodA(env, cls, m, p)
319+ elif T is JLong: result = env[].CallStaticLongMethodA(env, cls, m, p)
320+ elif T is JFloat: result = env[].CallStaticFloatMethodA(env, cls, m, p)
321+ elif T is JDouble: result = env[].CallStaticDoubleMethodA(env, cls, m, p)
322+ elif T is string:
323+ let s = env[].CallStaticObjectMethodA(env, cls, m, p)
324+ env.checkException()
325+ result = env.toNimString(s)
326+ env.deleteLocalRef(s)
327+ elif T is JObjectRef: result = env[].CallStaticObjectMethodA(env, cls, m, p)
328+ else: {.error: "libjava: unsupported Java return type " & $T.}
329+ env.checkException()
330+ finally:
331+ for t in temps: env.deleteLocalRef(t)
332+
333+proc call*[T](env: JNIEnv, obj: JObjectRef, cls: JClassRef, name, sig: string,
334+ args: varargs[JArg, toJArg]): T =
335+ let m = env.methodID(cls, name, sig)
336+ var temps: seq[JObjectRef]
337+ let vals = env.materialize(args, temps)
338+ let p = argPtr(vals)
339+ try:
340+ when T is void: env[].CallVoidMethodA(env, obj, m, p)
341+ elif T is bool: result = env[].CallBooleanMethodA(env, obj, m, p) != JNI_FALSE
342+ elif T is JByte: result = env[].CallByteMethodA(env, obj, m, p)
343+ elif T is JChar: result = env[].CallCharMethodA(env, obj, m, p)
344+ elif T is JShort: result = env[].CallShortMethodA(env, obj, m, p)
345+ elif T is JInt: result = env[].CallIntMethodA(env, obj, m, p)
346+ elif T is JLong: result = env[].CallLongMethodA(env, obj, m, p)
347+ elif T is JFloat: result = env[].CallFloatMethodA(env, obj, m, p)
348+ elif T is JDouble: result = env[].CallDoubleMethodA(env, obj, m, p)
349+ elif T is string:
350+ let s = env[].CallObjectMethodA(env, obj, m, p)
351+ env.checkException()
352+ result = env.toNimString(s)
353+ env.deleteLocalRef(s)
354+ elif T is JObjectRef: result = env[].CallObjectMethodA(env, obj, m, p)
355+ else: {.error: "libjava: unsupported Java return type " & $T.}
356+ env.checkException()
357+ finally:
358+ for t in temps: env.deleteLocalRef(t)
359+
360+proc call*[T](env: JNIEnv, obj: JObjectRef, name, sig: string,
361+ args: varargs[JArg, toJArg]): T =
362+ ## Same, resolving the method against the object's runtime class.
363+ let cls = env[].GetObjectClass(env, obj)
364+ try:
365+ result = call[T](env, obj, cls, name, sig, args)
366+ finally:
367+ env.deleteLocalRef(cls)
368+
369+proc newObject*(env: JNIEnv, cls: JClassRef, sig: string,
370+ args: varargs[JArg, toJArg]): JObjectRef =
371+ ## Constructor call. `sig` is the `<init>` descriptor, e.g. "(Ljava/lang/String;)V".
372+ let m = env.methodID(cls, "<init>", sig)
373+ var temps: seq[JObjectRef]
374+ let vals = env.materialize(args, temps)
375+ try:
376+ result = env[].NewObjectA(env, cls, m, argPtr(vals))
377+ env.checkException()
378+ finally:
379+ for t in temps: env.deleteLocalRef(t)
380+
381+proc toStringOf*(env: JNIEnv, obj: JObjectRef): string =
382+ ## obj.toString(), the universal debugging hammer.
383+ if obj == nil: return "null"
384+ call[string](env, obj, "toString", "()Ljava/lang/String;")
385+
386+# --------------------------------------------------------------------------
387+# Signature helper
388+# --------------------------------------------------------------------------
389+
390+proc descriptorOf*(javaType: string): string =
391+ ## "int" -> "I", "String" -> "Ljava/lang/String;", "byte[]" -> "[B".
392+ var t = javaType.strip()
393+ var dims = 0
394+ while t.endsWith("[]"):
395+ inc dims
396+ t = t[0 ..< t.len - 2].strip()
397+ let base = case t
398+ of "void": "V"
399+ of "boolean": "Z"
400+ of "byte": "B"
401+ of "char": "C"
402+ of "short": "S"
403+ of "int": "I"
404+ of "long": "J"
405+ of "float": "F"
406+ of "double": "D"
407+ of "String": "Ljava/lang/String;"
408+ of "Object": "Ljava/lang/Object;"
409+ else: "L" & t.replace('.', '/') & ";"
410+ "[".repeat(dims) & base
411+
412+proc signature*(params: openArray[string], returns: string): string =
413+ ## `signature(["int", "String"], "void")` == `"(ILjava/lang/String;)V"`.
414+ ## When in doubt, `javap -s YourClass` prints the real thing.
415+ result = "("
416+ for p in params: result.add descriptorOf(p)
417+ result.add ")"
418+ result.add descriptorOf(returns)
new file mode 100644
@@ -0,0 +1,418 @@
1+## Ergonomics on top of the raw JNI bindings: UTF-16-correct strings, Java
2+## exceptions surfaced as Nim exceptions, and typed method dispatch.
3+
4+import std/[strutils]
5+import ./jni
6+
7+type
8+ JavaError* = object of CatchableError
9+ ## A Java throwable that crossed the boundary. The Java-side exception is
10+ ## always cleared before this is raised -- a pending exception makes almost
11+ ## every subsequent JNI call undefined behaviour.
12+ className*: string
13+ javaMessage*: string
14+ stackTrace*: string
15+
16+ JavaVMError* = object of CatchableError
17+ ## The JVM itself misbehaved: failed to start, bad version, attach failed.
18+
19+# --------------------------------------------------------------------------
20+# UTF-16 <-> UTF-8
21+#
22+# Deliberately *not* using GetStringUTFChars: that hands back "modified UTF-8",
23+# which encodes supplementary characters as two 3-byte surrogates and NUL as
24+# 0xC0 0x80. Neither is valid UTF-8, so an emoji round-tripped through it comes
25+# out mangled. Converting from UTF-16 ourselves is the only correct route.
26+# --------------------------------------------------------------------------
27+
28+proc utf16ToUtf8(src: ptr UncheckedArray[JChar], n: int): string =
29+ result = newStringOfCap(n * 3)
30+ var i = 0
31+ while i < n:
32+ var cp = uint32(src[i])
33+ inc i
34+ if cp >= 0xD800'u32 and cp <= 0xDBFF'u32 and i < n:
35+ let lo = uint32(src[i])
36+ if lo >= 0xDC00'u32 and lo <= 0xDFFF'u32:
37+ cp = 0x10000'u32 + ((cp - 0xD800'u32) shl 10) + (lo - 0xDC00'u32)
38+ inc i
39+ if cp < 0x80'u32:
40+ result.add char(cp)
41+ elif cp < 0x800'u32:
42+ result.add char(0xC0'u32 or (cp shr 6))
43+ result.add char(0x80'u32 or (cp and 0x3F'u32))
44+ elif cp < 0x10000'u32:
45+ result.add char(0xE0'u32 or (cp shr 12))
46+ result.add char(0x80'u32 or ((cp shr 6) and 0x3F'u32))
47+ result.add char(0x80'u32 or (cp and 0x3F'u32))
48+ else:
49+ result.add char(0xF0'u32 or (cp shr 18))
50+ result.add char(0x80'u32 or ((cp shr 12) and 0x3F'u32))
51+ result.add char(0x80'u32 or ((cp shr 6) and 0x3F'u32))
52+ result.add char(0x80'u32 or (cp and 0x3F'u32))
53+
54+proc utf8ToUtf16(s: string): seq[JChar] =
55+ result = newSeqOfCap[JChar](s.len)
56+ var i = 0
57+ while i < s.len:
58+ let b0 = uint32(uint8(s[i]))
59+ var cp: uint32
60+ var width: int
61+ if b0 < 0x80'u32: (cp, width) = (b0, 1)
62+ elif b0 < 0xE0'u32: (cp, width) = (b0 and 0x1F'u32, 2)
63+ elif b0 < 0xF0'u32: (cp, width) = (b0 and 0x0F'u32, 3)
64+ else: (cp, width) = (b0 and 0x07'u32, 4)
65+ if i + width > s.len:
66+ cp = 0xFFFD'u32
67+ width = s.len - i
68+ else:
69+ for k in 1 ..< width:
70+ cp = (cp shl 6) or (uint32(uint8(s[i + k])) and 0x3F'u32)
71+ i += width
72+ if cp >= 0x10000'u32:
73+ let v = cp - 0x10000'u32
74+ result.add JChar(0xD800'u32 + (v shr 10))
75+ result.add JChar(0xDC00'u32 + (v and 0x3FF'u32))
76+ else:
77+ result.add JChar(cp)
78+
79+# --------------------------------------------------------------------------
80+# Strings
81+# --------------------------------------------------------------------------
82+
83+proc newJString*(env: JNIEnv, s: string): JStringRef =
84+ ## Builds a java.lang.String. Returns a *local* reference.
85+ var u16 = utf8ToUtf16(s)
86+ if u16.len == 0:
87+ result = env[].NewString(env, nil, 0)
88+ else:
89+ result = env[].NewString(env, addr u16[0], JSize(u16.len))
90+
91+proc toNimString*(env: JNIEnv, s: JStringRef): string =
92+ if s == nil: return ""
93+ let n = int env[].GetStringLength(env, s)
94+ let chars = env[].GetStringChars(env, s, nil)
95+ if chars == nil: return ""
96+ result = utf16ToUtf8(cast[ptr UncheckedArray[JChar]](chars), n)
97+ env[].ReleaseStringChars(env, s, chars)
98+
99+# --------------------------------------------------------------------------
100+# References
101+# --------------------------------------------------------------------------
102+
103+proc newGlobalRef*(env: JNIEnv, obj: JObjectRef): JObjectRef {.inline.} =
104+ env[].NewGlobalRef(env, obj)
105+
106+proc deleteGlobalRef*(env: JNIEnv, obj: JObjectRef) {.inline.} =
107+ if obj != nil: env[].DeleteGlobalRef(env, obj)
108+
109+proc deleteLocalRef*(env: JNIEnv, obj: JObjectRef) {.inline.} =
110+ if obj != nil: env[].DeleteLocalRef(env, obj)
111+
112+template withLocalFrame*(env: JNIEnv, capacity: int, body: untyped) =
113+ ## Every local reference created inside `body` is released on exit. Use this
114+ ## around loops -- local refs are *not* freed until the native frame returns,
115+ ## so a long-running loop without one will exhaust the local ref table.
116+ if env[].PushLocalFrame(env, JInt(capacity)) != JNI_OK:
117+ raise newException(JavaVMError, "PushLocalFrame failed (out of memory?)")
118+ try:
119+ body
120+ finally:
121+ discard env[].PopLocalFrame(env, nil)
122+
123+# --------------------------------------------------------------------------
124+# Exceptions
125+# --------------------------------------------------------------------------
126+
127+proc callStringMethod(env: JNIEnv, obj: JObjectRef, cls: JClassRef,
128+ name: string): string =
129+ ## Minimal no-exception-checking helper, used only while building an error
130+ ## report. Anything that goes wrong here yields "" rather than recursing.
131+ let m = env[].GetMethodID(env, cls, name.cstring, "()Ljava/lang/String;")
132+ if m == nil:
133+ env[].ExceptionClear(env)
134+ return ""
135+ let s = env[].CallObjectMethodA(env, obj, m, nil)
136+ if env[].ExceptionCheck(env) != JNI_FALSE:
137+ env[].ExceptionClear(env)
138+ return ""
139+ result = env.toNimString(s)
140+ env.deleteLocalRef(s)
141+
142+proc stackTraceOf(env: JNIEnv, throwable: JThrowableRef): string =
143+ ## throwable.printStackTrace(new PrintWriter(new StringWriter())).toString()
144+ let swCls = env[].FindClass(env, "java/io/StringWriter")
145+ let pwCls = env[].FindClass(env, "java/io/PrintWriter")
146+ let thCls = env[].FindClass(env, "java/lang/Throwable")
147+ if swCls == nil or pwCls == nil or thCls == nil:
148+ env[].ExceptionClear(env)
149+ return ""
150+
151+ let swInit = env[].GetMethodID(env, swCls, "<init>", "()V")
152+ let pwInit = env[].GetMethodID(env, pwCls, "<init>", "(Ljava/io/Writer;)V")
153+ let pst = env[].GetMethodID(env, thCls, "printStackTrace", "(Ljava/io/PrintWriter;)V")
154+ if swInit == nil or pwInit == nil or pst == nil:
155+ env[].ExceptionClear(env)
156+ return ""
157+
158+ let sw = env[].NewObjectA(env, swCls, swInit, nil)
159+ var a: array[1, JValue]
160+ a[0].l = sw
161+ let pw = env[].NewObjectA(env, pwCls, pwInit, addr a[0])
162+ a[0].l = pw
163+ env[].CallVoidMethodA(env, throwable, pst, addr a[0])
164+ if env[].ExceptionCheck(env) != JNI_FALSE:
165+ env[].ExceptionClear(env)
166+ else:
167+ result = env.callStringMethod(sw, swCls, "toString").strip(leading = false)
168+
169+ env.deleteLocalRef(pw)
170+ env.deleteLocalRef(sw)
171+
172+proc checkException*(env: JNIEnv) =
173+ ## Raises `JavaError` if a Java exception is pending, clearing it first.
174+ ## Call this after *every* JNI operation that can throw -- a Java exception
175+ ## does not unwind the C stack, it just sits there poisoning the thread.
176+ if env[].ExceptionCheck(env) == JNI_FALSE:
177+ return
178+
179+ let throwable = env[].ExceptionOccurred(env)
180+ env[].ExceptionClear(env)
181+ if throwable == nil:
182+ raise newException(JavaError, "unknown Java exception")
183+
184+ let
185+ thCls = env[].GetObjectClass(env, throwable)
186+ clsCls = env[].FindClass(env, "java/lang/Class")
187+ name = if clsCls != nil: env.callStringMethod(thCls, clsCls, "getName") else: ""
188+ msg = env.callStringMethod(throwable, thCls, "getMessage")
189+ trace = env.stackTraceOf(throwable)
190+
191+ var e = newException(JavaError,
192+ (if name.len > 0: name else: "java.lang.Throwable") &
193+ (if msg.len > 0: ": " & msg else: ""))
194+ e.className = name
195+ e.javaMessage = msg
196+ e.stackTrace = trace
197+
198+ env.deleteLocalRef(clsCls)
199+ env.deleteLocalRef(thCls)
200+ env.deleteLocalRef(throwable)
201+ raise e
202+
203+proc throwJava*(env: JNIEnv, class, message: string) =
204+ ## Raise an exception on the *Java* side. Useful from a native method
205+ ## registered with `RegisterNatives`.
206+ let cls = env[].FindClass(env, class.cstring)
207+ if cls == nil:
208+ env[].ExceptionClear(env)
209+ env[].FatalError(env, ("libjava: no such exception class: " & class).cstring)
210+ discard env[].ThrowNew(env, cls, message.cstring)
211+
212+# --------------------------------------------------------------------------
213+# Arguments
214+# --------------------------------------------------------------------------
215+
216+type
217+ JArgKind* = enum
218+ akValue, akString
219+
220+ JArg* = object
221+ ## An argument on its way to Java. Strings are kept as Nim strings and
222+ ## only turned into java.lang.String at call time, when an env is in hand.
223+ case kind*: JArgKind
224+ of akValue: val*: JValue
225+ of akString: str*: string
226+
227+proc toJArg*(x: bool): JArg =
228+ JArg(kind: akValue, val: JValue(z: if x: JNI_TRUE else: JNI_FALSE))
229+proc toJArg*(x: int8): JArg = JArg(kind: akValue, val: JValue(b: x))
230+proc toJArg*(x: int16): JArg = JArg(kind: akValue, val: JValue(s: x))
231+proc toJArg*(x: int32): JArg = JArg(kind: akValue, val: JValue(i: x))
232+proc toJArg*(x: int64): JArg = JArg(kind: akValue, val: JValue(j: x))
233+proc toJArg*(x: int): JArg = JArg(kind: akValue, val: JValue(i: JInt(x)))
234+ ## Nim's default int maps to Java `int`; use an explicit `int64` for `long`.
235+proc toJArg*(x: uint16): JArg = JArg(kind: akValue, val: JValue(c: x))
236+proc toJArg*(x: float32): JArg = JArg(kind: akValue, val: JValue(f: x))
237+proc toJArg*(x: float64): JArg = JArg(kind: akValue, val: JValue(d: x))
238+proc toJArg*(x: string): JArg = JArg(kind: akString, str: x)
239+proc toJArg*(x: JObjectRef): JArg = JArg(kind: akValue, val: JValue(l: x))
240+proc toJArg*(x: JArg): JArg = x
241+
242+proc asLong*(x: int64): JArg = JArg(kind: akValue, val: JValue(j: x))
243+proc asByte*(x: int): JArg = JArg(kind: akValue, val: JValue(b: JByte(x)))
244+proc asShort*(x: int): JArg = JArg(kind: akValue, val: JValue(s: JShort(x)))
245+proc asChar*(x: char): JArg = JArg(kind: akValue, val: JValue(c: JChar(ord x)))
246+proc asFloat*(x: float): JArg = JArg(kind: akValue, val: JValue(f: JFloat(x)))
247+
248+proc materialize(env: JNIEnv, args: openArray[JArg],
249+ temps: var seq[JObjectRef]): seq[JValue] =
250+ result = newSeq[JValue](args.len)
251+ for i, a in args:
252+ case a.kind
253+ of akValue:
254+ result[i] = a.val
255+ of akString:
256+ let s = env.newJString(a.str)
257+ temps.add s
258+ result[i] = JValue(l: s)
259+
260+template argPtr(vals: seq[JValue]): ptr JValue =
261+ if vals.len == 0: nil else: unsafeAddr vals[0]
262+
263+# --------------------------------------------------------------------------
264+# Lookup
265+# --------------------------------------------------------------------------
266+
267+proc findClass*(env: JNIEnv, name: string): JClassRef =
268+ ## `name` is a JVM binary name: "java/lang/String", "com/x/Outer$Inner".
269+ ## Returns a local reference; wrap in a global ref to keep it across calls.
270+ result = env[].FindClass(env, name.replace('.', '/').cstring)
271+ env.checkException()
272+ if result == nil:
273+ raise newException(JavaError, "class not found: " & name)
274+
275+proc methodID*(env: JNIEnv, cls: JClassRef, name, sig: string): JMethodID =
276+ result = env[].GetMethodID(env, cls, name.cstring, sig.cstring)
277+ env.checkException()
278+ if result == nil:
279+ raise newException(JavaError, "no such method: " & name & sig)
280+
281+proc staticMethodID*(env: JNIEnv, cls: JClassRef, name, sig: string): JMethodID =
282+ result = env[].GetStaticMethodID(env, cls, name.cstring, sig.cstring)
283+ env.checkException()
284+ if result == nil:
285+ raise newException(JavaError, "no such static method: " & name & sig)
286+
287+proc fieldID*(env: JNIEnv, cls: JClassRef, name, sig: string): JFieldID =
288+ result = env[].GetFieldID(env, cls, name.cstring, sig.cstring)
289+ env.checkException()
290+ if result == nil:
291+ raise newException(JavaError, "no such field: " & name & ":" & sig)
292+
293+proc staticFieldID*(env: JNIEnv, cls: JClassRef, name, sig: string): JFieldID =
294+ result = env[].GetStaticFieldID(env, cls, name.cstring, sig.cstring)
295+ env.checkException()
296+ if result == nil:
297+ raise newException(JavaError, "no such static field: " & name & ":" & sig)
298+
299+# --------------------------------------------------------------------------
300+# Invocation
301+#
302+# `T` selects the JNI call variant, so it must match the signature's return
303+# type. `string` is sugar for an object call plus a conversion.
304+# --------------------------------------------------------------------------
305+
306+proc callStatic*[T](env: JNIEnv, cls: JClassRef, name, sig: string,
307+ args: varargs[JArg, toJArg]): T =
308+ let m = env.staticMethodID(cls, name, sig)
309+ var temps: seq[JObjectRef]
310+ let vals = env.materialize(args, temps)
311+ let p = argPtr(vals)
312+ try:
313+ when T is void: env[].CallStaticVoidMethodA(env, cls, m, p)
314+ elif T is bool: result = env[].CallStaticBooleanMethodA(env, cls, m, p) != JNI_FALSE
315+ elif T is JByte: result = env[].CallStaticByteMethodA(env, cls, m, p)
316+ elif T is JChar: result = env[].CallStaticCharMethodA(env, cls, m, p)
317+ elif T is JShort: result = env[].CallStaticShortMethodA(env, cls, m, p)
318+ elif T is JInt: result = env[].CallStaticIntMethodA(env, cls, m, p)
319+ elif T is JLong: result = env[].CallStaticLongMethodA(env, cls, m, p)
320+ elif T is JFloat: result = env[].CallStaticFloatMethodA(env, cls, m, p)
321+ elif T is JDouble: result = env[].CallStaticDoubleMethodA(env, cls, m, p)
322+ elif T is string:
323+ let s = env[].CallStaticObjectMethodA(env, cls, m, p)
324+ env.checkException()
325+ result = env.toNimString(s)
326+ env.deleteLocalRef(s)
327+ elif T is JObjectRef: result = env[].CallStaticObjectMethodA(env, cls, m, p)
328+ else: {.error: "libjava: unsupported Java return type " & $T.}
329+ env.checkException()
330+ finally:
331+ for t in temps: env.deleteLocalRef(t)
332+
333+proc call*[T](env: JNIEnv, obj: JObjectRef, cls: JClassRef, name, sig: string,
334+ args: varargs[JArg, toJArg]): T =
335+ let m = env.methodID(cls, name, sig)
336+ var temps: seq[JObjectRef]
337+ let vals = env.materialize(args, temps)
338+ let p = argPtr(vals)
339+ try:
340+ when T is void: env[].CallVoidMethodA(env, obj, m, p)
341+ elif T is bool: result = env[].CallBooleanMethodA(env, obj, m, p) != JNI_FALSE
342+ elif T is JByte: result = env[].CallByteMethodA(env, obj, m, p)
343+ elif T is JChar: result = env[].CallCharMethodA(env, obj, m, p)
344+ elif T is JShort: result = env[].CallShortMethodA(env, obj, m, p)
345+ elif T is JInt: result = env[].CallIntMethodA(env, obj, m, p)
346+ elif T is JLong: result = env[].CallLongMethodA(env, obj, m, p)
347+ elif T is JFloat: result = env[].CallFloatMethodA(env, obj, m, p)
348+ elif T is JDouble: result = env[].CallDoubleMethodA(env, obj, m, p)
349+ elif T is string:
350+ let s = env[].CallObjectMethodA(env, obj, m, p)
351+ env.checkException()
352+ result = env.toNimString(s)
353+ env.deleteLocalRef(s)
354+ elif T is JObjectRef: result = env[].CallObjectMethodA(env, obj, m, p)
355+ else: {.error: "libjava: unsupported Java return type " & $T.}
356+ env.checkException()
357+ finally:
358+ for t in temps: env.deleteLocalRef(t)
359+
360+proc call*[T](env: JNIEnv, obj: JObjectRef, name, sig: string,
361+ args: varargs[JArg, toJArg]): T =
362+ ## Same, resolving the method against the object's runtime class.
363+ let cls = env[].GetObjectClass(env, obj)
364+ try:
365+ result = call[T](env, obj, cls, name, sig, args)
366+ finally:
367+ env.deleteLocalRef(cls)
368+
369+proc newObject*(env: JNIEnv, cls: JClassRef, sig: string,
370+ args: varargs[JArg, toJArg]): JObjectRef =
371+ ## Constructor call. `sig` is the `<init>` descriptor, e.g. "(Ljava/lang/String;)V".
372+ let m = env.methodID(cls, "<init>", sig)
373+ var temps: seq[JObjectRef]
374+ let vals = env.materialize(args, temps)
375+ try:
376+ result = env[].NewObjectA(env, cls, m, argPtr(vals))
377+ env.checkException()
378+ finally:
379+ for t in temps: env.deleteLocalRef(t)
380+
381+proc toStringOf*(env: JNIEnv, obj: JObjectRef): string =
382+ ## obj.toString(), the universal debugging hammer.
383+ if obj == nil: return "null"
384+ call[string](env, obj, "toString", "()Ljava/lang/String;")
385+
386+# --------------------------------------------------------------------------
387+# Signature helper
388+# --------------------------------------------------------------------------
389+
390+proc descriptorOf*(javaType: string): string =
391+ ## "int" -> "I", "String" -> "Ljava/lang/String;", "byte[]" -> "[B".
392+ var t = javaType.strip()
393+ var dims = 0
394+ while t.endsWith("[]"):
395+ inc dims
396+ t = t[0 ..< t.len - 2].strip()
397+ let base = case t
398+ of "void": "V"
399+ of "boolean": "Z"
400+ of "byte": "B"
401+ of "char": "C"
402+ of "short": "S"
403+ of "int": "I"
404+ of "long": "J"
405+ of "float": "F"
406+ of "double": "D"
407+ of "String": "Ljava/lang/String;"
408+ of "Object": "Ljava/lang/Object;"
409+ else: "L" & t.replace('.', '/') & ";"
410+ "[".repeat(dims) & base
411+
412+proc signature*(params: openArray[string], returns: string): string =
413+ ## `signature(["int", "String"], "void")` == `"(ILjava/lang/String;)V"`.
414+ ## When in doubt, `javap -s YourClass` prints the real thing.
415+ result = "("
416+ for p in params: result.add descriptorOf(p)
417+ result.add ")"
418+ result.add descriptorOf(returns)
added src/libjava/jni.nim +306 -0
new file mode 100644
@@ -0,0 +1,306 @@
1+## Raw JNI bindings.
2+##
3+## `JNIEnv` and `JavaVM` are, in C, pointers to tables of function pointers:
4+##
5+## typedef const struct JNINativeInterface_ *JNIEnv;
6+## typedef const struct JNIInvokeInterface_ *JavaVM;
7+##
8+## Both tables are declared here with `{.importc.}`, which means Nim never
9+## computes field offsets itself -- it emits `env->FindClass(env, name)` and
10+## lets the real <jni.h> decide the layout. That has two happy consequences:
11+## we only have to declare the fields we actually use, and the bindings cannot
12+## silently drift out of sync with the JDK they are compiled against.
13+##
14+## Only the `...A` call variants (taking `ptr JValue`) are bound. The variadic
15+## forms buy nothing here and lose type safety at the C boundary.
16+
17+import ./config
18+export config.javaHome
19+
20+{.pragma: jnih, importc, header: "jni.h".}
21+
22+# --------------------------------------------------------------------------
23+# Primitives
24+# --------------------------------------------------------------------------
25+
26+type
27+ JBoolean* = uint8
28+ JByte* = int8
29+ JChar* = uint16
30+ JShort* = int16
31+ JInt* = int32
32+ JLong* = int64
33+ JFloat* = cfloat
34+ JDouble* = cdouble
35+ JSize* = JInt
36+
37+const
38+ JNI_FALSE* = JBoolean(0)
39+ JNI_TRUE* = JBoolean(1)
40+
41+# In C every reference type is a `void*` typedef, so aliases are faithful here.
42+# The safety lives one layer up, in the `JObject` wrapper.
43+type
44+ JObjectRef* = pointer
45+ JClassRef* = JObjectRef
46+ JStringRef* = JObjectRef
47+ JArrayRef* = JObjectRef
48+ JThrowableRef* = JObjectRef
49+ JMethodID* = pointer
50+ JFieldID* = pointer
51+
52+type
53+ JValue* {.jnih, importc: "jvalue", union.} = object
54+ z*: JBoolean
55+ b*: JByte
56+ c*: JChar
57+ s*: JShort
58+ i*: JInt
59+ j*: JLong
60+ f*: JFloat
61+ d*: JDouble
62+ l*: JObjectRef
63+
64+# --------------------------------------------------------------------------
65+# Return codes and versions
66+# --------------------------------------------------------------------------
67+
68+const
69+ JNI_OK* = JInt(0)
70+ JNI_ERR* = JInt(-1)
71+ JNI_EDETACHED* = JInt(-2)
72+ JNI_EVERSION* = JInt(-3)
73+ JNI_ENOMEM* = JInt(-4)
74+ JNI_EEXIST* = JInt(-5)
75+ JNI_EINVAL* = JInt(-6)
76+
77+ JNI_VERSION_1_6* = JInt(0x00010006)
78+ JNI_VERSION_1_8* = JInt(0x00010008)
79+ JNI_VERSION_9* = JInt(0x00090000)
80+ JNI_VERSION_10* = JInt(0x000A0000)
81+ JNI_VERSION_19* = JInt(0x00130000)
82+ JNI_VERSION_20* = JInt(0x00140000)
83+ JNI_VERSION_21* = JInt(0x00150000)
84+
85+ JNI_COMMIT* = JInt(1)
86+ JNI_ABORT* = JInt(2)
87+
88+type
89+ ## Note the double indirection: C's `JNIEnv` is *itself* a pointer to the
90+ ## function table, and what you hold and pass around is a `JNIEnv *`.
91+ ## Hence `env[].FindClass(env, ...)`, which lowers to the idiomatic
92+ ## `(*env)->FindClass(env, ...)`.
93+ JNIEnv* = ptr ptr JNINativeInterface
94+ JavaVM* = ptr ptr JNIInvokeInterface
95+
96+ JNINativeMethod* {.jnih, importc: "JNINativeMethod".} = object
97+ name*: cstring
98+ signature*: cstring
99+ fnPtr*: pointer
100+
101+ # ------------------------------------------------------------------------
102+ # The invocation interface: JVM lifecycle and thread attachment.
103+ # ------------------------------------------------------------------------
104+ JNIInvokeInterface* {.jnih, importc: "const struct JNIInvokeInterface_", incompleteStruct.} = object
105+ DestroyJavaVM*: proc (vm: JavaVM): JInt {.cdecl, gcsafe, raises: [].}
106+ AttachCurrentThread*: proc (vm: JavaVM, penv: ptr pointer, args: pointer): JInt {.cdecl, gcsafe, raises: [].}
107+ DetachCurrentThread*: proc (vm: JavaVM): JInt {.cdecl, gcsafe, raises: [].}
108+ GetEnv*: proc (vm: JavaVM, penv: ptr pointer, version: JInt): JInt {.cdecl, gcsafe, raises: [].}
109+ AttachCurrentThreadAsDaemon*: proc (vm: JavaVM, penv: ptr pointer, args: pointer): JInt {.cdecl, gcsafe, raises: [].}
110+
111+ # ------------------------------------------------------------------------
112+ # The native interface: everything you do with a live JVM.
113+ # ------------------------------------------------------------------------
114+ JNINativeInterface* {.jnih, importc: "const struct JNINativeInterface_", incompleteStruct.} = object
115+ GetVersion*: proc (env: JNIEnv): JInt {.cdecl, gcsafe, raises: [].}
116+
117+ DefineClass*: proc (env: JNIEnv, name: cstring, loader: JObjectRef,
118+ buf: ptr JByte, len: JSize): JClassRef {.cdecl, gcsafe, raises: [].}
119+ FindClass*: proc (env: JNIEnv, name: cstring): JClassRef {.cdecl, gcsafe, raises: [].}
120+ GetSuperclass*: proc (env: JNIEnv, sub: JClassRef): JClassRef {.cdecl, gcsafe, raises: [].}
121+ IsAssignableFrom*: proc (env: JNIEnv, sub, sup: JClassRef): JBoolean {.cdecl, gcsafe, raises: [].}
122+
123+ # -- exceptions --------------------------------------------------------
124+ Throw*: proc (env: JNIEnv, obj: JThrowableRef): JInt {.cdecl, gcsafe, raises: [].}
125+ ThrowNew*: proc (env: JNIEnv, cls: JClassRef, msg: cstring): JInt {.cdecl, gcsafe, raises: [].}
126+ ExceptionOccurred*: proc (env: JNIEnv): JThrowableRef {.cdecl, gcsafe, raises: [].}
127+ ExceptionDescribe*: proc (env: JNIEnv) {.cdecl, gcsafe, raises: [].}
128+ ExceptionClear*: proc (env: JNIEnv) {.cdecl, gcsafe, raises: [].}
129+ FatalError*: proc (env: JNIEnv, msg: cstring) {.cdecl, gcsafe, raises: [].}
130+ ExceptionCheck*: proc (env: JNIEnv): JBoolean {.cdecl, gcsafe, raises: [].}
131+
132+ # -- references --------------------------------------------------------
133+ PushLocalFrame*: proc (env: JNIEnv, capacity: JInt): JInt {.cdecl, gcsafe, raises: [].}
134+ PopLocalFrame*: proc (env: JNIEnv, res: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
135+ NewGlobalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
136+ DeleteGlobalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
137+ DeleteLocalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
138+ NewLocalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
139+ EnsureLocalCapacity*: proc (env: JNIEnv, capacity: JInt): JInt {.cdecl, gcsafe, raises: [].}
140+ NewWeakGlobalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
141+ DeleteWeakGlobalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
142+ IsSameObject*: proc (env: JNIEnv, a, b: JObjectRef): JBoolean {.cdecl, gcsafe, raises: [].}
143+
144+ # -- objects -----------------------------------------------------------
145+ AllocObject*: proc (env: JNIEnv, cls: JClassRef): JObjectRef {.cdecl, gcsafe, raises: [].}
146+ NewObjectA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID,
147+ args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
148+ GetObjectClass*: proc (env: JNIEnv, obj: JObjectRef): JClassRef {.cdecl, gcsafe, raises: [].}
149+ IsInstanceOf*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef): JBoolean {.cdecl, gcsafe, raises: [].}
150+
151+ # -- instance methods --------------------------------------------------
152+ GetMethodID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JMethodID {.cdecl, gcsafe, raises: [].}
153+ CallObjectMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
154+ CallBooleanMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JBoolean {.cdecl, gcsafe, raises: [].}
155+ CallByteMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JByte {.cdecl, gcsafe, raises: [].}
156+ CallCharMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JChar {.cdecl, gcsafe, raises: [].}
157+ CallShortMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JShort {.cdecl, gcsafe, raises: [].}
158+ CallIntMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JInt {.cdecl, gcsafe, raises: [].}
159+ CallLongMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JLong {.cdecl, gcsafe, raises: [].}
160+ CallFloatMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JFloat {.cdecl, gcsafe, raises: [].}
161+ CallDoubleMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JDouble {.cdecl, gcsafe, raises: [].}
162+ CallVoidMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
163+
164+ CallNonvirtualObjectMethodA*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
165+ CallNonvirtualVoidMethodA*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
166+
167+ # -- instance fields ---------------------------------------------------
168+ GetFieldID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JFieldID {.cdecl, gcsafe, raises: [].}
169+ GetObjectField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JObjectRef {.cdecl, gcsafe, raises: [].}
170+ GetBooleanField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JBoolean {.cdecl, gcsafe, raises: [].}
171+ GetByteField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JByte {.cdecl, gcsafe, raises: [].}
172+ GetCharField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JChar {.cdecl, gcsafe, raises: [].}
173+ GetShortField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JShort {.cdecl, gcsafe, raises: [].}
174+ GetIntField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JInt {.cdecl, gcsafe, raises: [].}
175+ GetLongField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JLong {.cdecl, gcsafe, raises: [].}
176+ GetFloatField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JFloat {.cdecl, gcsafe, raises: [].}
177+ GetDoubleField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JDouble {.cdecl, gcsafe, raises: [].}
178+ SetObjectField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
179+ SetBooleanField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JBoolean) {.cdecl, gcsafe, raises: [].}
180+ SetByteField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JByte) {.cdecl, gcsafe, raises: [].}
181+ SetCharField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JChar) {.cdecl, gcsafe, raises: [].}
182+ SetShortField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JShort) {.cdecl, gcsafe, raises: [].}
183+ SetIntField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JInt) {.cdecl, gcsafe, raises: [].}
184+ SetLongField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JLong) {.cdecl, gcsafe, raises: [].}
185+ SetFloatField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JFloat) {.cdecl, gcsafe, raises: [].}
186+ SetDoubleField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JDouble) {.cdecl, gcsafe, raises: [].}
187+
188+ # -- static methods ----------------------------------------------------
189+ GetStaticMethodID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JMethodID {.cdecl, gcsafe, raises: [].}
190+ CallStaticObjectMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
191+ CallStaticBooleanMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JBoolean {.cdecl, gcsafe, raises: [].}
192+ CallStaticByteMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JByte {.cdecl, gcsafe, raises: [].}
193+ CallStaticCharMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JChar {.cdecl, gcsafe, raises: [].}
194+ CallStaticShortMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JShort {.cdecl, gcsafe, raises: [].}
195+ CallStaticIntMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JInt {.cdecl, gcsafe, raises: [].}
196+ CallStaticLongMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JLong {.cdecl, gcsafe, raises: [].}
197+ CallStaticFloatMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JFloat {.cdecl, gcsafe, raises: [].}
198+ CallStaticDoubleMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JDouble {.cdecl, gcsafe, raises: [].}
199+ CallStaticVoidMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
200+
201+ # -- static fields -----------------------------------------------------
202+ GetStaticFieldID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JFieldID {.cdecl, gcsafe, raises: [].}
203+ GetStaticObjectField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JObjectRef {.cdecl, gcsafe, raises: [].}
204+ GetStaticBooleanField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JBoolean {.cdecl, gcsafe, raises: [].}
205+ GetStaticIntField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JInt {.cdecl, gcsafe, raises: [].}
206+ GetStaticLongField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JLong {.cdecl, gcsafe, raises: [].}
207+ GetStaticDoubleField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JDouble {.cdecl, gcsafe, raises: [].}
208+ SetStaticObjectField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
209+ SetStaticIntField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JInt) {.cdecl, gcsafe, raises: [].}
210+ SetStaticLongField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JLong) {.cdecl, gcsafe, raises: [].}
211+ SetStaticDoubleField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JDouble) {.cdecl, gcsafe, raises: [].}
212+
213+ # -- strings -----------------------------------------------------------
214+ NewString*: proc (env: JNIEnv, unicode: ptr JChar, len: JSize): JStringRef {.cdecl, gcsafe, raises: [].}
215+ GetStringLength*: proc (env: JNIEnv, s: JStringRef): JSize {.cdecl, gcsafe, raises: [].}
216+ GetStringChars*: proc (env: JNIEnv, s: JStringRef, isCopy: ptr JBoolean): ptr JChar {.cdecl, gcsafe, raises: [].}
217+ ReleaseStringChars*: proc (env: JNIEnv, s: JStringRef, chars: ptr JChar) {.cdecl, gcsafe, raises: [].}
218+ NewStringUTF*: proc (env: JNIEnv, utf: cstring): JStringRef {.cdecl, gcsafe, raises: [].}
219+ GetStringUTFLength*: proc (env: JNIEnv, s: JStringRef): JSize {.cdecl, gcsafe, raises: [].}
220+ GetStringUTFChars*: proc (env: JNIEnv, s: JStringRef, isCopy: ptr JBoolean): cstring {.cdecl, gcsafe, raises: [].}
221+ ReleaseStringUTFChars*: proc (env: JNIEnv, s: JStringRef, chars: cstring) {.cdecl, gcsafe, raises: [].}
222+
223+ # -- arrays ------------------------------------------------------------
224+ GetArrayLength*: proc (env: JNIEnv, arr: JArrayRef): JSize {.cdecl, gcsafe, raises: [].}
225+ NewObjectArray*: proc (env: JNIEnv, len: JSize, cls: JClassRef, init: JObjectRef): JArrayRef {.cdecl, gcsafe, raises: [].}
226+ GetObjectArrayElement*: proc (env: JNIEnv, arr: JArrayRef, idx: JSize): JObjectRef {.cdecl, gcsafe, raises: [].}
227+ SetObjectArrayElement*: proc (env: JNIEnv, arr: JArrayRef, idx: JSize, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
228+
229+ NewBooleanArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
230+ NewByteArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
231+ NewCharArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
232+ NewShortArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
233+ NewIntArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
234+ NewLongArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
235+ NewFloatArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
236+ NewDoubleArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
237+
238+ GetByteArrayElements*: proc (env: JNIEnv, arr: JArrayRef, isCopy: ptr JBoolean): ptr JByte {.cdecl, gcsafe, raises: [].}
239+ ReleaseByteArrayElements*: proc (env: JNIEnv, arr: JArrayRef, elems: ptr JByte, mode: JInt) {.cdecl, gcsafe, raises: [].}
240+ GetIntArrayElements*: proc (env: JNIEnv, arr: JArrayRef, isCopy: ptr JBoolean): ptr JInt {.cdecl, gcsafe, raises: [].}
241+ ReleaseIntArrayElements*: proc (env: JNIEnv, arr: JArrayRef, elems: ptr JInt, mode: JInt) {.cdecl, gcsafe, raises: [].}
242+
243+ GetBooleanArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JBoolean) {.cdecl, gcsafe, raises: [].}
244+ GetByteArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JByte) {.cdecl, gcsafe, raises: [].}
245+ GetCharArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JChar) {.cdecl, gcsafe, raises: [].}
246+ GetShortArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JShort) {.cdecl, gcsafe, raises: [].}
247+ GetIntArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JInt) {.cdecl, gcsafe, raises: [].}
248+ GetLongArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JLong) {.cdecl, gcsafe, raises: [].}
249+ GetFloatArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JFloat) {.cdecl, gcsafe, raises: [].}
250+ GetDoubleArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JDouble) {.cdecl, gcsafe, raises: [].}
251+ SetBooleanArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JBoolean) {.cdecl, gcsafe, raises: [].}
252+ SetByteArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JByte) {.cdecl, gcsafe, raises: [].}
253+ SetCharArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JChar) {.cdecl, gcsafe, raises: [].}
254+ SetShortArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JShort) {.cdecl, gcsafe, raises: [].}
255+ SetIntArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JInt) {.cdecl, gcsafe, raises: [].}
256+ SetLongArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JLong) {.cdecl, gcsafe, raises: [].}
257+ SetFloatArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JFloat) {.cdecl, gcsafe, raises: [].}
258+ SetDoubleArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JDouble) {.cdecl, gcsafe, raises: [].}
259+
260+ # -- native methods, monitors, misc ------------------------------------
261+ RegisterNatives*: proc (env: JNIEnv, cls: JClassRef, methods: ptr JNINativeMethod, n: JInt): JInt {.cdecl, gcsafe, raises: [].}
262+ UnregisterNatives*: proc (env: JNIEnv, cls: JClassRef): JInt {.cdecl, gcsafe, raises: [].}
263+ MonitorEnter*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
264+ MonitorExit*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
265+ GetJavaVM*: proc (env: JNIEnv, vm: ptr JavaVM): JInt {.cdecl, gcsafe, raises: [].}
266+
267+ GetDirectBufferAddress*: proc (env: JNIEnv, buf: JObjectRef): pointer {.cdecl, gcsafe, raises: [].}
268+ GetDirectBufferCapacity*: proc (env: JNIEnv, buf: JObjectRef): JLong {.cdecl, gcsafe, raises: [].}
269+ NewDirectByteBuffer*: proc (env: JNIEnv, address: pointer, capacity: JLong): JObjectRef {.cdecl, gcsafe, raises: [].}
270+ GetObjectRefType*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
271+
272+# --------------------------------------------------------------------------
273+# VM startup arguments
274+# --------------------------------------------------------------------------
275+
276+type
277+ JavaVMOption* {.jnih, importc: "JavaVMOption".} = object
278+ optionString*: cstring
279+ extraInfo*: pointer
280+
281+ JavaVMInitArgs* {.jnih, importc: "JavaVMInitArgs".} = object
282+ version*: JInt
283+ nOptions*: JInt
284+ options*: ptr JavaVMOption
285+ ignoreUnrecognized*: JBoolean
286+
287+ JavaVMAttachArgs* {.jnih, importc: "JavaVMAttachArgs".} = object
288+ version*: JInt
289+ name*: cstring
290+ group*: JObjectRef
291+
292+# --------------------------------------------------------------------------
293+# The three exported entry points of libjvm
294+# --------------------------------------------------------------------------
295+
296+proc JNI_CreateJavaVM*(pvm: ptr JavaVM, penv: ptr pointer,
297+ args: pointer): JInt {.jnih, cdecl.}
298+ ## Creates a JVM in this process. At most one per process, ever -- a
299+ ## destroyed VM cannot be recreated.
300+
301+proc JNI_GetCreatedJavaVMs*(vmBuf: ptr JavaVM, bufLen: JSize,
302+ nVMs: ptr JSize): JInt {.jnih, cdecl.}
303+ ## Finds a VM already running in this process, e.g. when our shared library
304+ ## was loaded *by* a JVM rather than the other way round.
305+
306+proc JNI_GetDefaultJavaVMInitArgs*(args: pointer): JInt {.jnih, cdecl.}
new file mode 100644
@@ -0,0 +1,306 @@
1+## Raw JNI bindings.
2+##
3+## `JNIEnv` and `JavaVM` are, in C, pointers to tables of function pointers:
4+##
5+## typedef const struct JNINativeInterface_ *JNIEnv;
6+## typedef const struct JNIInvokeInterface_ *JavaVM;
7+##
8+## Both tables are declared here with `{.importc.}`, which means Nim never
9+## computes field offsets itself -- it emits `env->FindClass(env, name)` and
10+## lets the real <jni.h> decide the layout. That has two happy consequences:
11+## we only have to declare the fields we actually use, and the bindings cannot
12+## silently drift out of sync with the JDK they are compiled against.
13+##
14+## Only the `...A` call variants (taking `ptr JValue`) are bound. The variadic
15+## forms buy nothing here and lose type safety at the C boundary.
16+
17+import ./config
18+export config.javaHome
19+
20+{.pragma: jnih, importc, header: "jni.h".}
21+
22+# --------------------------------------------------------------------------
23+# Primitives
24+# --------------------------------------------------------------------------
25+
26+type
27+ JBoolean* = uint8
28+ JByte* = int8
29+ JChar* = uint16
30+ JShort* = int16
31+ JInt* = int32
32+ JLong* = int64
33+ JFloat* = cfloat
34+ JDouble* = cdouble
35+ JSize* = JInt
36+
37+const
38+ JNI_FALSE* = JBoolean(0)
39+ JNI_TRUE* = JBoolean(1)
40+
41+# In C every reference type is a `void*` typedef, so aliases are faithful here.
42+# The safety lives one layer up, in the `JObject` wrapper.
43+type
44+ JObjectRef* = pointer
45+ JClassRef* = JObjectRef
46+ JStringRef* = JObjectRef
47+ JArrayRef* = JObjectRef
48+ JThrowableRef* = JObjectRef
49+ JMethodID* = pointer
50+ JFieldID* = pointer
51+
52+type
53+ JValue* {.jnih, importc: "jvalue", union.} = object
54+ z*: JBoolean
55+ b*: JByte
56+ c*: JChar
57+ s*: JShort
58+ i*: JInt
59+ j*: JLong
60+ f*: JFloat
61+ d*: JDouble
62+ l*: JObjectRef
63+
64+# --------------------------------------------------------------------------
65+# Return codes and versions
66+# --------------------------------------------------------------------------
67+
68+const
69+ JNI_OK* = JInt(0)
70+ JNI_ERR* = JInt(-1)
71+ JNI_EDETACHED* = JInt(-2)
72+ JNI_EVERSION* = JInt(-3)
73+ JNI_ENOMEM* = JInt(-4)
74+ JNI_EEXIST* = JInt(-5)
75+ JNI_EINVAL* = JInt(-6)
76+
77+ JNI_VERSION_1_6* = JInt(0x00010006)
78+ JNI_VERSION_1_8* = JInt(0x00010008)
79+ JNI_VERSION_9* = JInt(0x00090000)
80+ JNI_VERSION_10* = JInt(0x000A0000)
81+ JNI_VERSION_19* = JInt(0x00130000)
82+ JNI_VERSION_20* = JInt(0x00140000)
83+ JNI_VERSION_21* = JInt(0x00150000)
84+
85+ JNI_COMMIT* = JInt(1)
86+ JNI_ABORT* = JInt(2)
87+
88+type
89+ ## Note the double indirection: C's `JNIEnv` is *itself* a pointer to the
90+ ## function table, and what you hold and pass around is a `JNIEnv *`.
91+ ## Hence `env[].FindClass(env, ...)`, which lowers to the idiomatic
92+ ## `(*env)->FindClass(env, ...)`.
93+ JNIEnv* = ptr ptr JNINativeInterface
94+ JavaVM* = ptr ptr JNIInvokeInterface
95+
96+ JNINativeMethod* {.jnih, importc: "JNINativeMethod".} = object
97+ name*: cstring
98+ signature*: cstring
99+ fnPtr*: pointer
100+
101+ # ------------------------------------------------------------------------
102+ # The invocation interface: JVM lifecycle and thread attachment.
103+ # ------------------------------------------------------------------------
104+ JNIInvokeInterface* {.jnih, importc: "const struct JNIInvokeInterface_", incompleteStruct.} = object
105+ DestroyJavaVM*: proc (vm: JavaVM): JInt {.cdecl, gcsafe, raises: [].}
106+ AttachCurrentThread*: proc (vm: JavaVM, penv: ptr pointer, args: pointer): JInt {.cdecl, gcsafe, raises: [].}
107+ DetachCurrentThread*: proc (vm: JavaVM): JInt {.cdecl, gcsafe, raises: [].}
108+ GetEnv*: proc (vm: JavaVM, penv: ptr pointer, version: JInt): JInt {.cdecl, gcsafe, raises: [].}
109+ AttachCurrentThreadAsDaemon*: proc (vm: JavaVM, penv: ptr pointer, args: pointer): JInt {.cdecl, gcsafe, raises: [].}
110+
111+ # ------------------------------------------------------------------------
112+ # The native interface: everything you do with a live JVM.
113+ # ------------------------------------------------------------------------
114+ JNINativeInterface* {.jnih, importc: "const struct JNINativeInterface_", incompleteStruct.} = object
115+ GetVersion*: proc (env: JNIEnv): JInt {.cdecl, gcsafe, raises: [].}
116+
117+ DefineClass*: proc (env: JNIEnv, name: cstring, loader: JObjectRef,
118+ buf: ptr JByte, len: JSize): JClassRef {.cdecl, gcsafe, raises: [].}
119+ FindClass*: proc (env: JNIEnv, name: cstring): JClassRef {.cdecl, gcsafe, raises: [].}
120+ GetSuperclass*: proc (env: JNIEnv, sub: JClassRef): JClassRef {.cdecl, gcsafe, raises: [].}
121+ IsAssignableFrom*: proc (env: JNIEnv, sub, sup: JClassRef): JBoolean {.cdecl, gcsafe, raises: [].}
122+
123+ # -- exceptions --------------------------------------------------------
124+ Throw*: proc (env: JNIEnv, obj: JThrowableRef): JInt {.cdecl, gcsafe, raises: [].}
125+ ThrowNew*: proc (env: JNIEnv, cls: JClassRef, msg: cstring): JInt {.cdecl, gcsafe, raises: [].}
126+ ExceptionOccurred*: proc (env: JNIEnv): JThrowableRef {.cdecl, gcsafe, raises: [].}
127+ ExceptionDescribe*: proc (env: JNIEnv) {.cdecl, gcsafe, raises: [].}
128+ ExceptionClear*: proc (env: JNIEnv) {.cdecl, gcsafe, raises: [].}
129+ FatalError*: proc (env: JNIEnv, msg: cstring) {.cdecl, gcsafe, raises: [].}
130+ ExceptionCheck*: proc (env: JNIEnv): JBoolean {.cdecl, gcsafe, raises: [].}
131+
132+ # -- references --------------------------------------------------------
133+ PushLocalFrame*: proc (env: JNIEnv, capacity: JInt): JInt {.cdecl, gcsafe, raises: [].}
134+ PopLocalFrame*: proc (env: JNIEnv, res: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
135+ NewGlobalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
136+ DeleteGlobalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
137+ DeleteLocalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
138+ NewLocalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
139+ EnsureLocalCapacity*: proc (env: JNIEnv, capacity: JInt): JInt {.cdecl, gcsafe, raises: [].}
140+ NewWeakGlobalRef*: proc (env: JNIEnv, obj: JObjectRef): JObjectRef {.cdecl, gcsafe, raises: [].}
141+ DeleteWeakGlobalRef*: proc (env: JNIEnv, obj: JObjectRef) {.cdecl, gcsafe, raises: [].}
142+ IsSameObject*: proc (env: JNIEnv, a, b: JObjectRef): JBoolean {.cdecl, gcsafe, raises: [].}
143+
144+ # -- objects -----------------------------------------------------------
145+ AllocObject*: proc (env: JNIEnv, cls: JClassRef): JObjectRef {.cdecl, gcsafe, raises: [].}
146+ NewObjectA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID,
147+ args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
148+ GetObjectClass*: proc (env: JNIEnv, obj: JObjectRef): JClassRef {.cdecl, gcsafe, raises: [].}
149+ IsInstanceOf*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef): JBoolean {.cdecl, gcsafe, raises: [].}
150+
151+ # -- instance methods --------------------------------------------------
152+ GetMethodID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JMethodID {.cdecl, gcsafe, raises: [].}
153+ CallObjectMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
154+ CallBooleanMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JBoolean {.cdecl, gcsafe, raises: [].}
155+ CallByteMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JByte {.cdecl, gcsafe, raises: [].}
156+ CallCharMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JChar {.cdecl, gcsafe, raises: [].}
157+ CallShortMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JShort {.cdecl, gcsafe, raises: [].}
158+ CallIntMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JInt {.cdecl, gcsafe, raises: [].}
159+ CallLongMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JLong {.cdecl, gcsafe, raises: [].}
160+ CallFloatMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JFloat {.cdecl, gcsafe, raises: [].}
161+ CallDoubleMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue): JDouble {.cdecl, gcsafe, raises: [].}
162+ CallVoidMethodA*: proc (env: JNIEnv, obj: JObjectRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
163+
164+ CallNonvirtualObjectMethodA*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
165+ CallNonvirtualVoidMethodA*: proc (env: JNIEnv, obj: JObjectRef, cls: JClassRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
166+
167+ # -- instance fields ---------------------------------------------------
168+ GetFieldID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JFieldID {.cdecl, gcsafe, raises: [].}
169+ GetObjectField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JObjectRef {.cdecl, gcsafe, raises: [].}
170+ GetBooleanField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JBoolean {.cdecl, gcsafe, raises: [].}
171+ GetByteField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JByte {.cdecl, gcsafe, raises: [].}
172+ GetCharField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JChar {.cdecl, gcsafe, raises: [].}
173+ GetShortField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JShort {.cdecl, gcsafe, raises: [].}
174+ GetIntField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JInt {.cdecl, gcsafe, raises: [].}
175+ GetLongField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JLong {.cdecl, gcsafe, raises: [].}
176+ GetFloatField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JFloat {.cdecl, gcsafe, raises: [].}
177+ GetDoubleField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID): JDouble {.cdecl, gcsafe, raises: [].}
178+ SetObjectField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
179+ SetBooleanField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JBoolean) {.cdecl, gcsafe, raises: [].}
180+ SetByteField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JByte) {.cdecl, gcsafe, raises: [].}
181+ SetCharField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JChar) {.cdecl, gcsafe, raises: [].}
182+ SetShortField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JShort) {.cdecl, gcsafe, raises: [].}
183+ SetIntField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JInt) {.cdecl, gcsafe, raises: [].}
184+ SetLongField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JLong) {.cdecl, gcsafe, raises: [].}
185+ SetFloatField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JFloat) {.cdecl, gcsafe, raises: [].}
186+ SetDoubleField*: proc (env: JNIEnv, obj: JObjectRef, f: JFieldID, v: JDouble) {.cdecl, gcsafe, raises: [].}
187+
188+ # -- static methods ----------------------------------------------------
189+ GetStaticMethodID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JMethodID {.cdecl, gcsafe, raises: [].}
190+ CallStaticObjectMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JObjectRef {.cdecl, gcsafe, raises: [].}
191+ CallStaticBooleanMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JBoolean {.cdecl, gcsafe, raises: [].}
192+ CallStaticByteMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JByte {.cdecl, gcsafe, raises: [].}
193+ CallStaticCharMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JChar {.cdecl, gcsafe, raises: [].}
194+ CallStaticShortMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JShort {.cdecl, gcsafe, raises: [].}
195+ CallStaticIntMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JInt {.cdecl, gcsafe, raises: [].}
196+ CallStaticLongMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JLong {.cdecl, gcsafe, raises: [].}
197+ CallStaticFloatMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JFloat {.cdecl, gcsafe, raises: [].}
198+ CallStaticDoubleMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue): JDouble {.cdecl, gcsafe, raises: [].}
199+ CallStaticVoidMethodA*: proc (env: JNIEnv, cls: JClassRef, m: JMethodID, args: ptr JValue) {.cdecl, gcsafe, raises: [].}
200+
201+ # -- static fields -----------------------------------------------------
202+ GetStaticFieldID*: proc (env: JNIEnv, cls: JClassRef, name, sig: cstring): JFieldID {.cdecl, gcsafe, raises: [].}
203+ GetStaticObjectField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JObjectRef {.cdecl, gcsafe, raises: [].}
204+ GetStaticBooleanField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JBoolean {.cdecl, gcsafe, raises: [].}
205+ GetStaticIntField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JInt {.cdecl, gcsafe, raises: [].}
206+ GetStaticLongField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JLong {.cdecl, gcsafe, raises: [].}
207+ GetStaticDoubleField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID): JDouble {.cdecl, gcsafe, raises: [].}
208+ SetStaticObjectField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
209+ SetStaticIntField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JInt) {.cdecl, gcsafe, raises: [].}
210+ SetStaticLongField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JLong) {.cdecl, gcsafe, raises: [].}
211+ SetStaticDoubleField*: proc (env: JNIEnv, cls: JClassRef, f: JFieldID, v: JDouble) {.cdecl, gcsafe, raises: [].}
212+
213+ # -- strings -----------------------------------------------------------
214+ NewString*: proc (env: JNIEnv, unicode: ptr JChar, len: JSize): JStringRef {.cdecl, gcsafe, raises: [].}
215+ GetStringLength*: proc (env: JNIEnv, s: JStringRef): JSize {.cdecl, gcsafe, raises: [].}
216+ GetStringChars*: proc (env: JNIEnv, s: JStringRef, isCopy: ptr JBoolean): ptr JChar {.cdecl, gcsafe, raises: [].}
217+ ReleaseStringChars*: proc (env: JNIEnv, s: JStringRef, chars: ptr JChar) {.cdecl, gcsafe, raises: [].}
218+ NewStringUTF*: proc (env: JNIEnv, utf: cstring): JStringRef {.cdecl, gcsafe, raises: [].}
219+ GetStringUTFLength*: proc (env: JNIEnv, s: JStringRef): JSize {.cdecl, gcsafe, raises: [].}
220+ GetStringUTFChars*: proc (env: JNIEnv, s: JStringRef, isCopy: ptr JBoolean): cstring {.cdecl, gcsafe, raises: [].}
221+ ReleaseStringUTFChars*: proc (env: JNIEnv, s: JStringRef, chars: cstring) {.cdecl, gcsafe, raises: [].}
222+
223+ # -- arrays ------------------------------------------------------------
224+ GetArrayLength*: proc (env: JNIEnv, arr: JArrayRef): JSize {.cdecl, gcsafe, raises: [].}
225+ NewObjectArray*: proc (env: JNIEnv, len: JSize, cls: JClassRef, init: JObjectRef): JArrayRef {.cdecl, gcsafe, raises: [].}
226+ GetObjectArrayElement*: proc (env: JNIEnv, arr: JArrayRef, idx: JSize): JObjectRef {.cdecl, gcsafe, raises: [].}
227+ SetObjectArrayElement*: proc (env: JNIEnv, arr: JArrayRef, idx: JSize, v: JObjectRef) {.cdecl, gcsafe, raises: [].}
228+
229+ NewBooleanArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
230+ NewByteArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
231+ NewCharArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
232+ NewShortArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
233+ NewIntArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
234+ NewLongArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
235+ NewFloatArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
236+ NewDoubleArray*: proc (env: JNIEnv, len: JSize): JArrayRef {.cdecl, gcsafe, raises: [].}
237+
238+ GetByteArrayElements*: proc (env: JNIEnv, arr: JArrayRef, isCopy: ptr JBoolean): ptr JByte {.cdecl, gcsafe, raises: [].}
239+ ReleaseByteArrayElements*: proc (env: JNIEnv, arr: JArrayRef, elems: ptr JByte, mode: JInt) {.cdecl, gcsafe, raises: [].}
240+ GetIntArrayElements*: proc (env: JNIEnv, arr: JArrayRef, isCopy: ptr JBoolean): ptr JInt {.cdecl, gcsafe, raises: [].}
241+ ReleaseIntArrayElements*: proc (env: JNIEnv, arr: JArrayRef, elems: ptr JInt, mode: JInt) {.cdecl, gcsafe, raises: [].}
242+
243+ GetBooleanArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JBoolean) {.cdecl, gcsafe, raises: [].}
244+ GetByteArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JByte) {.cdecl, gcsafe, raises: [].}
245+ GetCharArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JChar) {.cdecl, gcsafe, raises: [].}
246+ GetShortArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JShort) {.cdecl, gcsafe, raises: [].}
247+ GetIntArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JInt) {.cdecl, gcsafe, raises: [].}
248+ GetLongArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JLong) {.cdecl, gcsafe, raises: [].}
249+ GetFloatArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JFloat) {.cdecl, gcsafe, raises: [].}
250+ GetDoubleArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JDouble) {.cdecl, gcsafe, raises: [].}
251+ SetBooleanArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JBoolean) {.cdecl, gcsafe, raises: [].}
252+ SetByteArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JByte) {.cdecl, gcsafe, raises: [].}
253+ SetCharArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JChar) {.cdecl, gcsafe, raises: [].}
254+ SetShortArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JShort) {.cdecl, gcsafe, raises: [].}
255+ SetIntArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JInt) {.cdecl, gcsafe, raises: [].}
256+ SetLongArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JLong) {.cdecl, gcsafe, raises: [].}
257+ SetFloatArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JFloat) {.cdecl, gcsafe, raises: [].}
258+ SetDoubleArrayRegion*: proc (env: JNIEnv, arr: JArrayRef, start, len: JSize, buf: ptr JDouble) {.cdecl, gcsafe, raises: [].}
259+
260+ # -- native methods, monitors, misc ------------------------------------
261+ RegisterNatives*: proc (env: JNIEnv, cls: JClassRef, methods: ptr JNINativeMethod, n: JInt): JInt {.cdecl, gcsafe, raises: [].}
262+ UnregisterNatives*: proc (env: JNIEnv, cls: JClassRef): JInt {.cdecl, gcsafe, raises: [].}
263+ MonitorEnter*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
264+ MonitorExit*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
265+ GetJavaVM*: proc (env: JNIEnv, vm: ptr JavaVM): JInt {.cdecl, gcsafe, raises: [].}
266+
267+ GetDirectBufferAddress*: proc (env: JNIEnv, buf: JObjectRef): pointer {.cdecl, gcsafe, raises: [].}
268+ GetDirectBufferCapacity*: proc (env: JNIEnv, buf: JObjectRef): JLong {.cdecl, gcsafe, raises: [].}
269+ NewDirectByteBuffer*: proc (env: JNIEnv, address: pointer, capacity: JLong): JObjectRef {.cdecl, gcsafe, raises: [].}
270+ GetObjectRefType*: proc (env: JNIEnv, obj: JObjectRef): JInt {.cdecl, gcsafe, raises: [].}
271+
272+# --------------------------------------------------------------------------
273+# VM startup arguments
274+# --------------------------------------------------------------------------
275+
276+type
277+ JavaVMOption* {.jnih, importc: "JavaVMOption".} = object
278+ optionString*: cstring
279+ extraInfo*: pointer
280+
281+ JavaVMInitArgs* {.jnih, importc: "JavaVMInitArgs".} = object
282+ version*: JInt
283+ nOptions*: JInt
284+ options*: ptr JavaVMOption
285+ ignoreUnrecognized*: JBoolean
286+
287+ JavaVMAttachArgs* {.jnih, importc: "JavaVMAttachArgs".} = object
288+ version*: JInt
289+ name*: cstring
290+ group*: JObjectRef
291+
292+# --------------------------------------------------------------------------
293+# The three exported entry points of libjvm
294+# --------------------------------------------------------------------------
295+
296+proc JNI_CreateJavaVM*(pvm: ptr JavaVM, penv: ptr pointer,
297+ args: pointer): JInt {.jnih, cdecl.}
298+ ## Creates a JVM in this process. At most one per process, ever -- a
299+ ## destroyed VM cannot be recreated.
300+
301+proc JNI_GetCreatedJavaVMs*(vmBuf: ptr JavaVM, bufLen: JSize,
302+ nVMs: ptr JSize): JInt {.jnih, cdecl.}
303+ ## Finds a VM already running in this process, e.g. when our shared library
304+ ## was loaded *by* a JVM rather than the other way round.
305+
306+proc JNI_GetDefaultJavaVMInitArgs*(args: pointer): JInt {.jnih, cdecl.}
added src/libjava/vm.nim +171 -0
new file mode 100644
@@ -0,0 +1,171 @@
1+## JVM lifecycle: starting one in this process, or attaching to one that is
2+## already running, plus per-thread `JNIEnv` management.
3+
4+import std/[os, strutils]
5+import ./jni, ./core
6+
7+type
8+ JVM* = ref object
9+ ## Handle on the embedded JVM. There can only ever be one per process:
10+ ## the JNI specification has never supported creating a second, and a
11+ ## destroyed VM cannot be restarted.
12+ vm*: JavaVM
13+ version*: JInt
14+ owned: bool ## did we create it (and so may destroy it)?
15+ destroyed: bool
16+
17+var
18+ theVM: JVM
19+ threadEnv {.threadvar.}: JNIEnv
20+ threadAttached {.threadvar.}: bool
21+
22+proc currentJVM*(): JVM =
23+ ## The process-wide JVM, or nil if none has been started or found.
24+ theVM
25+
26+proc classPathOption(entries: openArray[string]): string =
27+ "-Djava.class.path=" & entries.join($PathSep)
28+
29+proc startJVM*(classPath: openArray[string] = [];
30+ options: openArray[string] = [];
31+ version: JInt = JNI_VERSION_10;
32+ ignoreUnrecognized = false): JVM =
33+ ## Boots a JVM in this process.
34+ ##
35+ ## `classPath` entries are directories or .jar files. `options` are raw JVM
36+ ## flags exactly as you would pass them to `java` ("-Xmx512m",
37+ ## "-Djava.awt.headless=true", "--enable-preview"). Calling this twice
38+ ## returns the existing VM rather than failing.
39+ if theVM != nil and not theVM.destroyed:
40+ return theVM
41+
42+ var opts: seq[string]
43+ if classPath.len > 0:
44+ opts.add classPathOption(classPath)
45+ for o in options:
46+ opts.add o
47+
48+ # Every optionString must still point at live memory when the VM reads it,
49+ # so these borrow directly from `opts`, which outlives the call. Taking
50+ # `.cstring` off a loop copy instead hands the JVM a dangling pointer.
51+ var vmOpts = newSeq[JavaVMOption](opts.len)
52+ for i in 0 ..< opts.len:
53+ vmOpts[i] = JavaVMOption(optionString: cstring(opts[i]), extraInfo: nil)
54+
55+ var args = JavaVMInitArgs(
56+ version: version,
57+ nOptions: JInt(opts.len),
58+ options: (if opts.len == 0: nil else: addr vmOpts[0]),
59+ ignoreUnrecognized: (if ignoreUnrecognized: JNI_TRUE else: JNI_FALSE))
60+
61+ var vm: JavaVM
62+ var envp: pointer
63+ let rc = JNI_CreateJavaVM(addr vm, addr envp, addr args)
64+ if rc != JNI_OK:
65+ let why = case rc
66+ of JNI_EVERSION: "unsupported JNI version (is the JDK older than this build expects?)"
67+ of JNI_ENOMEM: "out of memory"
68+ of JNI_EEXIST: "a JVM already exists in this process"
69+ of JNI_EINVAL: "invalid arguments -- check the JVM options"
70+ else: "error code " & $rc
71+ raise newException(JavaVMError, "JNI_CreateJavaVM failed: " & why)
72+
73+ threadEnv = cast[JNIEnv](envp)
74+ threadAttached = true
75+ result = JVM(vm: vm, version: version, owned: true, destroyed: false)
76+ theVM = result
77+
78+proc attachExistingJVM*(version: JInt = JNI_VERSION_10): JVM =
79+ ## Finds a JVM already running in this process instead of creating one --
80+ ## the case when this code is a shared library that a JVM loaded via
81+ ## `System.loadLibrary`. Returns nil if there is no VM.
82+ if theVM != nil and not theVM.destroyed:
83+ return theVM
84+ var vm: JavaVM
85+ var n: JSize
86+ if JNI_GetCreatedJavaVMs(addr vm, 1, addr n) != JNI_OK or n == 0:
87+ return nil
88+ result = JVM(vm: vm, version: version, owned: false, destroyed: false)
89+ theVM = result
90+
91+proc adoptJVM*(vm: JavaVM, version: JInt = JNI_VERSION_10): JVM =
92+ ## Wraps a `JavaVM` handed to us from outside, e.g. the one passed to
93+ ## `JNI_OnLoad`. We do not own it, so `destroy` will not tear it down.
94+ result = JVM(vm: vm, version: version, owned: false, destroyed: false)
95+ theVM = result
96+
97+# --------------------------------------------------------------------------
98+# Per-thread environments
99+#
100+# A JNIEnv belongs to exactly one thread and must never be shared between
101+# them; only the JavaVM handle is safe to pass around. Any thread the JVM did
102+# not itself start has to attach before it can make a single JNI call.
103+# --------------------------------------------------------------------------
104+
105+proc envOf*(vm: JavaVM, version: JInt = JNI_VERSION_10): JNIEnv =
106+ ## The calling thread's `JNIEnv`, from a raw `JavaVM` handle, attaching the
107+ ## thread on first use. Takes the raw handle rather than the `JVM` ref so it
108+ ## is safe to hand to a worker thread -- a `JavaVM` is the one JNI pointer
109+ ## that may legally cross threads.
110+ if threadEnv != nil:
111+ return threadEnv
112+
113+ var envp: pointer
114+ let rc = vm[].GetEnv(vm, addr envp, version)
115+ case rc
116+ of JNI_OK:
117+ threadEnv = cast[JNIEnv](envp)
118+ of JNI_EDETACHED:
119+ if vm[].AttachCurrentThread(vm, addr envp, nil) != JNI_OK:
120+ raise newException(JavaVMError, "AttachCurrentThread failed")
121+ threadEnv = cast[JNIEnv](envp)
122+ threadAttached = true
123+ else:
124+ raise newException(JavaVMError, "GetEnv failed with code " & $rc)
125+ threadEnv
126+
127+proc detachOf*(vm: JavaVM) =
128+ ## Detach the calling thread, given a raw handle.
129+ if not threadAttached: return
130+ discard vm[].DetachCurrentThread(vm)
131+ threadEnv = nil
132+ threadAttached = false
133+
134+proc env*(vm: JVM): JNIEnv =
135+ ## The calling thread's `JNIEnv`, attaching the thread to the JVM on first
136+ ## use. Cheap after the first call -- the result is cached thread-locally.
137+ if vm == nil or vm.destroyed:
138+ raise newException(JavaVMError, "no JVM running")
139+ envOf(vm.vm, vm.version)
140+
141+proc detachThread*(vm: JVM) =
142+ ## Detaches the calling thread. Required before a thread that attached
143+ ## itself exits -- the JVM will not shut down while attached threads remain.
144+ if vm == nil or vm.destroyed:
145+ return
146+ detachOf(vm.vm)
147+
148+template withAttachedThread*(vm: JVM, envName: untyped, body: untyped) =
149+ ## Attaches the current thread for the duration of `body`, binding its env
150+ ## to `envName`, and detaches again afterwards.
151+ block:
152+ let envName = vm.env()
153+ try:
154+ body
155+ finally:
156+ vm.detachThread()
157+
158+proc destroy*(vm: JVM) =
159+ ## Shuts the JVM down. `DestroyJavaVM` blocks until every non-daemon thread
160+ ## has finished, and the VM cannot be recreated afterwards.
161+ if vm == nil or vm.destroyed or not vm.owned:
162+ return
163+ discard vm.vm[].DestroyJavaVM(vm.vm)
164+ vm.destroyed = true
165+ threadEnv = nil
166+ threadAttached = false
167+ if theVM == vm:
168+ theVM = nil
169+
170+proc isRunning*(vm: JVM): bool =
171+ vm != nil and not vm.destroyed
new file mode 100644
@@ -0,0 +1,171 @@
1+## JVM lifecycle: starting one in this process, or attaching to one that is
2+## already running, plus per-thread `JNIEnv` management.
3+
4+import std/[os, strutils]
5+import ./jni, ./core
6+
7+type
8+ JVM* = ref object
9+ ## Handle on the embedded JVM. There can only ever be one per process:
10+ ## the JNI specification has never supported creating a second, and a
11+ ## destroyed VM cannot be restarted.
12+ vm*: JavaVM
13+ version*: JInt
14+ owned: bool ## did we create it (and so may destroy it)?
15+ destroyed: bool
16+
17+var
18+ theVM: JVM
19+ threadEnv {.threadvar.}: JNIEnv
20+ threadAttached {.threadvar.}: bool
21+
22+proc currentJVM*(): JVM =
23+ ## The process-wide JVM, or nil if none has been started or found.
24+ theVM
25+
26+proc classPathOption(entries: openArray[string]): string =
27+ "-Djava.class.path=" & entries.join($PathSep)
28+
29+proc startJVM*(classPath: openArray[string] = [];
30+ options: openArray[string] = [];
31+ version: JInt = JNI_VERSION_10;
32+ ignoreUnrecognized = false): JVM =
33+ ## Boots a JVM in this process.
34+ ##
35+ ## `classPath` entries are directories or .jar files. `options` are raw JVM
36+ ## flags exactly as you would pass them to `java` ("-Xmx512m",
37+ ## "-Djava.awt.headless=true", "--enable-preview"). Calling this twice
38+ ## returns the existing VM rather than failing.
39+ if theVM != nil and not theVM.destroyed:
40+ return theVM
41+
42+ var opts: seq[string]
43+ if classPath.len > 0:
44+ opts.add classPathOption(classPath)
45+ for o in options:
46+ opts.add o
47+
48+ # Every optionString must still point at live memory when the VM reads it,
49+ # so these borrow directly from `opts`, which outlives the call. Taking
50+ # `.cstring` off a loop copy instead hands the JVM a dangling pointer.
51+ var vmOpts = newSeq[JavaVMOption](opts.len)
52+ for i in 0 ..< opts.len:
53+ vmOpts[i] = JavaVMOption(optionString: cstring(opts[i]), extraInfo: nil)
54+
55+ var args = JavaVMInitArgs(
56+ version: version,
57+ nOptions: JInt(opts.len),
58+ options: (if opts.len == 0: nil else: addr vmOpts[0]),
59+ ignoreUnrecognized: (if ignoreUnrecognized: JNI_TRUE else: JNI_FALSE))
60+
61+ var vm: JavaVM
62+ var envp: pointer
63+ let rc = JNI_CreateJavaVM(addr vm, addr envp, addr args)
64+ if rc != JNI_OK:
65+ let why = case rc
66+ of JNI_EVERSION: "unsupported JNI version (is the JDK older than this build expects?)"
67+ of JNI_ENOMEM: "out of memory"
68+ of JNI_EEXIST: "a JVM already exists in this process"
69+ of JNI_EINVAL: "invalid arguments -- check the JVM options"
70+ else: "error code " & $rc
71+ raise newException(JavaVMError, "JNI_CreateJavaVM failed: " & why)
72+
73+ threadEnv = cast[JNIEnv](envp)
74+ threadAttached = true
75+ result = JVM(vm: vm, version: version, owned: true, destroyed: false)
76+ theVM = result
77+
78+proc attachExistingJVM*(version: JInt = JNI_VERSION_10): JVM =
79+ ## Finds a JVM already running in this process instead of creating one --
80+ ## the case when this code is a shared library that a JVM loaded via
81+ ## `System.loadLibrary`. Returns nil if there is no VM.
82+ if theVM != nil and not theVM.destroyed:
83+ return theVM
84+ var vm: JavaVM
85+ var n: JSize
86+ if JNI_GetCreatedJavaVMs(addr vm, 1, addr n) != JNI_OK or n == 0:
87+ return nil
88+ result = JVM(vm: vm, version: version, owned: false, destroyed: false)
89+ theVM = result
90+
91+proc adoptJVM*(vm: JavaVM, version: JInt = JNI_VERSION_10): JVM =
92+ ## Wraps a `JavaVM` handed to us from outside, e.g. the one passed to
93+ ## `JNI_OnLoad`. We do not own it, so `destroy` will not tear it down.
94+ result = JVM(vm: vm, version: version, owned: false, destroyed: false)
95+ theVM = result
96+
97+# --------------------------------------------------------------------------
98+# Per-thread environments
99+#
100+# A JNIEnv belongs to exactly one thread and must never be shared between
101+# them; only the JavaVM handle is safe to pass around. Any thread the JVM did
102+# not itself start has to attach before it can make a single JNI call.
103+# --------------------------------------------------------------------------
104+
105+proc envOf*(vm: JavaVM, version: JInt = JNI_VERSION_10): JNIEnv =
106+ ## The calling thread's `JNIEnv`, from a raw `JavaVM` handle, attaching the
107+ ## thread on first use. Takes the raw handle rather than the `JVM` ref so it
108+ ## is safe to hand to a worker thread -- a `JavaVM` is the one JNI pointer
109+ ## that may legally cross threads.
110+ if threadEnv != nil:
111+ return threadEnv
112+
113+ var envp: pointer
114+ let rc = vm[].GetEnv(vm, addr envp, version)
115+ case rc
116+ of JNI_OK:
117+ threadEnv = cast[JNIEnv](envp)
118+ of JNI_EDETACHED:
119+ if vm[].AttachCurrentThread(vm, addr envp, nil) != JNI_OK:
120+ raise newException(JavaVMError, "AttachCurrentThread failed")
121+ threadEnv = cast[JNIEnv](envp)
122+ threadAttached = true
123+ else:
124+ raise newException(JavaVMError, "GetEnv failed with code " & $rc)
125+ threadEnv
126+
127+proc detachOf*(vm: JavaVM) =
128+ ## Detach the calling thread, given a raw handle.
129+ if not threadAttached: return
130+ discard vm[].DetachCurrentThread(vm)
131+ threadEnv = nil
132+ threadAttached = false
133+
134+proc env*(vm: JVM): JNIEnv =
135+ ## The calling thread's `JNIEnv`, attaching the thread to the JVM on first
136+ ## use. Cheap after the first call -- the result is cached thread-locally.
137+ if vm == nil or vm.destroyed:
138+ raise newException(JavaVMError, "no JVM running")
139+ envOf(vm.vm, vm.version)
140+
141+proc detachThread*(vm: JVM) =
142+ ## Detaches the calling thread. Required before a thread that attached
143+ ## itself exits -- the JVM will not shut down while attached threads remain.
144+ if vm == nil or vm.destroyed:
145+ return
146+ detachOf(vm.vm)
147+
148+template withAttachedThread*(vm: JVM, envName: untyped, body: untyped) =
149+ ## Attaches the current thread for the duration of `body`, binding its env
150+ ## to `envName`, and detaches again afterwards.
151+ block:
152+ let envName = vm.env()
153+ try:
154+ body
155+ finally:
156+ vm.detachThread()
157+
158+proc destroy*(vm: JVM) =
159+ ## Shuts the JVM down. `DestroyJavaVM` blocks until every non-daemon thread
160+ ## has finished, and the VM cannot be recreated afterwards.
161+ if vm == nil or vm.destroyed or not vm.owned:
162+ return
163+ discard vm.vm[].DestroyJavaVM(vm.vm)
164+ vm.destroyed = true
165+ threadEnv = nil
166+ threadAttached = false
167+ if theVM == vm:
168+ theVM = nil
169+
170+proc isRunning*(vm: JVM): bool =
171+ vm != nil and not vm.destroyed
added tests/tlibjava.nim +194 -0
new file mode 100644
@@ -0,0 +1,194 @@
1+## Run with: nim c -r --path:src tests/tlibjava.nim
2+## Requires: javac -d build/classes examples/java/demo/Greeter.java
3+##
4+## Everything lives in one process on purpose -- a JVM can be created exactly
5+## once per process, so these cannot be split into separate test binaries.
6+
7+import std/[unittest, strutils]
8+import libjava
9+
10+let jvm = startJVM(classPath = ["build/classes"], options = ["-Xmx256m"])
11+let env = jvm.env
12+let greeter = env.newGlobalRef(env.findClass("demo/Greeter"))
13+
14+suite "vm":
15+ test "starts and reports a sane JNI version":
16+ check jvm.isRunning
17+ check env[].GetVersion(env) >= JNI_VERSION_1_8
18+
19+ test "starting twice returns the same VM":
20+ check startJVM().vm == jvm.vm
21+
22+ test "system properties are readable":
23+ let system = env.findClass("java/lang/System")
24+ let v = env.callStatic[:string](system, "getProperty",
25+ "(Ljava/lang/String;)Ljava/lang/String;", "java.version")
26+ check v.len > 0
27+ check v[0].isDigit
28+
29+suite "strings":
30+ test "round-trips ascii":
31+ let s = env.newJString("plain ascii")
32+ check env.toNimString(s) == "plain ascii"
33+ env.deleteLocalRef(s)
34+
35+ test "round-trips text outside the BMP":
36+ # The modified-UTF-8 path (GetStringUTFChars) mangles these; the
37+ # UTF-16 path must not.
38+ for original in ["héllo", "日本語", "🚀🙂", "mixed 🚀 日本 ascii"]:
39+ let s = env.newJString(original)
40+ check env.toNimString(s) == original
41+ env.deleteLocalRef(s)
42+
43+ test "round-trips through Java":
44+ let echoed = env.callStatic[:string](greeter, "shout",
45+ "(Ljava/lang/String;)Ljava/lang/String;", "café")
46+ check echoed == "CAFÉ — 🚀"
47+
48+ test "handles the empty string":
49+ let s = env.newJString("")
50+ check env.toNimString(s) == ""
51+ check env[].GetStringLength(env, s) == 0
52+ env.deleteLocalRef(s)
53+
54+suite "primitives":
55+ test "double":
56+ check env.callStatic[:JDouble](greeter, "mean", "(DD)D", 1.0, 2.0) == 1.5
57+
58+ test "long carries the full 64 bits":
59+ check env.callStatic[:JLong](greeter, "fib", "(J)J", asLong(90)) ==
60+ 2880067194370816120'i64
61+
62+ test "int arguments":
63+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "x")
64+ check env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 3) ==
65+ "Hello, x! Hello, x! Hello, x!"
66+ env.deleteLocalRef(g)
67+
68+ test "boolean returns":
69+ let str = env.newJString("abc")
70+ let strCls = env.findClass("java/lang/String")
71+ check env.call[:bool](str, strCls, "isEmpty", "()Z") == false
72+ env.deleteLocalRef(str)
73+
74+ test "math on the JDK":
75+ let math = env.findClass("java/lang/Math")
76+ check env.callStatic[:JDouble](math, "sqrt", "(D)D", 16.0) == 4.0
77+ check env.callStatic[:JInt](math, "max", "(II)I", 3, 7) == 7
78+
79+suite "objects":
80+ test "constructor and instance method":
81+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "nim")
82+ check g != nil
83+ check env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 1) ==
84+ "Hello, nim!"
85+ env.deleteLocalRef(g)
86+
87+ test "method resolved from the runtime class":
88+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "auto")
89+ check env.call[:string](g, "greet", "(I)Ljava/lang/String;", 1) == "Hello, auto!"
90+ env.deleteLocalRef(g)
91+
92+ test "static field access":
93+ let f = env.staticFieldID(greeter, "callCount", "I")
94+ let before = env[].GetStaticIntField(env, greeter, f)
95+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "counted")
96+ discard env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 1)
97+ check env[].GetStaticIntField(env, greeter, f) == before + 1
98+ env.deleteLocalRef(g)
99+
100+ test "toString works on anything":
101+ let list = env.callStatic[:JObjectRef](greeter, "words", "()Ljava/util/List;")
102+ check env.toStringOf(list) == "[alpha, beta]"
103+ check env.toStringOf(nil) == "null"
104+ env.deleteLocalRef(list)
105+
106+suite "exceptions":
107+ test "a Java throw becomes a Nim raise":
108+ expect JavaError:
109+ env.callStatic[:void](greeter, "explode", "()V")
110+
111+ test "carries class, message and stack trace":
112+ try:
113+ env.callStatic[:void](greeter, "explode", "()V")
114+ check false
115+ except JavaError as e:
116+ check e.className == "java.lang.IllegalStateException"
117+ check e.javaMessage == "deliberate failure from Java"
118+ check "demo.Greeter.explode" in e.stackTrace
119+
120+ test "the exception is cleared, so the env stays usable":
121+ try: env.callStatic[:void](greeter, "explode", "()V")
122+ except JavaError: discard
123+ check env[].ExceptionCheck(env) == JNI_FALSE
124+ check env.callStatic[:JDouble](greeter, "mean", "(DD)D", 2.0, 2.0) == 2.0
125+
126+ test "a missing method is reported, not a crash":
127+ expect JavaError:
128+ discard env.staticMethodID(greeter, "noSuchMethod", "()V")
129+
130+ test "a missing class is reported":
131+ expect JavaError:
132+ discard env.findClass("no/such/Class")
133+
134+ test "exceptions from inside Java propagate out of nested calls":
135+ let ints = env.findClass("java/lang/Integer")
136+ expect JavaError:
137+ discard env.callStatic[:JInt](ints, "parseInt",
138+ "(Ljava/lang/String;)I", "not a number")
139+
140+suite "references":
141+ test "local frames release what they contain":
142+ # 10k local refs without a frame would blow past the default table size.
143+ for i in 0 ..< 10_000:
144+ env.withLocalFrame(4):
145+ let s = env.newJString("temp " & $i)
146+ discard env.toNimString(s)
147+ check true
148+
149+ test "global refs survive frames":
150+ var g: JObjectRef
151+ env.withLocalFrame(4):
152+ g = env.newGlobalRef(env.newJString("kept"))
153+ check env.toNimString(g) == "kept"
154+ env.deleteGlobalRef(g)
155+
156+suite "signatures":
157+ test "descriptorOf":
158+ check descriptorOf("int") == "I"
159+ check descriptorOf("void") == "V"
160+ check descriptorOf("String") == "Ljava/lang/String;"
161+ check descriptorOf("byte[]") == "[B"
162+ check descriptorOf("String[][]") == "[[Ljava/lang/String;"
163+ check descriptorOf("java.util.List") == "Ljava/util/List;"
164+
165+ test "signature":
166+ check signature([], "void") == "()V"
167+ check signature(["int"], "String") == "(I)Ljava/lang/String;"
168+ check signature(["double", "double"], "double") == "(DD)D"
169+
170+ test "a built signature actually dispatches":
171+ check env.callStatic[:JDouble](greeter, "mean",
172+ signature(["double", "double"], "double"), 5.0, 7.0) == 6.0
173+
174+suite "threads":
175+ test "a worker thread attaches, calls Java, and detaches":
176+ var results: array[4, string]
177+ var threads: array[4, Thread[tuple[vm: JavaVM, idx: int, res: ptr string]]]
178+
179+ proc worker(a: tuple[vm: JavaVM, idx: int, res: ptr string]) {.thread.} =
180+ let tenv = envOf(a.vm)
181+ defer: detachOf(a.vm)
182+ let cls = tenv.findClass("demo/Greeter")
183+ a.res[] = tenv.callStatic[:string](cls, "shout",
184+ "(Ljava/lang/String;)Ljava/lang/String;", "thread " & $a.idx)
185+
186+ for i in 0 ..< 4:
187+ createThread(threads[i], worker, (jvm.vm, i, addr results[i]))
188+ joinThreads(threads)
189+
190+ for i in 0 ..< 4:
191+ check results[i].startsWith("THREAD " & $i)
192+
193+env.deleteGlobalRef(greeter)
194+jvm.destroy()
new file mode 100644
@@ -0,0 +1,194 @@
1+## Run with: nim c -r --path:src tests/tlibjava.nim
2+## Requires: javac -d build/classes examples/java/demo/Greeter.java
3+##
4+## Everything lives in one process on purpose -- a JVM can be created exactly
5+## once per process, so these cannot be split into separate test binaries.
6+
7+import std/[unittest, strutils]
8+import libjava
9+
10+let jvm = startJVM(classPath = ["build/classes"], options = ["-Xmx256m"])
11+let env = jvm.env
12+let greeter = env.newGlobalRef(env.findClass("demo/Greeter"))
13+
14+suite "vm":
15+ test "starts and reports a sane JNI version":
16+ check jvm.isRunning
17+ check env[].GetVersion(env) >= JNI_VERSION_1_8
18+
19+ test "starting twice returns the same VM":
20+ check startJVM().vm == jvm.vm
21+
22+ test "system properties are readable":
23+ let system = env.findClass("java/lang/System")
24+ let v = env.callStatic[:string](system, "getProperty",
25+ "(Ljava/lang/String;)Ljava/lang/String;", "java.version")
26+ check v.len > 0
27+ check v[0].isDigit
28+
29+suite "strings":
30+ test "round-trips ascii":
31+ let s = env.newJString("plain ascii")
32+ check env.toNimString(s) == "plain ascii"
33+ env.deleteLocalRef(s)
34+
35+ test "round-trips text outside the BMP":
36+ # The modified-UTF-8 path (GetStringUTFChars) mangles these; the
37+ # UTF-16 path must not.
38+ for original in ["héllo", "日本語", "🚀🙂", "mixed 🚀 日本 ascii"]:
39+ let s = env.newJString(original)
40+ check env.toNimString(s) == original
41+ env.deleteLocalRef(s)
42+
43+ test "round-trips through Java":
44+ let echoed = env.callStatic[:string](greeter, "shout",
45+ "(Ljava/lang/String;)Ljava/lang/String;", "café")
46+ check echoed == "CAFÉ — 🚀"
47+
48+ test "handles the empty string":
49+ let s = env.newJString("")
50+ check env.toNimString(s) == ""
51+ check env[].GetStringLength(env, s) == 0
52+ env.deleteLocalRef(s)
53+
54+suite "primitives":
55+ test "double":
56+ check env.callStatic[:JDouble](greeter, "mean", "(DD)D", 1.0, 2.0) == 1.5
57+
58+ test "long carries the full 64 bits":
59+ check env.callStatic[:JLong](greeter, "fib", "(J)J", asLong(90)) ==
60+ 2880067194370816120'i64
61+
62+ test "int arguments":
63+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "x")
64+ check env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 3) ==
65+ "Hello, x! Hello, x! Hello, x!"
66+ env.deleteLocalRef(g)
67+
68+ test "boolean returns":
69+ let str = env.newJString("abc")
70+ let strCls = env.findClass("java/lang/String")
71+ check env.call[:bool](str, strCls, "isEmpty", "()Z") == false
72+ env.deleteLocalRef(str)
73+
74+ test "math on the JDK":
75+ let math = env.findClass("java/lang/Math")
76+ check env.callStatic[:JDouble](math, "sqrt", "(D)D", 16.0) == 4.0
77+ check env.callStatic[:JInt](math, "max", "(II)I", 3, 7) == 7
78+
79+suite "objects":
80+ test "constructor and instance method":
81+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "nim")
82+ check g != nil
83+ check env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 1) ==
84+ "Hello, nim!"
85+ env.deleteLocalRef(g)
86+
87+ test "method resolved from the runtime class":
88+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "auto")
89+ check env.call[:string](g, "greet", "(I)Ljava/lang/String;", 1) == "Hello, auto!"
90+ env.deleteLocalRef(g)
91+
92+ test "static field access":
93+ let f = env.staticFieldID(greeter, "callCount", "I")
94+ let before = env[].GetStaticIntField(env, greeter, f)
95+ let g = env.newObject(greeter, "(Ljava/lang/String;)V", "counted")
96+ discard env.call[:string](g, greeter, "greet", "(I)Ljava/lang/String;", 1)
97+ check env[].GetStaticIntField(env, greeter, f) == before + 1
98+ env.deleteLocalRef(g)
99+
100+ test "toString works on anything":
101+ let list = env.callStatic[:JObjectRef](greeter, "words", "()Ljava/util/List;")
102+ check env.toStringOf(list) == "[alpha, beta]"
103+ check env.toStringOf(nil) == "null"
104+ env.deleteLocalRef(list)
105+
106+suite "exceptions":
107+ test "a Java throw becomes a Nim raise":
108+ expect JavaError:
109+ env.callStatic[:void](greeter, "explode", "()V")
110+
111+ test "carries class, message and stack trace":
112+ try:
113+ env.callStatic[:void](greeter, "explode", "()V")
114+ check false
115+ except JavaError as e:
116+ check e.className == "java.lang.IllegalStateException"
117+ check e.javaMessage == "deliberate failure from Java"
118+ check "demo.Greeter.explode" in e.stackTrace
119+
120+ test "the exception is cleared, so the env stays usable":
121+ try: env.callStatic[:void](greeter, "explode", "()V")
122+ except JavaError: discard
123+ check env[].ExceptionCheck(env) == JNI_FALSE
124+ check env.callStatic[:JDouble](greeter, "mean", "(DD)D", 2.0, 2.0) == 2.0
125+
126+ test "a missing method is reported, not a crash":
127+ expect JavaError:
128+ discard env.staticMethodID(greeter, "noSuchMethod", "()V")
129+
130+ test "a missing class is reported":
131+ expect JavaError:
132+ discard env.findClass("no/such/Class")
133+
134+ test "exceptions from inside Java propagate out of nested calls":
135+ let ints = env.findClass("java/lang/Integer")
136+ expect JavaError:
137+ discard env.callStatic[:JInt](ints, "parseInt",
138+ "(Ljava/lang/String;)I", "not a number")
139+
140+suite "references":
141+ test "local frames release what they contain":
142+ # 10k local refs without a frame would blow past the default table size.
143+ for i in 0 ..< 10_000:
144+ env.withLocalFrame(4):
145+ let s = env.newJString("temp " & $i)
146+ discard env.toNimString(s)
147+ check true
148+
149+ test "global refs survive frames":
150+ var g: JObjectRef
151+ env.withLocalFrame(4):
152+ g = env.newGlobalRef(env.newJString("kept"))
153+ check env.toNimString(g) == "kept"
154+ env.deleteGlobalRef(g)
155+
156+suite "signatures":
157+ test "descriptorOf":
158+ check descriptorOf("int") == "I"
159+ check descriptorOf("void") == "V"
160+ check descriptorOf("String") == "Ljava/lang/String;"
161+ check descriptorOf("byte[]") == "[B"
162+ check descriptorOf("String[][]") == "[[Ljava/lang/String;"
163+ check descriptorOf("java.util.List") == "Ljava/util/List;"
164+
165+ test "signature":
166+ check signature([], "void") == "()V"
167+ check signature(["int"], "String") == "(I)Ljava/lang/String;"
168+ check signature(["double", "double"], "double") == "(DD)D"
169+
170+ test "a built signature actually dispatches":
171+ check env.callStatic[:JDouble](greeter, "mean",
172+ signature(["double", "double"], "double"), 5.0, 7.0) == 6.0
173+
174+suite "threads":
175+ test "a worker thread attaches, calls Java, and detaches":
176+ var results: array[4, string]
177+ var threads: array[4, Thread[tuple[vm: JavaVM, idx: int, res: ptr string]]]
178+
179+ proc worker(a: tuple[vm: JavaVM, idx: int, res: ptr string]) {.thread.} =
180+ let tenv = envOf(a.vm)
181+ defer: detachOf(a.vm)
182+ let cls = tenv.findClass("demo/Greeter")
183+ a.res[] = tenv.callStatic[:string](cls, "shout",
184+ "(Ljava/lang/String;)Ljava/lang/String;", "thread " & $a.idx)
185+
186+ for i in 0 ..< 4:
187+ createThread(threads[i], worker, (jvm.vm, i, addr results[i]))
188+ joinThreads(threads)
189+
190+ for i in 0 ..< 4:
191+ check results[i].startsWith("THREAD " & $i)
192+
193+env.deleteGlobalRef(greeter)
194+jvm.destroy()