-
-
Save mukel/80f90f62100d513c274c8b2dbccf5d68 to your computer and use it in GitHub Desktop.
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 |
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.
@mukel I'm seeing (with GraalVM 21.1 JVM 11 on macOS):