Skip to content

Instantly share code, notes, and snippets.

@mukel
Created June 14, 2021 20:45
Show Gist options
  • Save mukel/80f90f62100d513c274c8b2dbccf5d68 to your computer and use it in GitHub Desktop.
Save mukel/80f90f62100d513c274c8b2dbccf5d68 to your computer and use it in GitHub Desktop.
Compiling Java code dynamically and running it in Espresso.
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
/**
* Some code taken from: https://www.soulmachine.me/blog/2015/07/22/compile-and-run-java-source-code-in-memory/
*/
public class DynamicJava {
static final String FILE_NAME = "Solution.java";
static final String SOURCE =
"public final class Solution {\n" +
" public static String greet(String name) {\n" +
" return \"Hello \" + name;\n" +
" }\n" +
"}\n";
static Map<String, byte[]> compileInTheHost(String fileName, String source) {
final InMemoryJavaCompiler compiler = new InMemoryJavaCompiler();
final Map<String, byte[]> classes = compiler.compile(fileName, source);
return classes;
}
static Value compileInTheGuest(Context context, String fileName, String source) {
Value bindings = context.getBindings("java");
Value InMemoryJavaCompiler_klass = bindings.getMember(InMemoryJavaCompiler.class.getName());
Value compiler = InMemoryJavaCompiler_klass.newInstance();
Value classes = compiler.invokeMember("compile", fileName, source);
return classes;
}
static void testInTheHost(Map<String, byte[]> classes) {
MemoryClassLoader loader = new MemoryClassLoader(classes);
String result;
try {
Class<?> Solution_class = loader.loadClass("Solution");
Method greet = Solution_class.getDeclaredMethod("greet", String.class);
result = (String) greet.invoke(null, "host");
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("(testInTheHost) result: " + result);
}
static Value toHost(Context context, Map<String, byte[]> classes) {
Value bindings = context.getBindings("java");
Value ByteArray_klass = bindings.getMember(byte[].class.getName());
Value HashMap_klass = bindings.getMember(HashMap.class.getName());
Value guestClasses = HashMap_klass.newInstance();
for (Map.Entry<String, byte[]> entry : classes.entrySet()) {
String key = entry.getKey();
byte[] value = entry.getValue();
Value guestValue = ByteArray_klass.newInstance(value.length);
for (int i = 0; i < value.length; ++i) {
guestValue.setArrayElement(i, value[i]);
}
// key is automatically converted into a guest String.
guestClasses.invokeMember("put", key, guestValue);
}
return guestClasses;
}
static void testInTheGuest(Context context, Value classes) {
Value bindings = context.getBindings("java");
Value MemoryClassLoader_klass = bindings.getMember(MemoryClassLoader.class.getName());
Value loader = MemoryClassLoader_klass.newInstance(classes);
Value Solution = loader.invokeMember("loadClass", "Solution");
Value result = Solution.getMember("static").invokeMember("greet", "Espresso");
System.out.println("(testInTheGuest) result: " + result.asString());
}
public static void main(String[] args) {
try (Context context = Context.newBuilder("java")
.allowAllAccess(true)
// To expose MemoryClassLoader to the guest.
.option("java.Classpath", System.getProperty("java.class.path"))
.build()) {
// Compile and run in the host.
Map<String, byte[]> classes_host = compileInTheHost(FILE_NAME, SOURCE);
testInTheHost(classes_host);
// Compile and run in the guest.
Value classes_guest = compileInTheGuest(context, FILE_NAME, SOURCE);
testInTheGuest(context, classes_guest);
// Compile in the host (already done) and run in the guest.
testInTheGuest(context, toHost(context, classes_host));
}
}
}
class MemoryClassLoader extends URLClassLoader {
private final Map<String, byte[]> classBytes = new ConcurrentHashMap<String, byte[]>();
public MemoryClassLoader(Map<String, byte[]> classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.tools.*;
/**
* Simple interface to Java compiler using JSR 199 Compiler API.
*/
public class InMemoryJavaCompiler {
private final javax.tools.JavaCompiler tool;
private StandardJavaFileManager stdManager;
public InMemoryJavaCompiler() {
tool = ToolProvider.getSystemJavaCompiler();
if (tool == null) {
throw new RuntimeException("Could not get Java compiler. Please, ensure that JDK is used instead of JRE.");
}
stdManager = tool.getStandardFileManager(null, null, null);
}
public Map<String, byte[]> compile(String fileName, String source) {
return compile(Collections.singletonMap(fileName, source), new PrintWriter(System.err), null, null);
}
/**
* compile given String source and return bytecodes as a Map.
*
* @param sources filename -> source
* @param err error writer where diagnostic messages are written
* @param sourcePath location of additional .java source files
* @param classPath location of additional .class files
*/
private Map<String, byte[]> compile(Map<String, String> sources,
Writer err, String sourcePath, String classPath) {
// to collect errors, warnings etc.
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<JavaFileObject>();
// create a new memory JavaFileManager
MemoryJavaFileManager fileManager = new MemoryJavaFileManager(stdManager);
// prepare the compilation unit
List<JavaFileObject> compilationUnits = sources.entrySet().stream()
.map(entry -> fileManager.makeStringSource(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
return compile(compilationUnits, fileManager, err, sourcePath, classPath);
}
private Map<String, byte[]> compile(final List<JavaFileObject> compUnits,
final MemoryJavaFileManager fileManager,
Writer err, String sourcePath, String classPath) {
// to collect errors, warnings etc.
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<JavaFileObject>();
// javac options
List<String> options = new ArrayList<String>();
options.add("-Xlint:all");
// options.add("-g:none");
options.add("-deprecation");
if (sourcePath != null) {
options.add("-sourcepath");
options.add(sourcePath);
}
if (classPath != null) {
options.add("-classpath");
options.add(classPath);
}
// create a compilation task
JavaCompiler.CompilationTask task =
tool.getTask(err, fileManager, diagnostics,
options, null, compUnits);
if (task.call() == false) {
PrintWriter perr = new PrintWriter(err);
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
perr.println(diagnostic);
}
perr.flush();
return null;
}
Map<String, byte[]> classBytes = fileManager.getClassBytes();
try {
fileManager.close();
} catch (IOException exp) {
}
return classBytes;
}
}
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.CharBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
/**
* JavaFileManager that keeps compiled .class bytes in memory.
*/
@SuppressWarnings("unchecked")
final class MemoryJavaFileManager extends ForwardingJavaFileManager {
/** Java source file extension. */
private final static String EXT = ".java";
private Map<String, byte[]> classBytes;
public MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
this.classBytes = new ConcurrentHashMap<>();
}
public Map<String, byte[]> getClassBytes() {
return classBytes;
}
public void close() throws IOException {
classBytes = null;
}
public void flush() throws IOException {
}
/**
* A file object used to represent Java source coming from a string.
*/
private static class StringInputBuffer extends SimpleJavaFileObject {
final String code;
StringInputBuffer(String fileName, String code) {
super(toURI(fileName), Kind.SOURCE);
this.code = code;
}
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}
}
/**
* A file object that stores Java bytecode into the classBytes map.
*/
private class ClassOutputBuffer extends SimpleJavaFileObject {
private String name;
ClassOutputBuffer(String name) {
super(toURI(name), Kind.CLASS);
this.name = name;
}
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream)out;
classBytes.put(name, bos.toByteArray());
}
};
}
}
public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
String className,
Kind kind,
FileObject sibling) throws IOException {
if (kind == Kind.CLASS) {
return new ClassOutputBuffer(className);
} else {
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
static JavaFileObject makeStringSource(String fileName, String code) {
return new StringInputBuffer(fileName, code);
}
static URI toURI(String name) {
File file = new File(name);
if (file.exists()) {
return file.toURI();
} else {
try {
final StringBuilder newUri = new StringBuilder();
newUri.append("mfm:///");
newUri.append(name.replace('.', '/'));
if(name.endsWith(EXT)) newUri.replace(newUri.length() - EXT.length(), newUri.length(), EXT);
return URI.create(newUri.toString());
} catch (Exception exp) {
return URI.create("mfm:///com/sun/script/java/java_source");
}
}
}
}
# On Linux, with a GraalVM with Java on Truffle (Espresso) installed (gu install espresso).
javac *.java
LD_DEBUG=unused java DynamicJava
@borkdude
Copy link

@mukel I'm seeing (with GraalVM 21.1 JVM 11 on macOS):

$ LD_DEBUG=unused java DynamicJava
(testInTheHost) result: Hello host
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.oracle.truffle.espresso.substitutions.Target_java_lang_ref_Reference (file:/Users/borkdude/Downloads/graalvm-ce-java11-21.1.0/Contents/Home/languages/java/espresso.jar) to method java.lang.ClassLoader.defineClass1(java.lang.ClassLoader,java.lang.String,byte[],int,int,java.security.ProtectionDomain,java.lang.String)
WARNING: Please consider reporting this to the maintainers of com.oracle.truffle.espresso.substitutions.Target_java_lang_ref_Reference
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Exception in thread "main" org.graalvm.polyglot.PolyglotException: java.lang.NullPointerException
	at com.oracle.truffle.nfi.NFIContext.getBackend(NFIContext.java:81)
	at com.oracle.truffle.nfi.NFIRootNode.execute(NFIRootNode.java:150)
	at org.graalvm.sdk/org.graalvm.polyglot.Context.getBindings(Context.java:514)
	at DynamicJava.compileInTheGuest(DynamicJava.java:33)
	at DynamicJava.main(DynamicJava.java:93)
Original Internal Error:
java.lang.NullPointerException
	at com.oracle.truffle.nfi.NFIContext.getBackend(NFIContext.java:81)
	at com.oracle.truffle.nfi.NFIRootNode.execute(NFIRootNode.java:150)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:613)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:584)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:534)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:518)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:463)
	at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:444)
	at com.oracle.truffle.espresso.ffi.nfi.NFINativeAccess.loadLibraryHelper(NFINativeAccess.java:174)
	at com.oracle.truffle.espresso.ffi.nfi.NFISulongNativeAccess.loadLibrary(NFISulongNativeAccess.java:43)
	at com.oracle.truffle.espresso.ffi.NativeAccess.loadLibrary(NativeAccess.java:77)
	at com.oracle.truffle.espresso.jni.JniEnv.<init>(JniEnv.java:326)
	at com.oracle.truffle.espresso.jni.JniEnv.create(JniEnv.java:374)
	at com.oracle.truffle.espresso.runtime.EspressoContext.getJNI(EspressoContext.java:653)
	at com.oracle.truffle.espresso.runtime.EspressoContext.spawnVM(EspressoContext.java:444)
	at com.oracle.truffle.espresso.runtime.EspressoContext.initializeContext(EspressoContext.java:397)
	at com.oracle.truffle.espresso.EspressoLanguage.initializeContext(EspressoLanguage.java:117)
	at com.oracle.truffle.espresso.EspressoLanguage.initializeContext(EspressoLanguage.java:54)
	at org.graalvm.truffle/com.oracle.truffle.api.TruffleLanguage$Env.postInit(TruffleLanguage.java:3610)
	at org.graalvm.truffle/com.oracle.truffle.api.LanguageAccessor$LanguageImpl.postInitEnv(LanguageAccessor.java:300)
	at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotLanguageContext.ensureInitialized(PolyglotLanguageContext.java:597)
	at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotContextImpl.getBindings(PolyglotContextImpl.java:849)
	at org.graalvm.sdk/org.graalvm.polyglot.Context.getBindings(Context.java:514)
	at DynamicJava.compileInTheGuest(DynamicJava.java:33)
	at DynamicJava.main(DynamicJava.java:93)
Caused by: Attached Guest Language Frames (1)

@mukel
Copy link
Author

mukel commented Jun 16, 2021

This deserves a better error message indeed.
Espresso on HotSpot is only supported on Linux ATM. Espresso uses the same native libraries shipped with GraalVM; when running on HotSpot, some libraries e.g. libjvm, libjava, libnio, libnet... are already loaded by HotSpot, these cannot be shared so we have to load the again. On Linux we rely on glibc's dlmopen to create isolated linking namespaces, allowing to spawn several Espresso contexts per process. Interestingly, Android also support linking namespaces, but we haven't found a way on MacOS or Windows.

We have a working Sulong back-end (already shipped with Espresso), but it requires OpenJDK native libraries compiled to LLVM bitcode, ATM we only have 8 and 11 (OpenJDK + LLVM bitcode) builds for Linux. The Sulong back-end has some limitations, but should allow to run on Espresso on HotSpot, on MacOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment