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 2 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`.

## 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
3 changes: 3 additions & 0 deletions substratevm/mx.substratevm/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,9 @@
"sdk:COLLECTIONS",
],
"requiresConcealed": {
"java.base": [
"sun.invoke.util"
],
"jdk.internal.vm.ci": [
"jdk.vm.ci.meta",
]
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
* 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 java.util.Arrays;

import com.oracle.svm.util.StringUtil;

import sun.invoke.util.Wrapper;

/*
* 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 boolean isValidTypeName(String name) {
return isValidFullyQualifiedClassName(removeTrailingArraySyntax(name), '.');
}

public static String reflectionNameToTypeName(String reflectionName) {
if (!isValidReflectionName(reflectionName)) {
return reflectionName;
}
int arrayDimension = 0;
String typeName = reflectionName;
while (typeName.startsWith("[")) {
arrayDimension++;
typeName = typeName.substring(1);
}
if (arrayDimension > 0) {
return arrayElementTypeToTypeName(typeName) + "[]".repeat(arrayDimension);
}
return typeName;
}

public static String jniNameToTypeName(String jniName) {
if (!isValidJNIName(jniName)) {
return jniName;
}
int arrayDimension = 0;
String typeName = jniName;
while (typeName.startsWith("[")) {
arrayDimension++;
typeName = typeName.substring(1);
}
if (arrayDimension > 0) {
typeName = arrayElementTypeToTypeName(typeName);
}
return typeName.replace('/', '.') + "[]".repeat(arrayDimension);
}

public static boolean isValidReflectionName(String name) {
if (name.startsWith("[")) {
return isValidWrappingArrayType(name, '.');
}
return isValidFullyQualifiedClassName(name, '.');
}

public static String typeNameToReflectionName(String typeName) {
if (!isValidTypeName(typeName)) {
return typeName;
}
int arrayDimension = 0;
String reflectionName = typeName;
while (reflectionName.endsWith("[]")) {
arrayDimension++;
reflectionName = reflectionName.substring(0, reflectionName.length() - "[]".length());
}
if (arrayDimension > 0) {
return "[".repeat(arrayDimension) + typeNameToArrayElementType(reflectionName);
}
return reflectionName;
}

public static String jniNameToReflectionName(String jniName) {
if (!isValidJNIName(jniName)) {
return jniName;
}
return jniName.replace('/', '.');
}

public static boolean isValidJNIName(String name) {
if (name.startsWith("[")) {
return isValidWrappingArrayType(name, '/');
}
return isValidFullyQualifiedClassName(name, '/');
}

public static String typeNameToJNIName(String typeName) {
if (!isValidTypeName(typeName)) {
return typeName;
}
int arrayDimension = 0;
String reflectionName = typeName;
while (reflectionName.endsWith("[]")) {
arrayDimension++;
reflectionName = reflectionName.substring(0, reflectionName.length() - "[]".length());
}
reflectionName = reflectionName.replace('.', '/');
if (arrayDimension > 0) {
reflectionName = typeNameToArrayElementType(reflectionName);
}
return "[".repeat(arrayDimension) + reflectionName;
}

public static String reflectionNameToJNIName(String reflectionName) {
if (!isValidReflectionName(reflectionName)) {
return reflectionName;
}
return reflectionName.replace('.', '/');
}

private static String removeTrailingArraySyntax(String name) {
String typeName = name;
while (typeName.endsWith("[]")) {
typeName = typeName.substring(0, typeName.length() - "[]".length());
}
return typeName;
}

private static boolean isValidWrappingArrayType(String name, char packageSeparator) {
String typeName = name;
while (typeName.startsWith("[")) {
typeName = typeName.substring(1);
}
return isValidArrayElementType(typeName, packageSeparator);
}

private static boolean isValidArrayElementType(String typeName, char packageSeparator) {
if (typeName.isEmpty()) {
return false;
}
return switch (typeName.charAt(0)) {
case 'L' ->
typeName.charAt(typeName.length() - 1) == ';' && isValidFullyQualifiedClassName(typeName.substring(1, typeName.length() - 1), packageSeparator);
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> typeName.length() == 1;
default -> false;
};
}

private static boolean isValidFullyQualifiedClassName(String name, char packageSeparator) {
int endOfPackageName = name.lastIndexOf(packageSeparator);
if (endOfPackageName == -1) {
return isValidIdentifier(name);
} else {
String packageName = name.substring(0, endOfPackageName);
String simpleName = name.substring(endOfPackageName + 1);
return isValidPackageName(packageName, packageSeparator) && isValidIdentifier(simpleName);
}
}

private static boolean isValidPackageName(String packageName, char separator) {
return Arrays.stream(StringUtil.split(packageName, Character.toString(separator))).allMatch(ClassNameSupport::isValidIdentifier);
}

private static boolean isValidIdentifier(String identifier) {
if (identifier.isEmpty()) {
return false;
}
return identifier.chars().allMatch(Character::isJavaIdentifierPart);
}

private static String arrayElementTypeToTypeName(String arrayElementType) {
return switch (arrayElementType.charAt(0)) {
case 'L' -> arrayElementType.substring(1, arrayElementType.length() - 1);
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' ->
Wrapper.forBasicType(arrayElementType.charAt(0)).primitiveSimpleName();
default -> null;
};
}

private static String typeNameToArrayElementType(String typeName) {
Class<?> primitiveType = forPrimitiveName(typeName);
if (primitiveType != null) {
return Wrapper.forPrimitiveType(primitiveType).basicTypeString();
}
return "L%s;".formatted(typeName);
}

// 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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ protected record TypeDescriptorWithOrigin(ConfigurationTypeDescriptor typeDescri
protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String, Object> data, boolean treatAllNameEntriesAsType) {
Object name = data.get(NAME_KEY);
if (name != null) {
NamedConfigurationTypeDescriptor typeDescriptor = new NamedConfigurationTypeDescriptor(asString(name));
NamedConfigurationTypeDescriptor typeDescriptor = NamedConfigurationTypeDescriptor.fromJSONName(asString(name));
return Optional.of(new TypeDescriptorWithOrigin(typeDescriptor, treatAllNameEntriesAsType));
} else {
throw failOnSchemaError("must have type or name specified for an element");
Expand All @@ -252,7 +252,7 @@ protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String

protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
if (typeObject instanceof String stringValue) {
return Optional.of(new NamedConfigurationTypeDescriptor(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)) {
Expand All @@ -271,6 +271,6 @@ protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object
private static ProxyConfigurationTypeDescriptor getProxyDescriptor(Object proxyObject) {
List<Object> proxyInterfaces = asList(proxyObject, "proxy interface content should be an interface list");
List<String> proxyInterfaceNames = proxyInterfaces.stream().map(obj -> asString(obj, "proxy")).toList();
return new ProxyConfigurationTypeDescriptor(proxyInterfaceNames);
return ProxyConfigurationTypeDescriptor.fromInterfaceTypeNames(proxyInterfaceNames);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,10 @@ public enum ConfigurationParserOption {
/**
* Treat the "name" entry in a legacy reflection configuration as a "type" entry.
*/
TREAT_ALL_NAME_ENTRIES_AS_TYPE
TREAT_ALL_NAME_ENTRIES_AS_TYPE,

/**
* Parse the given JNI configuration file into a reflection configuration.
*/
PARSE_JNI_INTO_REFLECTION
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@

import java.util.Collection;

import org.graalvm.nativeimage.ImageInfo;

import com.oracle.svm.util.LogUtils;

import jdk.graal.compiler.util.json.JsonPrintable;
import jdk.vm.ci.meta.MetaUtil;

/**
* Provides a representation of a Java type based on String type names. This is used to parse types
Expand All @@ -43,18 +38,6 @@
* </ul>
*/
public interface ConfigurationTypeDescriptor extends Comparable<ConfigurationTypeDescriptor>, JsonPrintable {
static String canonicalizeTypeName(String typeName) {
if (typeName == null) {
return null;
}
String name = typeName;
if (name.indexOf('[') != -1) {
/* accept "int[][]", "java.lang.String[]" */
name = MetaUtil.internalNameToJava(MetaUtil.toInternalName(name), true, true);
}
return name;
}

enum Kind {
NAMED,
PROXY
Expand All @@ -71,11 +54,4 @@ enum Kind {
* type. This is used to filter configurations based on a String-based class filter.
*/
Collection<String> getAllQualifiedJavaNames();

static String checkQualifiedJavaName(String javaName) {
if (ImageInfo.inImageBuildtimeCode() && !(javaName.indexOf('/') == -1 || javaName.indexOf('/') > javaName.lastIndexOf('.'))) {
LogUtils.warning("Type descriptor requires qualified Java name, not internal representation: %s", javaName);
}
return canonicalizeTypeName(javaName);
}
}
Loading
Loading