From cd18b3282c79f5ca04368644f9a82f37d10c7ab0 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 27 Mar 2022 14:52:57 +0200 Subject: [PATCH] #100: Prototype for compiling multiple classes in one go. --- .../main/java/org/joor/CompilationUnit.java | 56 +++ jOOR/src/main/java/org/joor/CompileUnit.java | 340 ++++++++++++++++++ jOOR/src/main/java/org/joor/Reflect.java | 5 + .../java/org/joor/test/CompileUnitTest.java | 75 ++++ 4 files changed, 476 insertions(+) create mode 100644 jOOR/src/main/java/org/joor/CompilationUnit.java create mode 100644 jOOR/src/main/java/org/joor/CompileUnit.java create mode 100644 jOOR/src/test/java/org/joor/test/CompileUnitTest.java diff --git a/jOOR/src/main/java/org/joor/CompilationUnit.java b/jOOR/src/main/java/org/joor/CompilationUnit.java new file mode 100644 index 0000000..1776d58 --- /dev/null +++ b/jOOR/src/main/java/org/joor/CompilationUnit.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joor; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class CompilationUnit { + + private Map files = new LinkedHashMap<>(); + + public static class Result { + private final Map> classes = new LinkedHashMap<>(); + + public void addResult(String className, Class clazz) { + classes.put(className, clazz); + } + + public Class getClass(String className) { + return classes.get(className); + } + + public int size() { + return classes.size(); + } + + } + + public static CompilationUnit create() { + return new CompilationUnit(); + } + + public static CompilationUnit.Result result() { + return new Result(); + } + + public CompilationUnit unit(String className, String content) { + files.put(className, content); + return this; + } + + public Map getFiles() { + return files; + } +} diff --git a/jOOR/src/main/java/org/joor/CompileUnit.java b/jOOR/src/main/java/org/joor/CompileUnit.java new file mode 100644 index 0000000..746e0c6 --- /dev/null +++ b/jOOR/src/main/java/org/joor/CompileUnit.java @@ -0,0 +1,340 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joor; + +/* [java-8] */ + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Stream; + +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; + +/** + * A utility that simplifies in-memory compilation of multiple new classes in the same unit. + * + * @author Lukas Eder + */ +public class CompileUnit { + + public static CompilationUnit.Result compileUnit(CompilationUnit unit, CompileOptions compileOptions) { + CompilationUnit.Result result = CompilationUnit.result(); + + // some classes may already be compiled so try to load them first + List files = new ArrayList<>(); + + Lookup lookup = MethodHandles.lookup(); + ClassLoader cl = lookup.lookupClass().getClassLoader(); + unit.getFiles().forEach((cn, code) -> { + try { + Class clazz = cl.loadClass(cn); + result.addResult(cn, clazz); + } + catch (ClassNotFoundException ignore) { + files.add(new CharSequenceJavaFileObject(cn, code)); + } + }); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + try { + ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null)); + StringWriter out = new StringWriter(); + + List options = new ArrayList<>(compileOptions.options); + if (!options.contains("-classpath")) { + StringBuilder classpath = new StringBuilder(); + String separator = System.getProperty("path.separator"); + String cp = System.getProperty("java.class.path"); + String mp = System.getProperty("jdk.module.path"); + + if (cp != null && !"".equals(cp)) + classpath.append(cp); + if (mp != null && !"".equals(mp)) + classpath.append(mp); + + if (cl instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) cl).getURLs()) { + if (classpath.length() > 0) + classpath.append(separator); + + if ("file".equals(url.getProtocol())) + classpath.append(new File(url.toURI())); + } + } + + options.addAll(Arrays.asList("-classpath", classpath.toString())); + } + + CompilationTask task = compiler.getTask(out, fileManager, null, options, null, files); + + if (!compileOptions.processors.isEmpty()) + task.setProcessors(compileOptions.processors); + + task.call(); + + if (fileManager.isEmpty()) + throw new ReflectException("Compilation error: " + out); + + // This works if we have private-access to the interfaces in the class hierarchy + if (Reflect.CACHED_LOOKUP_CONSTRUCTOR != null) { + for (CharSequenceJavaFileObject f : files) { + String className = f.getClassName(); + Class clazz = fileManager.loadAndReturnMainClass(className, + (name, bytes) -> Reflect.on(cl).call("defineClass", name, bytes, 0, bytes.length).get()); + if (clazz != null) { + result.addResult(className, clazz); + } + } + /* [java-9] */ + + // Lookup.defineClass() has only been introduced in Java 9. It is + // required to get private-access to interfaces in the class hierarchy + } else { + + // This method is called by client code from two levels up the current stack frame + // We need a private-access lookup from the class in that stack frame in order to get + // private-access to any local interfaces at that location. +// Class caller = StackWalker +// .getInstance(RETAIN_CLASS_REFERENCE) +// .walk(s -> s +// .skip(2) +// .findFirst() +// .get() +// .getDeclaringClass()); + + int index = 2; + for (CharSequenceJavaFileObject f : files) { + String className = f.getClassName(); + + Class caller = getClassFromIndex(index); + index++; + + // If the compiled class is in the same package as the caller class, then + // we can use the private-access Lookup of the caller class + if (className.startsWith(caller.getPackageName() + ".") && + + // [#74] This heuristic is necessary to prevent classes in subpackages of the caller to be loaded + // this way, as subpackages cannot access private content in super packages. + // The heuristic will work only with classes that follow standard naming conventions. + // A better implementation is difficult at this point. + Character.isUpperCase(className.charAt(caller.getPackageName().length() + 1))) { + Lookup privateLookup = MethodHandles.privateLookupIn(caller, lookup); + Class clazz = fileManager.loadAndReturnMainClass(className, + (name, bytes) -> privateLookup.defineClass(bytes)); + if (clazz != null) { + result.addResult(className, clazz); + } + } + + // Otherwise, use an arbitrary class loader. This approach doesn't allow for + // loading private-access interfaces in the compiled class's type hierarchy + else { + Compile.ByteArrayClassLoader c = new Compile.ByteArrayClassLoader(fileManager.classes()); + Class clazz = fileManager.loadAndReturnMainClass(className, + (name, bytes) -> c.loadClass(name)); + if (clazz != null) { + result.addResult(className, clazz); + } + } + } + } + /* [/java-9] */ + + return result; + } catch (ReflectException e) { + throw e; + } catch (Exception e) { + throw new ReflectException("Error while compiling unit " + unit, e); + } + } + + private static Class getClassFromIndex(int index) { + return StackWalker + .getInstance(RETAIN_CLASS_REFERENCE) + .walk(s -> s + .skip(index) + .findFirst() + .get() + .getDeclaringClass()); + } + + /* [java-9] */ + static final class ByteArrayClassLoader extends ClassLoader { + private final Map classes; + + ByteArrayClassLoader(Map classes) { + super(ByteArrayClassLoader.class.getClassLoader()); + + this.classes = classes; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] bytes = classes.get(name); + + if (bytes == null) + return super.findClass(name); + else + return defineClass(name, bytes, 0, bytes.length); + } + } + /* [/java-9] */ + + static final class JavaFileObject extends SimpleJavaFileObject { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + JavaFileObject(String name, Kind kind) { + super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind); + } + + byte[] getBytes() { + return os.toByteArray(); + } + + @Override + public OutputStream openOutputStream() { + return os; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return new String(os.toByteArray(), StandardCharsets.UTF_8); + } + } + + static final class ClassFileManager extends ForwardingJavaFileManager { + private final Map fileObjectMap; + private Map classes; + + ClassFileManager(StandardJavaFileManager standardManager) { + super(standardManager); + + fileObjectMap = new LinkedHashMap<>(); + } + + @Override + public JavaFileObject getJavaFileForOutput( + Location location, + String className, + JavaFileObject.Kind kind, + FileObject sibling + ) { + JavaFileObject result = new JavaFileObject(className, kind); + fileObjectMap.put(className, result); + return result; + } + + boolean isEmpty() { + return fileObjectMap.isEmpty(); + } + + Map classes() { + if (classes == null) { + classes = new LinkedHashMap<>(); + + for (Entry entry : fileObjectMap.entrySet()) + classes.put(entry.getKey(), entry.getValue().getBytes()); + } + + return classes; + } + + Class loadAndReturnMainClass(String mainClassName, ThrowingBiFunction> definer) throws Exception { + Class result = null; + + // [#117] We don't know the subclass hierarchy of the top level + // classes in the compilation unit, and we can't find out + // without either: + // + // - class loading them (which fails due to NoClassDefFoundError) + // - using a library like ASM (which is a big and painful dependency) + // + // Simple workaround: try until it works, in O(n^2), where n + // can be reasonably expected to be small. + Deque> queue = new ArrayDeque<>(classes().entrySet()); + int n1 = queue.size(); + + // Try at most n times + for (int i1 = 0; i1 < n1 && !queue.isEmpty(); i1++) { + int n2 = queue.size(); + + for (int i2 = 0; i2 < n2; i2++) { + Entry entry = queue.pop(); + + try { + Class c = definer.apply(entry.getKey(), entry.getValue()); + + if (mainClassName.equals(entry.getKey())) + result = c; + } catch (ReflectException e) { + queue.offer(entry); + } + } + } + + return result; + } + } + + @FunctionalInterface + interface ThrowingBiFunction { + R apply(T t, U u) throws Exception; + } + + static final class CharSequenceJavaFileObject extends SimpleJavaFileObject { + final CharSequence content; + final String className; + + public CharSequenceJavaFileObject(String className, CharSequence content) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.className = className; + this.content = content; + } + + public String getClassName() { + return className; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + } +} +/* [/java-8] */ diff --git a/jOOR/src/main/java/org/joor/Reflect.java b/jOOR/src/main/java/org/joor/Reflect.java index e885abf..8aa9713 100644 --- a/jOOR/src/main/java/org/joor/Reflect.java +++ b/jOOR/src/main/java/org/joor/Reflect.java @@ -25,6 +25,7 @@ import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -50,6 +51,10 @@ */ public class Reflect { + public static CompilationUnit.Result compileUnit(CompilationUnit unit) throws ReflectException { + return CompileUnit.compileUnit(unit, new CompileOptions()); + } + // --------------------------------------------------------------------- // Static API used as entrance points to the fluent API // --------------------------------------------------------------------- diff --git a/jOOR/src/test/java/org/joor/test/CompileUnitTest.java b/jOOR/src/test/java/org/joor/test/CompileUnitTest.java new file mode 100644 index 0000000..6d68ea4 --- /dev/null +++ b/jOOR/src/test/java/org/joor/test/CompileUnitTest.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joor.test; + +import org.joor.CompilationUnit; +import org.joor.Reflect; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CompileUnitTest { + + @Test + public void testSingleUnit() throws Exception { + CompilationUnit unit = CompilationUnit.create() + .unit("org.joor.test.CompileMultiTest1", + "package org.joor.test;\n" + + "class CompileMultiTest1 implements java.util.function.Supplier {\n" + + " public String get() {\n" + + " return \"Bye World!\";\n" + + " }\n" + + "}\n" + ); + + CompilationUnit.Result result = Reflect.compileUnit(unit); + assertEquals(1, result.size()); + + Reflect ref = Reflect.onClass(result.getClass("org.joor.test.CompileMultiTest1")); + Object out = ref.create().call("get").get(); + assertEquals("Bye World!", out); + } + + @Test + public void testDualUnit() throws Exception { + CompilationUnit unit = CompilationUnit.create() + .unit("org.joor.test.CompileMultiTest2", + "package org.joor.test;\n" + + "class CompileMultiTest2 implements java.util.function.Supplier {\n" + + " public String get() {\n" + + " return \"Bye World!\";\n" + + " }\n" + + "}\n" + ) + .unit("org.joor.test.CompileMultiTest3", + "package org.joor.test;\n" + + "class CompileMultiTest3 implements java.util.function.Supplier {\n" + + " public String get() {\n" + + " return \"Hi World!\";\n" + + " }\n" + + "}\n" + ); + + CompilationUnit.Result result = Reflect.compileUnit(unit); + assertEquals(2, result.size()); + + Reflect ref2 = Reflect.onClass(result.getClass("org.joor.test.CompileMultiTest2")); + Reflect ref3 = Reflect.onClass(result.getClass("org.joor.test.CompileMultiTest3")); + Object out2 = ref2.create().call("get").get(); + Object out3 = ref3.create().call("get").get(); + assertEquals("Bye World!", out2); + assertEquals("Hi World!", out3); + } + +}