Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 254 additions & 60 deletions src/main/java/net/openhft/compiler/CachedCompiler.java

Large diffs are not rendered by default.

138 changes: 109 additions & 29 deletions src/main/java/net/openhft/compiler/CompilerUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Unsafe;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.lang.management.ManagementFactory;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
Expand All @@ -40,35 +39,16 @@ public enum CompilerUtils {
public static final CachedCompiler CACHED_COMPILER = new CachedCompiler(null, null);

private static final Logger LOGGER = LoggerFactory.getLogger(CompilerUtils.class);
private static final Method DEFINE_CLASS_METHOD;
// Anchor/lookup class-definition strategy (issue #91). Resolved reflectively so the Java 8
// source root still compiles and runs: MethodHandles.Lookup#defineClass(byte[]) only exists
// on Java 9+. When null (Java 8) the anchor mode is unavailable and callers fall back to the
// Unsafe/ClassLoader path.
private static final Method LOOKUP_DEFINE_CLASS = resolveLookupDefineClass();
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String JAVA_CLASS_PATH = "java.class.path";
static JavaCompiler s_compiler;
static StandardJavaFileManager s_standardJavaFileManager;

/*
* Use sun.misc.Unsafe to gain access to ClassLoader.defineClass. This allows
* compiled bytecode to be defined without standard reflection checks. The
* fallback path calls setAccessible if the internal 'override' field is absent.
*/
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe u = (Unsafe) theUnsafe.get(null);
DEFINE_CLASS_METHOD = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
try {
Field f = AccessibleObject.class.getDeclaredField("override");
long offset = u.objectFieldOffset(f);
u.putBoolean(DEFINE_CLASS_METHOD, offset, true);
} catch (NoSuchFieldException e) {
DEFINE_CLASS_METHOD.setAccessible(true);
}
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException e) {
throw new AssertionError(e);
}
}

static {
reset();
}
Expand Down Expand Up @@ -190,13 +170,113 @@ public static void defineClass(@NotNull String className, @NotNull byte[] bytes)
* @throws AssertionError if {@code defineClass} cannot be invoked.
*/
public static Class<?> defineClass(@Nullable ClassLoader classLoader, @NotNull String className, @NotNull byte[] bytes) {
return LegacyClassDefiner.defineClass(classLoader, className, bytes);
}

/**
* Whether the anchor/lookup class-definition strategy (issue #91) is available on the
* running JVM. {@code true} on Java&nbsp;9+, {@code false} on Java&nbsp;8, where
* {@code MethodHandles.Lookup#defineClass(byte[])} does not exist.
*
* @return {@code true} if {@link #defineClass(MethodHandles.Lookup, byte[])} can be used.
*/
public static boolean isAnchorDefineClassSupported() {
return LOOKUP_DEFINE_CLASS != null;
}

/**
* Defines a class using the <em>anchor/lookup</em> strategy (issue #91): the class in
* {@code bytes} is defined in the package and {@link ClassLoader} of the supplied
* {@code anchor} {@link MethodHandles.Lookup} via the public
* {@code MethodHandles.Lookup#defineClass(byte[])} (Java&nbsp;9+).
* <p>
* Unlike {@link #defineClass(ClassLoader, String, byte[])} this uses <strong>no</strong>
* {@code sun.misc.Unsafe} and no {@code setAccessible}: the caller vouches for the target by
* handing over a full-privilege {@code Lookup} obtained in the destination package. The JDK
* enforces that the class in {@code bytes} is in the <em>same run-time package</em> as
* {@code anchor.lookupClass()} and that the anchor has {@code PACKAGE} access; a violation
* surfaces as {@link IllegalArgumentException}, not as a corrupted definition. This is the
* recommended path when the caller controls the destination package; use the compiler-owned
* child loader for arbitrary package names.
*
* @param anchor a {@code Lookup} with full privileges in the destination package.
* @param bytes compiled bytecode whose class is in the anchor's package.
* @return the defined class.
* @throws IllegalArgumentException if the lookup lacks package access or the bytes name another package.
* @throws UnsupportedOperationException on Java&nbsp;8, where the API does not exist.
*/
public static Class<?> defineClass(@NotNull MethodHandles.Lookup anchor, @NotNull byte[] bytes) {
Objects.requireNonNull(anchor, "anchor Lookup");
Objects.requireNonNull(bytes, "bytes");
final Method define = LOOKUP_DEFINE_CLASS;
if (define == null)
throw new UnsupportedOperationException(
"anchor/lookup class definition requires Java 9+ (MethodHandles.Lookup#defineClass)");
if ((anchor.lookupModes() & MethodHandles.Lookup.PACKAGE) == 0)
throw new IllegalArgumentException("anchor Lookup must have PACKAGE access");
try {
return (Class) DEFINE_CLASS_METHOD.invoke(classLoader, className, bytes, 0, bytes.length);
return (Class<?>) define.invoke(anchor, (Object) bytes);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
throw new IllegalStateException("Unable to invoke MethodHandles.Lookup#defineClass", e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof IllegalAccessException)
throw new IllegalArgumentException("anchor Lookup cannot define a class in this package", cause);
if (cause instanceof RuntimeException)
throw (RuntimeException) cause;
if (cause instanceof Error)
throw (Error) cause;
//noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
throw new AssertionError(e.getCause());
throw new AssertionError(cause);
}
}

private static Method resolveLookupDefineClass() {
try {
return MethodHandles.Lookup.class.getMethod("defineClass", byte[].class);
} catch (NoSuchMethodException e) {
return null; // Java 8: anchor mode unavailable; the Unsafe path remains.
}
}

/**
* Isolates the unsupported class-loader definition machinery so the lookup path never
* initialises or resolves {@code sun.misc.Unsafe}.
*/
private static final class LegacyClassDefiner {
private static final Method DEFINE_CLASS_METHOD = resolveDefineClassMethod();

private static Method resolveDefineClassMethod() {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
java.lang.reflect.Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Object unsafe = theUnsafe.get(null);
Method defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass", String.class, byte[].class, int.class, int.class);
try {
java.lang.reflect.Field override = AccessibleObject.class.getDeclaredField("override");
Method objectFieldOffset = unsafeClass.getMethod("objectFieldOffset", java.lang.reflect.Field.class);
long offset = (Long) objectFieldOffset.invoke(unsafe, override);
Method putBoolean = unsafeClass.getMethod("putBoolean", Object.class, long.class, boolean.class);
putBoolean.invoke(unsafe, defineClass, offset, true);
} catch (NoSuchFieldException e) {
defineClass.setAccessible(true);
}
return defineClass;
} catch (Exception e) {
throw new AssertionError(e);
}
}

private static Class<?> defineClass(ClassLoader classLoader, String className, byte[] bytes) {
try {
return (Class<?>) DEFINE_CLASS_METHOD.invoke(classLoader, className, bytes, 0, bytes.length);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
throw new AssertionError(e.getCause());
}
}
}

Expand Down
108 changes: 60 additions & 48 deletions src/main/java/net/openhft/compiler/MyJavaFileManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Unsafe;

import javax.tools.*;
import javax.tools.JavaFileObject.Kind;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
Expand All @@ -31,32 +28,11 @@
*/
public class MyJavaFileManager implements JavaFileManager {
private static final Logger LOG = LoggerFactory.getLogger(MyJavaFileManager.class);
private final static Unsafe unsafe;
private static final long OVERRIDE_OFFSET;

// Unsafe sets AccessibleObject.override for speed and JDK-9+ compatibility
static {
long offset;
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (Exception ex) {
throw new AssertionError(ex);
}
try {
Field f = AccessibleObject.class.getDeclaredField("override");
offset = unsafe.objectFieldOffset(f);
} catch (NoSuchFieldException e) {
offset = 0;
}
OVERRIDE_OFFSET = offset;
}

private final StandardJavaFileManager fileManager;

// synchronizing due to ConcurrentModificationException
private final Map<String, CloseableByteArrayOutputStream> buffers = Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<URI, Set<String>> outputsBySource = new HashMap<>();

/**
* Create a file manager that delegates to the provided instance while
Expand All @@ -77,7 +53,7 @@ public MyJavaFileManager(StandardJavaFileManager fileManager) {
* @return the module locations or an empty iterable
*/
public synchronized Iterable<Set<Location>> listLocationsForModules(final Location location) {
return invokeNamedMethodIfAvailable(location, "listLocationsForModules");
return invokeNamedMethodIfAvailable(location, "listLocationsForModules", Collections.<Set<Location>>emptyList());
}

/**
Expand All @@ -88,7 +64,7 @@ public synchronized Iterable<Set<Location>> listLocationsForModules(final Locati
* @return the inferred module name or {@code null}
*/
public synchronized String inferModuleName(final Location location) {
return invokeNamedMethodIfAvailable(location, "inferModuleName");
return invokeNamedMethodIfAvailable(location, "inferModuleName", (String) null);
}

public ClassLoader getClassLoader(Location location) {
Expand Down Expand Up @@ -153,9 +129,19 @@ public OutputStream openOutputStream() {
// CloseableByteArrayOutputStream.closed is used to filter partial results from getAllBuffers()
CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream();

// Reads from getAllBuffers() should be repeatable:
// ignore compile result in case compilation of this class was triggered before
buffers.putIfAbsent(className, baos);
// Compiler tasks are serialised by CachedCompiler, so replacing a previous
// result makes repeat compilation return the bytes produced by this task.
buffers.put(className, baos);
if (sibling != null) {
synchronized (outputsBySource) {
Set<String> outputs = outputsBySource.get(sibling.toUri());
if (outputs == null) {
outputs = new LinkedHashSet<>();
outputsBySource.put(sibling.toUri(), outputs);
}
outputs.add(className);
}
}

return baos;
}
Expand Down Expand Up @@ -187,6 +173,31 @@ public int isSupportedOption(String option) {
*/
public void clearBuffers() {
buffers.clear();
synchronized (outputsBySource) {
outputsBySource.clear();
}
}

void prepareForCompilation(Iterable<? extends JavaFileObject> sources) {
synchronized (outputsBySource) {
for (JavaFileObject source : sources)
outputsBySource.remove(source.toUri());
}
}

@NotNull
Map<String, byte[]> getBuffersForSources(Iterable<? extends JavaFileObject> sources) {
final Set<String> outputNames = new LinkedHashSet<>();
synchronized (outputsBySource) {
for (JavaFileObject source : sources) {
Set<String> outputs = outputsBySource.get(source.toUri());
if (outputs != null)
outputNames.addAll(outputs);
}
}
final Map<String, byte[]> result = getAllBuffers();
result.keySet().retainAll(outputNames);
return result;
}

/**
Expand Down Expand Up @@ -229,26 +240,27 @@ public Map<String, byte[]> getAllBuffers() {
}

/**
* Invoke a method by name on the delegate if it exists, using {@link Unsafe}
* to bypass accessibility checks when required.
* Invoke a public Java file-manager method by name when running on a JDK that exposes it.
*/
@SuppressWarnings("unchecked")
private <T> T invokeNamedMethodIfAvailable(final Location location, final String name) {
final Method[] methods = fileManager.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(name) && method.getParameterTypes().length == 1 &&
method.getParameterTypes()[0] == Location.class) {
try {
if (OVERRIDE_OFFSET == 0)
method.setAccessible(true);
else
unsafe.putBoolean(method, OVERRIDE_OFFSET, true);
return (T) method.invoke(fileManager, location);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new UnsupportedOperationException("Unable to invoke method " + name, e);
}
}
private <T> T invokeNamedMethodIfAvailable(final Location location, final String name, final T defaultValue) {
final Method method;
try {
method = JavaFileManager.class.getMethod(name, Location.class);
} catch (NoSuchMethodException e) {
return defaultValue;
}
try {
return (T) method.invoke(fileManager, location);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Unable to access method " + name, e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException)
throw (RuntimeException) cause;
if (cause instanceof Error)
throw (Error) cause;
throw new UnsupportedOperationException("Unable to invoke method " + name, cause);
}
throw new UnsupportedOperationException("Unable to find method " + name);
}
}
Loading