GraalVM - Exposing Java complex objects to JavaScript

1.4k views Asked by At

Using GraalVM, to expose Java objects to JavaScript, I am using ProxyObject to wrap them. For this purpose, I am using ProxyObject.fromMap method like the following:

ProxyObject exposedObject = ProxyObject.fromMap(objectMapper.convertValue(javaObject, Map.class));

Here, the javaObject is of Object class and can be arbitrarily complex. This method works for immediate members of javaObject, but not when the members are complex objects themselves. For example, if one of the members of javaObject happens to be a Map, like:

        final Map<String, Object> source = new HashMap<>();
        source.put("id", "1234567890");
        final Map<String, Object> sourceComponent = ImmutableMap.of("key", "value");
        source.put("complex", sourceComponent);

        // assuming the source is any object   
        ProxyObject exposedObject = ProxyObject.fromMap(objectMapper.convertValue(source, Map.class));

        // or knowing that source is in fact a map
        ProxyObject exposedObject = ProxyObject.fromMap(source);

this is what happens when the exposedObject is accessed in JavaScript:

exposedObject; // returns {complex: JavaObject[com.google.common.collect.SingletonImmutableBiMap], id: "1234567890"}

exposedObject.id; // returns 01234567890

exposedObject.complex; // returns {key=value}

exposedObject.complex.key; // returns undefined

So my question is how we can fully expose an arbitrarily complex and deep java object to javascript. Do we have to go through all members recursively and wrap them into ProxyObjects? Or is there a supported out-of-the-box method of achieving this?

Also, please let me know if my approach needs to change.

2

There are 2 answers

2
BoriS On

As the javadoc for ProxyObject [1] says "Interface to be implemented to mimic guest language objects that contain members.". This means that if you want the Java object to be used in JavaScript as if it was native to JavaScript it needs to be a ProxyObject.

On the other hand, as the website docs [2] show, Java objects passed into JavaScript can still be used as Java objects (i.e. they don't mimic JS objects by default). This means you can access fields, invoke methods, etc. The website docs show an example:

public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getBindings("js").putMember("javaObj", new MyClass());
        boolean valid = context.eval("js",
               "    javaObj.id         == 42"          +
               " && javaObj.text       == '42'"        +
               " && javaObj.arr[1]     == 42"          +
               " && javaObj.ret42()    == 42")
           .asBoolean();
        assert valid == true;
    }
}

[1] https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/proxy/ProxyObject.html

[2] https://www.graalvm.org/reference-manual/embed-languages/

0
Jiehong On

Got the same issue.

One way I've fixed it is to convert the java object into json, and then prepending that to the source: this way, the data is completely parsed into javascript.

For example, with jackson:

final String data = this.objectMapper.writeValueAsString(complexObject);
final String preScript = "const x = " + data + ";\n";
var result = context.eval(Source.newBuilder("js", preScript + script, "src.js").build());

This can correctly evaluate things like x.myMap.key.length, etc.

I've chosen this solution, because ppening the java access it too risky on my side.