graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

Create a way to expose members of a java object on the global scope of the script context

Open brentco opened this issue 4 years ago • 6 comments

Hello,

I am registering certain objects that contain methods that I want to be exposed in the script using Context#putMember(name, object).

For some of these methods I want them to be exposed in a "namespace", for example:

public class IVRModuleRuntime extends ScriptModuleRuntime<IVRModule> {
    @Api
    public void say(String message) {
        
    }
}

should be exposed under the IVR "namespace" so I would do context.putMember("IVR", ivrModuleRuntime):

IVR.say("this is IVR");

In contrast, I'd like this module to be exposed as root functions:

public class DebugModuleRuntime extends ScriptModuleRuntime<DebugModule> {

    @Api
    public void info(String message, Object... args) {
        
    }
}

I want to ultimately expose at the root, so inside the script I can do:

var test = "test";
info("this is a {}", test);

I couldn't find an easy way to do this, so I came up with this: https://gist.github.com/brentco/748be33f93e47c09c11d5a8e1b619a2a

Basically, I use reflection and a ProxyExecutable to invoke the right method in the module runtime based on the method's name and a list of arguments. I manually try to match that up to the right method and then call it.

It seems to be working for now, but this is not something I want to rely on. I doubt it has good performance and there are probably edge cases and whatnot. So I'd like to see this being implemented properly inside the API :)

brentco avatar Jun 04 '20 00:06 brentco

Hi,

in your examples, you are able to set IVR to the JS global object; and you are able to set say so that it can be called from JavaScript. I might fail to understand your root problem (all you need to do around reflection), but if you set info to the global object like you do with IVR, but have the info object you say be something callable (like say), this should work out of the box. A simple way to do this is:

        Context context = Context.newBuilder("js").allowHostAccess(HostAccess.ALL).build();
        Value bindings = context.getBindings("js");
        Consumer<Object> sayFn = (value) -> System.out.println(value);
        bindings.putMember("say", sayFn);
        context.eval("js", "say('test');");
        context.close();

Best, Christian

wirthi avatar Jun 10 '20 15:06 wirthi

Hi @wirthi It's how I expose all the methods. I expose an object with members under an identifier (say gets automatically exposed under IVR because I do putMember("IVR", objectThatContainsSay);. Indeed this works perfectly. However, say that I want to expose the contents of objectThatContainsSay on the root (without "IVR") then there is no way as far as I could see to expose it "automatically".

Instead, I have to manually expose each thing via reflection because I cannot do putMember("", objectThatContainsSay); where then I would have say() on the root instead of under "IVR". To get around this, I have the reflection stuff that exposes each member of objectThatContainsSay so that under the hood it would do "alright there is a say method in objectThatContainsSay so let's expose it as putMember("say", proxyThatCallsSayInObject)". This way I simulate the approximate behavior of doing putMember("", objectThatContainsSay);.

So the difference lies in that if you give an identifier, the contents of the object you pass into the method get automatically exposed under the identifier, or rather that the object is exposed which has the effect that the methods in it are exposed (e.g.: IVR.say(...)). In contrast, if you want it to be on the root, you cannot expose the contents (such as methods) of an object in the same way. Instead there is the proxy thing that does that (e.g.: say(...)).

I hope it's clearer now!

brentco avatar Jun 11 '20 07:06 brentco

Hi @brentco

thanks for your clarification. I better understand your usecase now. The reflection problem seems to be 100% on the side of your Java application, has nothing to do with JavaScript or the Context interface.

For the global object, yes, things are different as you cannot just modify that (and expect everything else to still work). Did you have a look at the suggestions in #44 already if you can use them?

-- Christian

wirthi avatar Jun 11 '20 10:06 wirthi

Sorry, I had JUST updated my thing to be a little more clear. The reflection part is a workaround for being able to achieve the goal. It seems #44 is similar but not the same.

Perhaps explaining my use case could help as well: I have objects in Java that contain methods that are to be exposed into the JavaScript. When I create a Context, I add all these objects into the context as members. Some of these objects I want exposed under a "namespace" such as IVR so that all the methods of those objects are under IVR.<methodName>(). Other objects I want to expose in the same way, but not in a "namespace" so on the root object, so that I can call <methodName>() in JavaScript.

My reflection proxy achieves this behavior, but I feel dirty doing it like that, knowing that somewhere in the graaljs code, the same is being done but better and complete, while my reflection proxy is only the bare minimum and probably not complete or possibly different in behavior.

So what I'm really asking for is a way to expose the content of a java object into the root JS object, much like you would otherwise expose that content if you put an identifier.

I don't think what is described in #44 matches with this.

brentco avatar Jun 11 '20 10:06 brentco

If you manually do that, you can expose them to the global object. See my example above. But you have to do that for every method you make available (unlike the case where you expose an object, and all its methods are automatically recognized, i.e. your IVR.say() example). If the methods you want to expose change, you have to manually change them in the JS global object (note that this might expose the problem as discussed in #146, that you cannot delete from the global object in JS).

-- Christian

wirthi avatar Jun 11 '20 10:06 wirthi

So there's no possibility that doing the same for the root object could be made? If not, then I'll have to keep my reflection proxy.

brentco avatar Jun 11 '20 12:06 brentco