Skip to content

Commit b66c3e8

Browse files
committed
Include JNI reachability metadata with reflection
1 parent e4f3652 commit b66c3e8

28 files changed

+578
-261
lines changed

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ The _reachability-metadata.json_ configuration contains a single object with one
131131
{
132132
"reflection":[],
133133
"resources":[],
134-
"bundles":[],
135-
"jni":[]
134+
"bundles":[]
136135
}
137136
```
138137
@@ -376,12 +375,14 @@ jclass clazz = FindClass(env, "jni/accessed/Type");
376375
```
377376
looks up the `jni.accessed.Type` class, which can then be used to instantiate `jni.accessed.Type`, invoke its methods or access its fields.
378377

379-
The metadata entry for the above call can *only* be provided via _reachability-metadata.json_. Specify the `type` entry in the `jni` field:
378+
The metadata entry for the above call can *only* be provided via _reachability-metadata.json_. Specify
379+
the `jniAccessible` field in the `type` entry in the `reflection` section:
380380
```json
381381
{
382-
"jni":[
382+
"reflection": [
383383
{
384-
"type": "jni.accessed.Type"
384+
"type": "jni.accessed.Type",
385+
"jniAccessibleType": true
385386
}
386387
]
387388
}
@@ -393,13 +394,15 @@ To access field values, we need to provide field names:
393394
```json
394395
{
395396
"type": "jni.accessed.Type",
397+
"jniAccessible": true,
396398
"fields": [{"name": "value"}]
397399
}
398400
```
399401
To access all fields one can use the following attributes:
400402
```json
401403
{
402404
"type": "jni.accessed.Type",
405+
"jniAccessible": true,
403406
"allDeclaredFields": true,
404407
"allPublicFields": true
405408
}
@@ -410,6 +413,7 @@ To call Java methods from JNI, we must provide metadata for the method signature
410413
```json
411414
{
412415
"type": "jni.accessed.Type",
416+
"jniAccessible": true,
413417
"methods": [
414418
{"name": "<methodName1>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]},
415419
{"name": "<methodName2>", "parameterTypes": ["<param-type1>", "<param-typeI>", "<param-typeN>"]}
@@ -420,6 +424,7 @@ As a convenience, one can allow method invocation for groups of methods by addin
420424
```json
421425
{
422426
"type": "jni.accessed.Type",
427+
"jniAccessible": true,
423428
"allDeclaredConstructors": true,
424429
"allPublicConstructors": true,
425430
"allDeclaredMethods": true,
@@ -429,7 +434,8 @@ As a convenience, one can allow method invocation for groups of methods by addin
429434
`allDeclaredConstructors` and `allDeclaredMethods` allow calls invocations of methods declared on a given type.
430435
`allPublicConstructors` and `allPublicMethods` allow invocations of all public methods defined on a type and all of its supertypes.
431436

432-
To allocate objects of a type with `AllocObject`, the metadata must be stored in the `reflection` section:
437+
To allocate objects of a type with `AllocObject`, the `unsafeAllocated` field must be set, but the `jniAccessible` field
438+
is not required:
433439
```json
434440
{
435441
"reflection": [

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@
207207
"title": "Allow objects of this class to be serialized and deserialized",
208208
"type": "boolean",
209209
"default": false
210+
},
211+
"jniAccessible": {
212+
"title": "Register the type, including all registered fields and methods, for runtime JNI access",
213+
"type": "boolean",
214+
"default": false
210215
}
211216
},
212217
"additionalProperties": false

substratevm/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This changelog summarizes major changes to GraalVM Native Image.
1818
* (GR-63494) Recurring callback support is no longer enabled by default. If this feature is needed, please specify `-H:+SupportRecurringCallback` at image build-time.
1919
* (GR-60209) New syntax for configuration of the [Foreign Function & Memory API](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md)
2020
* (GR-64584) Experimental option `-H:+RelativeCodePointers` to significantly reduce relocation entries in position-independent executables and shared libraries.
21+
* (GR-60238) JNI registration is now included as part of the `"reflection"` section of _reachability-metadata.json_ using the `"jniAccessible"` attribute. Registrations performed through the `"jni"` section of _reachability-metadata.json_ and through _jni-config.json_ will still be accepted and parsed correctly.
2122

2223
## GraalVM for JDK 24 (Internal Version 24.2.0)
2324
* (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.configure.test/src/com/oracle/svm/configure/test/config/OmitPreviousConfigTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ public void testSameConfig() {
9797
ConfigurationSet config = loadTraceProcessorFromResourceDirectory(PREVIOUS_CONFIG_DIR_NAME, omittedConfig);
9898
config = config.copyAndSubtract(omittedConfig);
9999

100-
assertTrue(config.getJniConfiguration().isEmpty());
101100
assertTrue(config.getReflectionConfiguration().isEmpty());
102101
assertTrue(config.getProxyConfiguration().isEmpty());
103102
assertTrue(config.getResourceConfiguration().isEmpty());
@@ -112,7 +111,7 @@ public void testConfigDifference() {
112111
config = config.copyAndSubtract(omittedConfig);
113112

114113
doTestGeneratedTypeConfig();
115-
doTestTypeConfig(config.getJniConfiguration());
114+
doTestTypeConfig(config.getReflectionConfiguration());
116115

117116
doTestProxyConfig(config.getProxyConfiguration());
118117

@@ -242,8 +241,8 @@ class TypeMethodsWithFlagsTest {
242241
final Map<ConfigurationMethod, ConfigurationMemberDeclaration> methodsThatMustExist = new HashMap<>();
243242
final Map<ConfigurationMethod, ConfigurationMemberDeclaration> methodsThatMustNotExist = new HashMap<>();
244243

245-
final TypeConfiguration previousConfig = new TypeConfiguration("");
246-
final TypeConfiguration currentConfig = new TypeConfiguration("");
244+
final TypeConfiguration previousConfig = new TypeConfiguration();
245+
final TypeConfiguration currentConfig = new TypeConfiguration();
247246

248247
TypeMethodsWithFlagsTest(ConfigurationMemberDeclaration methodKind) {
249248
this.methodKind = methodKind;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
7878
return UnresolvedConfigurationCondition.alwaysTrue();
7979
}
8080

81-
private NamedConfigurationTypeDescriptor checkConditionType(ConfigurationTypeDescriptor type) {
81+
private static NamedConfigurationTypeDescriptor checkConditionType(ConfigurationTypeDescriptor type) {
8282
if (!(type instanceof NamedConfigurationTypeDescriptor)) {
8383
failOnSchemaError("condition should be a fully qualified class name.");
8484
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public enum ConfigurationFile {
3838
REFLECTION("reflect", REFLECTION_KEY, true, true),
3939
RESOURCES("resource", RESOURCES_KEY, true, true),
4040
SERIALIZATION("serialization", SERIALIZATION_KEY, true, true),
41-
JNI("jni", JNI_KEY, true, true),
41+
JNI("jni", JNI_KEY, false, true),
4242
/* Deprecated metadata categories */
4343
DYNAMIC_PROXY("proxy", null, true, false),
4444
PREDEFINED_CLASSES_NAME("predefined-classes", null, true, false),

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,10 @@ public enum ConfigurationParserOption {
4747
/**
4848
* Treat the "name" entry in a legacy reflection configuration as a "type" entry.
4949
*/
50-
TREAT_ALL_NAME_ENTRIES_AS_TYPE
50+
TREAT_ALL_NAME_ENTRIES_AS_TYPE,
51+
52+
/**
53+
* Parse the given type configuration file as a JNI configuration.
54+
*/
55+
JNI_PARSER
5156
}

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

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,29 +97,30 @@ protected void parseClass(EconomicMap<String, Object> data) {
9797
T clazz = result.get();
9898
delegate.registerType(conditionResult.get(), clazz);
9999

100-
registerIfNotDefault(data, false, clazz, "allDeclaredConstructors", () -> delegate.registerDeclaredConstructors(condition, false, clazz));
101-
registerIfNotDefault(data, false, clazz, "allPublicConstructors", () -> delegate.registerPublicConstructors(condition, false, clazz));
102-
registerIfNotDefault(data, false, clazz, "allDeclaredMethods", () -> delegate.registerDeclaredMethods(condition, false, clazz));
103-
registerIfNotDefault(data, false, clazz, "allPublicMethods", () -> delegate.registerPublicMethods(condition, false, clazz));
104-
registerIfNotDefault(data, false, clazz, "allDeclaredFields", () -> delegate.registerDeclaredFields(condition, false, clazz));
105-
registerIfNotDefault(data, false, clazz, "allPublicFields", () -> delegate.registerPublicFields(condition, false, clazz));
100+
boolean jniAccessible = checkOption(ConfigurationParserOption.JNI_PARSER);
101+
registerIfNotDefault(data, false, clazz, "allDeclaredConstructors", () -> delegate.registerDeclaredConstructors(condition, false, jniAccessible, clazz));
102+
registerIfNotDefault(data, false, clazz, "allPublicConstructors", () -> delegate.registerPublicConstructors(condition, false, jniAccessible, clazz));
103+
registerIfNotDefault(data, false, clazz, "allDeclaredMethods", () -> delegate.registerDeclaredMethods(condition, false, jniAccessible, clazz));
104+
registerIfNotDefault(data, false, clazz, "allPublicMethods", () -> delegate.registerPublicMethods(condition, false, jniAccessible, clazz));
105+
registerIfNotDefault(data, false, clazz, "allDeclaredFields", () -> delegate.registerDeclaredFields(condition, false, jniAccessible, clazz));
106+
registerIfNotDefault(data, false, clazz, "allPublicFields", () -> delegate.registerPublicFields(condition, false, jniAccessible, clazz));
106107
registerIfNotDefault(data, isType, clazz, "allDeclaredClasses", () -> delegate.registerDeclaredClasses(queryCondition, clazz));
107108
registerIfNotDefault(data, isType, clazz, "allRecordComponents", () -> delegate.registerRecordComponents(queryCondition, clazz));
108109
registerIfNotDefault(data, isType, clazz, "allPermittedSubclasses", () -> delegate.registerPermittedSubclasses(queryCondition, clazz));
109110
registerIfNotDefault(data, isType, clazz, "allNestMembers", () -> delegate.registerNestMembers(queryCondition, clazz));
110111
registerIfNotDefault(data, isType, clazz, "allSigners", () -> delegate.registerSigners(queryCondition, clazz));
111112
registerIfNotDefault(data, isType, clazz, "allPublicClasses", () -> delegate.registerPublicClasses(queryCondition, clazz));
112-
registerIfNotDefault(data, isType, clazz, "queryAllDeclaredConstructors", () -> delegate.registerDeclaredConstructors(queryCondition, true, clazz));
113-
registerIfNotDefault(data, isType, clazz, "queryAllPublicConstructors", () -> delegate.registerPublicConstructors(queryCondition, true, clazz));
114-
registerIfNotDefault(data, isType, clazz, "queryAllDeclaredMethods", () -> delegate.registerDeclaredMethods(queryCondition, true, clazz));
115-
registerIfNotDefault(data, isType, clazz, "queryAllPublicMethods", () -> delegate.registerPublicMethods(queryCondition, true, clazz));
113+
registerIfNotDefault(data, isType, clazz, "queryAllDeclaredConstructors", () -> delegate.registerDeclaredConstructors(queryCondition, true, jniAccessible, clazz));
114+
registerIfNotDefault(data, isType, clazz, "queryAllPublicConstructors", () -> delegate.registerPublicConstructors(queryCondition, true, jniAccessible, clazz));
115+
registerIfNotDefault(data, isType, clazz, "queryAllDeclaredMethods", () -> delegate.registerDeclaredMethods(queryCondition, true, jniAccessible, clazz));
116+
registerIfNotDefault(data, isType, clazz, "queryAllPublicMethods", () -> delegate.registerPublicMethods(queryCondition, true, jniAccessible, clazz));
116117
if (isType) {
117118
/*
118119
* Fields cannot be registered as queried only by the user, we register them
119120
* unconditionally if the class is registered via "type".
120121
*/
121-
delegate.registerDeclaredFields(queryCondition, true, clazz);
122-
delegate.registerPublicFields(queryCondition, true, clazz);
122+
delegate.registerDeclaredFields(queryCondition, true, jniAccessible, clazz);
123+
delegate.registerPublicFields(queryCondition, true, jniAccessible, clazz);
123124
}
124125
registerIfNotDefault(data, false, clazz, "unsafeAllocated", () -> delegate.registerUnsafeAllocated(condition, clazz));
125126
MapCursor<String, Object> cursor = data.getEntries();
@@ -129,13 +130,13 @@ protected void parseClass(EconomicMap<String, Object> data) {
129130
try {
130131
switch (name) {
131132
case "methods":
132-
parseMethods(condition, false, asList(value, "Attribute 'methods' must be an array of method descriptors"), clazz);
133+
parseMethods(condition, false, asList(value, "Attribute 'methods' must be an array of method descriptors"), clazz, jniAccessible);
133134
break;
134135
case "queriedMethods":
135-
parseMethods(condition, true, asList(value, "Attribute 'queriedMethods' must be an array of method descriptors"), clazz);
136+
parseMethods(condition, true, asList(value, "Attribute 'queriedMethods' must be an array of method descriptors"), clazz, jniAccessible);
136137
break;
137138
case "fields":
138-
parseFields(condition, asList(value, "Attribute 'fields' must be an array of field descriptors"), clazz);
139+
parseFields(condition, asList(value, "Attribute 'fields' must be an array of field descriptors"), clazz, jniAccessible);
139140
break;
140141
}
141142
} catch (LinkageError e) {

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
*/
4646
public abstract class ReflectionConfigurationParser<C, T> extends ConditionalConfigurationParser {
4747
private static final String CONSTRUCTOR_NAME = "<init>";
48+
private static final String PARAMETER_TYPES_KEY = "parameterTypes";
4849

4950
protected final ConfigurationConditionResolver<C> conditionResolver;
5051
protected final ReflectionConfigurationParserDelegate<C, T> delegate;
@@ -59,14 +60,15 @@ public ReflectionConfigurationParser(ConfigurationConditionResolver<C> condition
5960
protected EnumSet<ConfigurationParserOption> supportedOptions() {
6061
EnumSet<ConfigurationParserOption> base = super.supportedOptions();
6162
base.add(ConfigurationParserOption.PRINT_MISSING_ELEMENTS);
63+
base.add(ConfigurationParserOption.JNI_PARSER);
6264
return base;
6365
}
6466

65-
public static <C, T> ReflectionConfigurationParser<C, T> create(String combinedFileKey, boolean combinedFileSchema,
67+
public static <C, T> ReflectionConfigurationParser<C, T> create(boolean combinedFileSchema,
6668
ConfigurationConditionResolver<C> conditionResolver, ReflectionConfigurationParserDelegate<C, T> delegate,
6769
EnumSet<ConfigurationParserOption> parserOptions) {
6870
if (combinedFileSchema) {
69-
return new ReflectionMetadataParser<>(combinedFileKey, conditionResolver, delegate, parserOptions);
71+
return new ReflectionMetadataParser<>(conditionResolver, delegate, parserOptions);
7072
} else {
7173
return new LegacyReflectionConfigurationParser<>(conditionResolver, delegate, parserOptions);
7274
}
@@ -80,47 +82,48 @@ protected void parseClassArray(List<Object> classes) {
8082

8183
protected abstract void parseClass(EconomicMap<String, Object> data);
8284

83-
protected void registerIfNotDefault(EconomicMap<String, Object> data, boolean defaultValue, T clazz, String propertyName, Runnable register) {
85+
protected boolean registerIfNotDefault(EconomicMap<String, Object> data, boolean defaultValue, T clazz, String propertyName, Runnable register) {
8486
if (data.containsKey(propertyName) ? asBoolean(data.get(propertyName), propertyName) : defaultValue) {
8587
try {
8688
register.run();
89+
return true;
8790
} catch (LinkageError e) {
8891
handleMissingElement("Could not register " + delegate.getTypeName(clazz) + ": " + propertyName + " for reflection.", e);
8992
}
9093
}
94+
return false;
9195
}
9296

93-
protected void parseFields(C condition, List<Object> fields, T clazz) {
97+
protected void parseFields(C condition, List<Object> fields, T clazz, boolean jniAccessible) {
9498
for (Object field : fields) {
95-
parseField(condition, asMap(field, "Elements of 'fields' array must be field descriptor objects"), clazz);
99+
parseField(condition, asMap(field, "Elements of 'fields' array must be field descriptor objects"), clazz, jniAccessible);
96100
}
97101
}
98102

99-
private void parseField(C condition, EconomicMap<String, Object> data, T clazz) {
103+
private void parseField(C condition, EconomicMap<String, Object> data, T clazz, boolean jniAccessible) {
100104
checkAttributes(data, "reflection field descriptor object", Collections.singleton("name"), Arrays.asList("allowWrite", "allowUnsafeAccess"));
101105
String fieldName = asString(data.get("name"), "name");
102106
boolean allowWrite = data.containsKey("allowWrite") && asBoolean(data.get("allowWrite"), "allowWrite");
103107

104108
try {
105-
delegate.registerField(condition, clazz, fieldName, allowWrite);
109+
delegate.registerField(condition, clazz, fieldName, allowWrite, jniAccessible);
106110
} catch (NoSuchFieldException e) {
107111
handleMissingElement("Field " + formatField(clazz, fieldName) + " not found.");
108112
} catch (LinkageError e) {
109113
handleMissingElement("Could not register field " + formatField(clazz, fieldName) + " for reflection.", e);
110114
}
111115
}
112116

113-
protected void parseMethods(C condition, boolean queriedOnly, List<Object> methods, T clazz) {
117+
protected void parseMethods(C condition, boolean queriedOnly, List<Object> methods, T clazz, boolean jniAccessible) {
114118
for (Object method : methods) {
115-
parseMethod(condition, queriedOnly, asMap(method, "Elements of 'methods' array must be method descriptor objects"), clazz);
119+
parseMethod(condition, queriedOnly, asMap(method, "Elements of 'methods' array must be method descriptor objects"), clazz, jniAccessible);
116120
}
117121
}
118122

119-
private void parseMethod(C condition, boolean queriedOnly, EconomicMap<String, Object> data, T clazz) {
120-
checkAttributes(data, "reflection method descriptor object", Collections.singleton("name"), Collections.singleton("parameterTypes"));
121-
String methodName = asString(data.get("name"), "name");
123+
private void parseMethod(C condition, boolean queriedOnly, EconomicMap<String, Object> data, T clazz, boolean jniAccessible) {
124+
String methodName = asString(data.get(NAME_KEY), NAME_KEY);
122125
List<T> methodParameterTypes = null;
123-
Object parameterTypes = data.get("parameterTypes");
126+
Object parameterTypes = data.get(PARAMETER_TYPES_KEY);
124127
if (parameterTypes != null) {
125128
methodParameterTypes = parseMethodParameters(clazz, methodName, asList(parameterTypes, "Attribute 'parameterTypes' must be a list of type names"));
126129
if (methodParameterTypes == null) {
@@ -132,9 +135,9 @@ private void parseMethod(C condition, boolean queriedOnly, EconomicMap<String, O
132135
if (methodParameterTypes != null) {
133136
try {
134137
if (isConstructor) {
135-
delegate.registerConstructor(condition, queriedOnly, clazz, methodParameterTypes);
138+
delegate.registerConstructor(condition, queriedOnly, clazz, methodParameterTypes, jniAccessible);
136139
} else {
137-
delegate.registerMethod(condition, queriedOnly, clazz, methodName, methodParameterTypes);
140+
delegate.registerMethod(condition, queriedOnly, clazz, methodName, methodParameterTypes, jniAccessible);
138141
}
139142
} catch (NoSuchMethodException e) {
140143
handleMissingElement("Method " + formatMethod(clazz, methodName, methodParameterTypes) + " not found.");
@@ -145,9 +148,9 @@ private void parseMethod(C condition, boolean queriedOnly, EconomicMap<String, O
145148
try {
146149
boolean found;
147150
if (isConstructor) {
148-
found = delegate.registerAllConstructors(condition, queriedOnly, clazz);
151+
found = delegate.registerAllConstructors(condition, queriedOnly, jniAccessible, clazz);
149152
} else {
150-
found = delegate.registerAllMethodsWithName(condition, queriedOnly, clazz, methodName);
153+
found = delegate.registerAllMethodsWithName(condition, queriedOnly, jniAccessible, clazz, methodName);
151154
}
152155
if (!found) {
153156
throw new JsonParserException("Method " + formatMethod(clazz, methodName) + " not found");

0 commit comments

Comments
 (0)