Skip to content

Commit f168e11

Browse files
committed
Include JNI reachability metadata with reflection
# Conflicts: # web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/WebImageFeature.java
1 parent 681a530 commit f168e11

28 files changed

+573
-293
lines changed

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 for runtime JNI access, including all registered fields and methods",
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
@@ -16,6 +16,7 @@ This changelog summarizes major changes to GraalVM Native Image.
1616
1. `run-time-initialized-jdk` shifts away from build-time initialization of the JDK, instead initializing most of it at run time. This transition is gradual, with individual components of the JDK becoming run-time initialized in each release. This process should complete with JDK 29 when this option should not be needed anymore. Unless you store classes from the JDK in the image heap, this option should not affect you. In case this option breaks your build, follow the suggestions in the error messages.
1717
* (GR-63494) Recurring callback support is no longer enabled by default. If this feature is needed, please specify `-H:+SupportRecurringCallback` at image build-time.
1818
* (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)
19+
* (GR-60238) JNI registration is now included as part of the `"reflection"` section of `reachability-metadata.json`. Registrations performed through the `"jni"` section of `reachability-metadata.json` and through `jni-config.json` will still be accepted and parsed correctly.
1920

2021
## GraalVM for JDK 24 (Internal Version 24.2.0)
2122
* (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/ClassNameSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private static int trailingArrayDimension(String name) {
202202
}
203203

204204
private static boolean endsWithTrailingArraySyntax(String string, int endIndex) {
205-
return string.charAt(endIndex - 2) == '[' && string.charAt(endIndex - 1) == ']';
205+
return endIndex >= "[]".length() && string.charAt(endIndex - 2) == '[' && string.charAt(endIndex - 1) == ']';
206206
}
207207

208208
// Copied from java.lang.Class from JDK 22

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)