Skip to content

Instantly share code, notes, and snippets.

@taylorwood

taylorwood/core.clj

Last active Sep 28, 2020
Embed
What would you like to do?
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

This comment has been minimized.

Copy link

@kolharsam kolharsam commented May 9, 2020

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

This comment has been minimized.

Copy link
Owner Author

@taylorwood taylorwood commented May 10, 2020

@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

This comment has been minimized.

Copy link

@kolharsam kolharsam commented May 10, 2020

@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

This comment has been minimized.

Copy link

@smichaut 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.