diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/registry/BootClassRegistry.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/registry/BootClassRegistry.java index 62dcaec30262..431f763717b2 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/registry/BootClassRegistry.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/registry/BootClassRegistry.java @@ -41,6 +41,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.hub.RuntimeClassLoading.ClassDefinitionInfo; import com.oracle.svm.core.hub.crema.CremaSupport; +import com.oracle.svm.core.jdk.BootLoaderClassPathSupport; import com.oracle.svm.espresso.classfile.descriptors.Symbol; import com.oracle.svm.espresso.classfile.descriptors.Type; import com.oracle.svm.espresso.classfile.descriptors.TypeSymbols; @@ -104,27 +105,16 @@ private FileSystem getFileSystem() { // synchronized until parallel class loading is implemented (GR-62338) @Override public synchronized Class doLoadClass(Symbol type) { - // Only looking into the jimage for now. There could be appended elements. - // see GraalServices.getSavedProperty("jdk.boot.class.path.append") String pkg = packageFromType(type); - if (pkg == null) { - return null; - } try { - String moduleName = ClassRegistries.getBootModuleForPackage(pkg); - if (moduleName == null) { - return null; - } - FileSystem fileSystem = getFileSystem(); - if (fileSystem == null) { - return null; + byte[] bytes = pkg == null ? null : loadFromJImage(type, pkg); + if (bytes == null) { + /* Preserve boot class path append semantics by looking there after the jimage. */ + bytes = loadFromAppendedBootClassPathBytes(type); } - var jrtTypePath = TypeSymbols.typeToName(type); - Path classPath = fileSystem.getPath("/modules/" + moduleName + "/" + jrtTypePath + ".class"); - if (!Files.exists(classPath)) { + if (bytes == null) { return null; } - byte[] bytes = Files.readAllBytes(classPath); Class loaded = defineClass(type, bytes, 0, bytes.length, ClassDefinitionInfo.EMPTY); CremaSupport.singleton().recordLoadingConstraint(type, DynamicHub.fromClass(loaded), null); return loaded; @@ -133,6 +123,27 @@ public synchronized Class doLoadClass(Symbol type) { } } + private byte[] loadFromJImage(Symbol type, String pkg) throws IOException { + String moduleName = ClassRegistries.getBootModuleForPackage(pkg); + if (moduleName == null) { + return null; + } + FileSystem fileSystem = getFileSystem(); + if (fileSystem == null) { + return null; + } + var typeName = TypeSymbols.typeToName(type); + Path classPath = fileSystem.getPath("/modules/" + moduleName + "/" + typeName + ".class"); + if (!Files.exists(classPath)) { + return null; + } + return Files.readAllBytes(classPath); + } + + private static byte[] loadFromAppendedBootClassPathBytes(Symbol type) throws IOException { + return BootLoaderClassPathSupport.getResourceBytes(TypeSymbols.typeToName(type) + ".class"); + } + private static String packageFromType(Symbol type) { int lastSlash = type.lastIndexOf((byte) '/'); if (lastSlash == -1) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BootLoaderClassPathSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BootLoaderClassPathSupport.java new file mode 100644 index 000000000000..28e64755b2d5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BootLoaderClassPathSupport.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, 2026, 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.core.jdk; + +import java.io.IOException; + +/// Accesses the runtime boot loader class path exposed via `ClassLoaders.bootLoader().ucp`. +/// This is used for resources and classes supplied with `-Xbootclasspath/a:` in +/// configurations where the boot loader is initialized at run time. +public final class BootLoaderClassPathSupport { + private BootLoaderClassPathSupport() { + } + + /// Looks up a resource on the boot loader's runtime class path and returns its bytes. + public static byte[] getResourceBytes(String resourceName) throws IOException { + Target_jdk_internal_loader_BuiltinClassLoader bootLoader = Target_jdk_internal_loader_ClassLoaders.bootLoader(); + if (bootLoader == null || bootLoader.ucp == null) { + return null; + } + Target_jdk_internal_loader_Resource resource = bootLoader.ucp.getResource(resourceName); + if (resource == null) { + return null; + } + return resource.getBytes(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_net_URLClassLoader.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_net_URLClassLoader.java index 4c931a946f5b..dfff869c1ada 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_net_URLClassLoader.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_net_URLClassLoader.java @@ -28,6 +28,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashMap; import java.util.WeakHashMap; @@ -41,6 +42,15 @@ final class Target_jdk_internal_loader_URLClassPath { /* Reset fields that can store a Zip file via sun.misc.URLClassPath$JarLoader.jar. */ + @Alias + public native URL findResource(String name); + + @Alias + public native Enumeration findResources(String name); + + @Alias + public native Target_jdk_internal_loader_Resource getResource(String name); + @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.NewInstance, declClass = ArrayList.class)// private ArrayList loaders; @@ -52,6 +62,13 @@ final class Target_jdk_internal_loader_URLClassPath { private ArrayList path; } +@TargetClass(className = "jdk.internal.loader.Resource") +@SuppressWarnings({"unused", "static-method"}) +final class Target_jdk_internal_loader_Resource { + @Alias + public native byte[] getBytes() throws java.io.IOException; +} + @TargetClass(URLClassLoader.class) @SuppressWarnings({"unused", "static-method"}) final class Target_java_net_URLClassLoader { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java index 397dc4c6e965..31685e404c69 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java @@ -29,6 +29,8 @@ import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -50,6 +52,8 @@ @SuppressWarnings({"unused", "static-method"}) final class Target_jdk_internal_loader_BuiltinClassLoader { + @Alias Target_jdk_internal_loader_URLClassPath ucp; + @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewConcurrentHashMap.class) // private Map moduleToReader; @@ -93,20 +97,51 @@ public InputStream findResourceAsStream(String mn, String name) throws IOExcepti @Substitute public URL findResource(String name) { - if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) { - /* Workaround for GR-73221 */ - return null; + if (!ClassRegistries.respectClassLoader()) { + // Return only image resources + return ResourcesHelper.nameToResourceURL(name); + } + + if (this == Target_jdk_internal_loader_ClassLoaders.bootLoader()) { + // Workaround for GR-73221: Only retrieve image resources for boot loader + URL url = ResourcesHelper.nameToResourceURL(name); + if (url != null) { + return url; + } } - return ResourcesHelper.nameToResourceURL(name); + + // TODO GR-73221: Also look into the modules defined to this loader + + return ucp == null ? null : ucp.findResource(name); } @Substitute - public Enumeration findResources(String name) { - if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) { - /* Workaround for GR-73221 */ - return null; + public Enumeration findResources(String name) throws IOException { + if (!ClassRegistries.respectClassLoader()) { + // Return only image resources + return ResourcesHelper.nameToResourceEnumerationURLs(name); + } + + List resources = new ArrayList<>(); + + if (this == Target_jdk_internal_loader_ClassLoaders.bootLoader()) { + // Workaround for GR-73221: Only retrieve image resources for boot loader + resources.addAll(ResourcesHelper.nameToResourceListURLs(name)); + } + + // TODO GR-73221: Also look into the modules defined to this loader + + if (ucp != null) { + Enumeration e = ucp.findResources(name); + if (resources.isEmpty()) { + return e; + } + while (e.hasMoreElements()) { + URL url = e.nextElement(); + resources.add(url); + } } - return ResourcesHelper.nameToResourceEnumerationURLs(name); + return resources.isEmpty() ? Collections.emptyEnumeration() : Collections.enumeration(resources); } @Substitute @@ -120,24 +155,6 @@ private URL findResource(ModuleReference mref, String name) { return ResourcesHelper.nameToResourceURL(module, name); } - @Substitute - private URL findResourceOnClassPath(String name) { - if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) { - /* Workaround for GR-73221 */ - return null; - } - return ResourcesHelper.nameToResourceURL(name); - } - - @Substitute - private Enumeration findResourcesOnClassPath(String name) { - if (ClassRegistries.respectClassLoader() && this != Target_jdk_internal_loader_ClassLoaders.bootLoader()) { - /* Workaround for GR-73221 */ - return null; - } - return ResourcesHelper.nameToResourceEnumerationURLs(name); - } - static final class NewConcurrentHashMap implements FieldValueTransformer { @Override public Object transform(Object receiver, Object originalValue) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/XOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/XOptions.java index c92edb5ab18f..df0e5f767be4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/XOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/XOptions.java @@ -24,12 +24,15 @@ */ package com.oracle.svm.core.option; +import java.io.File; + import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.SubstrateGCOptions; import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.jdk.SystemPropertiesSupport; import com.oracle.svm.shared.option.SubstrateOptionsParser; import jdk.graal.compiler.options.OptionKey; @@ -55,6 +58,12 @@ public static boolean parse(String keyAndValue, EconomicMap, Object long value = parse(xFlag, keyAndValue); xFlag.optionKey.update(values, value); return true; + } else if (keyAndValue.startsWith("bootclasspath/a:")) { + // Set the property read by jdk.internal.loader.ClassLoaders. + String value = keyAndValue.substring("bootclasspath/a:".length()); + String currentValue = SystemPropertiesSupport.singleton().getInitialProperty("jdk.boot.class.path.append", false); + SystemPropertiesSupport.singleton().initializeProperty("jdk.boot.class.path.append", appendPath(currentValue, value)); + return true; } return false; } @@ -93,6 +102,13 @@ private static long parse(XFlag xFlag, String keyAndValue) { } } + private static String appendPath(String paths, String toAppend) { + if (paths != null && !paths.isEmpty()) { + return toAppend != null && !toAppend.isEmpty() ? paths + File.pathSeparator + toAppend : paths; + } + return toAppend; + } + private static class XFlag { final String name; final RuntimeOptionKey optionKey; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java index fe6dae0a759a..57b8c4252584 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -47,7 +48,6 @@ import java.util.function.Function; import java.util.function.Predicate; -import com.oracle.svm.core.BuilderUtil; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; @@ -58,6 +58,7 @@ import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor; import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.svm.core.BuilderUtil; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.AnnotateOriginal; @@ -99,6 +100,7 @@ import jdk.internal.reflect.Reflection; import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ModifiersProvider; import jdk.vm.ci.meta.ResolvedJavaField; import jdk.vm.ci.meta.ResolvedJavaMethod; import jdk.vm.ci.meta.ResolvedJavaType; @@ -1003,7 +1005,20 @@ public static boolean isIncluded(TargetElement targetElementAnnotation, Class return SVMHost.evaluateOnlyWith(targetElementAnnotation.onlyWith(), context.toString(), originalClass); } - private static void register(Map substitutions, T annotated, T original, T target) { + /** + * Registers a mapping between annotated, original, and target objects in the provided + * substitutions map. Ensures that no conflicting substitutions are added, preserving + * consistency. + * + * @param substitutions The map where substitutions are maintained. Maps objects of type + * {@code T} to their substitutions. + * @param annotated The annotated object to be mapped to the target. May be null. + * @param original The original object to be mapped to the target or itself. May be null. + * @param target The target object to map annotated and original to. + * @param A type that extends {@code ModifiersProvider}. + * @throws IllegalArgumentException If attempting to add a conflicting substitution. + */ + private static void register(Map substitutions, T annotated, T original, T target) { if (annotated != null) { guarantee(!substitutions.containsKey(annotated) || substitutions.get(annotated).equals(original) || substitutions.get(annotated).equals(target), "Substitution: %s -> %s conflicts with previously registered: %s", annotated, target, substitutions.get(annotated)); @@ -1019,6 +1034,16 @@ private static void register(Map substitutions, T annotated, T origina guarantee(!substitutions.containsKey(original) || substitutions.get(original).equals(original) || substitutions.get(original).equals(target), "Substitution: %s -> %s conflicts with previously registered: %s", original, target, substitutions.get(original)); substitutions.put(original, target); + } else { + // GR-74443 + ResolvedJavaMethod originalMethod = (ResolvedJavaMethod) original; + if (!original.isStatic() && !originalMethod.isConstructor() && !originalMethod.isPrivate()) { + ResolvedJavaMethod aliasMethod = (ResolvedJavaMethod) Objects.requireNonNull(annotated); + ResolvedJavaMethod targetMethod = (ResolvedJavaMethod) target; + UserError.abort("Cannot have both an alias and a substitution to a non-static, non-, non-private method: %s -> %s", + aliasMethod.format("%H.%n(%p)"), + targetMethod.format("%H.%n(%p)")); + } } } }