Skip to content

Commit

Permalink
jOOQ#100: Prototype for compiling multiple classes in one go.
Browse files Browse the repository at this point in the history
  • Loading branch information
davsclaus committed Mar 27, 2022
1 parent a0122da commit cd18b32
Show file tree
Hide file tree
Showing 4 changed files with 476 additions and 0 deletions.
56 changes: 56 additions & 0 deletions jOOR/src/main/java/org/joor/CompilationUnit.java
Original file line number Diff line number Diff line change
@@ -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<String, String> files = new LinkedHashMap<>();

public static class Result {
private final Map<String, Class<?>> 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<String, String> getFiles() {
return files;
}
}
340 changes: 340 additions & 0 deletions jOOR/src/main/java/org/joor/CompileUnit.java
Original file line number Diff line number Diff line change
@@ -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<CharSequenceJavaFileObject> 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<String> 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<String, byte[]> classes;

ByteArrayClassLoader(Map<String, byte[]> 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<StandardJavaFileManager> {
private final Map<String, JavaFileObject> fileObjectMap;
private Map<String, byte[]> 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<String, byte[]> classes() {
if (classes == null) {
classes = new LinkedHashMap<>();

for (Entry<String, JavaFileObject> entry : fileObjectMap.entrySet())
classes.put(entry.getKey(), entry.getValue().getBytes());
}

return classes;
}

Class<?> loadAndReturnMainClass(String mainClassName, ThrowingBiFunction<String, byte[], Class<?>> 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<Entry<String, byte[]>> 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<String, byte[]> 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<T, U, R> {
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] */
Loading

0 comments on commit cd18b32

Please sign in to comment.