javagraalvmscriptenginegraaljs

How to load js files with multiple functions (of the same name per file) in java/graalvm and invoke functions by file name


I have a server application that loads several script files (for processing specific dataset-fields) at startup. the scripts should be parsed and the "expression"-data of the script should be stored in a map (by column name) so that they can be accessed and executed from there later.

There are two types of scripts. The "simple" ones just contain a process function, the complex ones currently have a structure similar to the example below (might have more private functions/fields):

// public
function process(input) {
    return _doSomething(input);
}
function selfTest() {
    if (process("123") !== "123") throw "failed";
    // ...
}

// private
var _allowedSymbols = ['H', 'L', 'M'];      
function _doSomething(input) {
    // _allowedSymbols is used here
}

process and selfTest are the "public" functions that will be used by the server-app. selfTest will be executed once after the file is loaded/evaluated and process will be executed for incoming data when needed.


I've started with the old JSR 223 way:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("graal.js");
engine.eval("function process(input) { return input.toUpperCase(); }");
// engine.eval("function process(input) { return input + '123'; }");
Invocable inv = (Invocable) engine;
Object result = inv.invokeFunction("process", "peter");

This approach has the problem that the function-data is stored in the javascript engine instance and therefore i can't have multiple "process" methods in my map.
I Could continue that way and dynamically generate name-prefixes for functions and global variables based on the column name... but thats... "ugly"...


I've tried the graalvm-context-way (with help from SO and Oleg, How to store function handles from ScriptManager for later usage?):

var ctx = Context.newBuilder("js").allowAllAccess(false).build();
var src = Source.newBuilder("js", "(function u(input) { return input.toUpperCase(); })", "test.js").build();
var script = ctx.eval(src);
var result = script.execute("peter");

That works for "simple" functions. But for complex scripts the function-expression way from above doesnt work.


EDIT (Solution):

Modified the answer from Oleg slightly and this seems to do the job...

var jsCode = """
(function() {
    function process(input) { return input; }
    function selfTest() { if (process("123") !== "123") throw "failed"; return true; }
    return { process, selfTest }; 
})();
             """;
var ctx2 = Context.newBuilder("js").allowAllAccess(false).build();
Source source = Source.newBuilder("js", jsCode, "test.js").build();
var script = ctx2.eval(source);
var fnProcess = script.getMember("process");
var result = fnProcess.execute("123");
var fnSelfTest = script.getMember("selfTest");
var result2 = fnSelfTest.execute();

Solution

  • It's either functions are declared in the top level namespace, and then name collisions are a problem, or they are in their custom scopes and then you have to have some way to access and call them.

    When you're evaluating a source like that:

    (function u(input) { return input.toUpperCase(); })
    

    The result of that evaluation is the last expression in the script. You can think of the line:

    var result = ctx.eval("js", "(function u(input) { return input.toUpperCase(); })");
    

    Approximately as if it'd be in JS like:

    result = (function u(input) { return input.toUpperCase(); })
    

    And consequently, you can run it with:

    result("HelloWorld"); 
    

    That means you can return multiple functions using helper objects:

    // public
    function process(input) {
        return _doSomething(input);
    }
    function selfTest() {
        if (process("123") !== "123") throw "failed";
        // ...
    }
    
    // private
    var _allowedSymbols = ['H', 'L', 'M'];      
    function _doSomething(input) {
        // _allowedSymbols is used here
    }
    
    returnMe = {process, selfTest}; 
    

    Which is exactly what you'd do in JavaScript (I think).

    Then you can either get the process function with

    var returnedObject = ctx.eval(src);
    var processFunction = returnedObject.getMember("process");
    var result = processFunction.execute("peter");
    

    And similarly access the selfTest function. It might not be ideal that you need to modify the JS sources for that, but I think that's necessary.