graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

Ability to change ClassLoader that is used to load polyglot and language implementations

Open devmattrick opened this issue 5 years ago • 10 comments

Currently, it appears that Graal uses Thread.currentThread().getContextClassLoader() to load the classes required for polyglot and language implementations. While this works fine in most scenarios, there are cases where it is desired to use a different ClassLoader than the current one.

In my use case, my code is running in a plugin environment, which uses its own separate ClassLoader implementation. Due to this, Graal is unable to find the classes required to run properly, resulting in the following error:

java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath.
	at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:800) ~[?:?]
	at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:732) ~[?:?]
	at org.graalvm.polyglot.Engine$Builder.build(Engine.java:505) ~[?:?]
	at cx.matthew.graaltest.GraalTestPlugin.onEnable(GraalTestPlugin.java:32) ~[?:?]
	at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:263) ~[patched_1.14.2.jar:git-Paper-97]
	at org.bukkit.plugin.java.JavaPluginLoader.enablePlugin(JavaPluginLoader.java:338) ~[patched_1.14.2.jar:git-Paper-97]
	at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:419) ~[patched_1.14.2.jar:git-Paper-97]
	at org.bukkit.craftbukkit.v1_14_R1.CraftServer.enablePlugin(CraftServer.java:464) ~[patched_1.14.2.jar:git-Paper-97]
	at org.bukkit.craftbukkit.v1_14_R1.CraftServer.enablePlugins(CraftServer.java:378) ~[patched_1.14.2.jar:git-Paper-97]
	at net.minecraft.server.v1_14_R1.MinecraftServer.a(MinecraftServer.java:465) ~[patched_1.14.2.jar:git-Paper-97]
	at net.minecraft.server.v1_14_R1.DedicatedServer.init(DedicatedServer.java:280) ~[patched_1.14.2.jar:git-Paper-97]
	at net.minecraft.server.v1_14_R1.MinecraftServer.run(MinecraftServer.java:856) ~[patched_1.14.2.jar:git-Paper-97]
	at java.lang.Thread.run(Thread.java:748) [?:1.8.0_212]

I'm aware that it's possible to manually add the classes to the classpath when running the main process, but this is not ideal because it required configuration on the end user's part that they may not be aware of and somewhat defeats the purpose of a simple plugin system.

There's also the workaround of changing the thread's context ClassLoader using Thread.currentThread().setContextClassLoader(classLoader) whenever Graal is called, but this seems like a hacky way to make it work.

It'd be great if it was possible to specify a custom ClassLoader that Graal will use.

devmattrick avatar Jun 22 '19 13:06 devmattrick

I ran into the same issue. We are migrating from Nashorn and use dedicated class loaders for JS scripts. It doesn't seem to work the Nashorn way which is to set the threads's context class loader to the required custom class loader before the engine is created:

        ScriptEngineManager manager = new ScriptEngineManager();
        Thread.currentThread().setContextClassLoader(createClassLoader());
        ScriptEngine engine = null;
        if (ISGRAAL)
            engine = GraalSetup.engine();

The engine then "remembers" the class loader and uses it whenever it is invoked.

This is a show stopper for use since we dynamically load JDBC drivers in this manner.

iitsoftware avatar Mar 27 '20 15:03 iitsoftware

The same problem in Quarkus after 1.3.0 release that has class loader changes

oleksiylukin avatar Apr 27 '20 16:04 oleksiylukin

Since GraalVM 20.1 you can specify a hostClassLoader when creating a Context (via Context.newBuilder("js").hostClassLoader(....).build();).

wirthi avatar Nov 24 '20 15:11 wirthi

i have added Context.newBuilder("js").hostClassLoader(....).build(); but still getting java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath. at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:873) at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:794) at org.graalvm.polyglot.Engine$Builder.build(Engine.java:546) at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.(GraalJSScriptEngine.java:268) at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.create(GraalJSScriptEngine.java:704)

pramodanarase avatar Mar 08 '21 13:03 pramodanarase

Hi @pramodanarase

can you please share a bit more about your setup. Ideally, what is your exact version and commandline you run java on and what additional setup do you use (maven, etc.)

Can you maybe share a minimized version of your application so we can inspect your code on our machines?

Thanks, Christian

wirthi avatar Mar 08 '21 13:03 wirthi

Graal Js version - 21.0.0 JDK - AdoptOpenJDK 1.8.0_212 Classloader - Application Class Loader and Custom Child ClassLoader with ChildFirst classloading option

error at getGraalEngine() method. If we not set mainClassLoader

`public class GraalJSUtil {

private static final HostAccess hostAccess = HostAccess.newBuilder()
        .allowListAccess(true)
        .build();

public static Object evalGraalJS(String func) {
    Object res = null;
   // ClassLoader mainClassLoader = Thread.currentThread().getContextClassLoader();
    //ClassLoader extensionClassLoader = GraalJSUtil.class.getClassLoader();
    //Thread.currentThread().setContextClassLoader(extensionClassLoader);
        GraalJSScriptEngine scriptEngine = getGraalEngine();
        try {
            Context graalContext = getGraalContext(scriptEngine);
            Value bindingMap = graalContext.getBindings("js");
            //set context variable
            //update binding 
            // evalvuate function
            Source source = GraalJSUtil.getSource(func);
            res = GraalJSUtil.evalFunction(source, graalContext);
        } finally {
            clearGraalContext(scriptEngine);
          //  Thread.currentThread().setContextClassLoader(mainClassLoader);
        }

    return res;
}

private static Object evalFunction(Source source, Context graalContext) {
    return graalContext.eval(source);
}

public static Context.Builder getGraalContextBuilder() {

    return Context.newBuilder("js")
            .hostClassLoader(GraalJSUtil.class.getClassLoader())
            .allowHostAccess(hostAccess);
}

public static ScriptEngine getGraalEngine() {
    ScriptEngine engine = null;
    //ClassLoader mainClassLoader = Thread.currentThread().getContextClassLoader();
    //ClassLoader extensionClassLoader = GraalJSUtil.class.getClassLoader();
    //Thread.currentThread().setContextClassLoader(extensionClassLoader);
    try {
        engine = GraalJSScriptEngine.create(null, GraalJSUtil.getGraalContextBuilder());
    } finally {
        //Thread.currentThread().setContextClassLoader(mainClassLoader);
    }
    return engine;
}

} `

pramodanarase avatar Mar 10 '21 04:03 pramodanarase

Hi @pramodanarase

thanks for your code. I'll have to dig deeper on this later, but there are two immediate observations I have:

JDK 8 On JDK 8, you won't get good performance out of Graal.js. On JDK8 - unless you use our custom GraalVM JDK8 build - the JavaScript interpreter will not be compiled to machine code but stay interpreted. If you use the GraalVM optimizing compiler (either by using GraalVM instead of OpenJDK as your JVM, or by manually integrating it on a JDK11+ as described in https://www.graalvm.org/reference-manual/js/RunOnJDK/ ) you will see a significant Graal.js performance boost. Depending on your usecase it can be as high as 10x or even 100x. Also, running on GraalVM will ensure that all the classes are set up properly and you don't land in class loader problems as you do (unless you manually specify a different setup).

ScriptEngine You are mixing GraalVM's polyglot Context and the legacy support for ScriptEngine. This is possible, but complicates things and might actually have an impact on your problem. If you only ever use the ScriptEngine internally and route all the access through your evalGraalJS method anyway, then you don't need the ScriptEngine at all (it is just a superfluous wrapper then). Just create a Context and use context.eval("js", ...) to execute your code. See https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Context.html#eval-java.lang.String-java.lang.CharSequence-

Best, Christian

wirthi avatar Mar 10 '21 09:03 wirthi

Is there any known workaround in latest versions to load the language implementation using a classloader while allowing to load (host) classes with another? We are unable to update to latest versions as the workaround using the context class loader no longer works.

thc202 avatar Sep 07 '22 09:09 thc202

Is there any known workaround in latest versions to load the language implementation using a classloader while allowing to load (host) classes with another? We are unable to update to latest versions as the workaround using the context class loader no longer works.

The "we" in this case is OWASP ZAP - we use graaljs for JS scripting in ZAP and we are stuck on v20.2.0 because of this problem 😦

psiinon avatar Sep 07 '22 09:09 psiinon

I believe we've fixed this in the upcoming GraalVM 22.3. Can you please try a recent developer build to see if the problem is resolved in that version?

woess avatar Sep 13 '22 14:09 woess

I assume this is fixed. If not, feel free to reopen.

woess avatar Apr 04 '23 19:04 woess

For the record, this is now working fine for us.

thc202 avatar Aug 31 '23 09:08 thc202