3

Running on GraalVM CE.

openjdk version "11.0.5" 2019-10-15
OpenJDK Runtime Environment (build 11.0.5+10-jvmci-19.3-b05-LTS)
OpenJDK 64-Bit GraalVM CE 19.3.0 (build 11.0.5+10-jvmci-19.3-b05-LTS, mixed mode, sharing)

Case 1:

import org.graalvm.polyglot.Context;

public class Test {

    static class Data {
        public String name = "HelloWorld";
        public String getName() {
            return this.name;
        }
    }

    public static void main(String[] args) {
        Context context = Context.newBuilder("js").allowHostAccess(true).build();
        context.getBindings("js").putMember("d", new Data());

        context.eval("js", "var x = d.name");

        System.out.println(
                context.getBindings("js").getMember("x").asString()
        );
    }
}

Result:

null

Why?

As I can understand, d passed correctly:

((Data) context.getBindings("js").getMember("d").as(Data.class)).name

returns "HelloWorld".

Case 2:

context.eval("js", "d.getName()");

Exception

Exception in thread "main" TypeError: invokeMember (getName) 
on JavaObject[task.Test$Data@35a3d49f (task.Test$Data)] failed due to: 
Unknown identifier: getName

But getName is public... What's wrong?

Woland
  • 623
  • 2
  • 13
  • 31

4 Answers4

3

When you use a context and add a Java Object to it, behind the scene, the IntropLibrary inside TruffleApi creates a HostObject and associate it with that object. This means that you don't use the object itself but rather a wrapper object.

When you call the getMember() method, the IntropLibrary can only access fields and methods of the hosted object that are publicly available. Since your inner class has default access (no access modifier), the API cannot find its members even though they are public. (a member of a class cannot have broader access than its class itself).

To solve this issue, all you have to do is make you inner class public

import org.graalvm.polyglot.Context;

public class Test {

  public static class Data {
    public String name = "HelloWorld";
    public String getName() {
        return this.name;
    }
  }

  public static void main(String[] args) {
    Context context = Context.newBuilder("js").allowHostAccess(true).build();
    context.getBindings("js").putMember("d", new Data());

    context.eval("js", "var x = d.name;");

    System.out.println(
        context.getBindings("js").getMember("x").asString()
    );
  }
}
A.Oubidar
  • 391
  • 2
  • 10
2

You need to annotate class field and method with @HostAccess.Export

By default only public classes, methods, and fields that are annotated with @HostAccess.Export are accessible to the guest language. This policy can be customized using Context.Builder.allowHostAccess(HostAccess) when constructing the context.

Example using a Java object from JavaScript:

 public class JavaRecord {
     @HostAccess.Export public int x;    
     @HostAccess.Export
     public String name() {
         return "foo";
     }
 }

Alternatively, you can use GraalVM JSR-223 ScriptEngine

GraalVM JavaScript provides a JSR-223 compliant javax.script.ScriptEngine implementation. Note that this is feature is provided for legacy reasons in order to allow easier migration for implementations currently based on a ScriptEngine. We strongly encourage users to use the org.graalvm.polyglot.Context interface

To set an option via Bindings, use Bindings.put(, true) before the engine's script context is initialized. Note that even a call to Bindings#get(String) may lead to context initialization. The following code shows how to enable polyglot.js.allowHostAccess via Bindings:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("polyglot.js.allowHostAccess", true);
bindings.put("polyglot.js.allowHostClassLookup", (Predicate<String>) s -> true);
bindings.put("javaObj", new Object());
engine.eval("(javaObj instanceof Java.type('java.lang.Object'));"); // would not work 

without allowHostAccess and allowHostClassLookup This example would not work if the user would call e.g. engine.eval("var x = 1;") before calling bindings.put("polyglot.js.allowHostAccess", true);, since any call to eval forces context initialization.

Ori Marko
  • 56,308
  • 23
  • 131
  • 233
  • At first I don't want use JSR-223, because as said in your citation "this is feature is provided for legacy reasons". And secondly I tried such way without success. I am looking for solution with polyglot Context. – Woland Nov 25 '19 at 06:38
  • @Woland updated, You need to annotate class field and method with `@HostAccess.Export` – Ori Marko Nov 25 '19 at 06:44
  • Thanks, but I still get `null` in Case 1. – Woland Nov 25 '19 at 07:07
  • My bad, works with `Export`. Need some time to investigate some cases. – Woland Nov 25 '19 at 07:20
0

GraalVM JavaScript enforces strict sandboxing rules by default, one of them being that JavaScript code cannot access host Java objects unless it is explicitly allowed by the user. The simplest way to allow your code to access context.eval("js", "d.getName()") is to pass the option polyglot.js.allowAllAccess=true as described in below link:

GraalVM JavaScript ScriptEngine implementation

Have a look at the Sample:

import org.graalvm.polyglot.Context;

public class Test {

    static class Data {
        public String name = "HelloWorld";
        public String getName() {
            return this.name;
        }
    }

    public static void main(String[] args) {
        Context context = Context.newBuilder("js").allowHostAccess(true).build();
        context.getBindings("js").putMember("d", new Data());

        context.eval("js", "var x = d.getName()");

        System.out.println(
                context.getBindings("js").getMember("d").as(Data.class)).name
        );
    }
}
Abhinav
  • 83
  • 1
  • 10
  • 1
    Notice that I already use `allowHostAccess(true)`. I also tried settings from your link `Value bindings = context.getBindings("js"); bindings.putMember("polyglot.js.allowHostAccess", true); bindings.putMember("polyglot.js.allowHostClassLookup", (Predicate) s -> true);`. No success. – Woland Nov 24 '19 at 20:58
0

To get full access to a java object in the usual js way, you can use the sj4js library.

This example is taken from the documentation...

public class TestObject extends JsProxyObject {
    
    // the property of the object
    private String name = "";
    
    // the constructor with the property 
    public TestObject (String name) {
        super ();
        
        this.name = name;
        
        // we hvae to initialize the proxy object
        // with all properties of this object
        init(this);
    }

    // this is a mandatory override, 
    // the proxy object needs this method 
    // to generate new objects if necessary
    @Override
    protected Object newInstance() {
        return new TestClass(this.name);
    }
    
    // the setter for the property name
    public void setName (String s) {
        this.name = s;
    }
    
    // the getter for the property name
    public String getName () {
        return this.name;
    }
}

And you can access this object as you would access a java object.

try (JScriptEngine engine = new JScriptEngine()) {
    engine.addObject("test", new TestClass("123"));
            
    engine.exec("test.name");
    // returns "123"

    engine.exec("test['name']")
    // returns "123"

    engine.exec("test.name = '456'")   
    engine.exec("test.name");
    // returns "456"
}