diff --git a/src/main/java/net/openhft/compiler/CachedCompiler.java b/src/main/java/net/openhft/compiler/CachedCompiler.java index 4f1c989..5477ba7 100644 --- a/src/main/java/net/openhft/compiler/CachedCompiler.java +++ b/src/main/java/net/openhft/compiler/CachedCompiler.java @@ -13,6 +13,7 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import java.io.*; +import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; @@ -49,6 +50,8 @@ public class CachedCompiler implements Closeable { private final Map>> loadedClassesMap = Collections.synchronizedMap(new WeakHashMap<>()); private final Map fileManagerMap = Collections.synchronizedMap(new WeakHashMap<>()); + private final Map> definitionLocks = Collections.synchronizedMap(new WeakHashMap<>()); + private final Map> incompleteLookupDefinitions = Collections.synchronizedMap(new WeakHashMap<>()); /** * Optional testing hook to replace the file manager implementation. *

@@ -139,6 +142,59 @@ public Class loadFromJava(@NotNull ClassLoader classLoader, return loadFromJava(classLoader, className, javaCode, DEFAULT_WRITER); } + /** + * Compile the source and define it using the anchor/lookup strategy (issue #91): + * the compiled classes are defined in the package and {@link ClassLoader} of the supplied + * {@code anchor} via {@link CompilerUtils#defineClass(MethodHandles.Lookup, byte[])}, using + * no {@code sun.misc.Unsafe}. The primary class must be declared in the same package as + * {@code anchor.lookupClass()}; the JDK rejects a cross-package definition. + *

+ * This is the opt-in counterpart to {@link #loadFromJava(ClassLoader, String, String)}: use + * it when the caller controls the destination package and can hand over a full-privilege + * {@code Lookup}; use the class-loader overload for arbitrary package names via a + * compiler-owned loader. + * + * @param anchor a {@code Lookup} with full privileges in the destination package. + * @param className expected binary name of the primary class (in the anchor's package). + * @param javaCode source code to compile. + * @return the loaded class, defined in the anchor's loader. + * @throws ClassNotFoundException if the compiled class cannot be found after definition. + * @throws IllegalArgumentException if the lookup lacks package access or names another package. + * @throws UnsupportedOperationException on Java 8, where anchor mode is unavailable. + */ + public Class loadFromJava(@NotNull MethodHandles.Lookup anchor, + @NotNull String className, + @NotNull String javaCode) throws ClassNotFoundException { + Objects.requireNonNull(anchor, "anchor"); + validateClassName(className); + if (!CompilerUtils.isAnchorDefineClassSupported()) + 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"); + final String anchorPackage = packageName(anchor.lookupClass().getName()); + if (!anchorPackage.equals(packageName(className))) + throw new IllegalArgumentException("class " + className + " is not in anchor package " + anchorPackage); + + final ClassLoader classLoader = anchor.lookupClass().getClassLoader(); + final Map> loadedClasses = loadedClassesFor(classLoader); + synchronized (definitionLock(classLoader, className)) { + Class primaryClass = loadedClass(loadedClasses, className); + if (primaryClass != null && !isLookupDefinitionIncomplete(classLoader, className)) + return primaryClass; + + final MyJavaFileManager fileManager = fileManagerFor(classLoader); + final Map compiled = compileFromJava(className, javaCode, DEFAULT_WRITER, fileManager); + if (!compiled.containsKey(className)) + throw new ClassNotFoundException(className); + + markLookupDefinitionIncomplete(classLoader, className); + primaryClass = defineCompiledWithLookup(anchor, className, compiled, loadedClasses); + markLookupDefinitionComplete(classLoader, className); + return primaryClass; + } + } + /** * Compile source code into byte arrays using the provided file manager. * Results are cached and reused on subsequent calls when compilation @@ -159,7 +215,8 @@ Map compileFromJava(@NotNull String className, /** * Compile source using the given writer and file manager. The resulting - * byte arrays are cached for the life of this compiler instance. + * byte arrays are cached for the life of this compiler instance, while the returned map + * contains only class names first produced by this compilation. * * @param className name of the primary class * @param javaCode source to compile @@ -173,40 +230,45 @@ Map compileFromJava(@NotNull String className, final @NotNull PrintWriter writer, MyJavaFileManager fileManager) { validateClassName(className); - Iterable compilationUnits; - if (sourceDir != null) { - String filename = className.replaceAll("\\.", '\\' + File.separator) + ".java"; - File file = safeResolve(sourceDir, filename); - writeText(file, javaCode); - if (s_standardJavaFileManager == null) - s_standardJavaFileManager = s_compiler.getStandardFileManager(null, null, null); - compilationUnits = s_standardJavaFileManager.getJavaFileObjects(file); - - } else { - javaFileObjects.put(className, new JavaSourceFromString(className, javaCode)); - compilationUnits = new ArrayList<>(javaFileObjects.values()); // To prevent CME from compiler code - } - // reuse the same file manager to allow caching of jar files - boolean ok = s_compiler.getTask(writer, fileManager, new DiagnosticListener() { - @Override - public void report(Diagnostic diagnostic) { - if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { - writer.println(diagnostic); - } - } - }, options, null, compilationUnits).call(); + synchronized (fileManager) { + final List currentCompilationUnits = new ArrayList<>(); + Iterable compilationUnits; + if (sourceDir != null) { + String filename = className.replaceAll("\\.", '\\' + File.separator) + ".java"; + File file = safeResolve(sourceDir, filename); + writeText(file, javaCode); + if (s_standardJavaFileManager == null) + s_standardJavaFileManager = s_compiler.getStandardFileManager(null, null, null); + for (JavaFileObject compilationUnit : s_standardJavaFileManager.getJavaFileObjects(file)) + currentCompilationUnits.add(compilationUnit); + compilationUnits = currentCompilationUnits; - if (!ok) { - // compilation error, so we want to exclude this file from future compilation passes - if (sourceDir == null) - javaFileObjects.remove(className); + } else { + JavaFileObject currentCompilationUnit = new JavaSourceFromString(className, javaCode); + javaFileObjects.put(className, currentCompilationUnit); + currentCompilationUnits.add(currentCompilationUnit); + compilationUnits = new ArrayList<>(javaFileObjects.values()); // To prevent CME from compiler code + } + fileManager.prepareForCompilation(currentCompilationUnits); + // Reuse the same file manager to cache jar files. Serialisation also prevents + // concurrent compiler tasks from interleaving writes into its output buffers. + boolean ok = s_compiler.getTask(writer, fileManager, new DiagnosticListener() { + @Override + public void report(Diagnostic diagnostic) { + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + writer.println(diagnostic); + } + } + }, options, null, compilationUnits).call(); - // nothing to return due to compiler error - return Collections.emptyMap(); - } else { - Map result = fileManager.getAllBuffers(); + if (!ok) { + // compilation error, so we want to exclude this file from future compilation passes + if (sourceDir == null) + javaFileObjects.remove(className); - return result; + return Collections.emptyMap(); + } + return fileManager.getBuffersForSources(currentCompilationUnits); } } @@ -239,45 +301,40 @@ public Class loadFromJava(@NotNull ClassLoader classLoader, if (clazz != null) return clazz; - MyJavaFileManager fileManager = fileManagerMap.get(classLoader); - if (fileManager == null) { - StandardJavaFileManager standardJavaFileManager = s_compiler.getStandardFileManager(null, null, null); - fileManager = getFileManager(standardJavaFileManager); - fileManagerMap.put(classLoader, fileManager); - } - final Map compiled = compileFromJava(className, javaCode, printWriter, fileManager); - for (Map.Entry entry : compiled.entrySet()) { - String className2 = entry.getKey(); - validateClassName(className2); + synchronized (definitionLock(classLoader, className)) { synchronized (loadedClassesMap) { - if (loadedClasses.containsKey(className2)) - continue; + clazz = loadedClasses.get(className); } - byte[] bytes = entry.getValue(); - if (classDir != null) { - String filename = className2.replaceAll("\\.", '\\' + File.separator) + ".class"; - boolean changed = writeBytes(safeResolve(classDir, filename), bytes); - if (changed) { - LOG.info("Updated {} in {}", className2, classDir); + if (clazz != null) + return clazz; + + MyJavaFileManager fileManager = fileManagerFor(classLoader); + final Map compiled = compileFromJava(className, javaCode, printWriter, fileManager); + for (Map.Entry entry : compiled.entrySet()) { + String className2 = entry.getKey(); + validateClassName(className2); + byte[] bytes = entry.getValue(); + if (classDir != null) { + String filename = className2.replaceAll("\\.", '\\' + File.separator) + ".class"; + boolean changed = writeBytes(safeResolve(classDir, filename), bytes); + if (changed) { + LOG.info("Updated {} in {}", className2, classDir); + } } - } - synchronized (className2.intern()) { // To prevent duplicate class definition error synchronized (loadedClassesMap) { if (loadedClasses.containsKey(className2)) continue; - } - - Class clazz2 = CompilerUtils.defineClass(classLoader, className2, bytes); - synchronized (loadedClassesMap) { + Class clazz2 = CompilerUtils.defineClass(classLoader, className2, bytes); loadedClasses.put(className2, clazz2); } } + synchronized (loadedClassesMap) { + loadedClasses.put(className, clazz = classLoader.loadClass(className)); + } + markLookupDefinitionComplete(classLoader, className); + return clazz; } - synchronized (loadedClassesMap) { - loadedClasses.put(className, clazz = classLoader.loadClass(className)); - } - return clazz; } /** @@ -304,6 +361,143 @@ public void setFileManagerOverride(Function> loadedClassesFor(ClassLoader classLoader) { + synchronized (loadedClassesMap) { + Map> loadedClasses = loadedClassesMap.get(classLoader); + if (loadedClasses == null) { + loadedClasses = new LinkedHashMap<>(); + loadedClassesMap.put(classLoader, loadedClasses); + } + return loadedClasses; + } + } + + private Object definitionLock(ClassLoader classLoader, String className) { + synchronized (definitionLocks) { + Map loaderLocks = definitionLocks.get(classLoader); + if (loaderLocks == null) { + loaderLocks = new HashMap<>(); + definitionLocks.put(classLoader, loaderLocks); + } + Object lock = loaderLocks.get(className); + if (lock == null) { + lock = new Object(); + loaderLocks.put(className, lock); + } + return lock; + } + } + + private Class loadedClass(Map> loadedClasses, String className) { + synchronized (loadedClassesMap) { + return loadedClasses.get(className); + } + } + + private Class defineWithLookup(MethodHandles.Lookup anchor, + String className, + byte[] bytes, + Map> loadedClasses) { + validateClassName(className); + synchronized (loadedClassesMap) { + Class loaded = loadedClasses.get(className); + if (loaded != null) + return loaded; + Class defined = CompilerUtils.defineClass(anchor, bytes); + loadedClasses.put(className, defined); + return defined; + } + } + + /** + * Define a compilation batch while allowing same-source supertypes to be emitted in any order. + * A failed batch remains marked incomplete so a later call cannot mistake a partially defined + * primary class for a successful cached load. + */ + private Class defineCompiledWithLookup(MethodHandles.Lookup anchor, + String primaryClassName, + Map compiled, + Map> loadedClasses) { + final Map pending = new LinkedHashMap<>(compiled); + Class primaryClass = null; + + while (!pending.isEmpty()) { + boolean madeProgress = false; + NoClassDefFoundError unresolvedDependency = null; + for (Iterator> iterator = pending.entrySet().iterator(); iterator.hasNext(); ) { + final Map.Entry entry = iterator.next(); + try { + final Class defined = defineWithLookup(anchor, entry.getKey(), entry.getValue(), loadedClasses); + if (entry.getKey().equals(primaryClassName)) + primaryClass = defined; + iterator.remove(); + madeProgress = true; + } catch (NoClassDefFoundError unresolved) { + // Lookup#defineClass resolves direct supertypes immediately. Another output + // from this batch may provide the missing class, so retry after making a pass. + unresolvedDependency = unresolved; + } + } + if (!madeProgress) { + if (unresolvedDependency != null) + throw unresolvedDependency; + throw new LinkageError("Unable to define compiled classes for " + primaryClassName); + } + } + + if (primaryClass == null) + primaryClass = loadedClass(loadedClasses, primaryClassName); + if (primaryClass == null) + throw new LinkageError("Primary class was not defined: " + primaryClassName); + return primaryClass; + } + + private boolean isLookupDefinitionIncomplete(ClassLoader classLoader, String className) { + synchronized (incompleteLookupDefinitions) { + final Set incomplete = incompleteLookupDefinitions.get(classLoader); + return incomplete != null && incomplete.contains(className); + } + } + + private void markLookupDefinitionIncomplete(ClassLoader classLoader, String className) { + synchronized (incompleteLookupDefinitions) { + Set incomplete = incompleteLookupDefinitions.get(classLoader); + if (incomplete == null) { + incomplete = new HashSet<>(); + incompleteLookupDefinitions.put(classLoader, incomplete); + } + incomplete.add(className); + } + } + + private void markLookupDefinitionComplete(ClassLoader classLoader, String className) { + synchronized (incompleteLookupDefinitions) { + final Set incomplete = incompleteLookupDefinitions.get(classLoader); + if (incomplete == null) + return; + incomplete.remove(className); + if (incomplete.isEmpty()) + incompleteLookupDefinitions.remove(classLoader); + } + } + + private static String packageName(String className) { + final int separator = className.lastIndexOf('.'); + return separator < 0 ? "" : className.substring(0, separator); + } + private static void validateClassName(String className) { Objects.requireNonNull(className, "className"); if (!CLASS_NAME_PATTERN.matcher(className).matches()) { diff --git a/src/main/java/net/openhft/compiler/CompilerUtils.java b/src/main/java/net/openhft/compiler/CompilerUtils.java index 0dd4c6b..fccefff 100644 --- a/src/main/java/net/openhft/compiler/CompilerUtils.java +++ b/src/main/java/net/openhft/compiler/CompilerUtils.java @@ -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; @@ -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(); } @@ -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 9+, {@code false} on Java 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 anchor/lookup 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 9+). + *

+ * Unlike {@link #defineClass(ClassLoader, String, byte[])} this uses no + * {@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 same run-time package 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 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()); + } } } diff --git a/src/main/java/net/openhft/compiler/MyJavaFileManager.java b/src/main/java/net/openhft/compiler/MyJavaFileManager.java index ba788b6..98b5829 100644 --- a/src/main/java/net/openhft/compiler/MyJavaFileManager.java +++ b/src/main/java/net/openhft/compiler/MyJavaFileManager.java @@ -6,7 +6,6 @@ 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; @@ -14,8 +13,6 @@ 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; @@ -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 buffers = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Map> outputsBySource = new HashMap<>(); /** * Create a file manager that delegates to the provided instance while @@ -77,7 +53,7 @@ public MyJavaFileManager(StandardJavaFileManager fileManager) { * @return the module locations or an empty iterable */ public synchronized Iterable> listLocationsForModules(final Location location) { - return invokeNamedMethodIfAvailable(location, "listLocationsForModules"); + return invokeNamedMethodIfAvailable(location, "listLocationsForModules", Collections.>emptyList()); } /** @@ -88,7 +64,7 @@ public synchronized Iterable> 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) { @@ -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 outputs = outputsBySource.get(sibling.toUri()); + if (outputs == null) { + outputs = new LinkedHashSet<>(); + outputsBySource.put(sibling.toUri(), outputs); + } + outputs.add(className); + } + } return baos; } @@ -187,6 +173,31 @@ public int isSupportedOption(String option) { */ public void clearBuffers() { buffers.clear(); + synchronized (outputsBySource) { + outputsBySource.clear(); + } + } + + void prepareForCompilation(Iterable sources) { + synchronized (outputsBySource) { + for (JavaFileObject source : sources) + outputsBySource.remove(source.toUri()); + } + } + + @NotNull + Map getBuffersForSources(Iterable sources) { + final Set outputNames = new LinkedHashSet<>(); + synchronized (outputsBySource) { + for (JavaFileObject source : sources) { + Set outputs = outputsBySource.get(source.toUri()); + if (outputs != null) + outputNames.addAll(outputs); + } + } + final Map result = getAllBuffers(); + result.keySet().retainAll(outputNames); + return result; } /** @@ -229,26 +240,27 @@ public Map 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 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 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); } } diff --git a/src/test/java/net/openhft/compiler/AnchorDefineClassTest.java b/src/test/java/net/openhft/compiler/AnchorDefineClassTest.java new file mode 100644 index 0000000..0f7477b --- /dev/null +++ b/src/test/java/net/openhft/compiler/AnchorDefineClassTest.java @@ -0,0 +1,307 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.compiler; + +import net.openhft.compiler.anchorone.AnchorOne; +import net.openhft.compiler.anchortwo.AnchorTwo; +import org.junit.Test; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +/** + * Anchor/lookup class-definition strategy (issue #91): the compiler can define a generated + * class in the caller's package and class loader through a {@link MethodHandles.Lookup} the + * caller supplies, using no {@code sun.misc.Unsafe} and no {@code setAccessible}. + *

+ * This test lives in {@code net.openhft.compiler} so the lookup it hands over + * ({@link MethodHandles#lookup()}) has full privileges in that package, and the generated class + * is declared in the same package - the constraint the JDK enforces on + * {@link java.lang.invoke.MethodHandles.Lookup#defineClass(byte[])}. + */ +public class AnchorDefineClassTest { + + @Test + public void anchorModeDefinesInCallerLoaderWithoutUnsafe() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + + final MethodHandles.Lookup anchor = MethodHandles.lookup(); + final String className = "net.openhft.compiler.AnchorGenerated"; + final String source = + "package net.openhft.compiler;\n" + + "import java.util.concurrent.Callable;\n" + + "public class AnchorGenerated implements Callable {\n" + + " public String call() { return \"anchored\"; }\n" + + "}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + final Class generated = cc.loadFromJava(anchor, className, source); + + // Pass-after: the class is defined in the anchor's own loader (not a fresh child), + // which is exactly what the non-Unsafe Lookup#defineClass path guarantees. + assertEquals(className, generated.getName()); + assertSame("class must be defined in the anchor's class loader", + anchor.lookupClass().getClassLoader(), generated.getClassLoader()); + assertEquals("net.openhft.compiler", generated.getPackage().getName()); + + @SuppressWarnings("unchecked") + final Callable instance = (Callable) generated.getDeclaredConstructor().newInstance(); + assertEquals("anchored", instance.call()); + } + } + + /** + * Negative control: the anchor strategy inherits the JDK's package check. A class declared + * in a different package than the anchor is rejected - a corrupted cross-loader definition + * cannot slip through, unlike a raw {@code ClassLoader.defineClass} via Unsafe. + */ + @Test + public void anchorModeRejectsForeignPackage() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + + final MethodHandles.Lookup anchor = MethodHandles.lookup(); + final String className = "net.openhft.compiler.foreign.ForeignGenerated"; + final String source = + "package net.openhft.compiler.foreign;\n" + + "public class ForeignGenerated {\n" + + " public int value() { return 42; }\n" + + "}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + try { + cc.loadFromJava(anchor, className, source); + fail("expected the JDK to reject a class outside the anchor's package"); + } catch (IllegalArgumentException expected) { + // MethodHandles.Lookup#defineClass throws IllegalArgumentException for a class + // that is not in the same package as the lookup class. + } + } + } + + @Test + public void nullArgumentsRejected() { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + try { + CompilerUtils.defineClass((MethodHandles.Lookup) null, new byte[0]); + fail("null anchor must be rejected"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void repeatedLoadReturnsCachedClass() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String className = "net.openhft.compiler.AnchorRepeated"; + final String source = "package net.openhft.compiler; public class AnchorRepeated {}"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class first = cc.loadFromJava(MethodHandles.lookup(), className, source); + Class second = cc.loadFromJava(MethodHandles.lookup(), className, source); + assertSame(first, second); + } + } + + @Test + public void consecutiveClassesDoNotRedefineRetainedOutput() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class first = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorFirst", + "package net.openhft.compiler; public class AnchorFirst {}"); + Class second = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorSecond", + "package net.openhft.compiler; public class AnchorSecond {}"); + + assertEquals("net.openhft.compiler.AnchorFirst", first.getName()); + assertEquals("net.openhft.compiler.AnchorSecond", second.getName()); + } + } + + @Test + public void lookupsForTwoPackagesMayShareOneLoader() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + assertSame(AnchorOne.class.getClassLoader(), AnchorTwo.class.getClassLoader()); + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class first = cc.loadFromJava(AnchorOne.lookup(), + "net.openhft.compiler.anchorone.GeneratedOne", + "package net.openhft.compiler.anchorone; public class GeneratedOne {}"); + Class second = cc.loadFromJava(AnchorTwo.lookup(), + "net.openhft.compiler.anchortwo.GeneratedTwo", + "package net.openhft.compiler.anchortwo; public class GeneratedTwo {}"); + + assertEquals(AnchorOne.class.getPackage(), first.getPackage()); + assertEquals(AnchorTwo.class.getPackage(), second.getPackage()); + } + } + + @Test + public void nestedAndAnonymousClassesAreDefinedWithPrimary() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String source = + "package net.openhft.compiler;\n" + + "public class AnchorNested {\n" + + " public static class Nested { public int value() { return 7; } }\n" + + " public Runnable anonymous() { return new Runnable() { public void run() {} }; }\n" + + "}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class outer = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorNested", source); + Object instance = outer.getDeclaredConstructor().newInstance(); + Runnable anonymous = (Runnable) outer.getMethod("anonymous").invoke(instance); + + assertEquals("net.openhft.compiler.AnchorNested$1", anonymous.getClass().getName()); + assertEquals("net.openhft.compiler.AnchorNested$Nested", outer.getDeclaredClasses()[0].getName()); + } + } + + @Test + public void sameSourceSuperclassBeforePrimaryIsDefinedFirst() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String source = + "package net.openhft.compiler;\n" + + "class AnchorBaseBefore {}\n" + + "public class AnchorDerivedBefore extends AnchorBaseBefore {}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class derived = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorDerivedBefore", source); + assertEquals("net.openhft.compiler.AnchorBaseBefore", derived.getSuperclass().getName()); + } + } + + @Test + public void sameSourceSuperclassAfterPrimaryIsRetried() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String source = + "package net.openhft.compiler;\n" + + "public class AnchorDerivedAfter extends AnchorBaseAfter {}\n" + + "class AnchorBaseAfter {}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class derived = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorDerivedAfter", source); + assertEquals("net.openhft.compiler.AnchorBaseAfter", derived.getSuperclass().getName()); + } + } + + @Test + public void failedAuxiliaryDefinitionDoesNotCompletePrimaryCache() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String primaryName = "net.openhft.compiler.AnchorPartialRetry"; + final String helperName = primaryName + "Helper"; + final String source = + "package net.openhft.compiler;\n" + + "public class AnchorPartialRetry {\n" + + " public Object helper() { return new AnchorPartialRetryHelper(); }\n" + + "}\n" + + "class AnchorPartialRetryHelper {}\n"; + final AtomicBoolean corruptFirstHelper = new AtomicBoolean(true); + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + cc.setFileManagerOverride(delegate -> new MyJavaFileManager(delegate) { + @Override + Map getBuffersForSources(Iterable sources) { + Map compiled = super.getBuffersForSources(sources); + if (!corruptFirstHelper.compareAndSet(true, false)) + return compiled; + + Map ordered = new LinkedHashMap<>(); + ordered.put(primaryName, compiled.get(primaryName)); + byte[] helper = compiled.get(helperName); + ordered.put(helperName, Arrays.copyOf(helper, helper.length - 1)); + return ordered; + } + }); + + try { + cc.loadFromJava(MethodHandles.lookup(), primaryName, source); + fail("corrupt auxiliary class must fail definition"); + } catch (ClassFormatError expected) { + // The primary was defined first, but the batch did not complete. + } + assertEquals(primaryName, + Class.forName(primaryName, false, getClass().getClassLoader()).getName()); + + Class primary = cc.loadFromJava(MethodHandles.lookup(), primaryName, source); + Object helper = primary.getMethod("helper").invoke(primary.getDeclaredConstructor().newInstance()); + assertEquals(helperName, helper.getClass().getName()); + } + } + + @Test + public void loadingDoesNotInitialisePrimaryClass() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final String source = + "package net.openhft.compiler;\n" + + "public class AnchorInitialisation {\n" + + " static { if (true) throw new RuntimeException(\"initialised\"); }\n" + + "}\n"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class generated = cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorInitialisation", source); + assertEquals("net.openhft.compiler.AnchorInitialisation", generated.getName()); + try { + generated.getDeclaredConstructor().newInstance(); + fail("construction should initialise the class"); + } catch (ExceptionInInitializerError expected) { + assertEquals("initialised", expected.getCause().getMessage()); + } + } + } + + @Test + public void lookupWithoutPackageAccessIsCallerError() { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + try { + CompilerUtils.defineClass(MethodHandles.publicLookup(), new byte[0]); + fail("publicLookup must not be allowed to define a class"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("PACKAGE")); + } + } + + @Test + public void concurrentSameClassCompilationDefinesOnce() throws Exception { + assumeTrue("anchor mode needs Java 9+", CompilerUtils.isAnchorDefineClassSupported()); + final int count = 8; + final ExecutorService executor = Executors.newFixedThreadPool(count); + final CountDownLatch start = new CountDownLatch(1); + final String source = "package net.openhft.compiler; public class AnchorConcurrent {}"; + + try (CachedCompiler cc = new CachedCompiler(null, null)) { + List>> futures = new ArrayList<>(); + for (int i = 0; i < count; i++) { + futures.add(executor.submit(() -> { + start.await(); + return cc.loadFromJava(MethodHandles.lookup(), + "net.openhft.compiler.AnchorConcurrent", source); + })); + } + start.countDown(); + Class expected = futures.get(0).get(); + for (Future> future : futures) + assertSame(expected, future.get()); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/net/openhft/compiler/CachedCompilerAdditionalTest.java b/src/test/java/net/openhft/compiler/CachedCompilerAdditionalTest.java index 535bc8d..5caa45d 100644 --- a/src/test/java/net/openhft/compiler/CachedCompilerAdditionalTest.java +++ b/src/test/java/net/openhft/compiler/CachedCompilerAdditionalTest.java @@ -42,6 +42,32 @@ public void compileFromJavaReturnsBytecode() throws Exception { } } + @Test + public void compileFromJavaReturnsOnlyCurrentSourceOutputs() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull("System compiler required", compiler); + + try (StandardJavaFileManager standardManager = compiler.getStandardFileManager(null, null, null)) { + CachedCompiler cachedCompiler = new CachedCompiler(null, null); + MyJavaFileManager fileManager = new MyJavaFileManager(standardManager); + Map first = cachedCompiler.compileFromJava( + "coverage.CurrentA", + "package coverage; public class CurrentA { static class Nested {} }", + fileManager); + Map second = cachedCompiler.compileFromJava( + "coverage.CurrentB", + "package coverage; public class CurrentB { Runnable r = new Runnable() { public void run() {} }; }", + fileManager); + + assertTrue(first.containsKey("coverage.CurrentA")); + assertTrue(first.containsKey("coverage.CurrentA$Nested")); + assertTrue(second.containsKey("coverage.CurrentB")); + assertTrue(second.containsKey("coverage.CurrentB$1")); + assertFalse(second.containsKey("coverage.CurrentA")); + assertFalse(second.containsKey("coverage.CurrentA$Nested")); + } + } + @Test public void compileFromJavaReturnsEmptyMapOnFailure() throws Exception { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); diff --git a/src/test/java/net/openhft/compiler/MyJavaFileManagerTest.java b/src/test/java/net/openhft/compiler/MyJavaFileManagerTest.java index 693519f..be5ec23 100644 --- a/src/test/java/net/openhft/compiler/MyJavaFileManagerTest.java +++ b/src/test/java/net/openhft/compiler/MyJavaFileManagerTest.java @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; public class MyJavaFileManagerTest { @@ -155,25 +156,49 @@ public void listLocationsForModulesAndInferModuleNameDeferToDelegate() throws IO } @Test - public void invokeNamedMethodHandlesMissingMethods() throws Exception { + public void compilesAndLoadsClassWithoutEncapsulationFlags() throws Exception { + // Issue #91: runtime compilation must succeed on strongly-encapsulated JDKs + // (JEP 403, JDK 17/21/25) without requiring + // --add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED. This test runs + // with no such flags configured; the module-location reflection in + // MyJavaFileManager must therefore not fail the compilation. + try (CachedCompiler cc = new CachedCompiler(null, null)) { + Class clazz = cc.loadFromJava("eg.Issue91", + "package eg;\n" + + "public class Issue91 implements java.util.concurrent.Callable {\n" + + " public String call() {\n" + + " return \"ok-\" + System.getProperty(\"java.specification.version\");\n" + + " }\n" + + "}\n"); + Object instance = clazz.getDeclaredConstructor().newInstance(); + @SuppressWarnings("unchecked") + java.util.concurrent.Callable callable = (java.util.concurrent.Callable) instance; + assertTrue(callable.call().startsWith("ok-")); + } + } + + @Test + public void invokeNamedMethodReturnsDefaultWhenMethodMissing() throws Exception { + // Behaviour updated for issue #91: when the delegate does not expose the + // named method (e.g. a Java 8 StandardJavaFileManager), the helper now + // returns the caller-supplied neutral default rather than throwing, so + // compilation degrades gracefully instead of failing. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); assertNotNull("System compiler required", compiler); try (StandardJavaFileManager delegate = compiler.getStandardFileManager(null, null, null)) { MyJavaFileManager manager = new MyJavaFileManager(delegate); java.lang.reflect.Method method = MyJavaFileManager.class.getDeclaredMethod( - "invokeNamedMethodIfAvailable", javax.tools.JavaFileManager.Location.class, String.class); + "invokeNamedMethodIfAvailable", javax.tools.JavaFileManager.Location.class, String.class, Object.class); method.setAccessible(true); - try { - method.invoke(manager, StandardLocation.CLASS_PATH, "nonExistingMethod"); - fail("Expected UnsupportedOperationException when method is absent"); - } catch (java.lang.reflect.InvocationTargetException expected) { - assertTrue(expected.getCause() instanceof UnsupportedOperationException); - } + Object sentinel = new Object(); + Object result = method.invoke(manager, StandardLocation.CLASS_PATH, "nonExistingMethod", sentinel); + assertSame(sentinel, result); } } @Test public void invokeNamedMethodWrapsInvocationFailures() throws Exception { + assumeTrue("module methods need Java 9+", hasJavaFileManagerMethod("listLocationsForModules")); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); assertNotNull("System compiler required", compiler); try (StandardJavaFileManager base = compiler.getStandardFileManager(null, null, null)) { @@ -182,7 +207,7 @@ public void invokeNamedMethodWrapsInvocationFailures() throws Exception { new Class[]{StandardJavaFileManager.class}, (proxyInstance, method, args) -> { if ("listLocationsForModules".equals(method.getName())) { - throw new InvocationTargetException(new IOException("forced")); + throw new IOException("forced"); } try { return method.invoke(base, args); @@ -192,10 +217,10 @@ public void invokeNamedMethodWrapsInvocationFailures() throws Exception { }); MyJavaFileManager manager = new MyJavaFileManager(proxy); java.lang.reflect.Method method = MyJavaFileManager.class.getDeclaredMethod( - "invokeNamedMethodIfAvailable", javax.tools.JavaFileManager.Location.class, String.class); + "invokeNamedMethodIfAvailable", javax.tools.JavaFileManager.Location.class, String.class, Object.class); method.setAccessible(true); try { - method.invoke(manager, StandardLocation.CLASS_PATH, "listLocationsForModules"); + method.invoke(manager, StandardLocation.CLASS_PATH, "listLocationsForModules", null); fail("Expected invocation failure to be wrapped"); } catch (InvocationTargetException expected) { Throwable cause = expected.getCause(); @@ -208,6 +233,34 @@ public void invokeNamedMethodWrapsInvocationFailures() throws Exception { } } + @Test + public void invokeNamedMethodPropagatesRuntimeFailures() throws Exception { + assumeTrue("module methods need Java 9+", hasJavaFileManagerMethod("inferModuleName")); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull("System compiler required", compiler); + try (StandardJavaFileManager base = compiler.getStandardFileManager(null, null, null)) { + StandardJavaFileManager proxy = (StandardJavaFileManager) Proxy.newProxyInstance( + StandardJavaFileManager.class.getClassLoader(), + new Class[]{StandardJavaFileManager.class}, + (proxyInstance, method, args) -> { + if ("inferModuleName".equals(method.getName())) + throw new IllegalStateException("forced runtime failure"); + try { + return method.invoke(base, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + MyJavaFileManager manager = new MyJavaFileManager(proxy); + try { + manager.inferModuleName(StandardLocation.CLASS_PATH); + fail("runtime failure must not be hidden as missing module information"); + } catch (IllegalStateException expected) { + assertEquals("forced runtime failure", expected.getMessage()); + } + } + } + @Test @SuppressWarnings("unchecked") public void getAllBuffersSkipsEntriesWhenFutureFails() throws Exception { @@ -249,6 +302,16 @@ private static javax.tools.JavaFileManager.Location resolveSystemModules() { } } + private static boolean hasJavaFileManagerMethod(String name) { + try { + javax.tools.JavaFileManager.class.getMethod( + name, javax.tools.JavaFileManager.Location.class); + return true; + } catch (NoSuchMethodException ignored) { + return false; + } + } + private static byte[] readFully(InputStream is) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[1024]; diff --git a/src/test/java/net/openhft/compiler/anchorone/AnchorOne.java b/src/test/java/net/openhft/compiler/anchorone/AnchorOne.java new file mode 100644 index 0000000..6637d5b --- /dev/null +++ b/src/test/java/net/openhft/compiler/anchorone/AnchorOne.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.compiler.anchorone; + +import java.lang.invoke.MethodHandles; + +public final class AnchorOne { + private AnchorOne() { + } + + public static MethodHandles.Lookup lookup() { + return MethodHandles.lookup(); + } +} diff --git a/src/test/java/net/openhft/compiler/anchortwo/AnchorTwo.java b/src/test/java/net/openhft/compiler/anchortwo/AnchorTwo.java new file mode 100644 index 0000000..6855349 --- /dev/null +++ b/src/test/java/net/openhft/compiler/anchortwo/AnchorTwo.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.compiler.anchortwo; + +import java.lang.invoke.MethodHandles; + +public final class AnchorTwo { + private AnchorTwo() { + } + + public static MethodHandles.Lookup lookup() { + return MethodHandles.lookup(); + } +}