Skip to content

[GR-48191] Enable lambda reflection queries #11350

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

Merged
merged 1 commit into from
Jun 18, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/reference-manual/native-image/ReachabilityMetadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,40 @@ Metadata, for proxy classes, is in the form an ordered collection of interfaces
}
```

To provide metadata for a lambda class, the following metadata must be added to the `reflection` array in
_reachability-metadata.json_

```json
{
"type": {
"lambda": {
"declaringClass": "FullyQualifiedLambdaDeclaringType",
"declaringMethod": {
"name": "declaringMethodName",
"parameterType": [
"FullyQualifiedParameterType1",
"...",
"FullyQualifiedParameterType2"
]
},
"interfaces": [
"FullyQualifiedLambdaInterface1",
"...",
"FullyQualifiedLamdbaInterface2"
]
}
}
}
```

The `"declaringClass"` field specifies in which class, and the optional `"declaringMethod"` field specifies in which
method the lambda is defined.
If `"declaringMethod"` is not specified, the lambda class is searched through all methods of the specified declaring
class.
The `"interfaces"` field specifies which interfaces are implemented by the lambda class.
Such a definition can match multiple lambda classes. If that is the case, the registration entry applies to all those
classes.

Invocation of methods above without the provided metadata will result in throwing `MissingReflectionRegistrationError` which extends `java.lang.Error` and
should not be handled. Note that even if a type does not exist on the classpath, the methods above will throw a `MissingReflectionRegistrationError`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,47 @@
"title": "Fully qualified name of the interface defining the proxy class",
"type": "string"
}
},
"lambda": {
"title": "Lambda class descriptor",
"type": "object",
"properties": {
"declaringClass": {
"title": "The class in which the lambda class is defined",
"type": "#/$defs/type"
},
"declaringMethod": {
"title": "The method in which the lambda class is defined",
"type": "#/$defs/method",
"default": {}
},
"interfaces": {
"title": "Non-empty list of interfaces implemented by the lambda class",
"type": "array",
"items": {
"title": "Fully qualified name of the interface implemented by the lambda class",
"type": "string"
}
},
"required": [
"declaringClass",
"interfaces"
],
"additionalProperties": false
}
}
},
"required": [
"proxy"
"oneOf": [
{
"required": [
"proxy"
]
},
{
"required": [
"lambda"
]
}
],
"additionalProperties": false
}
Expand Down
3 changes: 2 additions & 1 deletion substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-60208) Adds the Tracing Agent support for applications using the Foreign Function & Memory (FFM) API. The agent generates FFM configuration in _foreign-config.json_. Additionally, support for FFM configurations has been added to the `native-image-configure` tool.
* (GR-64787) Enable `--install-exit-handlers` by default for executables and deprecate the option. If shared libraries were using this flag, the same functionality can be restored by using `-H:+InstallExitHandlers`.
* (GR-47881) Remove the total number of loaded types, fields, and methods from the build output, deprecated these metrics in the build output schema, and removed already deprecated build output metrics.
* (GR-64619) Missing registration errors are now subclasses of `LinkageError`
* (GR-64619) Missing registration errors are now subclasses of `LinkageError`.
* (GR-63591) Resource bundle registration is now included as part of the `"resources"` section of _reachability-metadata.json_. When this is the case, the bundle name is specified using the `"bundle"` field.
* (GR-57827) Move the initialization of security providers from build time to runtime.
* (GR-57827) Security providers can now be initialized at run time (instead of build time) when using the option `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk`.
Run-time initialization of security providers helps reduce image heap size by avoiding unnecessary objects inclusion.
* (GR-48191) Enable lambda classes to be registered for reflection and serialization in _reachability-metadata.json_. The format is detailed [here](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ReachabilityMetadata.md).

## GraalVM for JDK 24 (Internal Version 24.2.0)
* (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
import com.oracle.svm.agent.stackaccess.EagerlyLoadedJavaStackAccess;
import com.oracle.svm.agent.stackaccess.InterceptedState;
import com.oracle.svm.agent.tracing.core.Tracer;
import com.oracle.svm.configure.LambdaConfigurationTypeDescriptor;
import com.oracle.svm.configure.NamedConfigurationTypeDescriptor;
import com.oracle.svm.configure.ProxyConfigurationTypeDescriptor;
import com.oracle.svm.configure.trace.AccessAdvisor;
import com.oracle.svm.core.c.function.CEntryPointOptions;
import com.oracle.svm.core.jni.headers.JNIEnvironment;
Expand Down Expand Up @@ -194,8 +197,8 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
if (tracer != null) {
tracer.traceCall(context,
function,
getClassOrProxyInterfaceNames(env, clazz),
getClassOrProxyInterfaceNames(env, declaringClass),
getTypeDescriptor(env, clazz),
getTypeDescriptor(env, declaringClass),
getClassNameOr(env, callerClass, null, Tracer.UNKNOWN_VALUE),
result,
stackTrace,
Expand Down Expand Up @@ -224,7 +227,7 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
* @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) {
static Object getTypeDescriptor(JNIEnvironment env, JNIObjectHandle clazz) {
if (clazz.equal(nullHandle())) {
return null;
}
Expand All @@ -233,17 +236,30 @@ private static Object getClassOrProxyInterfaceNames(JNIEnvironment env, JNIObjec
if (Support.clearException(env)) {
return Tracer.UNKNOWN_VALUE;
}

if (!isProxy) {
return getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE);
String className = getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE);
if (className == null || className.equals(Tracer.UNKNOWN_VALUE)) {
return className;
}

JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
if (Support.clearException(env)) {
return Tracer.UNKNOWN_VALUE;
boolean isLambda = className.contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING);
if (isProxy || isLambda) {
JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
if (Support.clearException(env)) {
return Tracer.UNKNOWN_VALUE;
}
Object interfaceNames = getClassArrayNames(env, interfaces);
if (interfaceNames.equals(Tracer.EXPLICIT_NULL) || interfaceNames.equals(Tracer.UNKNOWN_VALUE)) {
return interfaceNames;
}
List<String> interfaceNameString = Arrays.asList((String[]) interfaceNames);
if (isProxy) {
return ProxyConfigurationTypeDescriptor.fromInterfaceReflectionNames(interfaceNameString);
} else if (isLambda) {
String declaringClass = className.substring(0, className.indexOf(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING));
return LambdaConfigurationTypeDescriptor.fromReflectionNames(declaringClass, interfaceNameString);
}
}

return getClassArrayNames(env, interfaces);
return NamedConfigurationTypeDescriptor.fromReflectionName(className);
}

private static boolean forName(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*/
package com.oracle.svm.agent;

import static com.oracle.svm.agent.BreakpointInterceptor.getTypeDescriptor;
import static com.oracle.svm.core.jni.JNIObjectHandles.nullHandle;
import static com.oracle.svm.jvmtiagentbase.Support.check;
import static com.oracle.svm.jvmtiagentbase.Support.checkJni;
Expand Down Expand Up @@ -94,8 +95,8 @@ private static void traceCall(JNIEnvironment env, String function, JNIObjectHand

tracer.traceCall("jni",
function,
getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE),
getClassNameOr(env, declaringClass, null, Tracer.UNKNOWN_VALUE),
getTypeDescriptor(env, clazz),
getTypeDescriptor(env, declaringClass),
getClassNameOr(env, callerClass, null, Tracer.UNKNOWN_VALUE),
result,
state.getFullStackTraceOrNull(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -49,6 +51,9 @@

import jdk.graal.compiler.util.json.JsonParser;
import jdk.graal.compiler.util.json.JsonParserException;
import jdk.graal.compiler.util.json.JsonPrintable;
import jdk.graal.compiler.util.json.JsonPrinter;
import jdk.graal.compiler.util.json.JsonWriter;

public abstract class ConfigurationParser {
public static InputStream openStream(URI uri) throws IOException {
Expand All @@ -63,6 +68,11 @@ public static InputStream openStream(URI uri) throws IOException {
public static final String NAME_KEY = "name";
public static final String TYPE_KEY = "type";
public static final String PROXY_KEY = "proxy";
public static final String LAMBDA_KEY = "lambda";
public static final String DECLARING_CLASS_KEY = "declaringClass";
public static final String DECLARING_METHOD_KEY = "declaringMethod";
public static final String INTERFACES_KEY = "interfaces";
public static final String PARAMETER_TYPES_KEY = "parameterTypes";
public static final String REFLECTION_KEY = "reflection";
public static final String JNI_KEY = "jni";
public static final String FOREIGN_KEY = "foreign";
Expand Down Expand Up @@ -252,20 +262,22 @@ protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String
}
}

protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
protected Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
if (typeObject instanceof String stringValue) {
return Optional.of(NamedConfigurationTypeDescriptor.fromJSONName(stringValue));
} else {
EconomicMap<String, Object> type = asMap(typeObject, "type descriptor should be a string or object");
if (type.containsKey(PROXY_KEY)) {
checkHasExactlyOneAttribute(type, "type descriptor object", Set.of(PROXY_KEY));
return Optional.of(getProxyDescriptor(type.get(PROXY_KEY)));
} else if (type.containsKey(LAMBDA_KEY)) {
return Optional.of(getLambdaDescriptor(type.get(LAMBDA_KEY)));
}
/*
* We return if we find a future version of a type descriptor (as a JSON object) instead
* of failing parsing.
*/
// TODO warn
// TODO warn (GR-65606)
return Optional.empty();
}
}
Expand All @@ -275,4 +287,54 @@ private static ProxyConfigurationTypeDescriptor getProxyDescriptor(Object proxyO
List<String> proxyInterfaceNames = proxyInterfaces.stream().map(obj -> asString(obj, "proxy")).toList();
return ProxyConfigurationTypeDescriptor.fromInterfaceTypeNames(proxyInterfaceNames);
}

private LambdaConfigurationTypeDescriptor getLambdaDescriptor(Object lambdaObject) {
EconomicMap<String, Object> lambda = asMap(lambdaObject, "lambda type descriptor should be an object");
checkAttributes(lambda, "lambda descriptor object", List.of(DECLARING_CLASS_KEY, INTERFACES_KEY), List.of(DECLARING_METHOD_KEY));
Optional<ConfigurationTypeDescriptor> declaringType = parseTypeContents(lambda.get(DECLARING_CLASS_KEY));
if (declaringType.isEmpty()) {
throw new JsonParserException("Could not parse lambda declaring type");
}
ConfigurationMethodDescriptor method = null;
if (lambda.containsKey(DECLARING_METHOD_KEY)) {
EconomicMap<String, Object> methodObject = asMap(lambda.get(DECLARING_METHOD_KEY), "lambda declaring method descriptor should be an object");
method = parseMethod(methodObject);
}
List<?> interfaceNames = asList(lambda.get(INTERFACES_KEY), "lambda implemented interfaces must be specified");
if (interfaceNames.isEmpty()) {
throw new JsonParserException("Lambda interfaces must not be empty");
}
List<NamedConfigurationTypeDescriptor> interfaces = interfaceNames.stream().map(s -> NamedConfigurationTypeDescriptor.fromJSONName(asString(s))).toList();
return new LambdaConfigurationTypeDescriptor(declaringType.get(), method, interfaces);
}

public record ConfigurationMethodDescriptor(String name, List<NamedConfigurationTypeDescriptor> parameterTypes) implements JsonPrintable, Comparable<ConfigurationMethodDescriptor> {
@Override
public int compareTo(ConfigurationMethodDescriptor other) {
return Comparator.comparing(ConfigurationMethodDescriptor::name)
.thenComparing((a, b) -> Arrays.compare(a.parameterTypes.toArray(ConfigurationTypeDescriptor[]::new), b.parameterTypes.toArray(ConfigurationTypeDescriptor[]::new)))
.compare(this, other);
}

@Override
public void printJson(JsonWriter writer) throws IOException {
writer.appendObjectStart();
writer.quote(NAME_KEY).appendFieldSeparator().quote(name);
if (parameterTypes != null) {
writer.appendSeparator().quote(PARAMETER_TYPES_KEY).appendFieldSeparator();
JsonPrinter.printCollection(writer, parameterTypes, ConfigurationTypeDescriptor::compareTo, ConfigurationTypeDescriptor::printJson);
}
}
}

protected ConfigurationMethodDescriptor parseMethod(EconomicMap<String, Object> methodJson) {
checkAttributes(methodJson, "method descriptor", List.of(NAME_KEY), List.of(PARAMETER_TYPES_KEY));
String name = asString(methodJson.get(NAME_KEY));
List<NamedConfigurationTypeDescriptor> parameterTypes = null;
if (methodJson.containsKey(PARAMETER_TYPES_KEY)) {
List<?> parameterTypesStrings = asList(methodJson.get(PARAMETER_TYPES_KEY), "parameter types list");
parameterTypes = parameterTypesStrings.stream().map(s -> NamedConfigurationTypeDescriptor.fromJSONName(asString(s))).toList();
}
return new ConfigurationMethodDescriptor(name, parameterTypes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
public interface ConfigurationTypeDescriptor extends Comparable<ConfigurationTypeDescriptor>, JsonPrintable {
enum Kind {
NAMED,
PROXY
PROXY,
LAMBDA
}

Kind getDescriptorType();
Expand Down
Loading