Clojure interoperability with the native world

copying @borkdude

Things are looking up on expanding the native front for clojure with espresso (jvm on truffle on graal, which enables dynamic JIT’d code and reflection for native-image).

I just got this working in jshell-espresso as demonstrated here, which is running clojure through espresso:

(require '[clojure.pprint :refer [pprint]])

(defn test-program [n]
  (dotimes [i n]
    (println [:hello i])))

(println [:testing-clojure-from-espresso!])

(test-program 10)

;;let's see if macros work...
(defmacro dumb-macro [n]
  `[~@(->> (range n) (map inc))])

(pprint (dumb-macro 10))

(defprotocol IBlah
  (blah [this]))
(defrecord rec [x]
  IBlah
  (blah [this] (pprint [:blah x])))

(blah (->rec {:hello {:world {:how {:are :you?}}}}))
:done!

I can’t get the Xmx options to work yet (I think there’s going to be some heavy GC for real programs), but still, it works. The jar is an earlier uberjar from a test, which in this case is just bundling clojure 1.10 and spec together.

joinr@DESKTOP-UIAPQD5:~/repos/graalvm-demos/espresso-jshell$ ./espresso-jshell -Xmx4g --class-path truffletest-0.1.0-SNAPSHOT-standalone.jar
|  Welcome to JShell -- Version 11.0.10
|  For an introduction type: /help intro

jshell> String [] args = {"-e", "(load-file \"script.clj\")"}
args ==> String[2] { "-e", "(load-file \"script.clj\")" }

jshell> clojure.main.main(args)
[:testing-clojure-from-espresso!]
[:hello 0]
[:hello 1]
[:hello 2]
[:hello 3]
[:hello 4]
[:hello 5]
[:hello 6]
[:hello 7]
[:hello 8]
[:hello 9]
[1 2 3 4 5 6 7 8 9 10]
[:blah {:hello {:world {:how {:are :you?}}}}]
:done!

jshell> /exit

This is normally impossible, but jshell-espresso has hooks to allow dynamic classloading (essential for clojure function definition and anything touching eval or generating bytecode at runtime) that is running on espresso at runtime. I’m also very very curious to see if (via espresso) one can capture image-based development akin to common lisp/small talk since we have the JVM state in memory. It may be possible to have the equivalent of sbcl’s save-lisp-and-die to enable fast-loading tooling at some point. Performance is supposed to improve rapidly. Having trouble with the default clojure.main repl (it’s not playing nice with jshell-espresso, probably due to clobbering io), but evaluating scripts at runtime works (with all of clojure). At the moment this is akin to shipping an entire jdk though (espresso-jshell is like 80mb; curious if that can be tree-shaked down)…but I can imagine tying this back into a bundled native-image somehow for a shared lib or smaller executable (exposing an api via JNA/JNI as well for “portability”). It’d be kind of odd though, since you would still end up with a model of multiple “jvms” per shared library, absent some additional infrastructure. I think it’s at least looking possible going forward.

If you can live with closed-world assumptions (no eval, no runtime codegen or class loading), then exposing a c ABI via JNA/JNI is probably acceptable as well. At this point though, clojure is just the implementation language hidden behind a portable C interface, so maybe not pushing clojure code up front so much as you indicated (aside from submitting queries to datalevin in EDN and receiving results in EDN maybe).

I think the libpython-clj (and libjulia, and libapl, and clojisr) are trying very hard to achieve the same bridging strategy (with an eye toward clojure “taking” at first, but also “giving” at some point). The “taking” strategy is more to provide a path for python/r users (much like clojure did for java/ruby and some python) to migrate toward clojure, and allow clojure users to tap into the disparate ecosystems (or wrap existing code).

5 Likes