Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GR-47365] Integrate proxy registration to the reflection config #8822

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 20 additions & 16 deletions docs/reference-manual/native-image/ReachabilityMetadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,22 +323,26 @@ The following methods are evaluated at build time when called with constant argu

### Dynamic Proxy Metadata in JSON

Dynamic proxy metadata should be specified in a _proxy-config.json_ file and conform to the JSON schema defined in
[proxy-config-schema-v1.0.0.json](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/proxy-config-schema-v1.0.0.json).
The schema also includes further details and explanations how this configuration works. Here is the example of the proxy-config.json:
Dynamic proxy metadata should be specified as part of a _reflect-config.json_ file by adding `"proxy"`-type entries, conforming to the JSON schema defined in [config-type-schema-v1.1.0.json](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/config-type-schema-v1.0.0.json).
It enables you to register members of a proxy class for reflection the same way as it would be done for a named class.
The order in which interfaces are given matters and the interfaces will be passed in the same order to generate the proxy class.
The schema also includes further details and explanations how this configuration works.
Here is an example of dynamic proxy metadata in reflect-config.json:
```json
[
{
"condition": {
"typeReachable": "<condition-class>"
},
"interfaces": [
"IA",
"IB"
]
"type": { "proxy": [
"IA",
"IB"
]}
}
]
```
Contents of _proxy-config.json_ files will still be parsed and honored by Native Image, but this file is now deprecated
and the [Native Image agent](AutomaticMetadataCollection.md) outputs proxy metadata to reflect-config.json.

## Serialization
Java can serialize any class that implements the `Serializable` interface.
Expand Down Expand Up @@ -388,6 +392,14 @@ The schema also includes further details and explanations how this configuration
},
"type": "<fully-qualified-class-name>",
"customTargetConstructorClass": "<custom-target-constructor-class>"
},
{
"condition": {
"typeReachable": "<condition-class>"
},
"type": {
"proxy": ["<fully-qualified-interface-name-1>", "<fully-qualified-interface-name-n>"]
}
}
],
"lambdaCapturingTypes": [
Expand All @@ -397,15 +409,7 @@ The schema also includes further details and explanations how this configuration
},
"name": "<fully-qualified-class-name>"
}
],
"proxies": [
{
"condition": {
"typeReachable": "<condition-class>"
},
"interfaces": ["<fully-qualified-interface-name-1>", "<fully-qualified-interface-name-n>"]
}
]
]
}
```

Expand Down
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-51172) Add support to catch OutOfMemoryError exceptions on native image if there is no memory left.
* (GR-43837) `--report-unsupported-elements-at-runtime` is now enabled by default and the option is deprecated.
* (GR-53359) Provide the `.debug_gdb_scripts` section that triggers auto-loading of `svmhelpers.py` in GDB. Remove single and double quotes from `ClassLoader.nameAndId` in the debuginfo.
* (GR-47365) Include dynamic proxy metadata in the reflection metadata with the syntax `"type": { "proxy": [<interface list>] }`. This allows members of proxy classes to be accessed reflectively. `proxy-config.json` is now deprecated but will still be honored.

## GraalVM for JDK 22 (Internal Version 24.0.0)
* (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
if (tracer != null) {
tracer.traceCall(context,
function,
getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE),
getClassNameOr(env, declaringClass, null, Tracer.UNKNOWN_VALUE),
getClassOrProxyInterfaceNames(env, clazz),
getClassOrProxyInterfaceNames(env, declaringClass),
getClassNameOr(env, callerClass, null, Tracer.UNKNOWN_VALUE),
result,
stackTrace,
Expand All @@ -210,6 +210,39 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
}
}

/**
* If the given class is a proxy, returns an array containing the names of its implemented
* interfaces, else returns the class name. This prevents classes with arbitrary names from
* being exposed outside the agent, since those names only make sense within a single execution
* of the program.
*
* @param env JNI environment of the thread running the JVMTI callback.
* @param clazz Handle to the class.
* @return The interface, or the original class if it is not a proxy or implements multiple
* interfaces.
*/
private static Object getClassOrProxyInterfaceNames(JNIEnvironment env, JNIObjectHandle clazz) {
if (clazz.equal(nullHandle())) {
return null;
}

boolean isProxy = Support.callStaticBooleanMethodL(env, agent.handles().getJavaLangReflectProxy(env), agent.handles().getJavaLangReflectProxyIsProxyClass(env), clazz);
if (Support.clearException(env)) {
return Tracer.UNKNOWN_VALUE;
}

if (!isProxy) {
return getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE);
}

JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
if (Support.clearException(env)) {
return Tracer.UNKNOWN_VALUE;
}

return getClassArrayNames(env, interfaces);
}

private static boolean forName(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
JNIObjectHandle callerClass = state.getDirectCallerClass();
JNIObjectHandle name = getObjectArgument(thread, 0);
Expand All @@ -232,7 +265,7 @@ private static boolean getDeclaredFields(JNIEnvironment jni, JNIObjectHandle thr
private static boolean handleGetFields(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
JNIObjectHandle callerClass = state.getDirectCallerClass();
JNIObjectHandle self = getReceiver(thread);
traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, self), nullHandle(), callerClass, bp.specification.methodName, null, state.getFullStackTraceOrNull());
traceReflectBreakpoint(jni, self, nullHandle(), callerClass, bp.specification.methodName, null, state.getFullStackTraceOrNull());
return true;
}

Expand All @@ -256,8 +289,7 @@ private static boolean handleGetMethods(JNIEnvironment jni, JNIObjectHandle thre
JNIObjectHandle callerClass = state.getDirectCallerClass();
JNIObjectHandle self = getReceiver(thread);
/* When reflection metadata tracking is disabled, all methods are considered invoked */
traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, self), nullHandle(), callerClass, bp.specification.methodName, trackReflectionMetadata ? null : true,
state.getFullStackTraceOrNull());
traceReflectBreakpoint(jni, self, nullHandle(), callerClass, bp.specification.methodName, trackReflectionMetadata ? null : true, state.getFullStackTraceOrNull());
return true;
}

Expand Down Expand Up @@ -315,8 +347,7 @@ private static boolean handleGetField(JNIEnvironment jni, JNIObjectHandle thread
declaring = nullHandle();
}
}
traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, self), getClassOrSingleProxyInterface(jni, declaring), callerClass, bp.specification.methodName, name.notEqual(nullHandle()),
state.getFullStackTraceOrNull(), fromJniString(jni, name));
traceReflectBreakpoint(jni, self, declaring, callerClass, bp.specification.methodName, name.notEqual(nullHandle()), state.getFullStackTraceOrNull(), fromJniString(jni, name));
return true;
}

Expand Down Expand Up @@ -379,8 +410,7 @@ private static boolean getConstructor(JNIEnvironment jni, JNIObjectHandle thread
JNIObjectHandle self = getReceiver(thread);
JNIObjectHandle paramTypesHandle = getObjectArgument(thread, 1);
Object paramTypes = getClassArrayNames(jni, paramTypesHandle);
traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, self), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(),
paramTypes);
traceReflectBreakpoint(jni, self, nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), paramTypes);
return true;
}

Expand Down Expand Up @@ -410,8 +440,7 @@ private static boolean handleGetMethod(JNIEnvironment jni, JNIObjectHandle threa
}
String name = fromJniString(jni, nameHandle);
Object paramTypes = getClassArrayNames(jni, paramTypesHandle);
traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, self), getClassOrSingleProxyInterface(jni, declaring), callerClass, bp.specification.methodName,
nameHandle.notEqual(nullHandle()), state.getFullStackTraceOrNull(), name, paramTypes);
traceReflectBreakpoint(jni, self, declaring, callerClass, bp.specification.methodName, nameHandle.notEqual(nullHandle()), state.getFullStackTraceOrNull(), name, paramTypes);
return true;
}

Expand Down Expand Up @@ -476,8 +505,7 @@ private static boolean handleInvokeMethod(JNIEnvironment jni, JNIObjectHandle th
}
Object paramTypes = getClassArrayNames(jni, paramTypesHandle);

traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, declaring), getClassOrSingleProxyInterface(jni, declaring), callerClass, "invokeMethod", declaring.notEqual(nullHandle()),
state.getFullStackTraceOrNull(), name, paramTypes);
traceReflectBreakpoint(jni, declaring, declaring, callerClass, "invokeMethod", declaring.notEqual(nullHandle()), state.getFullStackTraceOrNull(), name, paramTypes);

/*
* Calling Class.newInstance through Method.invoke should register the class for reflective
Expand Down Expand Up @@ -524,8 +552,7 @@ private static boolean handleInvokeConstructor(JNIEnvironment jni, @SuppressWarn
}
Object paramTypes = getClassArrayNames(jni, paramTypesHandle);

traceReflectBreakpoint(jni, getClassOrSingleProxyInterface(jni, declaring), getClassOrSingleProxyInterface(jni, declaring), callerClass, "invokeConstructor", declaring.notEqual(nullHandle()),
state.getFullStackTraceOrNull(), paramTypes);
traceReflectBreakpoint(jni, declaring, declaring, callerClass, "invokeConstructor", declaring.notEqual(nullHandle()), state.getFullStackTraceOrNull(), paramTypes);
return true;
}

Expand Down Expand Up @@ -1521,39 +1548,6 @@ private static JNIMethodId resolveBreakpointMethod(JNIEnvironment jni, JNIObject
return method;
}

/**
* If the given class is a proxy implementing a single interface, returns this interface. This
* prevents classes with arbitrary names from being exposed outside the agent, since those names
* only make sense within a single execution of the program.
*
* @param env JNI environment of the thread running the JVMTI callback.
* @param clazz Handle to the class.
* @return The interface, or the original class if it is not a proxy or implements multiple
* interfaces.
*/
public static JNIObjectHandle getClassOrSingleProxyInterface(JNIEnvironment env, JNIObjectHandle clazz) {
boolean isProxy = Support.callStaticBooleanMethodL(env, agent.handles().getJavaLangReflectProxy(env), agent.handles().getJavaLangReflectProxyIsProxyClass(env), clazz);
if (Support.clearException(env) || !isProxy) {
return clazz;
}

JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
if (Support.clearException(env) || interfaces.equal(nullHandle())) {
return clazz;
}

int interfacesLength = Support.jniFunctions().getGetArrayLength().invoke(env, interfaces);
guarantee(!Support.clearException(env));
if (interfacesLength != 1) {
return clazz;
}

JNIObjectHandle iface = Support.jniFunctions().getGetObjectArrayElement().invoke(env, interfaces, 0);
guarantee(!Support.clearException(env) && iface.notEqual(nullHandle()));

return iface;
}

private static void bindNativeBreakpoint(JNIEnvironment jni, NativeBreakpoint bp, CodePointer originalAddress, WordPointer newAddressPtr) {
assert !recursive.get();
bp.replacedFunction = originalAddress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import com.oracle.svm.configure.config.ResourceConfiguration;
import com.oracle.svm.configure.config.SerializationConfiguration;
import com.oracle.svm.configure.config.TypeConfiguration;
import com.oracle.svm.core.configure.ConfigurationTypeDescriptor;
import com.oracle.svm.core.configure.NamedConfigurationTypeDescriptor;
import com.oracle.svm.core.util.VMError;

public class OmitPreviousConfigTests {
Expand Down Expand Up @@ -142,8 +144,8 @@ private static void doTestTypeConfig(TypeConfiguration typeConfig) {
}

private static void doTestExpectedMissingTypes(TypeConfiguration typeConfig) {
Assert.assertNull(typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), "FlagTestA"));
Assert.assertNull(typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), "FlagTestB"));
Assert.assertNull(typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), new NamedConfigurationTypeDescriptor("FlagTestA")));
Assert.assertNull(typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), new NamedConfigurationTypeDescriptor("FlagTestB")));
}

private static void doTestTypeFlags(TypeConfiguration typeConfig) {
Expand Down Expand Up @@ -201,7 +203,7 @@ private static void doTestSerializationConfig(SerializationConfiguration seriali
}

private static ConfigurationType getConfigTypeOrFail(TypeConfiguration typeConfig, String typeName) {
ConfigurationType type = typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), typeName);
ConfigurationType type = typeConfig.get(UnresolvedConfigurationCondition.alwaysTrue(), new NamedConfigurationTypeDescriptor(typeName));
Assert.assertNotNull(type);
return type;
}
Expand Down Expand Up @@ -289,14 +291,14 @@ void setFlags(ConfigurationType config) {
}
}

String getTypeName() {
return TEST_CLASS_NAME_PREFIX + "_" + methodKind.name();
ConfigurationTypeDescriptor getTypeName() {
return new NamedConfigurationTypeDescriptor(TEST_CLASS_NAME_PREFIX + "_" + methodKind.name());
}

void doTest() {
TypeConfiguration currentConfigWithoutPrevious = currentConfig.copyAndSubtract(previousConfig);

String name = getTypeName();
ConfigurationTypeDescriptor name = getTypeName();
ConfigurationType configurationType = currentConfigWithoutPrevious.get(UnresolvedConfigurationCondition.alwaysTrue(), name);
if (methodsThatMustExist.size() == 0) {
Assert.assertNull("Generated configuration type " + name + " exists. Expected it to be cleared as it is empty.", configurationType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.List;
import java.util.Objects;

import com.oracle.svm.core.configure.NamedConfigurationTypeDescriptor;
import com.oracle.svm.core.util.json.JsonPrintable;
import com.oracle.svm.core.util.json.JsonWriter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

import com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberAccessibility;
import com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberDeclaration;
import com.oracle.svm.core.configure.ConfigurationTypeDescriptor;
import com.oracle.svm.core.util.json.JsonPrintable;
import com.oracle.svm.core.util.json.JsonPrinter;
import com.oracle.svm.core.util.json.JsonWriter;
Expand Down Expand Up @@ -103,10 +104,6 @@ static ConfigurationType copyAndMerge(ConfigurationType type, ConfigurationType
private ConfigurationMemberAccessibility allDeclaredConstructorsAccess = ConfigurationMemberAccessibility.NONE;
private ConfigurationMemberAccessibility allPublicConstructorsAccess = ConfigurationMemberAccessibility.NONE;

public ConfigurationType(UnresolvedConfigurationCondition condition, String qualifiedJavaName, boolean includeAllElements) {
this(condition, new NamedConfigurationTypeDescriptor(qualifiedJavaName), includeAllElements);
}

public ConfigurationType(UnresolvedConfigurationCondition condition, ConfigurationTypeDescriptor typeDescriptor, boolean includeAllElements) {
this.condition = condition;
this.typeDescriptor = typeDescriptor;
Expand Down Expand Up @@ -445,8 +442,8 @@ public synchronized void setAllPublicConstructors(ConfigurationMemberAccessibili
public synchronized void printJson(JsonWriter writer) throws IOException {
writer.append('{').indent().newline();
ConfigurationConditionPrintable.printConditionAttribute(condition, writer);
/* GR-50385: Replace with "type" (and flip boolean entries below) */
writer.quote("name").append(":");
/* GR-50385: Flip boolean entries below when "type" includes them by default. */
writer.quote("type").append(":");
typeDescriptor.printJson(writer);

optionallyPrintJsonBoolean(writer, allDeclaredFields, "allDeclaredFields");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberAccessibility;
import com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberDeclaration;
import com.oracle.svm.core.TypeResult;
import com.oracle.svm.core.configure.ConfigurationTypeDescriptor;
import com.oracle.svm.core.configure.ReflectionConfigurationParserDelegate;
import com.oracle.svm.core.util.VMError;

Expand All @@ -43,10 +44,10 @@ public ParserConfigurationAdapter(TypeConfiguration configuration) {
}

@Override
public TypeResult<ConfigurationType> resolveType(UnresolvedConfigurationCondition condition, String typeName, boolean allowPrimitives, boolean includeAllElements) {
ConfigurationType type = configuration.get(condition, typeName);
ConfigurationType result = type != null ? type : new ConfigurationType(condition, typeName, includeAllElements);
return TypeResult.forType(typeName, result);
public TypeResult<ConfigurationType> resolveType(UnresolvedConfigurationCondition condition, ConfigurationTypeDescriptor typeDescriptor, boolean allowPrimitives, boolean includeAllElements) {
ConfigurationType type = configuration.get(condition, typeDescriptor);
ConfigurationType result = type != null ? type : new ConfigurationType(condition, typeDescriptor, includeAllElements);
return TypeResult.forType(typeDescriptor.toString(), result);
}

@Override
Expand Down