Skip to content

Commit

Permalink
Update unsafe class defining to work on Java 17+ (close #1529)
Browse files Browse the repository at this point in the history
Java 17 has removed Unsafe.defineAnonymousClass in favor of Lookup.defineHiddenClass

PiperOrigin-RevId: 399274715
  • Loading branch information
mcculls authored and Guice Team committed Sep 27, 2021
1 parent 25fe439 commit cf759d4
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 84 deletions.
3 changes: 2 additions & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
<phase>test</phase>
<goals><goal>test</goal></goals>
<configuration>
<argLine>-Dguice_custom_class_loading=ANONYMOUS</argLine>
<!-- enable ShowHiddenFrames to help tests find AOP frames on Java17 -->
<argLine>-Dguice_custom_class_loading=ANONYMOUS -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames</argLine>
</configuration>
</execution>
<execution>
Expand Down
6 changes: 3 additions & 3 deletions core/src/com/google/inject/internal/InternalFlags.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ public enum IncludeStackTraceOption {
public enum CustomClassLoadingOption {
/**
* Define fast/enhanced types in the same class loader as their original type, never creates
* class loaders. Uses Unsafe.defineAnonymousClass to gain access to existing class loaders.
* class loaders. Uses {@link sun.misc.Unsafe} to gain access to existing class loaders.
*/
OFF,

/**
* Define fast/enhanced types with Unsafe.defineAnonymousClass, never creates class loaders.
* This is faster than regular class loading and anonymous classes are easier to unload.
* Define fast/enhanced types anonymously as hidden nest-mates, never creates class loaders.
* This is faster than regular class loading and the resulting classes are easier to unload.
*
* <p>Note: with this option you cannot look up fast/enhanced types by name or mock/spy them.
*/
Expand Down
49 changes: 49 additions & 0 deletions core/src/com/google/inject/internal/aop/AnonymousClassDefiner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 Google Inc.
*
* 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 com.google.inject.internal.aop;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* {@link ClassDefiner} that defines classes using {@code sun.misc.Unsafe#defineAnonymousClass}.
*
* @author [email protected] (Stuart McCulloch)
*/
final class AnonymousClassDefiner implements ClassDefiner {

private static final Object THE_UNSAFE;
private static final Method ANONYMOUS_DEFINE_METHOD;

static {
try {
Class<?> unsafeType = Class.forName("sun.misc.Unsafe");
Field theUnsafeField = unsafeType.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
THE_UNSAFE = theUnsafeField.get(null);
ANONYMOUS_DEFINE_METHOD =
unsafeType.getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}

@Override
public Class<?> define(Class<?> hostClass, byte[] bytecode) throws Exception {
return (Class<?>) ANONYMOUS_DEFINE_METHOD.invoke(THE_UNSAFE, hostClass, bytecode, null);
}
}
13 changes: 9 additions & 4 deletions core/src/com/google/inject/internal/aop/ClassDefining.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,19 @@ public static Class<?> define(Class<?> hostClass, byte[] bytecode) throws Except
return ClassDefinerHolder.INSTANCE.define(hostClass, bytecode);
}

/** Returns true if the ClassDefiner has access to package-private members. */
/** Returns true if the current class definer allows access to package-private members. */
public static boolean hasPackageAccess() {
return ClassDefinerHolder.IS_UNSAFE;
}

/** Does the given class host new types anonymously, meaning they are not visible by name? */
public static boolean isAnonymousHost(Class<?> hostClass) {
return ClassDefinerHolder.IS_UNSAFE && UnsafeClassDefiner.isAnonymousHost(hostClass);
/** Returns true if it's possible to load by name proxies defined from the given host. */
public static boolean canLoadProxyByName(Class<?> hostClass) {
return !ClassDefinerHolder.IS_UNSAFE || UnsafeClassDefiner.canLoadProxyByName(hostClass);
}

/** Returns true if it's possible to downcast to proxies defined from the given host. */
public static boolean canDowncastToProxy(Class<?> hostClass) {
return !ClassDefinerHolder.IS_UNSAFE || UnsafeClassDefiner.canDowncastToProxy(hostClass);
}

/** Binds the preferred {@link ClassDefiner} instance. */
Expand Down
15 changes: 8 additions & 7 deletions core/src/com/google/inject/internal/aop/Enhancer.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ final class Enhancer extends AbstractGlueGenerator {
super(hostClass, ENHANCER_BY_GUICE_MARKER);
this.bridgeDelegates = bridgeDelegates;

// CHECKCAST(proxyName) fails when hosted anonymously; hostName works in that scenario
this.checkcastToProxy = ClassDefining.isAnonymousHost(hostClass) ? hostName : proxyName;
// with defineAnonymousClass we can't downcast to the proxy and must use host instead
this.checkcastToProxy = ClassDefining.canDowncastToProxy(hostClass) ? proxyName : hostName;
}

@Override
Expand Down Expand Up @@ -222,11 +222,8 @@ private void setupInvokerTable(ClassWriter cw) {
Handle trampolineHandle =
new Handle(H_INVOKESTATIC, proxyName, TRAMPOLINE_NAME, TRAMPOLINE_DESCRIPTOR, false);

if (ClassDefining.isAnonymousHost(hostClass)) {
// proxy class is anonymous we can't create our lambda glue, store raw trampoline instead
mv.visitLdcInsn(trampolineHandle);
} else {
// otherwise generate lambda glue to make the raw trampoline look like an invoker table
if (ClassDefining.canLoadProxyByName(hostClass)) {
// generate lambda glue to make the raw trampoline look like an invoker table

mv.visitMethodInsn(
INVOKESTATIC,
Expand Down Expand Up @@ -254,6 +251,10 @@ private void setupInvokerTable(ClassWriter cw) {
"getTarget",
"()Ljava/lang/invoke/MethodHandle;",
false);

} else {
// proxy class is hidden so we can't create our lambda glue, store raw trampoline instead
mv.visitLdcInsn(trampolineHandle);
}

mv.visitFieldInsn(PUTSTATIC, proxyName, INVOKERS_NAME, INVOKERS_DESCRIPTOR);
Expand Down
38 changes: 38 additions & 0 deletions core/src/com/google/inject/internal/aop/GeneratedClassDefiner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2021 Google Inc.
*
* 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 com.google.inject.internal.aop;

import java.util.function.BiFunction;

/**
* {@link ClassDefiner} that defines classes using a generated access function.
*
* @author [email protected] (Stuart McCulloch)
*/
final class GeneratedClassDefiner implements ClassDefiner {

private final BiFunction<ClassLoader, byte[], Class<?>> defineAccess;

GeneratedClassDefiner(BiFunction<ClassLoader, byte[], Class<?>> defineAccess) {
this.defineAccess = defineAccess;
}

@Override
public Class<?> define(Class<?> hostClass, byte[] bytecode) {
return defineAccess.apply(hostClass.getClassLoader(), bytecode);
}
}
81 changes: 81 additions & 0 deletions core/src/com/google/inject/internal/aop/HiddenClassDefiner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2021 Google Inc.
*
* 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 com.google.inject.internal.aop;

import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* {@link ClassDefiner} that defines classes using {@code MethodHandles.Lookup#defineHiddenClass}.
*
* @author [email protected] (Stuart McCulloch)
*/
final class HiddenClassDefiner implements ClassDefiner {

private static final Object THE_UNSAFE;
private static final Object TRUSTED_LOOKUP_BASE;
private static final Object TRUSTED_LOOKUP_OFFSET;
private static final Method GET_OBJECT_METHOD;
private static final Object HIDDEN_CLASS_OPTIONS;
private static final Method HIDDEN_DEFINE_METHOD;

static {
try {
Class<?> unsafeType = Class.forName("sun.misc.Unsafe");
Field theUnsafeField = unsafeType.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
THE_UNSAFE = theUnsafeField.get(null);
Field trustedLookupField = Lookup.class.getDeclaredField("IMPL_LOOKUP");
Method baseMethod = unsafeType.getMethod("staticFieldBase", Field.class);
TRUSTED_LOOKUP_BASE = baseMethod.invoke(THE_UNSAFE, trustedLookupField);
Method offsetMethod = unsafeType.getMethod("staticFieldOffset", Field.class);
TRUSTED_LOOKUP_OFFSET = offsetMethod.invoke(THE_UNSAFE, trustedLookupField);
GET_OBJECT_METHOD = unsafeType.getMethod("getObject", Object.class, long.class);
HIDDEN_CLASS_OPTIONS = classOptions("NESTMATE");
HIDDEN_DEFINE_METHOD =
Lookup.class.getMethod(
"defineHiddenClass", byte[].class, boolean.class, HIDDEN_CLASS_OPTIONS.getClass());
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}

@Override
public Class<?> define(Class<?> hostClass, byte[] bytecode) throws Exception {
Lookup trustedLookup =
(Lookup) GET_OBJECT_METHOD.invoke(THE_UNSAFE, TRUSTED_LOOKUP_BASE, TRUSTED_LOOKUP_OFFSET);
Lookup definedLookup =
(Lookup)
HIDDEN_DEFINE_METHOD.invoke(
trustedLookup.in(hostClass), bytecode, false, HIDDEN_CLASS_OPTIONS);
return definedLookup.lookupClass();
}

/** Creates {@link MethodHandles.Lookup.ClassOption} array with the named options. */
@SuppressWarnings("unchecked")
private static Object classOptions(String... options) throws ClassNotFoundException {
@SuppressWarnings("rawtypes") // Unavoidable, only way to use Enum.valueOf
Class optionClass = Class.forName(Lookup.class.getName() + "$ClassOption");
Object classOptions = Array.newInstance(optionClass, options.length);
for (int i = 0; i < options.length; i++) {
Array.set(classOptions, i, Enum.valueOf(optionClass, options[i]));
}
return classOptions;
}
}
Loading

0 comments on commit cf759d4

Please sign in to comment.