Skip to content

Instantly share code, notes, and snippets.

@mukel
Created June 14, 2021 20:45
Show Gist options
  • Select an option

  • Save mukel/80f90f62100d513c274c8b2dbccf5d68 to your computer and use it in GitHub Desktop.

Select an option

Save mukel/80f90f62100d513c274c8b2dbccf5d68 to your computer and use it in GitHub Desktop.

Revisions

  1. mukel created this gist Jun 14, 2021.
    119 changes: 119 additions & 0 deletions DynamicJava.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,119 @@
    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);
    }
    }
    100 changes: 100 additions & 0 deletions InMemoryJavaCompiler.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,100 @@
    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;
    }
    }
    115 changes: 115 additions & 0 deletions MemoryFileManager.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,115 @@
    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");
    }
    }
    }
    }
    3 changes: 3 additions & 0 deletions run.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    # On Linux, with a GraalVM with Java on Truffle (Espresso) installed (gu install espresso).
    javac *.java
    LD_DEBUG=unused java DynamicJava