oracle / graal

GraalVM compiles Java applications into native executables that start instantly, scale fast, and use fewer compute resources šŸš€
https://www.graalvm.org
Other
20.21k stars 1.62k forks source link

TypeError: Unknown identifier - cannot call host method in Java modular app unless type is exported #4038

Open renatoathaydes opened 2 years ago

renatoathaydes commented 2 years ago

Issue Description

When using the Java module system, a JS script executed through a Context instance cannot see non-exported types and methods from the host, even when loaded from the same module and allowing all access in the given context.

Steps to reproduce the issue

  1. Create a Java module.
  2. In that module, create a Context instance and allow all access to the host.
  3. Load a JS script with SomeClass.class.getResource("path").
  4. Execute the script using the Context and pass a Java object with a type from the same module into it (arg to execute).
  5. In the JS script, attempt to call a public method on the Java object.

Here's how the Context is created:

        jsEngine = Context.newBuilder("js")
                .allowAllAccess(true)
                .allowNativeAccess(true)
                .build();

To fully reproduce the problem, you can checkout branch java-modularization of my project at https://github.com/renatoathaydes/rawhttp/tree/java-modularization

Build with ./gradlew jlink.

Run the CLI with the following commands:

cd rawhttp-cli
echo 'GET https://www.graalvm.org' > get.http
build/image/bin/java --module rawhttp.cli/rawhttp.cli.Main run get.http -p stats

The script: https://github.com/renatoathaydes/rawhttp/blob/665f4496db10929f928e5fc40141948f3ae2f384/rawhttp-req-in-edit/src/main/resources/com/athaydes/rawhttp/reqinedit/js/response_handler.js#L4

The Java type it will try to use: https://github.com/renatoathaydes/rawhttp/blob/java-modularization/rawhttp-req-in-edit/src/main/java/com/athaydes/rawhttp/reqinedit/js/internal/JsTestReporter.java

When the script calls reporter.failure({name: test.name, time: time, error: e});, it will fail with the following error:

org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (failure) on com.athaydes.rawhttp.reqinedit.js.internal.JsTestReporter@12bd8a64 failed due to: Unknown identifier: failure

I had expected that method call to correctly reach this method:

public final class JsTestReporter {
    public void failure(Value objectMirror) { ... }
}

If I change the module-info.java file to export this package, then it works.

// I really don't want to do this
exports com.athaydes.rawhttp.reqinedit.js.internal;

GraalVM environment:

More details

I would expect the script code to have access to the public methods of a Java type from the same module that loaded the script. If that's not the case by design, it should still be possible to explicitly "open" the module's package somehow only for the script (supposing the script has a "module" that's NOT the module loading the script?).

The only workaround that currently works is to export the package containing the Java type to ALL modules. This is undesirable as it effectively breaks the modularity of the Java application.

I realize that when I load a script with SomeClass.class.getResource("path") all I get is a byte-stream, hence it's probably impossible for the script to be made to "belong" to the same module as SomeClass. However, it seems to me that it should be a a basic feature of GraalVM polyglot support to load scripts using a particular Java module, otherwise it becomes very difficult to develop truly polyglot applications using the Java module system. I am, of course, assuming there's no way to do this already as I tried to find this in the docs but couldn't find any mention of the module system in polyglot applications at all (sorry if I am mistaken).

munishchouhan commented 2 years ago

@renatoathaydes thanks for reporting the issue, we will check it out and get back to you

munishchouhan commented 2 years ago

@renatoathaydes I tried the reproducer and got the same error as you reported in https://github.com/oracle/graal/issues/4047 Is there another reproducer for the error reported in this issue?

renatoathaydes commented 2 years ago

In that ticket, they say issue #4047 is fixed on graalvm ce 22.0.0-dev build, if you try that, maybe you can reproduce? Otherwise, you need to remove the crypto module and use a non-https URL, I believe.

munishchouhan commented 2 years ago

@renatoathaydes please provide a reproducer specific to this issue

renatoathaydes commented 2 years ago

Here you go: https://github.com/renatoathaydes/graalvm-js-jlink-demo

It's a very simple problem. I wanted to be able to load and run JS code from within a Java module, however when running with JPMS, JS scripts can only see exported packages from any module. Non-exported members of even the module loading the JS script itself are not visible.

The demo project demonstrates that.

Check the README page for an explanation of how to build/run it.

Everything is working fine, but if you remove the line exports demo.pub from module-info.java, you will see that the default script, as well as any script you try to run, won't be able to see the JavaType anymore.

Example successful run (no JPMS):

programming/projects/graalvm-js-jlink-demo  master āœ—                                                                                                                                               4m āš‘  
ā–¶ java -cp build/libs/graalvm-js-jlink-demo-1.0-SNAPSHOT.jar demo.Main                                  
Calling Java type method
Got Java message: hello world
done

Example bad run using JPMS:

programming/projects/graalvm-js-jlink-demo  master āœ—                                                                                                                                               4m āš‘  
ā–¶ java -p build/libs/graalvm-js-jlink-demo-1.0-SNAPSHOT.jar -m "demo.app/demo.Main"                  
Calling Java type method
Exception in thread "main" TypeError: invokeMember (getMessage) on demo.pub.JavaType@62e6b5c8 failed due to: Unknown identifier: getMessage
        at <js> :program(Unnamed:2:64-84)
        at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:379)
        at demo.app/demo.Main.main(Main.java:42)
renatoathaydes commented 2 years ago

Perhaps, the ideal solution would be a "flag" in the Context to tell the JS engine whether to run the JS script with visibility into the module that's loading it or not.

In my case, the JS code I am running is considered as an "internal" part of the module that loads it, so I would want its code to be treated as if it were part of the Java module itself (i.e. having the same visibility, exactly, as the Java classes within the same module - including what it can see from other modules - it currently appears the JS code automatically "imports" everything exported from any module).

munishchouhan commented 2 years ago

@renatoathaydes thanks for the reproducer, I will raise it to javascript team