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

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

Add a Clojure interop example

Calls Clojure from Nim through its documented Java API, which needs no
Clojure-specific support in the library -- clojure.java.api.Clojure/var to
resolve a var into a clojure.lang.IFn, then IFn/invoke to call it. Requiring
a namespace, reading a keyword and walking a returned map are the same pair
aimed at clojure.core.

The one sharp edge worth showing: IFn/invoke is Object-typed, so primitives
have to be boxed through java.lang.Long.valueOf before they can be passed.

deps.edn pins the dependency; `just clojure-cp` resolves it with `clojure
-Spath` into build/clojure-classpath, which the example reads. .cpcache/ is
ignored and cleaned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T14:43:08-07:00 Browse files
0f4b377 parent: 066bac4
modified .gitignore +4 -0
@@ -2,8 +2,12 @@
22 build/
33 out/
44 nimcache/
5+.cpcache/
56 *.class
67
78 # Compiled test/example binaries (nim c -r drops these next to the source)
89 /tests/tlibjava
910 /examples/demo
11+
12+# Clojure example binary
13+/examples/clojure_demo
@@ -2,8 +2,12 @@
2 build/2 build/
3 out/3 out/
4 nimcache/4 nimcache/
5+.cpcache/
5 *.class6 *.class
6 7
7 # Compiled test/example binaries (nim c -r drops these next to the source)8 # Compiled test/example binaries (nim c -r drops these next to the source)
8 /tests/tlibjava9 /tests/tlibjava
9 /examples/demo10 /examples/demo
11+
12+# Clojure example binary
13+/examples/clojure_demo
modified README.md +26 -0
@@ -42,6 +42,32 @@ this code is itself a shared library loaded *by* a JVM.
4242 | `libjava/core` | strings, exceptions, typed calls, signatures |
4343 | `libjava/vm` | JVM lifecycle, per-thread `JNIEnv` |
4444
45+| Example | |
46+|---|---|
47+| `examples/demo.nim` | Java: statics, objects, fields, exceptions, collections |
48+| `examples/clojure_demo.nim` | Clojure via `clojure.java.api.Clojure` |
49+
50+## Other JVM languages
51+
52+Anything that compiles to JVM bytecode is reachable without a line of
53+language-specific support — it's just jars on the classpath.
54+`examples/clojure_demo.nim` calls Clojure through its documented Java API,
55+which is two members wide:
56+
57+```nim
58+# clojure.java.api.Clojure/var -> a clojure.lang.IFn; then IFn/invoke
59+let greet = env.callStatic[:JObjectRef](clojure, "var",
60+ "(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;", "demo.core", "greet")
61+discard env.call[:JObjectRef](greet, ifn, "invoke",
62+ "(Ljava/lang/Object;)Ljava/lang/Object;", arg)
63+```
64+
65+Requiring a namespace, reading a keyword and walking a Clojure map are all that
66+same pair pointed at `clojure.core`. Run it with `just clojure`, which needs the
67+`clojure` CLI on `PATH`; `deps.edn` pins the dependency and `just clojure-cp`
68+resolves it into `build/clojure-classpath`. Note that `IFn/invoke` takes
69+`Object`, so primitives must be boxed — `java.lang.Long.valueOf` in the example.
70+
4571 ## Calling
4672
4773 Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
@@ -42,6 +42,32 @@ this code is itself a shared library loaded *by* a JVM.
42 | `libjava/core` | strings, exceptions, typed calls, signatures |42 | `libjava/core` | strings, exceptions, typed calls, signatures |
43 | `libjava/vm` | JVM lifecycle, per-thread `JNIEnv` |43 | `libjava/vm` | JVM lifecycle, per-thread `JNIEnv` |
44 44
45+| Example | |
46+|---|---|
47+| `examples/demo.nim` | Java: statics, objects, fields, exceptions, collections |
48+| `examples/clojure_demo.nim` | Clojure via `clojure.java.api.Clojure` |
49+
50+## Other JVM languages
51+
52+Anything that compiles to JVM bytecode is reachable without a line of
53+language-specific support — it's just jars on the classpath.
54+`examples/clojure_demo.nim` calls Clojure through its documented Java API,
55+which is two members wide:
56+
57+```nim
58+# clojure.java.api.Clojure/var -> a clojure.lang.IFn; then IFn/invoke
59+let greet = env.callStatic[:JObjectRef](clojure, "var",
60+ "(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;", "demo.core", "greet")
61+discard env.call[:JObjectRef](greet, ifn, "invoke",
62+ "(Ljava/lang/Object;)Ljava/lang/Object;", arg)
63+```
64+
65+Requiring a namespace, reading a keyword and walking a Clojure map are all that
66+same pair pointed at `clojure.core`. Run it with `just clojure`, which needs the
67+`clojure` CLI on `PATH`; `deps.edn` pins the dependency and `just clojure-cp`
68+resolves it into `build/clojure-classpath`. Note that `IFn/invoke` takes
69+`Object`, so primitives must be boxed — `java.lang.Long.valueOf` in the example.
70+
45 ## Calling71 ## Calling
46 72
47 Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact73 Method signatures are JVM descriptors. `javap -s SomeClass` prints the exact
added deps.edn +5 -0
new file mode 100644
@@ -0,0 +1,5 @@
1+;; Dependencies for the Clojure interop example (examples/clojure_demo.nim).
2+;; `just clojure` turns this into build/clojure-classpath via `clojure -Spath`.
3+;; The Nim library itself needs none of this -- only a JDK.
4+{:paths ["examples/clojure"]
5+ :deps {org.clojure/clojure {:mvn/version "1.12.5"}}}
new file mode 100644
@@ -0,0 +1,5 @@
1+;; Dependencies for the Clojure interop example (examples/clojure_demo.nim).
2+;; `just clojure` turns this into build/clojure-classpath via `clojure -Spath`.
3+;; The Nim library itself needs none of this -- only a JDK.
4+{:paths ["examples/clojure"]
5+ :deps {org.clojure/clojure {:mvn/version "1.12.5"}}}
added examples/clojure/demo/core.clj +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+(ns demo.core
2+ (:require [clojure.string :as str]))
3+
4+(defn greet
5+ "Plain function, plain arguments — the easy case from JNI."
6+ [name times]
7+ (str/join " " (repeat times (str "Hello, " name "!"))))
8+
9+(defn stats
10+ "Returns a Clojure map. From Nim this arrives as a clojure.lang.IPersistentMap,
11+ which is navigable with clojure.core/get rather than any Java accessor."
12+ [xs]
13+ (let [xs (seq xs)
14+ n (count xs)]
15+ {:count n
16+ :sum (reduce + xs)
17+ :mean (/ (double (reduce + xs)) n)
18+ :max (apply max xs)}))
19+
20+(defn fizzbuzz
21+ "Returns a lazy seq, realised on the Nim side by calling into it."
22+ [n]
23+ (map (fn [i]
24+ (cond (zero? (mod i 15)) "fizzbuzz"
25+ (zero? (mod i 3)) "fizz"
26+ (zero? (mod i 5)) "buzz"
27+ :else (str i)))
28+ (range 1 (inc n))))
29+
30+(defn explode
31+ "Clojure's ex-info carries a data map; it still surfaces in Nim as a JavaError."
32+ []
33+ (throw (ex-info "deliberate failure from Clojure" {:origin :demo.core})))
new file mode 100644
@@ -0,0 +1,33 @@
1+(ns demo.core
2+ (:require [clojure.string :as str]))
3+
4+(defn greet
5+ "Plain function, plain arguments — the easy case from JNI."
6+ [name times]
7+ (str/join " " (repeat times (str "Hello, " name "!"))))
8+
9+(defn stats
10+ "Returns a Clojure map. From Nim this arrives as a clojure.lang.IPersistentMap,
11+ which is navigable with clojure.core/get rather than any Java accessor."
12+ [xs]
13+ (let [xs (seq xs)
14+ n (count xs)]
15+ {:count n
16+ :sum (reduce + xs)
17+ :mean (/ (double (reduce + xs)) n)
18+ :max (apply max xs)}))
19+
20+(defn fizzbuzz
21+ "Returns a lazy seq, realised on the Nim side by calling into it."
22+ [n]
23+ (map (fn [i]
24+ (cond (zero? (mod i 15)) "fizzbuzz"
25+ (zero? (mod i 3)) "fizz"
26+ (zero? (mod i 5)) "buzz"
27+ :else (str i)))
28+ (range 1 (inc n))))
29+
30+(defn explode
31+ "Clojure's ex-info carries a data map; it still surfaces in Nim as a JavaError."
32+ []
33+ (throw (ex-info "deliberate failure from Clojure" {:origin :demo.core})))
added examples/clojure_demo.nim +111 -0
new file mode 100644
@@ -0,0 +1,111 @@
1+## Calling Clojure from Nim.
2+##
3+## Run with: just clojure
4+##
5+## There is no Clojure-specific JNI. Clojure is a jar on the classpath, and the
6+## whole language is reachable through two members of its documented Java API:
7+##
8+## clojure.java.api.Clojure/var -- look a var up by namespace and name,
9+## handing back a clojure.lang.IFn
10+## clojure.lang.IFn/invoke -- call it
11+##
12+## Everything else here -- requiring a namespace, reading a keyword, pulling a
13+## value out of a Clojure map -- is those two calls applied to `clojure.core`.
14+
15+import std/[strformat, os, strutils]
16+import libjava
17+
18+proc classPath(): seq[string] =
19+ ## Resolved from deps.edn by `clojure -Spath`; `just clojure` puts it here.
20+ let f = "build/clojure-classpath"
21+ if not fileExists(f):
22+ quit "missing " & f & " -- run `just clojure` rather than nim directly"
23+ result = readFile(f).strip().split(':')
24+
25+proc main() =
26+ let jvm = startJVM(classPath = classPath(), options = ["-Xmx512m"])
27+ defer: jvm.destroy()
28+ let env = jvm.env
29+
30+ # --- the two entry points -------------------------------------------------
31+ let clojure = env.findClass("clojure/java/api/Clojure")
32+ let ifn = env.findClass("clojure/lang/IFn")
33+
34+ proc lookup(ns, name: string): JObjectRef =
35+ ## clojure.java.api.Clojure/var -- note both parameters are Object, so the
36+ ## strings we pass are boxed as java.lang.String and read by Clojure itself.
37+ env.callStatic[:JObjectRef](clojure, "var",
38+ "(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;", ns, name)
39+
40+ proc read(s: string): JObjectRef =
41+ ## clojure.java.api.Clojure/read -- the reader, for keywords and literals.
42+ env.callStatic[:JObjectRef](clojure, "read",
43+ "(Ljava/lang/String;)Ljava/lang/Object;", s)
44+
45+ proc invoke(f: JObjectRef, a: JObjectRef): JObjectRef =
46+ env.call[:JObjectRef](f, ifn, "invoke",
47+ "(Ljava/lang/Object;)Ljava/lang/Object;", a)
48+
49+ proc invoke(f: JObjectRef, a, b: JObjectRef): JObjectRef =
50+ env.call[:JObjectRef](f, ifn, "invoke",
51+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", a, b)
52+
53+ # Boxing: Clojure's IFn takes Objects, so primitives must become java.lang.Long.
54+ let longCls = env.findClass("java/lang/Long")
55+ proc box(n: int64): JObjectRef =
56+ env.callStatic[:JObjectRef](longCls, "valueOf", "(J)Ljava/lang/Long;", asLong(n))
57+
58+ # --- load our namespace ---------------------------------------------------
59+ # (require 'demo.core) -- the symbol comes from the reader, not from a string.
60+ block:
61+ let require = lookup("clojure.core", "require")
62+ discard require.invoke(read("demo.core"))
63+ echo "required demo.core"
64+
65+ # --- call a plain function ------------------------------------------------
66+ block:
67+ let greet = lookup("demo.core", "greet")
68+ let s = greet.invoke(env.newJString("world"), box(2))
69+ echo "greet = ", env.toStringOf(s)
70+
71+ # --- a function returning a Clojure map -----------------------------------
72+ block:
73+ env.withLocalFrame(16):
74+ let stats = lookup("demo.core", "stats")
75+ let vec = lookup("clojure.core", "vector")
76+ let data = env.call[:JObjectRef](vec, ifn, "invoke",
77+ "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
78+ box(3), box(1), box(4), box(1))
79+ let m = stats.invoke(data)
80+ echo "stats = ", env.toStringOf(m)
81+
82+ # Navigate the map with clojure.core/get rather than a Java accessor.
83+ let get = lookup("clojure.core", "get")
84+ let sum = get.invoke(m, read(":sum"))
85+ echo " :sum = ", env.toStringOf(sum)
86+
87+ # Unbox properly: every Clojure number is a java.lang.Number.
88+ let numberCls = env.findClass("java/lang/Number")
89+ echo " :sum as Nim int = ",
90+ env.call[:JLong](sum, numberCls, "longValue", "()J")
91+
92+ # --- a lazy seq, realised from Nim ----------------------------------------
93+ block:
94+ env.withLocalFrame(32):
95+ let fizzbuzz = lookup("demo.core", "fizzbuzz")
96+ let s = fizzbuzz.invoke(box(15))
97+ # pr-str gives us Clojure's own printer rather than Java's toString.
98+ let prStr = lookup("clojure.core", "pr-str")
99+ echo "fizzbuzz = ", env.toStringOf(prStr.invoke(s))
100+
101+ # --- ex-info arriving as a Nim exception ----------------------------------
102+ block:
103+ let explode = lookup("demo.core", "explode")
104+ try:
105+ discard env.call[:JObjectRef](explode, ifn, "invoke", "()Ljava/lang/Object;")
106+ except JavaError as e:
107+ echo &"caught {e.className}: {e.javaMessage}"
108+
109+ echo "done"
110+
111+main()
new file mode 100644
@@ -0,0 +1,111 @@
1+## Calling Clojure from Nim.
2+##
3+## Run with: just clojure
4+##
5+## There is no Clojure-specific JNI. Clojure is a jar on the classpath, and the
6+## whole language is reachable through two members of its documented Java API:
7+##
8+## clojure.java.api.Clojure/var -- look a var up by namespace and name,
9+## handing back a clojure.lang.IFn
10+## clojure.lang.IFn/invoke -- call it
11+##
12+## Everything else here -- requiring a namespace, reading a keyword, pulling a
13+## value out of a Clojure map -- is those two calls applied to `clojure.core`.
14+
15+import std/[strformat, os, strutils]
16+import libjava
17+
18+proc classPath(): seq[string] =
19+ ## Resolved from deps.edn by `clojure -Spath`; `just clojure` puts it here.
20+ let f = "build/clojure-classpath"
21+ if not fileExists(f):
22+ quit "missing " & f & " -- run `just clojure` rather than nim directly"
23+ result = readFile(f).strip().split(':')
24+
25+proc main() =
26+ let jvm = startJVM(classPath = classPath(), options = ["-Xmx512m"])
27+ defer: jvm.destroy()
28+ let env = jvm.env
29+
30+ # --- the two entry points -------------------------------------------------
31+ let clojure = env.findClass("clojure/java/api/Clojure")
32+ let ifn = env.findClass("clojure/lang/IFn")
33+
34+ proc lookup(ns, name: string): JObjectRef =
35+ ## clojure.java.api.Clojure/var -- note both parameters are Object, so the
36+ ## strings we pass are boxed as java.lang.String and read by Clojure itself.
37+ env.callStatic[:JObjectRef](clojure, "var",
38+ "(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;", ns, name)
39+
40+ proc read(s: string): JObjectRef =
41+ ## clojure.java.api.Clojure/read -- the reader, for keywords and literals.
42+ env.callStatic[:JObjectRef](clojure, "read",
43+ "(Ljava/lang/String;)Ljava/lang/Object;", s)
44+
45+ proc invoke(f: JObjectRef, a: JObjectRef): JObjectRef =
46+ env.call[:JObjectRef](f, ifn, "invoke",
47+ "(Ljava/lang/Object;)Ljava/lang/Object;", a)
48+
49+ proc invoke(f: JObjectRef, a, b: JObjectRef): JObjectRef =
50+ env.call[:JObjectRef](f, ifn, "invoke",
51+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", a, b)
52+
53+ # Boxing: Clojure's IFn takes Objects, so primitives must become java.lang.Long.
54+ let longCls = env.findClass("java/lang/Long")
55+ proc box(n: int64): JObjectRef =
56+ env.callStatic[:JObjectRef](longCls, "valueOf", "(J)Ljava/lang/Long;", asLong(n))
57+
58+ # --- load our namespace ---------------------------------------------------
59+ # (require 'demo.core) -- the symbol comes from the reader, not from a string.
60+ block:
61+ let require = lookup("clojure.core", "require")
62+ discard require.invoke(read("demo.core"))
63+ echo "required demo.core"
64+
65+ # --- call a plain function ------------------------------------------------
66+ block:
67+ let greet = lookup("demo.core", "greet")
68+ let s = greet.invoke(env.newJString("world"), box(2))
69+ echo "greet = ", env.toStringOf(s)
70+
71+ # --- a function returning a Clojure map -----------------------------------
72+ block:
73+ env.withLocalFrame(16):
74+ let stats = lookup("demo.core", "stats")
75+ let vec = lookup("clojure.core", "vector")
76+ let data = env.call[:JObjectRef](vec, ifn, "invoke",
77+ "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
78+ box(3), box(1), box(4), box(1))
79+ let m = stats.invoke(data)
80+ echo "stats = ", env.toStringOf(m)
81+
82+ # Navigate the map with clojure.core/get rather than a Java accessor.
83+ let get = lookup("clojure.core", "get")
84+ let sum = get.invoke(m, read(":sum"))
85+ echo " :sum = ", env.toStringOf(sum)
86+
87+ # Unbox properly: every Clojure number is a java.lang.Number.
88+ let numberCls = env.findClass("java/lang/Number")
89+ echo " :sum as Nim int = ",
90+ env.call[:JLong](sum, numberCls, "longValue", "()J")
91+
92+ # --- a lazy seq, realised from Nim ----------------------------------------
93+ block:
94+ env.withLocalFrame(32):
95+ let fizzbuzz = lookup("demo.core", "fizzbuzz")
96+ let s = fizzbuzz.invoke(box(15))
97+ # pr-str gives us Clojure's own printer rather than Java's toString.
98+ let prStr = lookup("clojure.core", "pr-str")
99+ echo "fizzbuzz = ", env.toStringOf(prStr.invoke(s))
100+
101+ # --- ex-info arriving as a Nim exception ----------------------------------
102+ block:
103+ let explode = lookup("demo.core", "explode")
104+ try:
105+ discard env.call[:JObjectRef](explode, ifn, "invoke", "()Ljava/lang/Object;")
106+ except JavaError as e:
107+ echo &"caught {e.className}: {e.javaMessage}"
108+
109+ echo "done"
110+
111+main()
modified justfile +12 -1
@@ -17,6 +17,17 @@ test:
1717 demo:
1818 nimble demo
1919
20+# Resolve the Clojure classpath from deps.edn into build/clojure-classpath
21+clojure-cp:
22+ #!/usr/bin/env bash
23+ set -euo pipefail
24+ mkdir -p build
25+ clojure -Spath > build/clojure-classpath
26+
27+# Run the Clojure interop example (needs the `clojure` CLI on PATH)
28+clojure: clojure-cp
29+ nim c -r --path:src examples/clojure_demo.nim
30+
2031 # Compile the example binary into out/ (release build, nothing left in-tree)
2132 build: java
2233 nim c -d:release --path:src --nimcache:build/nimcache -o:out/demo examples/demo.nim
@@ -36,7 +47,7 @@ docs:
3647
3748 # Remove build outputs and compiled test/example binaries
3849 clean:
39- rm -rf build nimcache out
50+ rm -rf build nimcache out .cpcache
4051 rm -f tests/tlibjava examples/demo
4152
4253 # Print the JDK this build will link against
@@ -17,6 +17,17 @@ test:
17 demo:17 demo:
18 nimble demo18 nimble demo
19 19
20+# Resolve the Clojure classpath from deps.edn into build/clojure-classpath
21+clojure-cp:
22+ #!/usr/bin/env bash
23+ set -euo pipefail
24+ mkdir -p build
25+ clojure -Spath > build/clojure-classpath
26+
27+# Run the Clojure interop example (needs the `clojure` CLI on PATH)
28+clojure: clojure-cp
29+ nim c -r --path:src examples/clojure_demo.nim
30+
20 # Compile the example binary into out/ (release build, nothing left in-tree)31 # Compile the example binary into out/ (release build, nothing left in-tree)
21 build: java32 build: java
22 nim c -d:release --path:src --nimcache:build/nimcache -o:out/demo examples/demo.nim33 nim c -d:release --path:src --nimcache:build/nimcache -o:out/demo examples/demo.nim
@@ -36,7 +47,7 @@ docs:
36 47
37 # Remove build outputs and compiled test/example binaries48 # Remove build outputs and compiled test/example binaries
38 clean:49 clean:
39- rm -rf build nimcache out50+ rm -rf build nimcache out .cpcache
40 rm -f tests/tlibjava examples/demo51 rm -f tests/tlibjava examples/demo
41 52
42 # Print the JDK this build will link against53 # Print the JDK this build will link against