Skip to content

Commit e4f3652

Browse files
committed
Clarify type name handling from different sources
1 parent 55f97ec commit e4f3652

25 files changed

+503
-132
lines changed

substratevm/mx.substratevm/suite.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,11 @@
11811181
"sdk:NATIVEIMAGE",
11821182
"com.oracle.svm.configure",
11831183
],
1184+
"requiresConcealed": {
1185+
"jdk.internal.vm.ci": [
1186+
"jdk.vm.ci.meta"
1187+
],
1188+
},
11841189
"checkstyle": "com.oracle.svm.test",
11851190
"workingSets": "SVM",
11861191
"annotationProcessors": [
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.configure.test.config;
26+
27+
import java.util.List;
28+
29+
import org.junit.Assert;
30+
import org.junit.Test;
31+
32+
import com.oracle.svm.configure.ClassNameSupport;
33+
import com.oracle.svm.configure.test.AddExports;
34+
35+
import jdk.vm.ci.meta.MetaUtil;
36+
37+
@AddExports({"jdk.internal.vm.ci/jdk.vm.ci.meta"})
38+
public class ClassNameSupportTest {
39+
@Test
40+
public void testNameAliases() {
41+
for (Class<?> clazz : List.of(Object.class, int.class, EnclosingClass.class, EnclosingClass.InnerClass.class, EnclosingClass.StaticInnerClass.class,
42+
new EnclosingClass().anonymousClass().getClass(), Object[].class, int[].class, EnclosingClass.InnerClass[].class)) {
43+
testNameAliasesForClass(clazz);
44+
}
45+
}
46+
47+
private static void testNameAliasesForClass(Class<?> clazz) {
48+
String typeName = clazz.getTypeName();
49+
String reflectionName = clazz.getName();
50+
51+
Assert.assertTrue(ClassNameSupport.isValidTypeName(typeName));
52+
Assert.assertTrue(ClassNameSupport.isValidReflectionName(reflectionName));
53+
54+
Assert.assertEquals(typeName, ClassNameSupport.reflectionNameToTypeName(reflectionName));
55+
Assert.assertEquals(reflectionName, ClassNameSupport.typeNameToReflectionName(typeName));
56+
57+
/* Primitive classes cannot be accessed through JNI */
58+
if (!clazz.isPrimitive()) {
59+
String internalName = MetaUtil.toInternalName(reflectionName);
60+
String jniName = internalName.startsWith("L") ? internalName.substring(1, internalName.length() - 1) : internalName;
61+
62+
Assert.assertTrue(ClassNameSupport.isValidJNIName(jniName));
63+
64+
Assert.assertEquals(typeName, ClassNameSupport.jniNameToTypeName(jniName));
65+
Assert.assertEquals(reflectionName, ClassNameSupport.jniNameToReflectionName(jniName));
66+
Assert.assertEquals(jniName, ClassNameSupport.typeNameToJNIName(typeName));
67+
Assert.assertEquals(jniName, ClassNameSupport.reflectionNameToJNIName(reflectionName));
68+
}
69+
}
70+
}
71+
72+
class EnclosingClass {
73+
Object anonymousClass() {
74+
return new Object() {
75+
@Override
76+
public String toString() {
77+
return null;
78+
}
79+
};
80+
}
81+
82+
class InnerClass {
83+
}
84+
85+
static class StaticInnerClass {
86+
}
87+
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.configure;
26+
27+
import jdk.vm.ci.meta.JavaKind;
28+
29+
/**
30+
* There isn't a single standard way of referring to classes by name in the Java ecosystem. In the
31+
* context of Native Image reflection, there are three main ways of referring to a class:
32+
*
33+
* <ul>
34+
* <li>The "type name": this is the result of calling {@code getTypeName()} on a {@code Class}
35+
* object. This is a human-readable name and is the preferred way of specifying classes in JSON
36+
* metadata files.</li>
37+
* <li>The "reflection name": this is used for calls to {@link Class#forName(String)} and others
38+
* using the same syntax. It is the binary name of the class except for array classes, where it is
39+
* formed using the internal name of the class.</li>
40+
* <li>The "JNI name": this is used for calls to {code FindClass} through JNI. This name is similar
41+
* to the reflection name but uses '/' instead of '.' as package separator.</li>
42+
* </ul>
43+
*
44+
* This class provides utility methods to be able to switch between those names and avoid confusion
45+
* about which format a given string is encoded as.
46+
*
47+
* Here is a breakdown of the various names of different types of classes:
48+
*
49+
* <pre>
50+
* | Type | Type name | Reflection name | JNI name |
51+
* | --------------- | ------------------- | -------------------- | -------------------- |
52+
* | Regular class | package.ClassName | package.ClassName | package/ClassName |
53+
* | Primitive type | type | - | - |
54+
* | Array type | package.ClassName[] | [Lpackage.ClassName; | [Lpackage/ClassName; |
55+
* | Primitive array | type[] | [T | [T |
56+
* | Inner class | package.Outer$Inner | package.Outer$Inner | package/Outer$Inner |
57+
* | Anonymous class | package.ClassName$1 | package.ClassName$1 | package/ClassName$1 |
58+
* </pre>
59+
*/
60+
public class ClassNameSupport {
61+
public static String reflectionNameToTypeName(String reflectionName) {
62+
if (!isValidReflectionName(reflectionName)) {
63+
return reflectionName;
64+
}
65+
return reflectionNameToTypeNameUnchecked(reflectionName);
66+
}
67+
68+
public static String jniNameToTypeName(String jniName) {
69+
if (!isValidJNIName(jniName)) {
70+
return jniName;
71+
}
72+
return reflectionNameToTypeNameUnchecked(jniNameToReflectionNameUnchecked(jniName));
73+
}
74+
75+
private static String reflectionNameToTypeNameUnchecked(String reflectionName) {
76+
int arrayDimension = wrappingArrayDimension(reflectionName);
77+
if (arrayDimension > 0) {
78+
return arrayElementTypeToTypeName(reflectionName, arrayDimension) + "[]".repeat(arrayDimension);
79+
}
80+
return reflectionName;
81+
}
82+
83+
public static String typeNameToReflectionName(String typeName) {
84+
if (!isValidTypeName(typeName)) {
85+
return typeName;
86+
}
87+
return typeNameToReflectionNameUnchecked(typeName);
88+
}
89+
90+
public static String typeNameToJNIName(String typeName) {
91+
if (!isValidTypeName(typeName)) {
92+
return typeName;
93+
}
94+
return reflectionNameToJNINameUnchecked(typeNameToReflectionNameUnchecked(typeName));
95+
}
96+
97+
private static String typeNameToReflectionNameUnchecked(String typeName) {
98+
int arrayDimension = trailingArrayDimension(typeName);
99+
if (arrayDimension > 0) {
100+
return "[".repeat(arrayDimension) + typeNameToArrayElementType(typeName.substring(0, typeName.length() - arrayDimension * 2));
101+
}
102+
return typeName;
103+
}
104+
105+
public static String jniNameToReflectionName(String jniName) {
106+
if (!isValidJNIName(jniName)) {
107+
return jniName;
108+
}
109+
return jniNameToReflectionNameUnchecked(jniName);
110+
}
111+
112+
private static String jniNameToReflectionNameUnchecked(String jniName) {
113+
return jniName.replace('/', '.');
114+
}
115+
116+
public static String reflectionNameToJNIName(String reflectionName) {
117+
if (!isValidReflectionName(reflectionName)) {
118+
return reflectionName;
119+
}
120+
return reflectionNameToJNINameUnchecked(reflectionName);
121+
}
122+
123+
private static String reflectionNameToJNINameUnchecked(String reflectionName) {
124+
return reflectionName.replace('.', '/');
125+
}
126+
127+
private static String arrayElementTypeToTypeName(String arrayElementType, int startIndex) {
128+
char typeChar = arrayElementType.charAt(startIndex);
129+
return switch (typeChar) {
130+
case 'L' -> arrayElementType.substring(startIndex + 1, arrayElementType.length() - 1);
131+
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> JavaKind.fromPrimitiveOrVoidTypeChar(typeChar).getJavaName();
132+
default -> null;
133+
};
134+
}
135+
136+
private static String typeNameToArrayElementType(String typeName) {
137+
Class<?> primitiveType = forPrimitiveName(typeName);
138+
if (primitiveType != null) {
139+
return String.valueOf(JavaKind.fromJavaClass(primitiveType).getTypeChar());
140+
}
141+
return "L" + typeName + ";";
142+
}
143+
144+
public static boolean isValidTypeName(String name) {
145+
return isValidFullyQualifiedClassName(name, 0, name.length() - trailingArrayDimension(name) * 2, '.');
146+
}
147+
148+
public static boolean isValidReflectionName(String name) {
149+
return isValidWrappingArraySyntaxName(name, '.');
150+
}
151+
152+
public static boolean isValidJNIName(String name) {
153+
return isValidWrappingArraySyntaxName(name, '/');
154+
}
155+
156+
private static boolean isValidWrappingArraySyntaxName(String name, char packageSeparator) {
157+
int arrayDimension = wrappingArrayDimension(name);
158+
if (arrayDimension > 0) {
159+
return isValidWrappingArrayElementType(name, arrayDimension, packageSeparator);
160+
}
161+
return isValidFullyQualifiedClassName(name, 0, name.length(), packageSeparator);
162+
}
163+
164+
private static boolean isValidWrappingArrayElementType(String name, int startIndex, char packageSeparator) {
165+
if (startIndex == name.length()) {
166+
return false;
167+
}
168+
return switch (name.charAt(startIndex)) {
169+
case 'L' ->
170+
name.charAt(name.length() - 1) == ';' && isValidFullyQualifiedClassName(name, startIndex + 1, name.length() - 1, packageSeparator);
171+
case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z' -> startIndex == name.length() - 1;
172+
default -> false;
173+
};
174+
}
175+
176+
private static boolean isValidFullyQualifiedClassName(String name, int startIndex, int endIndex, char packageSeparator) {
177+
int lastPackageSeparatorIndex = -1;
178+
for (int i = startIndex; i < endIndex; ++i) {
179+
char current = name.charAt(i);
180+
if (current == packageSeparator) {
181+
if (lastPackageSeparatorIndex == i - 1) {
182+
return false;
183+
}
184+
lastPackageSeparatorIndex = i;
185+
} else if (!Character.isJavaIdentifierPart(current)) {
186+
return false;
187+
}
188+
}
189+
return true;
190+
}
191+
192+
private static int wrappingArrayDimension(String name) {
193+
int arrayDimension = 0;
194+
while (arrayDimension < name.length() && name.charAt(arrayDimension) == '[') {
195+
arrayDimension++;
196+
}
197+
return arrayDimension;
198+
}
199+
200+
private static int trailingArrayDimension(String name) {
201+
int arrayDimension = 0;
202+
while (endsWithTrailingArraySyntax(name, name.length() - arrayDimension * 2)) {
203+
arrayDimension++;
204+
}
205+
return arrayDimension;
206+
}
207+
208+
private static boolean endsWithTrailingArraySyntax(String string, int endIndex) {
209+
return endIndex >= "[]".length() && string.charAt(endIndex - 2) == '[' && string.charAt(endIndex - 1) == ']';
210+
}
211+
212+
// Copied from java.lang.Class from JDK 22
213+
public static Class<?> forPrimitiveName(String primitiveName) {
214+
return switch (primitiveName) {
215+
// Integral types
216+
case "int" -> int.class;
217+
case "long" -> long.class;
218+
case "short" -> short.class;
219+
case "char" -> char.class;
220+
case "byte" -> byte.class;
221+
222+
// Floating-point types
223+
case "float" -> float.class;
224+
case "double" -> double.class;
225+
226+
// Other types
227+
case "boolean" -> boolean.class;
228+
case "void" -> void.class;
229+
230+
default -> null;
231+
};
232+
}
233+
}

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
6060
Object object = conditionObject.get(TYPE_REACHED_KEY);
6161
var condition = parseTypeContents(object);
6262
if (condition.isPresent()) {
63-
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
64-
return UnresolvedConfigurationCondition.create(className);
63+
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
64+
return UnresolvedConfigurationCondition.create(namedDescriptor);
6565
}
6666
} else if (conditionObject.containsKey(TYPE_REACHABLE_KEY)) {
6767
if (runtimeCondition && !checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED)) {
@@ -70,12 +70,19 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap<String, Ob
7070
Object object = conditionObject.get(TYPE_REACHABLE_KEY);
7171
var condition = parseTypeContents(object);
7272
if (condition.isPresent()) {
73-
String className = ((NamedConfigurationTypeDescriptor) condition.get()).name();
74-
return UnresolvedConfigurationCondition.create(className, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
73+
NamedConfigurationTypeDescriptor namedDescriptor = checkConditionType(condition.get());
74+
return UnresolvedConfigurationCondition.create(namedDescriptor, checkOption(ConfigurationParserOption.TREAT_ALL_TYPE_REACHABLE_CONDITIONS_AS_TYPE_REACHED));
7575
}
7676
}
7777
}
7878
return UnresolvedConfigurationCondition.alwaysTrue();
7979
}
8080

81+
private NamedConfigurationTypeDescriptor checkConditionType(ConfigurationTypeDescriptor type) {
82+
if (!(type instanceof NamedConfigurationTypeDescriptor)) {
83+
failOnSchemaError("condition should be a fully qualified class name.");
84+
}
85+
return (NamedConfigurationTypeDescriptor) type;
86+
}
87+
8188
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ protected record TypeDescriptorWithOrigin(ConfigurationTypeDescriptor typeDescri
243243
protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String, Object> data, boolean treatAllNameEntriesAsType) {
244244
Object name = data.get(NAME_KEY);
245245
if (name != null) {
246-
NamedConfigurationTypeDescriptor typeDescriptor = new NamedConfigurationTypeDescriptor(asString(name));
246+
NamedConfigurationTypeDescriptor typeDescriptor = NamedConfigurationTypeDescriptor.fromJSONName(asString(name));
247247
return Optional.of(new TypeDescriptorWithOrigin(typeDescriptor, treatAllNameEntriesAsType));
248248
} else {
249249
throw failOnSchemaError("must have type or name specified for an element");
@@ -252,7 +252,7 @@ protected static Optional<TypeDescriptorWithOrigin> parseName(EconomicMap<String
252252

253253
protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object typeObject) {
254254
if (typeObject instanceof String stringValue) {
255-
return Optional.of(new NamedConfigurationTypeDescriptor(stringValue));
255+
return Optional.of(NamedConfigurationTypeDescriptor.fromJSONName(stringValue));
256256
} else {
257257
EconomicMap<String, Object> type = asMap(typeObject, "type descriptor should be a string or object");
258258
if (type.containsKey(PROXY_KEY)) {
@@ -271,6 +271,6 @@ protected static Optional<ConfigurationTypeDescriptor> parseTypeContents(Object
271271
private static ProxyConfigurationTypeDescriptor getProxyDescriptor(Object proxyObject) {
272272
List<Object> proxyInterfaces = asList(proxyObject, "proxy interface content should be an interface list");
273273
List<String> proxyInterfaceNames = proxyInterfaces.stream().map(obj -> asString(obj, "proxy")).toList();
274-
return new ProxyConfigurationTypeDescriptor(proxyInterfaceNames);
274+
return ProxyConfigurationTypeDescriptor.fromInterfaceTypeNames(proxyInterfaceNames);
275275
}
276276
}

0 commit comments

Comments
 (0)