Skip to content

Instantly share code, notes, and snippets.

@taylorwood
Last active November 3, 2021 13:47
Show Gist options
  • Save taylorwood/bb3ebfec5d5de3cccc867a9eba216c18 to your computer and use it in GitHub Desktop.
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
(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")
@kolharsam
Copy link

Hey @taylorwood

Thanks for sharing this. I was trying it out and I was getting this error on the js-reduce method:

Execution error (PolyglotException) at <js>/:=> (Unnamed:1).
TypeError: invokeMember (reduce) on JavaObject[(0 1 2 3 4 5 6 7 8 9) (clojure.lang.LongRange)] failed due to: Unknown identifier: reduce

Any help I could get from you?
I'm running Java 8 & GraalVM 20

@taylorwood
Copy link
Author

@kolharsam I think some Graal security stuff has changed since I wrote this. Try setting (.allowHostAccess context HostAccess/ALL). This works for me now:

(js-reduce + (range 10))
=> 45

More info here https://github.com/graalvm/graaljs/blob/master/docs/user/ScriptEngine.md. Thanks!

@kolharsam
Copy link

@taylorwood, thanks for helping out! It works for me too now.

I just had a few more questions if you don't mind:

  1. (.as (eval-js "[{}]") Object) => I get the output for this as {} - any possible explanations?
  2. I want to play around with this a little more. Could you point me to something interesting?
  3. I also want to learn more about GraalVM any place that offers the best info apart from the docs?

Thanks again, @taylorwood

@smichaut
Copy link

smichaut commented Sep 27, 2020

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

@The-Alchemist
Copy link

@taylorwood very informative, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment