Skip to content

[GR-60238] Include JNI reachability metadata with reflection #11066

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@
"title": "Allow objects of this class to be serialized and deserialized",
"type": "boolean",
"default": false
},
"jniAccessible": {
"title": "Register the type for runtime JNI access, including all registered fields and methods",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
Expand Down
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This changelog summarizes major changes to GraalVM Native Image.
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.
* (GR-63494) Recurring callback support is no longer enabled by default. If this feature is needed, please specify `-H:+SupportRecurringCallback` at image build-time.
* (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)
* (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.

## 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 @@ -35,12 +35,12 @@
import java.util.function.Function;
import java.util.function.Predicate;

import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition;
import org.junit.Assert;
import org.junit.Test;

import com.oracle.svm.configure.ConfigurationTypeDescriptor;
import com.oracle.svm.configure.NamedConfigurationTypeDescriptor;
import com.oracle.svm.configure.UnresolvedConfigurationCondition;
import com.oracle.svm.configure.config.ConfigurationFileCollection;
import com.oracle.svm.configure.config.ConfigurationMemberInfo;
import com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberAccessibility;
Expand Down Expand Up @@ -97,7 +97,6 @@ public void testSameConfig() {
ConfigurationSet config = loadTraceProcessorFromResourceDirectory(PREVIOUS_CONFIG_DIR_NAME, omittedConfig);
config = config.copyAndSubtract(omittedConfig);

assertTrue(config.getJniConfiguration().isEmpty());
assertTrue(config.getReflectionConfiguration().isEmpty());
assertTrue(config.getProxyConfiguration().isEmpty());
assertTrue(config.getResourceConfiguration().isEmpty());
Expand All @@ -112,7 +111,7 @@ public void testConfigDifference() {
config = config.copyAndSubtract(omittedConfig);

doTestGeneratedTypeConfig();
doTestTypeConfig(config.getJniConfiguration());
doTestTypeConfig(config.getReflectionConfiguration());

doTestProxyConfig(config.getProxyConfiguration());

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

final TypeConfiguration previousConfig = new TypeConfiguration("");
final TypeConfiguration currentConfig = new TypeConfiguration("");
final TypeConfiguration previousConfig = new TypeConfiguration();
final TypeConfiguration currentConfig = new TypeConfiguration();

TypeMethodsWithFlagsTest(ConfigurationMemberDeclaration methodKind) {
this.methodKind = methodKind;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@
import java.util.Locale;

import org.graalvm.nativeimage.impl.ConfigurationCondition;
import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition;
import org.junit.Assert;
import org.junit.Test;

import com.oracle.svm.configure.ConfigurationParserOption;
import com.oracle.svm.configure.ResourceConfigurationParser;
import com.oracle.svm.configure.ResourcesRegistry;
import com.oracle.svm.configure.UnresolvedConfigurationCondition;
import com.oracle.svm.configure.config.ResourceConfiguration;
import com.oracle.svm.configure.config.conditional.ConfigurationConditionResolver;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.svm.configure;

import jdk.vm.ci.meta.JavaKind;

/*
* There isn't a single standard way of referring to classes by name in the Java ecosystem.
* In the context of Native Image reflection, there are three main ways of referring to a
* class:
*
* * The "type name": this is the result of calling {@code getTypeName()} on a {@code Class}
* object. This is a human-readable name and is the preferred way of specifying classes in
* JSON metadata files.
* * The "reflection name": this is used for calls to {@link Class#forName(String)} and others
* using the same syntax. It is the binary name of the class except for array classes, where
* it is formed using the internal name of the class.
* * The "JNI name": this is used for calls to {code FindClass} through JNI. This name is similar
* to the reflection name but uses '/' instead of '.' as package separator.
*
* This class provides utility methods to be able to switch between those names and avoid
* confusion about which format a given string is encoded as.
*
* Here is a breakdown of the various names of different types of classes:
* | Type | Type name | Reflection name | JNI name |
* | --------------- | ------------------- | -------------------- | -------------------- |
* | Regular class | package.ClassName | package.ClassName | package/ClassName |
* | Primitive type | type | - | - |
* | Array type | package.ClassName[] | [Lpackage.ClassName; | [Lpackage/ClassName; |
* | Primitive array | type[] | [T | [T |
* | Inner class | package.Outer$Inner | package.Outer$Inner | package/Outer$Inner |
* | Anonymous class | package.ClassName$1 | package.ClassName$1 | package/ClassName$1 |
*/
public class ClassNameSupport {
public static String reflectionNameToTypeName(String reflectionName) {
if (!isValidReflectionName(reflectionName)) {
return reflectionName;
}
return reflectionNameToTypeNameUnchecked(reflectionName);
}

public static String jniNameToTypeName(String jniName) {
if (!isValidJNIName(jniName)) {
return jniName;
}
return reflectionNameToTypeNameUnchecked(jniNameToReflectionNameUnchecked(jniName));
}

private static String reflectionNameToTypeNameUnchecked(String reflectionName) {
int arrayDimension = wrappingArrayDimension(reflectionName);
if (arrayDimension > 0) {
return arrayElementTypeToTypeName(reflectionName, arrayDimension) + "[]".repeat(arrayDimension);
}
return reflectionName;
}

public static String typeNameToReflectionName(String typeName) {
if (!isValidTypeName(typeName)) {
return typeName;
}
return typeNameToReflectionNameUnchecked(typeName);
}

public static String typeNameToJNIName(String typeName) {
if (!isValidTypeName(typeName)) {
return typeName;
}
return reflectionNameToJNINameUnchecked(typeNameToReflectionNameUnchecked(typeName));
}

private static String typeNameToReflectionNameUnchecked(String typeName) {
int arrayDimension = trailingArrayDimension(typeName);
if (arrayDimension > 0) {
return "[".repeat(arrayDimension) + typeNameToArrayElementType(typeName.substring(0, typeName.length() - arrayDimension * 2));
}
return typeName;
}

public static String jniNameToReflectionName(String jniName) {
if (!isValidJNIName(jniName)) {
return jniName;
}
return jniNameToReflectionNameUnchecked(jniName);
}

private static String jniNameToReflectionNameUnchecked(String jniName) {
return jniName.replace('/', '.');
}

public static String reflectionNameToJNIName(String reflectionName) {
if (!isValidReflectionName(reflectionName)) {
return reflectionName;
}
return reflectionNameToJNINameUnchecked(reflectionName);
}

private static String reflectionNameToJNINameUnchecked(String reflectionName) {
return reflectionName.replace('.', '/');
}

private static String arrayElementTypeToTypeName(String arrayElementType, int startIndex) {
char typeChar = arrayElementType.charAt(startIndex);
return switch (typeChar) {
case 'L' -> arrayElementType.substring(startIndex + 1, arrayElementType.length() - 1);
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> JavaKind.fromPrimitiveOrVoidTypeChar(typeChar).getJavaName();
default -> null;
};
}

private static String typeNameToArrayElementType(String typeName) {
Class<?> primitiveType = forPrimitiveName(typeName);
if (primitiveType != null) {
return String.valueOf(JavaKind.fromJavaClass(primitiveType).getTypeChar());
}
return "L" + typeName + ";";
}

public static boolean isValidTypeName(String name) {
return isValidFullyQualifiedClassName(name, 0, name.length() - trailingArrayDimension(name) * 2, '.');
}

public static boolean isValidReflectionName(String name) {
return isValidWrappingArraySyntaxName(name, '.');
}

public static boolean isValidJNIName(String name) {
return isValidWrappingArraySyntaxName(name, '/');
}

private static boolean isValidWrappingArraySyntaxName(String name, char packageSeparator) {
int arrayDimension = wrappingArrayDimension(name);
if (arrayDimension > 0) {
return isValidWrappingArrayElementType(name, arrayDimension, packageSeparator);
}
return isValidFullyQualifiedClassName(name, 0, name.length(), packageSeparator);
}

private static boolean isValidWrappingArrayElementType(String name, int startIndex, char packageSeparator) {
if (startIndex == name.length()) {
return false;
}
return switch (name.charAt(startIndex)) {
case 'L' ->
name.charAt(name.length() - 1) == ';' && isValidFullyQualifiedClassName(name, startIndex + 1, name.length() - 1, packageSeparator);
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> startIndex == name.length() - 1;
default -> false;
};
}

private static boolean isValidFullyQualifiedClassName(String name, int startIndex, int endIndex, char packageSeparator) {
int lastPackageSeparatorIndex = -1;
for (int i = startIndex; i < endIndex; ++i) {
char current = name.charAt(i);
if (current == packageSeparator) {
if (lastPackageSeparatorIndex == i - 1) {
return false;
}
lastPackageSeparatorIndex = i;
} else if (!Character.isJavaIdentifierPart(current)) {
return false;
}
}
return true;
}

private static int wrappingArrayDimension(String name) {
int arrayDimension = 0;
while (arrayDimension < name.length() && name.charAt(arrayDimension) == '[') {
arrayDimension++;
}
return arrayDimension;
}

private static int trailingArrayDimension(String name) {
int arrayDimension = 0;
while (endsWithTrailingArraySyntax(name, name.length() - arrayDimension * 2)) {
arrayDimension++;
}
return arrayDimension;
}

private static boolean endsWithTrailingArraySyntax(String string, int endIndex) {
return string.charAt(endIndex - 2) == '[' && string.charAt(endIndex - 1) == ']';
}

// Copied from java.lang.Class from JDK 22
public static Class<?> forPrimitiveName(String primitiveName) {
return switch (primitiveName) {
// Integral types
case "int" -> int.class;
case "long" -> long.class;
case "short" -> short.class;
case "char" -> char.class;
case "byte" -> byte.class;

// Floating-point types
case "float" -> float.class;
case "double" -> double.class;

// Other types
case "boolean" -> boolean.class;
case "void" -> void.class;

default -> null;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@
*/
package com.oracle.svm.configure;

import static org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition.TYPE_REACHABLE_KEY;
import static org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition.TYPE_REACHED_KEY;
import static com.oracle.svm.configure.UnresolvedConfigurationCondition.TYPE_REACHABLE_KEY;
import static com.oracle.svm.configure.UnresolvedConfigurationCondition.TYPE_REACHED_KEY;

import java.util.EnumSet;

import org.graalvm.collections.EconomicMap;
import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition;

public abstract class ConditionalConfigurationParser extends ConfigurationParser {
public static final String CONDITIONAL_KEY = "condition";
Expand Down Expand Up @@ -61,8 +60,8 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
Object object = conditionObject.get(TYPE_REACHED_KEY);
var condition = parseTypeContents(object);
if (condition.isPresent()) {
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
return UnresolvedConfigurationCondition.create(className);
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
return UnresolvedConfigurationCondition.create(namedDescriptor);
}
} else if (conditionObject.containsKey(TYPE_REACHABLE_KEY)) {
if (runtimeCondition && !checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED)) {
Expand All @@ -71,12 +70,19 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
Object object = conditionObject.get(TYPE_REACHABLE_KEY);
var condition = parseTypeContents(object);
if (condition.isPresent()) {
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
return UnresolvedConfigurationCondition.create(className, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
return UnresolvedConfigurationCondition.create(namedDescriptor, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
}
}
}
return UnresolvedConfigurationCondition.alwaysTrue();
}

private NamedConfigurationTypeDescriptor checkConditionType(ConfigurationTypeDescriptor type) {
if (!(type instanceof NamedConfigurationTypeDescriptor)) {
failOnSchemaError("condition should be a fully qualified class name.");
}
return (NamedConfigurationTypeDescriptor) type;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
import java.util.Comparator;
import java.util.function.Function;

import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition;

public record ConditionalElement<T>(UnresolvedConfigurationCondition condition, T element) {

public static <T extends Comparable<T>> Comparator<ConditionalElement<T>> comparator() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
import java.util.EnumSet;
import java.util.function.Consumer;

import org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition;

import jdk.graal.compiler.util.json.JsonPrintable;
import jdk.graal.compiler.util.json.JsonWriter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public enum ConfigurationFile {
REFLECTION("reflect", REFLECTION_KEY, true, true),
RESOURCES("resource", RESOURCES_KEY, true, true),
SERIALIZATION("serialization", SERIALIZATION_KEY, true, true),
JNI("jni", JNI_KEY, true, true),
JNI("jni", JNI_KEY, false, true),
/* Deprecated metadata categories */
DYNAMIC_PROXY("proxy", null, true, false),
PREDEFINED_CLASSES_NAME("predefined-classes", null, true, false),
Expand Down
Loading
Loading