Last active
November 3, 2021 13:47
-
-
Save taylorwood/bb3ebfec5d5de3cccc867a9eba216c18 to your computer and use it in GitHub Desktop.
GraalVM polyglot interop between Clojure and JavaScript https://blog.taylorwood.io/2018/11/26/graal-polyglot.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns polydact.core | |
(:import (clojure.lang IFn) | |
(org.graalvm.polyglot Context Value) | |
(org.graalvm.polyglot.proxy ProxyArray ProxyExecutable ProxyObject))) | |
(set! *warn-on-reflection* true) | |
(comment | |
(do | |
(def context | |
(-> (Context/newBuilder (into-array ["js"])) | |
(.allowHostAccess HostAccess/ALL) | |
(.build))) | |
(defn ^Value eval-js [code] | |
(.eval ^Context context "js" code))) | |
(.as (eval-js "Number.MAX_VALUE") Object) | |
;=> 1.7976931348623157E308 | |
(type *1) | |
;=> java.lang.Double | |
(.as (eval-js "[{}]") Object) | |
;=> {"0" {}} | |
(.as (eval-js "[{}]") java.util.List) | |
;=> ({}) | |
#_cool!) | |
(defn- execute | |
[^Value execable & args] | |
(.execute execable (object-array args))) | |
(declare value->clj) | |
(defmacro ^:private reify-ifn | |
"Convenience macro for reifying IFn for executable polyglot Values." | |
[v] | |
(let [invoke-arity | |
(fn [n] | |
(let [args (map #(symbol (str "arg" (inc %))) (range n))] | |
(if (seq args) | |
;; TODO test edge case for final `invoke` arity w/varargs | |
`(~'invoke [this# ~@args] (value->clj (execute ~v ~@args))) | |
`(~'invoke [this#] (value->clj (execute ~v))))))] | |
`(reify IFn | |
~@(map invoke-arity (range 22)) | |
(~'applyTo [this# args#] (value->clj (apply execute ~v args#)))))) | |
(macroexpand '(reify-ifn v)) | |
(defn proxy-fn | |
"Returns a ProxyExecutable instance for given function, allowing it to be | |
invoked from polyglot contexts." | |
[f] | |
(reify ProxyExecutable | |
(execute [_this args] | |
(apply f (map value->clj args))))) | |
(defn value->clj | |
"Returns a Clojure (or Java) value for given polyglot Value if possible, | |
otherwise throws." | |
[^Value v] | |
(cond | |
(.isNull v) nil | |
(.isHostObject v) (.asHostObject v) | |
(.isBoolean v) (.asBoolean v) | |
(.isString v) (.asString v) | |
(.isNumber v) (.as v Number) | |
(.canExecute v) (reify-ifn v) | |
(.hasArrayElements v) (into [] | |
(for [i (range (.getArraySize v))] | |
(value->clj (.getArrayElement v i)))) | |
(.hasMembers v) (into {} | |
(for [k (.getMemberKeys v)] | |
[k (value->clj (.getMember v k))])) | |
:else (throw (Exception. "Unsupported value")))) | |
(comment | |
(def js->clj (comp value->clj eval-js)) | |
(js->clj "[{}]") | |
;=> [{}] | |
(js->clj "false") | |
;=> false | |
(js->clj "3 / 3.33") | |
;=> 0.9009009009009009 | |
(js->clj "123123123123123123123123123123123") | |
;=> 1.2312312312312312E32 | |
(def doubler (js->clj "(n) => {return n * 2;}")) | |
(doubler 2) | |
;=> 4 | |
(js->clj "m = {foo: 1, bar: '2', baz: {0: false}};") | |
;=> {"foo" 1, "bar" "2", "baz" {"0" false}} | |
(def factorial | |
(eval-js " | |
var m = []; | |
function factorial (n) { | |
if (n == 0 || n == 1) return 1; | |
if (m[n] > 0) return m[n]; | |
return m[n] = factorial(n - 1) * n; | |
} | |
x = {fn: factorial, memos: m};")) | |
((get (value->clj factorial) "fn") 12) | |
;=> 479001600 | |
(get (value->clj factorial) "memos") | |
;=> [nil nil 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600] | |
((get (value->clj factorial) "fn") 24) | |
;=> 6.204484017332394E23 | |
(get (value->clj factorial) "memos") | |
;=> [nil nil 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600 ... truncated for brevity] | |
(eval-js "var foo = 0xFFFF") | |
(eval-js "console.log(foo);") | |
;=> #object[org.graalvm.polyglot.Value 0x3f9d2028 "undefined"] | |
;65535 | |
(js->clj "1 + '1'") | |
;=> "11" | |
(js->clj "['foo', 10, 2].sort()") | |
;=> [10 2 "foo"] | |
(def js-aset | |
(js->clj "(arr, idx, val) => { arr[idx] = val; return arr; }")) | |
(js-aset (ProxyArray/fromArray (object-array [1 2 3])) 1 nil) | |
;=> [1 nil 3] | |
(sort [{:b nil} \a 1 "a" "A" #{\a} :foo -1 0 {:a nil} "bar"]) | |
(def js-sort | |
(js->clj "(...vs) => { return vs.sort(); }")) | |
(apply js-sort [{:b nil} \a 1 "a" "A" #{\a} :foo -1 0 {:a nil} "bar"]) | |
;=> [-1 0 1 "A" #{\a} :foo {:a nil} {:b nil} "a" "a" "bar"] | |
(def variadic-fn | |
(js->clj "(x, y, ...z) => { return [x, y, z]; }")) | |
(apply variadic-fn :foo :bar (range 3)) | |
;=> [:foo :bar [0 1 2]] | |
(def ->json | |
(js->clj "(x) => { return JSON.stringify(x); }")) | |
(->json [1 2 3]) | |
;=> "[1,2,3]" | |
(->json (ProxyObject/fromMap {"foo" 1, "bar" nil})) | |
;=> "{\"foo\":1,\"bar\":null}" | |
(def json-> | |
(js->clj "(x) => { return JSON.parse(x); }")) | |
(json-> (->json [1 2 3])) | |
;=> [1 2 3] | |
(json-> (->json (ProxyObject/fromMap {"foo" 1}))) | |
;=> {"foo" 1} | |
(def json-object | |
(js->clj "(m) => { return m.foo + m.foo; }")) | |
(json-object (ProxyObject/fromMap {"foo" 1})) | |
;=> 2 | |
(def clj-lambda | |
(js->clj " | |
m = {foo: [1, 2, 3], | |
bar: { | |
baz: ['a', 'z'] | |
}}; | |
(fn) => { return fn(m); } | |
")) | |
(clj-lambda | |
(proxy-fn #(clojure.walk/prewalk | |
(fn [v] (if (and (vector? v) | |
(not (map-entry? v))) | |
(vec (reverse v)) | |
v)) | |
%))) | |
;=> {"foo" [3 2 1], "bar" {"baz" ["z" "a"]}} | |
(def js-reduce | |
(let [reduce (js->clj "(f, coll) => { return coll.reduce(f); }") | |
reduce-init (js->clj "(f, coll, init) => { return coll.reduce(f, init); }")] | |
(fn | |
([f coll] (reduce f coll)) | |
([f init coll] (reduce-init f coll init))))) | |
(js-reduce + (range 10)) | |
;=> 45 | |
(js-reduce + -5.5 (range 10)) | |
;=> 39.5 | |
(js-reduce (fn [acc elem] | |
(assoc acc (keyword (str elem)) (doubler elem))) | |
{} | |
(range 5)) | |
;=> {:0 0, :1 2, :2 4, :3 6, :4 8} | |
(def log-coll | |
(js->clj "(coll) => { for (i in coll) console.log(coll[i]); }")) | |
(log-coll (repeatedly 3 #(do (prn 'sleeping) | |
(Thread/sleep 100) | |
(rand)))) | |
(log-coll (range)) | |
"nice") |
@taylorwood, thanks for helping out! It works for me too now.
I just had a few more questions if you don't mind:
(.as (eval-js "[{}]") Object)
=> I get the output for this as{}
- any possible explanations?- I want to play around with this a little more. Could you point me to something interesting?
- I also want to learn more about GraalVM any place that offers the best info apart from the docs?
Thanks again, @taylorwood
Hello @taylorwood,
I have found your interesting post on GraalVM Polyglot with Clojure and tried to play with it! Unfortunately when I try to run it with lein I have the following error
Exception in thread "main" java.lang.IllegalArgumentException: Could not find option with name version., compiling:(core.clj:30:14)
which refers to this line:
(def context (.build (Context/newBuilder (into-array ["js"]))))
No idea what I am doing wrong ! I am using GraalVM 20.1, java11 on Linux... The problem arise with dependency Clojure 1.9.0 or 1.10.1...
Thanks for any advice
@taylorwood very informative, thank you!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@kolharsam I think some Graal security stuff has changed since I wrote this. Try setting
(.allowHostAccess context HostAccess/ALL)
. This works for me now:More info here https://github.com/graalvm/graaljs/blob/master/docs/user/ScriptEngine.md. Thanks!