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>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() | ||