-
-
Save borkdude/cbe40c8793e2d63c3d703e8cd7815590 to your computer and use it in GitHub Desktop.
Compiling Java code dynamically and running it in Espresso.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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