Skip to content

Instantly share code, notes, and snippets.

@borkdude
Forked from mukel/DynamicJava.java
Created June 15, 2021 08:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save borkdude/cbe40c8793e2d63c3d703e8cd7815590 to your computer and use it in GitHub Desktop.
Save borkdude/cbe40c8793e2d63c3d703e8cd7815590 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment