Skip to content

Commit f1c3a80

Browse files
committed
Enable lambda reflection queries
1 parent 7c93f0d commit f1c3a80

22 files changed

+533
-97
lines changed

docs/reference-manual/native-image/ReachabilityMetadata.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,40 @@ Metadata, for proxy classes, is in the form an ordered collection of interfaces
241241
}
242242
```
243243

244+
To provide metadata for a lambda class, the following metadata must be added to the `reflection` array in
245+
_reachability-metadata.json_
246+
247+
```json
248+
{
249+
"type": {
250+
"lambda": {
251+
"declaringClass": "FullyQualifiedLambdaDeclaringType",
252+
"declaringMethod": {
253+
"name": "declaringMethodName",
254+
"parameterType": [
255+
"FullyQualifiedParameterType1",
256+
"...",
257+
"FullyQualifiedParameterType2"
258+
]
259+
},
260+
"interfaces": [
261+
"FullyQualifiedLambdaInterface1",
262+
"...",
263+
"FullyQualifiedLamdbaInterface2"
264+
]
265+
}
266+
}
267+
}
268+
```
269+
270+
The `"declaringClass"` field specifies in which class, and the optional `"declaringMethod"` field specifies in which
271+
method the lambda is defined.
272+
If `"declaringMethod"` is not specified, the lambda class is searched through all methods of the specified declaring
273+
class.
274+
The `"interfaces"` field specifies which interfaces are implemented by the lambda class.
275+
Such a definition can match multiple lambda classes. If that is the case, the registration entry applies to all those
276+
classes.
277+
244278
Invocation of methods above without the provided metadata will result in throwing `MissingReflectionRegistrationError` which extends `java.lang.Error` and
245279
should not be handled. Note that even if a type does not exist on the classpath, the methods above will throw a `MissingReflectionRegistrationError`.
246280

docs/reference-manual/native-image/assets/reachability-metadata-schema-v1.1.0.json

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,47 @@
355355
"title": "Fully qualified name of the interface defining the proxy class",
356356
"type": "string"
357357
}
358+
},
359+
"lambda": {
360+
"title": "Lambda class descriptor",
361+
"type": "object",
362+
"properties": {
363+
"declaringClass": {
364+
"title": "The class in which the lambda class is defined",
365+
"type": "#/$defs/type"
366+
},
367+
"declaringMethod": {
368+
"title": "The method in which the lambda class is defined",
369+
"type": "#/$defs/method",
370+
"default": {}
371+
},
372+
"interfaces": {
373+
"title": "Non-empty list of interfaces implemented by the lambda class",
374+
"type": "array",
375+
"items": {
376+
"title": "Fully qualified name of the interface implemented by the lambda class",
377+
"type": "string"
378+
}
379+
},
380+
"required": [
381+
"declaringClass",
382+
"interfaces"
383+
],
384+
"additionalProperties": false
385+
}
358386
}
359387
},
360-
"required": [
361-
"proxy"
388+
"oneOf": [
389+
{
390+
"required": [
391+
"proxy"
392+
]
393+
},
394+
{
395+
"required": [
396+
"lambda"
397+
]
398+
}
362399
],
363400
"additionalProperties": false
364401
}

substratevm/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ This changelog summarizes major changes to GraalVM Native Image.
2626
* (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.
2727
* (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`.
2828
* (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.
29-
* (GR-64619) Missing registration errors are now subclasses of `LinkageError`
29+
* (GR-64619) Missing registration errors are now subclasses of `LinkageError`.
3030
* (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.
3131
* (GR-57827) Move the initialization of security providers from build time to runtime.
3232
* (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`.
3333
Run-time initialization of security providers helps reduce image heap size by avoiding unnecessary objects inclusion.
34+
* (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).
3435

3536
## GraalVM for JDK 24 (Internal Version 24.2.0)
3637
* (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.

substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
import com.oracle.svm.agent.stackaccess.EagerlyLoadedJavaStackAccess;
8181
import com.oracle.svm.agent.stackaccess.InterceptedState;
8282
import com.oracle.svm.agent.tracing.core.Tracer;
83+
import com.oracle.svm.configure.LambdaConfigurationTypeDescriptor;
84+
import com.oracle.svm.configure.NamedConfigurationTypeDescriptor;
85+
import com.oracle.svm.configure.ProxyConfigurationTypeDescriptor;
8386
import com.oracle.svm.configure.trace.AccessAdvisor;
8487
import com.oracle.svm.core.c.function.CEntryPointOptions;
8588
import com.oracle.svm.core.jni.headers.JNIEnvironment;
@@ -194,8 +197,8 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
194197
if (tracer != null) {
195198
tracer.traceCall(context,
196199
function,
197-
getClassOrProxyInterfaceNames(env, clazz),
198-
getClassOrProxyInterfaceNames(env, declaringClass),
200+
getTypeDescriptor(env, clazz),
201+
getTypeDescriptor(env, declaringClass),
199202
getClassNameOr(env, callerClass, null, Tracer.UNKNOWN_VALUE),
200203
result,
201204
stackTrace,
@@ -224,7 +227,7 @@ private static void traceBreakpoint(JNIEnvironment env, String context, JNIObjec
224227
* @return The interface, or the original class if it is not a proxy or implements multiple
225228
* interfaces.
226229
*/
227-
private static Object getClassOrProxyInterfaceNames(JNIEnvironment env, JNIObjectHandle clazz) {
230+
static Object getTypeDescriptor(JNIEnvironment env, JNIObjectHandle clazz) {
228231
if (clazz.equal(nullHandle())) {
229232
return null;
230233
}
@@ -233,17 +236,30 @@ private static Object getClassOrProxyInterfaceNames(JNIEnvironment env, JNIObjec
233236
if (Support.clearException(env)) {
234237
return Tracer.UNKNOWN_VALUE;
235238
}
236-
237-
if (!isProxy) {
238-
return getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE);
239+
String className = getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE);
240+
if (className == null || className.equals(Tracer.UNKNOWN_VALUE)) {
241+
return className;
239242
}
240-
241-
JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
242-
if (Support.clearException(env)) {
243-
return Tracer.UNKNOWN_VALUE;
243+
boolean isLambda = className.contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING);
244+
if (isProxy || isLambda) {
245+
JNIObjectHandle interfaces = Support.callObjectMethod(env, clazz, agent.handles().javaLangClassGetInterfaces);
246+
if (Support.clearException(env)) {
247+
return Tracer.UNKNOWN_VALUE;
248+
}
249+
Object interfaceNames = getClassArrayNames(env, interfaces);
250+
if (interfaceNames.equals(Tracer.EXPLICIT_NULL) || interfaceNames.equals(Tracer.UNKNOWN_VALUE)) {
251+
return interfaceNames;
252+
}
253+
List<String> interfaceNameString = Arrays.asList((String[]) interfaceNames);
254+
if (isProxy) {
255+
return ProxyConfigurationTypeDescriptor.fromInterfaceReflectionNames(interfaceNameString);
256+
} else if (isLambda) {
257+
String declaringClass = className.substring(0, className.indexOf(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING));
258+
return LambdaConfigurationTypeDescriptor.fromReflectionNames(declaringClass, interfaceNameString);
259+
}
244260
}
245261

246-
return getClassArrayNames(env, interfaces);
262+
return NamedConfigurationTypeDescriptor.fromReflectionName(className);
247263
}
248264

249265
private static boolean forName(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {

substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/JniCallInterceptor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package com.oracle.svm.agent;
2626

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

9596
tracer.traceCall("jni",
9697
function,
97-
getClassNameOr(env, clazz, null, Tracer.UNKNOWN_VALUE),
98-
getClassNameOr(env, declaringClass, null, Tracer.UNKNOWN_VALUE),
98+
getTypeDescriptor(env, clazz),
99+
getTypeDescriptor(env, declaringClass),
99100
getClassNameOr(env, callerClass, null, Tracer.UNKNOWN_VALUE),
100101
result,
101102
state.getFullStackTraceOrNull(),

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConfigurationParser.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
import java.io.Reader;
3333
import java.net.URI;
3434
import java.net.URL;
35+
import java.util.Arrays;
3536
import java.util.Collection;
3637
import java.util.Collections;
38+
import java.util.Comparator;
3739
import java.util.EnumSet;
3840
import java.util.HashMap;
3941
import java.util.HashSet;
@@ -49,6 +51,9 @@
4951

5052
import jdk.graal.compiler.util.json.JsonParser;
5153
import jdk.graal.compiler.util.json.JsonParserException;
54+
import jdk.graal.compiler.util.json.JsonPrintable;
55+
import jdk.graal.compiler.util.json.JsonPrinter;
56+
import jdk.graal.compiler.util.json.JsonWriter;
5257

5358
public abstract class ConfigurationParser {
5459
public static InputStream openStream(URI uri) throws IOException {
@@ -63,6 +68,11 @@ public static InputStream openStream(URI uri) throws IOException {
6368
public static final String NAME_KEY = "name";
6469
public static final String TYPE_KEY = "type";
6570
public static final String PROXY_KEY = "proxy";
71+
public static final String LAMBDA_KEY = "lambda";
72+
public static final String DECLARING_CLASS_KEY = "declaringClass";
73+
public static final String DECLARING_METHOD_KEY = "declaringMethod";
74+
public static final String INTERFACES_KEY = "interfaces";
75+
public static final String PARAMETER_TYPES_KEY = "parameterTypes";
6676
public static final String REFLECTION_KEY = "reflection";
6777
public static final String JNI_KEY = "jni";
6878
public static final String FOREIGN_KEY = "foreign";
@@ -252,20 +262,22 @@ protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String
252262
}
253263
}
254264

255-
protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
265+
protected Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
256266
if (typeObject instanceof String stringValue) {
257267
return Optional.of(NamedConfigurationTypeDescriptor.fromJSONName(stringValue));
258268
} else {
259269
EconomicMap<String, Object> type = asMap(typeObject, "type descriptor should be a string or object");
260270
if (type.containsKey(PROXY_KEY)) {
261271
checkHasExactlyOneAttribute(type, "type descriptor object", Set.of(PROXY_KEY));
262272
return Optional.of(getProxyDescriptor(type.get(PROXY_KEY)));
273+
} else if (type.containsKey(LAMBDA_KEY)) {
274+
return Optional.of(getLambdaDescriptor(type.get(LAMBDA_KEY)));
263275
}
264276
/*
265277
* We return if we find a future version of a type descriptor (as a JSON object) instead
266278
* of failing parsing.
267279
*/
268-
// TODO warn
280+
// TODO warn (GR-65606)
269281
return Optional.empty();
270282
}
271283
}
@@ -275,4 +287,54 @@ private static ProxyConfigurationTypeDescriptor getProxyDescriptor(Object proxyO
275287
List<String> proxyInterfaceNames = proxyInterfaces.stream().map(obj -> asString(obj, "proxy")).toList();
276288
return ProxyConfigurationTypeDescriptor.fromInterfaceTypeNames(proxyInterfaceNames);
277289
}
290+
291+
private LambdaConfigurationTypeDescriptor getLambdaDescriptor(Object lambdaObject) {
292+
EconomicMap<String, Object> lambda = asMap(lambdaObject, "lambda type descriptor should be an object");
293+
checkAttributes(lambda, "lambda descriptor object", List.of(DECLARING_CLASS_KEY, INTERFACES_KEY), List.of(DECLARING_METHOD_KEY));
294+
Optional<ConfigurationTypeDescriptor> declaringType = parseTypeContents(lambda.get(DECLARING_CLASS_KEY));
295+
if (declaringType.isEmpty()) {
296+
throw new JsonParserException("Could not parse lambda declaring type");
297+
}
298+
ConfigurationMethodDescriptor method = null;
299+
if (lambda.containsKey(DECLARING_METHOD_KEY)) {
300+
EconomicMap<String, Object> methodObject = asMap(lambda.get(DECLARING_METHOD_KEY), "lambda declaring method descriptor should be an object");
301+
method = parseMethod(methodObject);
302+
}
303+
List<?> interfaceNames = asList(lambda.get(INTERFACES_KEY), "lambda implemented interfaces must be specified");
304+
if (interfaceNames.isEmpty()) {
305+
throw new JsonParserException("Lambda interfaces must not be empty");
306+
}
307+
List<NamedConfigurationTypeDescriptor> interfaces = interfaceNames.stream().map(s -> NamedConfigurationTypeDescriptor.fromJSONName(asString(s))).toList();
308+
return new LambdaConfigurationTypeDescriptor(declaringType.get(), method, interfaces);
309+
}
310+
311+
public record ConfigurationMethodDescriptor(String name, List<NamedConfigurationTypeDescriptor> parameterTypes) implements JsonPrintable, Comparable<ConfigurationMethodDescriptor> {
312+
@Override
313+
public int compareTo(ConfigurationMethodDescriptor other) {
314+
return Comparator.comparing(ConfigurationMethodDescriptor::name)
315+
.thenComparing((a, b) -> Arrays.compare(a.parameterTypes.toArray(ConfigurationTypeDescriptor[]::new), b.parameterTypes.toArray(ConfigurationTypeDescriptor[]::new)))
316+
.compare(this, other);
317+
}
318+
319+
@Override
320+
public void printJson(JsonWriter writer) throws IOException {
321+
writer.appendObjectStart();
322+
writer.quote(NAME_KEY).appendFieldSeparator().quote(name);
323+
if (parameterTypes != null) {
324+
writer.appendSeparator().quote(PARAMETER_TYPES_KEY).appendFieldSeparator();
325+
JsonPrinter.printCollection(writer, parameterTypes, ConfigurationTypeDescriptor::compareTo, ConfigurationTypeDescriptor::printJson);
326+
}
327+
}
328+
}
329+
330+
protected ConfigurationMethodDescriptor parseMethod(EconomicMap<String, Object> methodJson) {
331+
checkAttributes(methodJson, "method descriptor", List.of(NAME_KEY), List.of(PARAMETER_TYPES_KEY));
332+
String name = asString(methodJson.get(NAME_KEY));
333+
List<NamedConfigurationTypeDescriptor> parameterTypes = null;
334+
if (methodJson.containsKey(PARAMETER_TYPES_KEY)) {
335+
List<?> parameterTypesStrings = asList(methodJson.get(PARAMETER_TYPES_KEY), "parameter types list");
336+
parameterTypes = parameterTypesStrings.stream().map(s -> NamedConfigurationTypeDescriptor.fromJSONName(asString(s))).toList();
337+
}
338+
return new ConfigurationMethodDescriptor(name, parameterTypes);
339+
}
278340
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConfigurationTypeDescriptor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
public interface ConfigurationTypeDescriptor extends Comparable<ConfigurationTypeDescriptor>, JsonPrintable {
4141
enum Kind {
4242
NAMED,
43-
PROXY
43+
PROXY,
44+
LAMBDA
4445
}
4546

4647
Kind getDescriptorType();

0 commit comments

Comments
 (0)