Skip to content

Commit 8f20e09

Browse files
committed
[GR-74302] Use URLClassPath for built-in app loader resources
PullRequest: graal/23662
2 parents f81f003 + 06969c8 commit 8f20e09

6 files changed

Lines changed: 179 additions & 45 deletions

File tree

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/registry/BootClassRegistry.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.oracle.svm.core.hub.DynamicHub;
4242
import com.oracle.svm.core.hub.RuntimeClassLoading.ClassDefinitionInfo;
4343
import com.oracle.svm.core.hub.crema.CremaSupport;
44+
import com.oracle.svm.core.jdk.BootLoaderClassPathSupport;
4445
import com.oracle.svm.espresso.classfile.descriptors.Symbol;
4546
import com.oracle.svm.espresso.classfile.descriptors.Type;
4647
import com.oracle.svm.espresso.classfile.descriptors.TypeSymbols;
@@ -104,27 +105,16 @@ private FileSystem getFileSystem() {
104105
// synchronized until parallel class loading is implemented (GR-62338)
105106
@Override
106107
public synchronized Class<?> doLoadClass(Symbol<Type> type) {
107-
// Only looking into the jimage for now. There could be appended elements.
108-
// see GraalServices.getSavedProperty("jdk.boot.class.path.append")
109108
String pkg = packageFromType(type);
110-
if (pkg == null) {
111-
return null;
112-
}
113109
try {
114-
String moduleName = ClassRegistries.getBootModuleForPackage(pkg);
115-
if (moduleName == null) {
116-
return null;
117-
}
118-
FileSystem fileSystem = getFileSystem();
119-
if (fileSystem == null) {
120-
return null;
110+
byte[] bytes = pkg == null ? null : loadFromJImage(type, pkg);
111+
if (bytes == null) {
112+
/* Preserve boot class path append semantics by looking there after the jimage. */
113+
bytes = loadFromAppendedBootClassPathBytes(type);
121114
}
122-
var jrtTypePath = TypeSymbols.typeToName(type);
123-
Path classPath = fileSystem.getPath("/modules/" + moduleName + "/" + jrtTypePath + ".class");
124-
if (!Files.exists(classPath)) {
115+
if (bytes == null) {
125116
return null;
126117
}
127-
byte[] bytes = Files.readAllBytes(classPath);
128118
Class<?> loaded = defineClass(type, bytes, 0, bytes.length, ClassDefinitionInfo.EMPTY);
129119
CremaSupport.singleton().recordLoadingConstraint(type, DynamicHub.fromClass(loaded), null);
130120
return loaded;
@@ -133,6 +123,27 @@ public synchronized Class<?> doLoadClass(Symbol<Type> type) {
133123
}
134124
}
135125

126+
private byte[] loadFromJImage(Symbol<Type> type, String pkg) throws IOException {
127+
String moduleName = ClassRegistries.getBootModuleForPackage(pkg);
128+
if (moduleName == null) {
129+
return null;
130+
}
131+
FileSystem fileSystem = getFileSystem();
132+
if (fileSystem == null) {
133+
return null;
134+
}
135+
var typeName = TypeSymbols.typeToName(type);
136+
Path classPath = fileSystem.getPath("/modules/" + moduleName + "/" + typeName + ".class");
137+
if (!Files.exists(classPath)) {
138+
return null;
139+
}
140+
return Files.readAllBytes(classPath);
141+
}
142+
143+
private static byte[] loadFromAppendedBootClassPathBytes(Symbol<Type> type) throws IOException {
144+
return BootLoaderClassPathSupport.getResourceBytes(TypeSymbols.typeToName(type) + ".class");
145+
}
146+
136147
private static String packageFromType(Symbol<Type> type) {
137148
int lastSlash = type.lastIndexOf((byte) '/');
138149
if (lastSlash == -1) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2026, 2026, 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.core.jdk;
26+
27+
import java.io.IOException;
28+
29+
/// Accesses the runtime boot loader class path exposed via `ClassLoaders.bootLoader().ucp`.
30+
/// This is used for resources and classes supplied with `-Xbootclasspath/a:` in
31+
/// configurations where the boot loader is initialized at run time.
32+
public final class BootLoaderClassPathSupport {
33+
private BootLoaderClassPathSupport() {
34+
}
35+
36+
/// Looks up a resource on the boot loader's runtime class path and returns its bytes.
37+
public static byte[] getResourceBytes(String resourceName) throws IOException {
38+
Target_jdk_internal_loader_BuiltinClassLoader bootLoader = Target_jdk_internal_loader_ClassLoaders.bootLoader();
39+
if (bootLoader == null || bootLoader.ucp == null) {
40+
return null;
41+
}
42+
Target_jdk_internal_loader_Resource resource = bootLoader.ucp.getResource(resourceName);
43+
if (resource == null) {
44+
return null;
45+
}
46+
return resource.getBytes();
47+
}
48+
}

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_net_URLClassLoader.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.net.URL;
2929
import java.net.URLClassLoader;
3030
import java.util.ArrayList;
31+
import java.util.Enumeration;
3132
import java.util.HashMap;
3233
import java.util.WeakHashMap;
3334

@@ -41,6 +42,15 @@ final class Target_jdk_internal_loader_URLClassPath {
4142

4243
/* Reset fields that can store a Zip file via sun.misc.URLClassPath$JarLoader.jar. */
4344

45+
@Alias
46+
public native URL findResource(String name);
47+
48+
@Alias
49+
public native Enumeration<URL> findResources(String name);
50+
51+
@Alias
52+
public native Target_jdk_internal_loader_Resource getResource(String name);
53+
4454
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.NewInstance, declClass = ArrayList.class)//
4555
private ArrayList<?> loaders;
4656

@@ -52,6 +62,13 @@ final class Target_jdk_internal_loader_URLClassPath {
5262
private ArrayList<URL> path;
5363
}
5464

65+
@TargetClass(className = "jdk.internal.loader.Resource")
66+
@SuppressWarnings({"unused", "static-method"})
67+
final class Target_jdk_internal_loader_Resource {
68+
@Alias
69+
public native byte[] getBytes() throws java.io.IOException;
70+
}
71+
5572
@TargetClass(URLClassLoader.class)
5673
@SuppressWarnings({"unused", "static-method"})
5774
final class Target_java_net_URLClassLoader {

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.lang.module.ModuleReader;
3030
import java.lang.module.ModuleReference;
3131
import java.net.URL;
32+
import java.util.ArrayList;
33+
import java.util.Collections;
3234
import java.util.Enumeration;
3335
import java.util.List;
3436
import java.util.Map;
@@ -50,6 +52,8 @@
5052
@SuppressWarnings({"unused", "static-method"})
5153
final class Target_jdk_internal_loader_BuiltinClassLoader {
5254

55+
@Alias Target_jdk_internal_loader_URLClassPath ucp;
56+
5357
@Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewConcurrentHashMap.class) //
5458
private Map<ModuleReference, ModuleReader> moduleToReader;
5559

@@ -93,20 +97,51 @@ public InputStream findResourceAsStream(String mn, String name) throws IOExcepti
9397

9498
@Substitute
9599
public URL findResource(String name) {
96-
if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
97-
/* Workaround for GR-73221 */
98-
return null;
100+
if (!ClassRegistries.respectClassLoader()) {
101+
// Return only image resources
102+
return ResourcesHelper.nameToResourceURL(name);
103+
}
104+
105+
if (this == Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
106+
// Workaround for GR-73221: Only retrieve image resources for boot loader
107+
URL url = ResourcesHelper.nameToResourceURL(name);
108+
if (url != null) {
109+
return url;
110+
}
99111
}
100-
return ResourcesHelper.nameToResourceURL(name);
112+
113+
// TODO GR-73221: Also look into the modules defined to this loader
114+
115+
return ucp == null ? null : ucp.findResource(name);
101116
}
102117

103118
@Substitute
104-
public Enumeration<URL> findResources(String name) {
105-
if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
106-
/* Workaround for GR-73221 */
107-
return null;
119+
public Enumeration<URL> findResources(String name) throws IOException {
120+
if (!ClassRegistries.respectClassLoader()) {
121+
// Return only image resources
122+
return ResourcesHelper.nameToResourceEnumerationURLs(name);
123+
}
124+
125+
List<URL> resources = new ArrayList<>();
126+
127+
if (this == Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
128+
// Workaround for GR-73221: Only retrieve image resources for boot loader
129+
resources.addAll(ResourcesHelper.nameToResourceListURLs(name));
130+
}
131+
132+
// TODO GR-73221: Also look into the modules defined to this loader
133+
134+
if (ucp != null) {
135+
Enumeration<URL> e = ucp.findResources(name);
136+
if (resources.isEmpty()) {
137+
return e;
138+
}
139+
while (e.hasMoreElements()) {
140+
URL url = e.nextElement();
141+
resources.add(url);
142+
}
108143
}
109-
return ResourcesHelper.nameToResourceEnumerationURLs(name);
144+
return resources.isEmpty() ? Collections.emptyEnumeration() : Collections.enumeration(resources);
110145
}
111146

112147
@Substitute
@@ -120,24 +155,6 @@ private URL findResource(ModuleReference mref, String name) {
120155
return ResourcesHelper.nameToResourceURL(module, name);
121156
}
122157

123-
@Substitute
124-
private URL findResourceOnClassPath(String name) {
125-
if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
126-
/* Workaround for GR-73221 */
127-
return null;
128-
}
129-
return ResourcesHelper.nameToResourceURL(name);
130-
}
131-
132-
@Substitute
133-
private Enumeration<URL> findResourcesOnClassPath(String name) {
134-
if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) {
135-
/* Workaround for GR-73221 */
136-
return null;
137-
}
138-
return ResourcesHelper.nameToResourceEnumerationURLs(name);
139-
}
140-
141158
static final class NewConcurrentHashMap implements FieldValueTransformer {
142159
@Override
143160
public Object transform(Object receiver, Object originalValue) {

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/XOptions.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
*/
2525
package com.oracle.svm.core.option;
2626

27+
import java.io.File;
28+
2729
import org.graalvm.collections.EconomicMap;
2830
import org.graalvm.nativeimage.Platform;
2931
import org.graalvm.nativeimage.Platforms;
3032

3133
import com.oracle.svm.core.SubstrateGCOptions;
3234
import com.oracle.svm.core.SubstrateOptions;
35+
import com.oracle.svm.core.jdk.SystemPropertiesSupport;
3336
import com.oracle.svm.shared.option.SubstrateOptionsParser;
3437

3538
import jdk.graal.compiler.options.OptionKey;
@@ -55,6 +58,12 @@ public static boolean parse(String keyAndValue, EconomicMap<OptionKey<?>, Object
5558
long value = parse(xFlag, keyAndValue);
5659
xFlag.optionKey.update(values, value);
5760
return true;
61+
} else if (keyAndValue.startsWith("bootclasspath/a:")) {
62+
// Set the property read by jdk.internal.loader.ClassLoaders.<clinit>
63+
String value = keyAndValue.substring("bootclasspath/a:".length());
64+
String currentValue = SystemPropertiesSupport.singleton().getInitialProperty("jdk.boot.class.path.append", false);
65+
SystemPropertiesSupport.singleton().initializeProperty("jdk.boot.class.path.append", appendPath(currentValue, value));
66+
return true;
5867
}
5968
return false;
6069
}
@@ -93,6 +102,13 @@ private static long parse(XFlag xFlag, String keyAndValue) {
93102
}
94103
}
95104

105+
private static String appendPath(String paths, String toAppend) {
106+
if (paths != null && !paths.isEmpty()) {
107+
return toAppend != null && !toAppend.isEmpty() ? paths + File.pathSeparator + toAppend : paths;
108+
}
109+
return toAppend;
110+
}
111+
96112
private static class XFlag {
97113
final String name;
98114
final RuntimeOptionKey<Long> optionKey;

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@
4040
import java.util.HashMap;
4141
import java.util.List;
4242
import java.util.Map;
43+
import java.util.Objects;
4344
import java.util.Optional;
4445
import java.util.Set;
4546
import java.util.concurrent.ConcurrentHashMap;
4647
import java.util.function.BooleanSupplier;
4748
import java.util.function.Function;
4849
import java.util.function.Predicate;
4950

50-
import com.oracle.svm.core.BuilderUtil;
5151
import org.graalvm.nativeimage.AnnotationAccess;
5252
import org.graalvm.nativeimage.ImageSingletons;
5353
import org.graalvm.nativeimage.Platform;
@@ -58,6 +58,7 @@
5858
import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor;
5959
import com.oracle.graal.pointsto.meta.AnalysisField;
6060
import com.oracle.graal.pointsto.meta.AnalysisUniverse;
61+
import com.oracle.svm.core.BuilderUtil;
6162
import com.oracle.svm.core.SubstrateOptions;
6263
import com.oracle.svm.core.annotate.Alias;
6364
import com.oracle.svm.core.annotate.AnnotateOriginal;
@@ -99,6 +100,7 @@
99100

100101
import jdk.internal.reflect.Reflection;
101102
import jdk.vm.ci.meta.MetaAccessProvider;
103+
import jdk.vm.ci.meta.ModifiersProvider;
102104
import jdk.vm.ci.meta.ResolvedJavaField;
103105
import jdk.vm.ci.meta.ResolvedJavaMethod;
104106
import jdk.vm.ci.meta.ResolvedJavaType;
@@ -1003,7 +1005,20 @@ public static boolean isIncluded(TargetElement targetElementAnnotation, Class<?>
10031005
return SVMHost.evaluateOnlyWith(targetElementAnnotation.onlyWith(), context.toString(), originalClass);
10041006
}
10051007

1006-
private static <T> void register(Map<T, T> substitutions, T annotated, T original, T target) {
1008+
/**
1009+
* Registers a mapping between annotated, original, and target objects in the provided
1010+
* substitutions map. Ensures that no conflicting substitutions are added, preserving
1011+
* consistency.
1012+
*
1013+
* @param substitutions The map where substitutions are maintained. Maps objects of type
1014+
* {@code T} to their substitutions.
1015+
* @param annotated The annotated object to be mapped to the target. May be null.
1016+
* @param original The original object to be mapped to the target or itself. May be null.
1017+
* @param target The target object to map annotated and original to.
1018+
* @param <T> A type that extends {@code ModifiersProvider}.
1019+
* @throws IllegalArgumentException If attempting to add a conflicting substitution.
1020+
*/
1021+
private static <T extends ModifiersProvider> void register(Map<T, T> substitutions, T annotated, T original, T target) {
10071022
if (annotated != null) {
10081023
guarantee(!substitutions.containsKey(annotated) || substitutions.get(annotated).equals(original) || substitutions.get(annotated).equals(target),
10091024
"Substitution: %s -> %s conflicts with previously registered: %s", annotated, target, substitutions.get(annotated));
@@ -1019,6 +1034,16 @@ private static <T> void register(Map<T, T> substitutions, T annotated, T origina
10191034
guarantee(!substitutions.containsKey(original) || substitutions.get(original).equals(original) || substitutions.get(original).equals(target),
10201035
"Substitution: %s -> %s conflicts with previously registered: %s", original, target, substitutions.get(original));
10211036
substitutions.put(original, target);
1037+
} else {
1038+
// GR-74443
1039+
ResolvedJavaMethod originalMethod = (ResolvedJavaMethod) original;
1040+
if (!original.isStatic() && !originalMethod.isConstructor() && !originalMethod.isPrivate()) {
1041+
ResolvedJavaMethod aliasMethod = (ResolvedJavaMethod) Objects.requireNonNull(annotated);
1042+
ResolvedJavaMethod targetMethod = (ResolvedJavaMethod) target;
1043+
UserError.abort("Cannot have both an alias and a substitution to a non-static, non-<init>, non-private method: %s -> %s",
1044+
aliasMethod.format("%H.%n(%p)"),
1045+
targetMethod.format("%H.%n(%p)"));
1046+
}
10221047
}
10231048
}
10241049
}

0 commit comments

Comments
 (0)