graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

Classloader problem when using Graal JS within a custom Maven plugin

Open esc-mhild opened this issue 4 years ago • 5 comments

Classloader problem

When building a custom Maven plugin that accesses a polyglot Context, execution within the graalvm of this built plugin in a second Maven project fails:

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:954)
    at org.graalvm.polyglot.Engine$PolyglotInvalid.createHostAccess (Engine.java:885)
    at org.graalvm.polyglot.Engine$Builder.build (Engine.java:564)
    at org.graalvm.polyglot.Context$Builder.build (Context.java:1725)

This can be fixed by setting the current thread's classloader:

Thread.currentThread().setContextClassLoader(Context.class.getClassLoader());
... polyglot code ...

Executing a main method (via Graal's java) compiled in the same library does not require this hack, however. Hence, the issue appears to result from the combination of Graal JS with the Maven classloading mechanism.

Possibly related issues are:

How to use maven-compiler-plugin

Unfortunately, this issue about runtime behaviour is wrapped up in questions about the proper build-time environment, in particular, when the goal is to:

  • use the maven-compiler-plugin to compile the project accessing the polyglot Context, and
  • rely on Graal VM for execution without bundling org.graalvm.js:js into the application.

It was difficult to understand which of the examples might apply and dedicated examples for the following simple, standard cases might be helpful to others as well.

1.) Plain, non-Graal compiler

Using the Java 8 or 11 compiler that ships with maven-compiler-plugin succeeds if org.graalvm.js:js is included as a dependency with either the <optional>true</optional> flag or <scope>provided</scope>. The Graal JS library is not bundled into the application and the a simple test can be successfully executed using Graal's java command.

Is that the correct way of compiling?

2.) Graal's javac compiler

When enabling use of Graal's own javac by means of <forceJavacCompilerUse>true</forceJavacCompilerUse>, the org.graalvm.js:js dependency is still required.

This probably means that additional compiler options need to be provided. Which ones?

Your help on the compile process might full well be enough to make my problem evaporate!

Many thanks in advance,

M.

Environment

java --version
openjdk 11.0.12 2021-07-20
OpenJDK Runtime Environment GraalVM CE 21.2.0 (build 11.0.12+6-jvmci-21.2-b08)
OpenJDK 64-Bit Server VM GraalVM CE 21.2.0 (build 11.0.12+6-jvmci-21.2-b08, mixed mode, sharing)

javac --version # javac points at graal vm's bin/javac
javac 11.0.12

mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: /usr/share/maven-3.8.1
Java version: 11.0.12, vendor: GraalVM Community, runtime: /usr/lib/jvm/graalvm-ce-java11-linux-amd64-21.2.0
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-142-generic", arch: "amd64", family: "unix"

Full Example

Custom Plugin

<?xml version="1.0" encoding="UTF-8"?>
<project
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
	xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.esc.mvn</groupId>
	<artifactId>graal-test-maven-plugin</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<packaging>maven-plugin</packaging>
	<properties>
		<graalvm.version>21.2.0</graalvm.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.apache.maven.plugin-tools</groupId>
			<artifactId>maven-plugin-annotations</artifactId>
			<version>3.6.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.maven</groupId>
			<artifactId>maven-plugin-api</artifactId>
			<version>3.8.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.graalvm.js</groupId>
			<artifactId>js</artifactId>
			<version>${graalvm.version}</version>
			<!-- both work -->
			<scope>provided</scope>
			<!-- <optional>true</optional> -->
		</dependency>
	</dependencies>
	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.8.1</version>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-plugin-plugin</artifactId>
					<version>3.6.1</version>
				</plugin>
			</plugins>
		</pluginManagement>

		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<!-- just using Graal's bin/javac is not enough, must probably add more compiler 
						options -->
					<!-- <forceJavacCompilerUse>true</forceJavacCompilerUse> -->
					<!-- both work -->
					<release>11</release>
					<testRelease>11</testRelease>
					<!-- <source>1.8</source> -->
					<!-- <target>1.8</target> -->
					<!-- <testSource>1.8</testSource> -->
					<!-- <testTarget>1.8</testTarget> -->
					<compilerArgs>
						<arg>-parameters</arg>
					</compilerArgs>
					<testCompilerArgs>
						<arg>-parameters</arg>
					</testCompilerArgs>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-plugin-plugin</artifactId>
				<executions>
					<execution>
						<id>default-descriptor</id>
						<phase>process-classes</phase>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>
package com.esc.graaltest;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;

@Mojo(name = "compile",
	requiresProject = false,
	defaultPhase = LifecyclePhase.COMPILE,
	threadSafe = false)
public class TestMojo extends AbstractMojo {

	@Override
	public void execute() {

		new Test().test();

	}

}
package com.esc.graaltest;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

public class Test {

	static public void main(String[] args) {

		new Test().test();

	}

	public void test() {

		ClassLoader cl = null;

		if("true".equals(System.getProperty("HACK"))) {
			// code below fails without this HACK
			cl = Thread.currentThread().getContextClassLoader();
			Thread.currentThread().setContextClassLoader(Context.class.getClassLoader());
		}

		Context context = Context.newBuilder("js").build();

		Value o = context.eval("js", "'hi there'");

		System.out.println("Graal JS says: " + o.asString());

		if("true".equals(System.getProperty("HACK")))
			Thread.currentThread().setContextClassLoader(cl);

	}

}

Running the main Test

No problem here. Starting in the directory containing the POM:

cd target/classes
java com.esc.graaltest.Test
Graal JS says: hi there

Using the Custom Plugin

In a second directory, we have a POM using the custom plugin built before:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.esc.test</groupId>
  <artifactId>graal-test</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <build>
    <plugins>
      <plugin>
        <groupId>com.esc.mvn</groupId>
        <artifactId>graal-test-maven-plugin</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <executions>
          <execution>
            <id>test</id>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

With the classloader hack, the project builds correctly:

mvn -D HACK=true clean compile
[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.esc.test:graal-test >-----------------------
[INFO] Building graal-test 1.0.0-SNAPSHOT
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] 
[INFO] --- graal-test-maven-plugin:1.0.0-SNAPSHOT:compile (test) @ graal-test ---
Graal JS says: hi there
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.699 s
[INFO] Finished at: 2021-08-12T13:10:34-04:00
[INFO] ------------------------------------------------------------------------

Without the classloader hack, the project fails:

mvn -e -D HACK=false clean compile
[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.esc.test:graal-test >-----------------------
[INFO] Building graal-test 1.0.0-SNAPSHOT
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] 
[INFO] --- graal-test-maven-plugin:1.0.0-SNAPSHOT:compile (test) @ graal-test ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.132 s
[INFO] Finished at: 2021-08-12T13:19:11-04:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal com.esc.mvn:graal-test-maven-plugin:1.0.0-SNAPSHOT:compile (test) on project graal-test: Execution test of goal com.esc.mvn:graal-test-maven-plugin:1.0.0-SNAPSHOT:compile failed: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath. -> [Help 1]
org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal com.esc.mvn:graal-test-maven-plugin:1.0.0-SNAPSHOT:compile (test) on project graal-test: Execution test of goal com.esc.mvn:graal-test-maven-plugin:1.0.0-SNAPSHOT:compile failed: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath.
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:215)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
    at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
    at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957)
    at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289)
    at org.apache.maven.cli.MavenCli.main (MavenCli.java:193)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (Native Method)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62)
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke (Method.java:566)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347)
Caused by: org.apache.maven.plugin.PluginExecutionException: Execution test of goal com.esc.mvn:graal-test-maven-plugin:1.0.0-SNAPSHOT:compile failed: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath.
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:148)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:210)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
    at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
    at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957)
    at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289)
    at org.apache.maven.cli.MavenCli.main (MavenCli.java:193)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (Native Method)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62)
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke (Method.java:566)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347)
Caused by: 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:954)
    at org.graalvm.polyglot.Engine$PolyglotInvalid.createHostAccess (Engine.java:885)
    at org.graalvm.polyglot.Engine$Builder.build (Engine.java:564)
    at org.graalvm.polyglot.Context$Builder.build (Context.java:1725)
    at com.esc.graaltest.Test.test (Test.java:24)
    at com.esc.graaltest.TestMojo.execute (TestMojo.java:16)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:137)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:210)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
    at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
    at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957)
    at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289)
    at org.apache.maven.cli.MavenCli.main (MavenCli.java:193)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (Native Method)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62)
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke (Method.java:566)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347)
[ERROR] 
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException

esc-mhild avatar Aug 12 '21 17:08 esc-mhild

Thank you for reporting this issue @MatthiasHild. We will take a look into it and get back to you.

rodrigar-mx avatar Aug 16 '21 16:08 rodrigar-mx

Hi @MatthiasHild. Please replace

<dependency>
	<groupId>org.graalvm.js</groupId>
	<artifactId>js</artifactId>
	<version>${graalvm.version}</version>
	<!-- both work -->
	<scope>provided</scope>
	<!-- <optional>true</optional> -->
</dependency>

with

<dependency>
        <groupId>org.graalvm.sdk</groupId>
        <artifactId>graal-sdk</artifactId>
        <version>${graalvm.version}</version>
</dependency>
<dependency>
        <groupId>org.graalvm.js</groupId>
        <artifactId>js</artifactId>
        <version>${graalvm.version}</version>
        <scope>runtime</scope>
</dependency>

and you would no longer need to invoke the classloader.

rodrigar-mx avatar Aug 17 '21 19:08 rodrigar-mx

Rodrigo,

Thank you for investigating.

I don't think your proposal is a solution that takes advantage of Graal's full performance potential.

Perhaps, my statement of one of the two goals could have been clearer as I was trying to find a general formulation:

rely on Graal VM for execution without bundling org.graalvm.js:js into the application.

In this specific context, what we want is not to put org.graalvm.js:js onto the classpath (scope runtime does exactly that). We don't want this because it is bad for performance: We want to run within GraalVM and not lose its "runtime compilation" (see logs below).

With your proposal, we get GraalVM's vehement protestations:

[INFO] --- graal-test-maven-plugin:1.0.0-SNAPSHOT:compile (test) @ graal-test ---
[To redirect Truffle log output to a file use one of the following options:
* '--log.file=<path>' if the option is passed using a guest language launcher.
* '-Dpolyglot.log.file=<path>' if the option is passed using the host Java launcher.
* Configure logging using the polyglot embedding API.]
[engine] WARNING: The polyglot context is using an implementation that does not support runtime compilation.
The guest application code will therefore be executed in interpreted mode only.
Execution only in interpreted mode will strongly impact the guest application performance.
For more information on using GraalVM see https://www.graalvm.org/java/quickstart/.
To disable this warning the '--engine.WarnInterpreterOnly=false' option or use the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.
Graal JS says: hi there

Finally, a comment on a detail of your proposal, namely the splitting the js and graal-sdk dependencies. I mention this side issue only to make sure there are no misunderstandings, but it is not central to the problem. Your splitting of the dependencies makes not effective difference in the present scenario. Yes, org.graalvm.sdk:graal-sdk is all we need to compile Test ( and TestMojo in the presence of the hack logic) but you don't even need it with scope compile - provided is enough. The problem is the runtime dependence on org.graalvm.js:js. Once you include it at runtime, you might as well used it at compile time and then ditch the explicit mention of the transitive org.graalvm.sdk:graal-sdk dependency. That's why I used the stronger dependency on org.graalvm.js:js to make experimentation easier - I had explored your proposal before. Of course, I made this depencency provided to avoid the performance degradation.

Best wishes,

M.

esc-mhild avatar Aug 18 '21 16:08 esc-mhild

BTW, the comment on Graal JS Issue 182 concerning the new ContextBuilder.hostClassLoader method may intend to suggest a follow-on hack around the Thread.currentThread().setContextClassLoader(Context.class.getClassLoader()) hack already mentioned by the reporter of that issue. When changing the current thread's class loader (to satisfy org.graalvm.polyglot.Engine), instantiation of Java objects from within Javascript now uses the wrong class loader - which may be offset by setting ContextBuilder.hostClassLoader to the current thread's original class loader that was so rudely replace by the first hack. Oh my!

esc-mhild avatar Aug 20 '21 15:08 esc-mhild

I'd like to known if there is an work around for now ?

valbendan avatar Jan 17 '22 00:01 valbendan

In the upcoming GraalVM 22.3, the language classes should now always be found and successfully loaded regardless of the current context class loader, as long as the org.graalvm.polyglot.* classes are loaded by the correct class loader.

mvn -e -D HACK=false clean compile worked for me using a recent developer build.

I hope this fixes the issue for you. Feel free to reopen if you still have trouble with 22.3.x.

woess avatar Sep 13 '22 14:09 woess