diff --git a/docs/reference-manual/native-image/assets/config-condition-schema-v1.0.0.json b/docs/reference-manual/native-image/assets/config-condition-schema-v1.0.0.json index 05f2bc350e7f5..9525c76c810cd 100644 --- a/docs/reference-manual/native-image/assets/config-condition-schema-v1.0.0.json +++ b/docs/reference-manual/native-image/assets/config-condition-schema-v1.0.0.json @@ -4,12 +4,18 @@ "title": "JSON schema for the conditions used in GraalVM Native Image configuration files", "properties": { "typeReachable": { - "type": "string", + "deprecated": true, + "$ref": "config-type-schema-v1.0.0.json", "title": "Fully qualified name of a class that must be reachable in order to register the type for reflection" + }, + "typeReached": { + "$ref": "config-type-schema-v1.0.0.json", + "title": "Fully qualified name of a class that must be reached in order to register the type for reflection" } }, - "required": [ - "typeReachable" + "oneOf": [ + "typeReachable", + "typeReached" ], "additionalProperties": false, "type": "object" diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ConfigurationCondition.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ConfigurationCondition.java index 24ee156098290..f2ed67daf0bc2 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ConfigurationCondition.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ConfigurationCondition.java @@ -43,17 +43,17 @@ import java.util.Objects; /** - * A condition that describes if a reflectively accessed element in Native Image is visible by the + * A condition that describes if a reflectively-accessed element in Native Image is visible by the * user. *

- * Currently, there is only one type of condition (typeReached) so this is a single + * Currently, there is only one type of condition (typeReached) so this is a final * class instead of the class hierarchy. The {@link ConfigurationCondition#type} represents the * {@link Class<>} that needs to be reached by analysis in order for an element to be visible. */ public final class ConfigurationCondition { /* Cached to save space: it is used as a marker for all non-conditional elements */ - private static final ConfigurationCondition JAVA_LANG_OBJECT_REACHED = new ConfigurationCondition(Object.class); + private static final ConfigurationCondition JAVA_LANG_OBJECT_REACHED = new ConfigurationCondition(Object.class, true); public static ConfigurationCondition alwaysTrue() { return JAVA_LANG_OBJECT_REACHED; @@ -61,18 +61,22 @@ public static ConfigurationCondition alwaysTrue() { private final Class type; - public static ConfigurationCondition create(Class type) { + private final boolean runtimeChecked; + + public static ConfigurationCondition create(Class type, boolean runtimeChecked) { + Objects.requireNonNull(type); if (JAVA_LANG_OBJECT_REACHED.getType().equals(type)) { return JAVA_LANG_OBJECT_REACHED; } - return new ConfigurationCondition(type); + return new ConfigurationCondition(type, runtimeChecked); } public boolean isAlwaysTrue() { return ConfigurationCondition.alwaysTrue().equals(this); } - private ConfigurationCondition(Class type) { + private ConfigurationCondition(Class type, boolean runtimeChecked) { + this.runtimeChecked = runtimeChecked; this.type = type; } @@ -80,6 +84,10 @@ public Class getType() { return type; } + public boolean isRuntimeChecked() { + return runtimeChecked; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -88,13 +96,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ConfigurationCondition condition = (ConfigurationCondition) o; - return Objects.equals(type, condition.type); + ConfigurationCondition that = (ConfigurationCondition) o; + return runtimeChecked == that.runtimeChecked && Objects.equals(type, that.type); } @Override public int hashCode() { - return Objects.hash(type); + return Objects.hash(type, runtimeChecked); } } diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/UnresolvedConfigurationCondition.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/UnresolvedConfigurationCondition.java index 9dda1db6b546c..ffaf597dcdbe0 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/UnresolvedConfigurationCondition.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/UnresolvedConfigurationCondition.java @@ -43,22 +43,27 @@ import java.util.Objects; /** - * This is an unresolved version of the {@link ConfigurationCondition} used only during parsing. + * Represents a {@link ConfigurationCondition} during parsing before it is resolved in a context of + * the classpath. */ -public class UnresolvedConfigurationCondition implements Comparable { +public final class UnresolvedConfigurationCondition implements Comparable { + private static final UnresolvedConfigurationCondition JAVA_LANG_OBJECT_REACHED = new UnresolvedConfigurationCondition(Object.class.getTypeName(), true); + public static final String TYPE_REACHED_KEY = "typeReached"; + public static final String TYPE_REACHABLE_KEY = "typeReachable"; private final String typeName; - private static final UnresolvedConfigurationCondition JAVA_LANG_OBJECT_REACHED = new UnresolvedConfigurationCondition(Object.class.getTypeName()); + private final boolean runtimeChecked; - public static UnresolvedConfigurationCondition create(String typeName) { + public static UnresolvedConfigurationCondition create(String typeName, boolean runtimeChecked) { Objects.requireNonNull(typeName); if (JAVA_LANG_OBJECT_REACHED.getTypeName().equals(typeName)) { return JAVA_LANG_OBJECT_REACHED; } - return new UnresolvedConfigurationCondition(typeName); + return new UnresolvedConfigurationCondition(typeName, runtimeChecked); } - protected UnresolvedConfigurationCondition(String typeName) { + private UnresolvedConfigurationCondition(String typeName, boolean runtimeChecked) { this.typeName = typeName; + this.runtimeChecked = runtimeChecked; } public static UnresolvedConfigurationCondition alwaysTrue() { @@ -69,6 +74,14 @@ public String getTypeName() { return typeName; } + public boolean isRuntimeChecked() { + return runtimeChecked; + } + + public boolean isAlwaysTrue() { + return typeName.equals(JAVA_LANG_OBJECT_REACHED.getTypeName()); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -77,26 +90,28 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - UnresolvedConfigurationCondition condition = (UnresolvedConfigurationCondition) o; - return Objects.equals(typeName, condition.typeName); + UnresolvedConfigurationCondition that = (UnresolvedConfigurationCondition) o; + return runtimeChecked == that.runtimeChecked && Objects.equals(typeName, that.typeName); } @Override public int hashCode() { - return Objects.hash(typeName); + return Objects.hash(typeName, runtimeChecked); } @Override - public String toString() { - return "[\"typeReachable\": \"" + typeName + "\"" + "]"; + public int compareTo(UnresolvedConfigurationCondition o) { + int res = Boolean.compare(runtimeChecked, o.runtimeChecked); + if (res != 0) { + return res; + } + return typeName.compareTo(o.typeName); } @Override - public int compareTo(UnresolvedConfigurationCondition c) { - return this.typeName.compareTo(c.typeName); + public String toString() { + var field = runtimeChecked ? TYPE_REACHED_KEY : TYPE_REACHABLE_KEY; + return "[" + field + ": \"" + typeName + "\"" + "]"; } - public boolean isAlwaysTrue() { - return typeName.equals(JAVA_LANG_OBJECT_REACHED.getTypeName()); - } } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationConditionPrintable.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationConditionPrintable.java index 9d2ab29abaa18..7d826854b05ef 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationConditionPrintable.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationConditionPrintable.java @@ -25,7 +25,7 @@ package com.oracle.svm.configure.config; import static com.oracle.svm.core.configure.ConfigurationParser.CONDITIONAL_KEY; -import static com.oracle.svm.core.configure.ConfigurationParser.TYPE_REACHABLE_KEY; +import static org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition.TYPE_REACHABLE_KEY; import java.io.IOException; diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationComputer.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationComputer.java index dbb6d2e2a8299..523067b2d60ee 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationComputer.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/conditional/ConditionalConfigurationComputer.java @@ -171,7 +171,7 @@ private ConfigurationSet deduceConditionalConfiguration(Map value : methodCallNodes.values()) { for (MethodCallNode node : value) { String className = node.methodInfo.getJavaDeclaringClassName(); - UnresolvedConfigurationCondition condition = UnresolvedConfigurationCondition.create(className); + UnresolvedConfigurationCondition condition = UnresolvedConfigurationCondition.create(className, false); var resolveCondition = ConfigurationConditionResolver.identityResolver().resolveCondition(condition); addConfigurationWithCondition(configurationSet, node.configuration, resolveCondition.get()); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/ClassInitializationInfo.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/ClassInitializationInfo.java index f13625d036df1..48282e9b6ffd8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/ClassInitializationInfo.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/ClassInitializationInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2024, 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 @@ -50,30 +50,65 @@ /** * Information about the runtime class initialization state of a {@link DynamicHub class}, and - * {@link #initialize implementation} of class initialization according to the Java VM - * specification. - * + * {@link #slowPath(ClassInitializationInfo, DynamicHub)} implementation} of class initialization + * according to the Java VM specification. + *

* The information is not directly stored in {@link DynamicHub} because 1) the class initialization * state is mutable while {@link DynamicHub} must be immutable, and 2) few classes require * initialization at runtime so factoring out the information reduces image size. + *

+ * The {@link #typeReached} for all super types must have a value whose ordinal is greater or equal + * to its own value. Concretely, {@link TypeReached#REACHED} types must have all super types + * {@link TypeReached#REACHED} or {@link TypeReached#UNTRACKED}, and {@link TypeReached#UNTRACKED} + * types' super types must be {@link TypeReached#UNTRACKED}. This is verified in + * com.oracle.svm.hosted.meta.UniverseBuilder#checkHierarchyForTypeReachedConstraints. */ @InternalVMMethod public final class ClassInitializationInfo { - /** - * Singleton for classes that are already initialized during image building and do not need - * class initialization at runtime, but have {@code } methods. + /* + * The singletons are here to reduce image size for build-time initialized classes that are + * UNTRACKED for type reached. */ - public static final ClassInitializationInfo INITIALIZED_INFO_SINGLETON = new ClassInitializationInfo(InitState.FullyInitialized, true); + private static final ClassInitializationInfo NO_INITIALIZER_NO_TRACKING = new ClassInitializationInfo(InitState.FullyInitialized, false, true, false); + private static final ClassInitializationInfo INITIALIZED_NO_TRACKING = new ClassInitializationInfo(InitState.FullyInitialized, true, true, false); + private static final ClassInitializationInfo FAILED_NO_TRACKING = new ClassInitializationInfo(InitState.InitializationError, false); + + public static ClassInitializationInfo forFailedInfo(boolean typeReachedTracked) { + if (typeReachedTracked) { + return new ClassInitializationInfo(InitState.InitializationError, typeReachedTracked); + } else { + return FAILED_NO_TRACKING; + } + } /** * Singleton for classes that are already initialized during image building and do not need * class initialization at runtime, and don't have {@code } methods. */ - public static final ClassInitializationInfo NO_INITIALIZER_INFO_SINGLETON = new ClassInitializationInfo(InitState.FullyInitialized, false); + public static ClassInitializationInfo forNoInitializerInfo(boolean typeReachedTracked) { + if (typeReachedTracked) { + return new ClassInitializationInfo(InitState.FullyInitialized, false, true, typeReachedTracked); + } else { + return NO_INITIALIZER_NO_TRACKING; + } + } + + /** + * For classes that are already initialized during image building and do not need class + * initialization at runtime, but have {@code } methods. + */ + public static ClassInitializationInfo forInitializedInfo(boolean typeReachedTracked) { + if (typeReachedTracked) { + return new ClassInitializationInfo(InitState.FullyInitialized, true, true, typeReachedTracked); + } else { + return INITIALIZED_NO_TRACKING; + } + } - /** Singleton for classes that failed to link during image building. */ - public static final ClassInitializationInfo FAILED_INFO_SINGLETON = new ClassInitializationInfo(InitState.InitializationError); + public boolean requiresSlowPath() { + return slowPathRequired; + } enum InitState { /** @@ -89,6 +124,12 @@ enum InitState { InitializationError } + public enum TypeReached { + NOT_REACHED, + REACHED, + UNTRACKED, + } + interface ClassInitializerFunctionPointer extends CFunctionPointer { @InvokeJavaFunctionPointer void invoke(); @@ -100,16 +141,28 @@ interface ClassInitializerFunctionPointer extends CFunctionPointer { */ private final FunctionPointerHolder classInitializer; + /** + * Marks that this class has been reached at run time. + */ + private TypeReached typeReached; + /** * The current initialization state. */ private InitState initState; + + /** + * Requires one less check after lowering {@link EnsureClassInitializedNode}. true + * if class is not build-time initialized or typeReached != TypeReached.UNTRACKED. + */ + private boolean slowPathRequired; + /** * The thread that is currently initializing the class. We use the platform thread instead of a * potential virtual thread because initializers like that of {@code sun.nio.ch.Poller} can * switch to the carrier thread and encounter the class that is being initialized again and * would wait for its initialization in the virtual thread to complete and therefore deadlock. - * + *

* We also pin the virtual thread because it must not continue initialization on a different * platform thread, and also because if the platform thread switches to a different virtual * thread which encounters the class being initialized, it would wrongly be considered reentrant @@ -133,25 +186,48 @@ interface ClassInitializerFunctionPointer extends CFunctionPointer { * at native image's build time or run time. */ private boolean hasInitializer; + private boolean buildTimeInitialized; + + public boolean isTypeReached() { + assert typeReached != TypeReached.UNTRACKED : "We should never emit a check for untracked types as this was known at build time"; + return typeReached == TypeReached.REACHED; + } @Platforms(Platform.HOSTED_ONLY.class) - private ClassInitializationInfo(InitState initState, boolean hasInitializer) { - this(initState); + public TypeReached getTypeReached() { + return typeReached; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setTypeReached() { + VMError.guarantee(typeReached != TypeReached.UNTRACKED, "Must not modify untracked types as nodes for checks have already been omitted."); + typeReached = TypeReached.REACHED; + slowPathRequired = initState != InitState.FullyInitialized; + } + + @Platforms(Platform.HOSTED_ONLY.class) + private ClassInitializationInfo(InitState initState, boolean hasInitializer, boolean buildTimeInitialized, boolean typeReachedTracked) { + this(initState, typeReachedTracked); this.hasInitializer = hasInitializer; + this.buildTimeInitialized = buildTimeInitialized; } @Platforms(Platform.HOSTED_ONLY.class) - private ClassInitializationInfo(InitState initState) { + private ClassInitializationInfo(InitState initState, boolean typeReachedTracked) { this.classInitializer = null; + this.typeReached = typeReachedTracked ? TypeReached.NOT_REACHED : TypeReached.UNTRACKED; this.initState = initState; + this.slowPathRequired = typeReached != TypeReached.UNTRACKED || initState != InitState.FullyInitialized; this.initLock = initState == InitState.FullyInitialized ? null : new ReentrantLock(); this.hasInitializer = true; } @Platforms(Platform.HOSTED_ONLY.class) - public ClassInitializationInfo(CFunctionPointer classInitializer) { + public ClassInitializationInfo(CFunctionPointer classInitializer, boolean typeReachedTracked) { this.classInitializer = classInitializer == null || classInitializer.isNull() ? null : new FunctionPointerHolder(classInitializer); this.initState = InitState.Linked; + this.typeReached = typeReachedTracked ? TypeReached.NOT_REACHED : TypeReached.UNTRACKED; + this.slowPathRequired = true; this.initLock = new ReentrantLock(); this.hasInitializer = classInitializer != null; } @@ -164,6 +240,14 @@ public boolean isInitialized() { return initState == InitState.FullyInitialized; } + public boolean isInitializationError() { + return initState == InitState.InitializationError; + } + + public boolean isBuildTimeInitialized() { + return buildTimeInitialized; + } + private boolean isBeingInitialized() { return initState == InitState.BeingInitialized; } @@ -176,17 +260,74 @@ private boolean isReentrantInitialization(IsolateThread thread) { return thread.equal(initThread); } + /** + * Marks the hierarchy of hub as reached. + *

+ * Locking is not needed as the whole type hierarchy (up until + * TypeReached.UNTRACKED types) is marked as reached every time we enter the class + * initialization slow path. The hierarchy of a type can be marked as reached multiple times in + * following cases: + *

+ * + */ + private static void markReached(DynamicHub hub) { + var current = hub; + do { + ClassInitializationInfo clinitInfo = current.getClassInitializationInfo(); + if (clinitInfo.typeReached == TypeReached.UNTRACKED) { + break; + } + clinitInfo.typeReached = TypeReached.REACHED; + if (clinitInfo.isInitialized()) { + clinitInfo.slowPathRequired = false; + } + reachInterfaces(current); + + current = current.getSuperHub(); + } while (current != null); + } + + private static void reachInterfaces(DynamicHub hub) { + for (DynamicHub superInterface : hub.getInterfaces()) { + if (superInterface.getClassInitializationInfo().typeReached == TypeReached.REACHED) { + return; + } + + if (hub.getClassInitializationInfo().typeReached != TypeReached.UNTRACKED) { + superInterface.getClassInitializationInfo().typeReached = TypeReached.REACHED; + } + reachInterfaces(superInterface); + } + } + /** * Perform class initialization. This is the slow-path that should only be called after checking * {@link #isInitialized}. - * - * Steps refer to the JVM specification for class initialization: - * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 + *

+ * Steps refer to the + * JVM + * specification for class initialization. */ @SubstrateForeignCallTarget(stubCallingConvention = true) - private static void initialize(ClassInitializationInfo info, DynamicHub hub) { + private static void slowPath(ClassInitializationInfo info, DynamicHub hub) { IsolateThread self = CurrentIsolate.getCurrentThread(); + /* + * Types are marked as reached before any initialization is performed. Reason: the results + * should be visible in class initializers of the whole hierarchy as they could use + * reflection. + */ + markReached(hub); + + if (info.isInitialized()) { + return; + } + /* * GR-43118: If a predefined class is not loaded, and the caller class is loaded, set the * classloader of the initialized class to the class loader of the caller class. @@ -386,6 +527,9 @@ private void setInitializationStateAndNotify(InitState state) { initLock.lock(); try { this.initState = state; + if (initState == InitState.FullyInitialized) { + this.slowPathRequired = false; + } this.initThread = WordFactory.nullPointer(); /* Make sure previous stores are all done, notably the initState. */ Unsafe.getUnsafe().storeFence(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedNode.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedNode.java index 4ff3cfc76e73b..abaf9fdebb56b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedNode.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedNode.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.classinitialization; +import org.graalvm.word.LocationIdentity; + import jdk.graal.compiler.core.common.type.StampFactory; import jdk.graal.compiler.graph.Node; import jdk.graal.compiler.graph.Node.NodeIntrinsicFactory; @@ -42,8 +44,6 @@ import jdk.graal.compiler.nodes.spi.CanonicalizerTool; import jdk.graal.compiler.nodes.spi.Lowerable; import jdk.graal.compiler.nodes.type.StampTool; -import org.graalvm.word.LocationIdentity; - import jdk.vm.ci.meta.ConstantReflectionProvider; import jdk.vm.ci.meta.ResolvedJavaType; @@ -109,9 +109,12 @@ public ResolvedJavaType constantTypeOrNull(ConstantReflectionProvider constantRe public Node canonical(CanonicalizerTool tool) { ResolvedJavaType type = constantTypeOrNull(tool.getConstantReflection()); if (type != null) { - for (FrameState cur = stateAfter; cur != null; cur = cur.outerFrameState()) { - if (!needsRuntimeInitialization(cur.getMethod().getDeclaringClass(), type)) { - return null; + TypeReachedProvider typeReachedProvider = (TypeReachedProvider) tool.getConstantReflection(); + if (!typeReachedProvider.initializationCheckRequired(type)) { + for (FrameState cur = stateAfter; cur != null; cur = cur.outerFrameState()) { + if (!needsRuntimeInitialization(cur.getMethod().getDeclaringClass(), type)) { + return null; + } } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedSnippets.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedSnippets.java index 460afaf9e0bd0..7bb257ed0a6da 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedSnippets.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/EnsureClassInitializedSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2024, 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 @@ -54,10 +54,10 @@ import jdk.graal.compiler.replacements.Snippets; public final class EnsureClassInitializedSnippets extends SubstrateTemplates implements Snippets { - private static final SubstrateForeignCallDescriptor INITIALIZE = SnippetRuntime.findForeignCall(ClassInitializationInfo.class, "initialize", HAS_SIDE_EFFECT, LocationIdentity.any()); + private static final SubstrateForeignCallDescriptor SLOW_PATH = SnippetRuntime.findForeignCall(ClassInitializationInfo.class, "slowPath", HAS_SIDE_EFFECT, LocationIdentity.any()); public static final SubstrateForeignCallDescriptor[] FOREIGN_CALLS = new SubstrateForeignCallDescriptor[]{ - INITIALIZE, + SLOW_PATH, }; /** @@ -73,13 +73,13 @@ private static void ensureClassIsInitializedSnippet(@Snippet.NonNullParameter Dy */ ClassInitializationInfo infoNonNull = (ClassInitializationInfo) PiNode.piCastNonNull(info, SnippetAnchorNode.anchor()); - if (BranchProbabilityNode.probability(BranchProbabilityNode.EXTREMELY_SLOW_PATH_PROBABILITY, !infoNonNull.isInitialized())) { - callInitialize(INITIALIZE, infoNonNull, DynamicHub.toClass(hub)); + if (BranchProbabilityNode.probability(BranchProbabilityNode.EXTREMELY_SLOW_PATH_PROBABILITY, infoNonNull.requiresSlowPath())) { + callSlowPath(SLOW_PATH, infoNonNull, DynamicHub.toClass(hub)); } } @NodeIntrinsic(value = ForeignCallWithExceptionNode.class) - private static native void callInitialize(@ConstantNodeParameter ForeignCallDescriptor descriptor, ClassInitializationInfo info, Class clazz); + private static native void callSlowPath(@ConstantNodeParameter ForeignCallDescriptor descriptor, ClassInitializationInfo info, Class clazz); @SuppressWarnings("unused") public static void registerLowerings(OptionValues options, Providers providers, Map, NodeLoweringProvider> lowerings) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/TypeReachedProvider.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/TypeReachedProvider.java new file mode 100644 index 0000000000000..1ea21454bf7a3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/classinitialization/TypeReachedProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 2024, 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.classinitialization; + +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Used to determine if EnsureClassInitializedNode is required for type-reached + * checking. + */ +public interface TypeReachedProvider { + /** + * Check whether a type needs a class-initialization node to mark it as reached. + */ + boolean initializationCheckRequired(ResolvedJavaType type); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConditionalRuntimeValue.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConditionalRuntimeValue.java new file mode 100644 index 0000000000000..b9f080d841407 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConditionalRuntimeValue.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024, 2024, 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.configure; + +import java.util.Set; +import java.util.function.Predicate; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; + +/** + * A image-heap stored {@link ConditionalRuntimeValue#value} that is guarded by run-time computed + * {@link ConditionalRuntimeValue#conditions}. + *

+ * {@link ConditionalRuntimeValue#conditions} are stored as an array to save space in the image + * heap. This is subject to further optimizations. + * + * @param type of the stored value. + */ +public final class ConditionalRuntimeValue { + private final Class[] conditions; + private boolean satisfied; + volatile T value; + + @Platforms(Platform.HOSTED_ONLY.class) + public ConditionalRuntimeValue(Set> conditions, T value) { + if (!conditions.isEmpty()) { + this.conditions = conditions.toArray(Class[]::new); + } else { + this.conditions = null; + satisfied = true; + } + + VMError.guarantee(conditions.stream().noneMatch(c -> c.equals(Object.class)), "java.lang.Object must not be in conditions as it is always true."); + this.value = value; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public T getValueUnconditionally() { + return value; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public Set> getConditions() { + return conditions == null ? Set.of() : Set.of(conditions); + } + + public T getValue(Predicate> conditionSatisfied) { + if (satisfied) { + return value; + } else { + for (Class element : conditions) { + if (conditionSatisfied.test(element)) { + satisfied = true; + break; + } + } + if (satisfied) { + return value; + } + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationConditionResolver.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationConditionResolver.java index 435eeab05b1c3..bfbba32b23cc4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationConditionResolver.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationConditionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, 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 @@ -39,7 +39,7 @@ public TypeResult resolveCondition(UnresolvedC @Override public UnresolvedConfigurationCondition alwaysTrue() { - return UnresolvedConfigurationCondition.create("java.lang.Object"); + return UnresolvedConfigurationCondition.alwaysTrue(); } }; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java index 4597c9d8595e7..695a99266ae0e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java @@ -125,6 +125,11 @@ public static final class Options { @Option(help = "When configuration files do not match their schema, abort the image build instead of emitting a warning.")// public static final HostedOptionKey StrictConfiguration = new HostedOptionKey<>(false); + @Option(help = "Testing flag: the typeReachable condition is treated as typeReached so the semantics of programs can change.")// + public static final HostedOptionKey TreatAllReachableConditionsAsReached = new HostedOptionKey<>(false); + + @Option(help = "Testing flag: every type is considered as it participates in a typeReachable condition.")// + public static final HostedOptionKey TreatAllUserSpaceTypesAsTrackedForTypeReached = new HostedOptionKey<>(false); @Option(help = "Warn when reflection and JNI configuration files have elements that could not be found on the classpath or modulepath.", type = OptionType.Expert)// public static final HostedOptionKey WarnAboutMissingReflectionOrJNIMetadataElements = new HostedOptionKey<>(false); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java index 370b680d1a6c2..68aa4015b2160 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationParser.java @@ -24,6 +24,10 @@ */ package com.oracle.svm.core.configure; +import static com.oracle.svm.core.configure.ConfigurationFiles.Options.TreatAllReachableConditionsAsReached; +import static org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition.TYPE_REACHABLE_KEY; +import static org.graalvm.nativeimage.impl.UnresolvedConfigurationCondition.TYPE_REACHED_KEY; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -37,6 +41,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.graalvm.collections.EconomicMap; @@ -61,9 +66,8 @@ public static InputStream openStream(URI uri) throws IOException { } public static final String CONDITIONAL_KEY = "condition"; - public static final String TYPE_REACHABLE_KEY = "typeReachable"; - - public static final String TYPE_REACHED_KEY = "typeReached"; + public static final String NAME_KEY = "name"; + public static final String TYPE_KEY = "type"; private final Map> seenUnknownAttributesByType = new HashMap<>(); private final boolean strictSchema; @@ -155,7 +159,7 @@ protected void checkHasExactlyOneAttribute(EconomicMap map, Stri */ protected void warnOrFailOnSchemaError(String message) { if (strictSchema) { - throw new JSONParserException(message); + failOnSchemaError(message); } else { LogUtils.warning(message); } @@ -204,15 +208,53 @@ protected UnresolvedConfigurationCondition parseCondition(EconomicMap conditionObject = asMap(conditionData, "Attribute 'condition' must be an object"); + if (conditionObject.containsKey(TYPE_REACHABLE_KEY) && conditionObject.containsKey(TYPE_REACHED_KEY)) { + failOnSchemaError("condition can not have both '" + TYPE_REACHED_KEY + "' and '" + TYPE_REACHABLE_KEY + "' set."); + } - Object conditionType = conditionObject.get(TYPE_REACHABLE_KEY); - if (conditionType instanceof String) { - return UnresolvedConfigurationCondition.create((String) conditionType); - } else { - warnOrFailOnSchemaError("'" + TYPE_REACHABLE_KEY + "' should be of type string"); + if (conditionObject.containsKey(TYPE_REACHED_KEY)) { + Object object = conditionObject.get(TYPE_REACHED_KEY); + var condition = parseTypeContents(object); + if (condition.isPresent()) { + return UnresolvedConfigurationCondition.create(condition.get(), true); + } + } else if (conditionObject.containsKey(TYPE_REACHABLE_KEY)) { + Object object = conditionObject.get(TYPE_REACHABLE_KEY); + var condition = parseTypeContents(object); + if (condition.isPresent()) { + return UnresolvedConfigurationCondition.create(condition.get(), TreatAllReachableConditionsAsReached.getValue()); + } } } return UnresolvedConfigurationCondition.alwaysTrue(); } + private static JSONParserException failOnSchemaError(String message) { + throw new JSONParserException(message); + } + + protected static Optional parseType(EconomicMap data) { + Object typeObject = data.get(TYPE_KEY); + Object name = data.get(NAME_KEY); + if (typeObject != null) { + return parseTypeContents(typeObject); + } else if (name != null) { + return Optional.of(asString(name)); + } else { + throw failOnSchemaError("must have type or name specified for an element"); + } + } + + protected static Optional parseTypeContents(Object typeObject) { + if (typeObject instanceof String stringValue) { + return Optional.of(stringValue); + } else { + /* + * We return if we find a future version of a type descriptor (as a JSON object) instead + * of failing parsing. + */ + asMap(typeObject, "type descriptor should be a string or object"); + return Optional.empty(); + } + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java index 95edc68f319e6..4068ff1497a5c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ReflectionConfigurationParser.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.graalvm.collections.EconomicMap; @@ -83,27 +84,9 @@ private void parseClass(EconomicMap data) { return; } - String className; - Object typeObject = data.get("type"); - /* - * Classes registered using the old ("class") syntax will require elements (fields, methods, - * constructors, ...) to be registered for runtime queries, whereas the new ("type") syntax - * will automatically register all elements as queried. - */ - if (typeObject != null) { - if (typeObject instanceof String stringValue) { - className = stringValue; - } else { - /* - * We warn if we find a future version of a type descriptor (as a JSON object) - * instead of failing parsing. - */ - asMap(typeObject, "type descriptor should be a string or object"); - handleMissingElement("Unsupported type descriptor of type object"); - return; - } - } else { - className = asString(data.get("name"), "class name should be a string"); + Optional className = parseType(data); + if (className.isEmpty()) { + return; } /* @@ -111,9 +94,9 @@ private void parseClass(EconomicMap data) { * allow getDeclaredMethods() and similar bulk queries at run time. */ C condition = conditionResult.get(); - TypeResult result = delegate.resolveType(condition, className, true, false); + TypeResult result = delegate.resolveType(condition, className.get(), true, false); if (!result.isPresent()) { - handleMissingElement("Could not resolve class " + className + " for reflection configuration.", result.getException()); + handleMissingElement("Could not resolve class " + className.get() + " for reflection configuration.", result.getException()); return; } T clazz = result.get(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/SerializationConfigurationParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/SerializationConfigurationParser.java index 8f8e3361428e7..0441d92027577 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/SerializationConfigurationParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/SerializationConfigurationParser.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.impl.RuntimeSerializationSupport; @@ -38,8 +39,6 @@ public class SerializationConfigurationParser extends ConfigurationParser { - public static final String NAME_KEY = "name"; - public static final String TYPE_KEY = "type"; public static final String CUSTOM_TARGET_CONSTRUCTOR_CLASS_KEY = "customTargetConstructorClass"; private static final String SERIALIZATION_TYPES_KEY = "types"; private static final String LAMBDA_CAPTURING_SERIALIZATION_TYPES_KEY = "lambdaCapturingTypes"; @@ -107,29 +106,18 @@ private void parseSerializationDescriptorObject(EconomicMap data return; } - String targetSerializationClass; - Object typeObject = data.get(TYPE_KEY); - if (typeObject != null) { - if (typeObject instanceof String stringValue) { - targetSerializationClass = stringValue; - } else { - /* - * We return if we find a future version of a type descriptor (as a JSON object) - * instead of failing parsing. - */ - asMap(typeObject, "type descriptor should be a string or object"); - return; - } - } else { - targetSerializationClass = asString(data.get(NAME_KEY)); + Optional targetSerializationClass; + targetSerializationClass = parseType(data); + if (targetSerializationClass.isEmpty()) { + return; } if (lambdaCapturingType) { - serializationSupport.registerLambdaCapturingClass(condition.get(), targetSerializationClass); + serializationSupport.registerLambdaCapturingClass(condition.get(), targetSerializationClass.get()); } else { Object optionalCustomCtorValue = data.get(CUSTOM_TARGET_CONSTRUCTOR_CLASS_KEY); String customTargetConstructorClass = optionalCustomCtorValue != null ? asString(optionalCustomCtorValue) : null; - serializationSupport.registerWithTargetConstructorClass(condition.get(), targetSerializationClass, customTargetConstructorClass); + serializationSupport.registerWithTargetConstructorClass(condition.get(), targetSerializationClass.get(), customTargetConstructorClass); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java index 56fc69ef51e30..b3e1566495a7c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java @@ -26,91 +26,123 @@ import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.impl.ConfigurationCondition; +import com.oracle.svm.core.configure.ConditionalRuntimeValue; import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; -import com.oracle.svm.util.ReflectionUtil; @AutomaticallyRegisteredImageSingleton public final class ClassForNameSupport { - static ClassForNameSupport singleton() { + public static ClassForNameSupport singleton() { return ImageSingletons.lookup(ClassForNameSupport.class); } /** The map used to collect registered classes. */ - private final EconomicMap knownClasses = ImageHeapMap.create(); + private final EconomicMap> knownClasses = ImageHeapMap.create(); private static final Object NEGATIVE_QUERY = new Object(); @Platforms(Platform.HOSTED_ONLY.class) - public static void registerClass(Class clazz) { + public void registerClass(Class clazz) { + registerClass(ConfigurationCondition.alwaysTrue(), clazz); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerClass(ConfigurationCondition condition, Class clazz) { assert !clazz.isPrimitive() : "primitive classes cannot be looked up by name"; if (PredefinedClassesSupport.isPredefined(clazz)) { return; // must be defined at runtime before it can be looked up } String name = clazz.getName(); - Object currentValue = singleton().knownClasses.get(name); - /* - * If the class is already registered as negative, it means that it exists but is not - * accessible through the builder class loader, and it was already registered by name (as - * negative query) before this point. In that case, we update the map to contain the actual - * class. - */ - VMError.guarantee(currentValue == null || currentValue == clazz || currentValue instanceof Throwable || - (currentValue == NEGATIVE_QUERY && ReflectionUtil.lookupClass(true, name) == null), - "Invalid Class.forName value for %s: %s", name, currentValue); - - if (currentValue == NEGATIVE_QUERY) { - singleton().knownClasses.put(name, clazz); - } else { + ConditionalRuntimeValue exisingEntry = knownClasses.get(name); + Object currentValue = exisingEntry == null ? null : exisingEntry.getValueUnconditionally(); + + if (currentValue == null || // never seen + currentValue == NEGATIVE_QUERY || + currentValue == clazz) { + currentValue = clazz; + var cond = updateConditionalValue(exisingEntry, currentValue, condition); + knownClasses.put(name, cond); + } else if (currentValue instanceof Throwable) { // failed at linking time + var cond = updateConditionalValue(exisingEntry, currentValue, condition); /* * If the class has already been seen as throwing an error, we don't overwrite this - * error + * error. Nevertheless, we have to update the set of conditionals to be correct. */ - singleton().knownClasses.putIfAbsent(name, clazz); + knownClasses.put(name, cond); + } else { + throw VMError.shouldNotReachHere(""" + Invalid Class.forName value for %s: %s + If the class is already registered as negative, it means that it exists but is not + accessible through the builder class loader, and it was already registered by name (as + negative query) before this point. In that case, we update the map to contain the actual + class. + """, name, currentValue); + } + } + + private static ConditionalRuntimeValue updateConditionalValue(ConditionalRuntimeValue existingConditionalValue, Object newValue, + ConfigurationCondition additionalCondition) { + Set> resConditions = Set.of(); + if (!additionalCondition.isAlwaysTrue()) { + Class conditionClass = additionalCondition.getType(); + if (existingConditionalValue != null) { + Set> conditions = existingConditionalValue.getConditions(); + resConditions = Stream.concat(conditions.stream(), Stream.of(conditionClass)) + .collect(Collectors.toUnmodifiableSet()); + } else { + resConditions = Set.of(conditionClass); + } } + return new ConditionalRuntimeValue<>(resConditions, newValue); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerExceptionForClass(String className, Throwable t) { - Object currentValue = singleton().knownClasses.get(className); - VMError.guarantee(currentValue == null || currentValue.getClass() == t.getClass()); - singleton().knownClasses.put(className, t); + public void registerExceptionForClass(ConfigurationCondition condition, String className, Throwable t) { + Set> typeSet = condition.isAlwaysTrue() ? Set.of() : Set.of(condition.getType()); + knownClasses.put(className, new ConditionalRuntimeValue<>(typeSet, t)); } @Platforms(Platform.HOSTED_ONLY.class) - public static void registerNegativeQuery(String className) { + public void registerNegativeQuery(ConfigurationCondition condition, String className) { /* * If the class is not accessible by the builder class loader, but was already registered * through registerClass(Class), we don't overwrite the actual class or exception. */ - singleton().knownClasses.putIfAbsent(className, NEGATIVE_QUERY); + Set> typeSet = condition.isAlwaysTrue() ? Set.of() : Set.of(condition.getType()); + knownClasses.putIfAbsent(className, new ConditionalRuntimeValue<>(typeSet, NEGATIVE_QUERY)); } - public static Class forNameOrNull(String className, ClassLoader classLoader) { + public Class forNameOrNull(String className, ClassLoader classLoader) { try { return forName(className, classLoader, true); } catch (ClassNotFoundException e) { - throw VMError.shouldNotReachHere("ClassForNameSupport.forNameOrNull should not throw", e); + throw VMError.shouldNotReachHere("ClassForNameSupport#forNameOrNull should not throw", e); } } - public static Class forName(String className, ClassLoader classLoader) throws ClassNotFoundException { + public Class forName(String className, ClassLoader classLoader) throws ClassNotFoundException { return forName(className, classLoader, false); } - private static Class forName(String className, ClassLoader classLoader, boolean returnNullOnException) throws ClassNotFoundException { + private Class forName(String className, ClassLoader classLoader, boolean returnNullOnException) throws ClassNotFoundException { if (className == null) { return null; } - Object result = singleton().knownClasses.get(className); + var conditional = knownClasses.get(className); + Object result = conditional == null ? null : conditional.getValue(cls -> DynamicHub.fromClass(cls).isReached()); if (result == NEGATIVE_QUERY || className.endsWith("[]")) { /* Querying array classes with their "TypeName[]" name always throws */ result = new ClassNotFoundException(className); @@ -146,7 +178,7 @@ private static Class forName(String className, ClassLoader classLoader, boole throw VMError.shouldNotReachHere("Class.forName result should be Class, ClassNotFoundException or Error: " + result); } - public static int count() { - return singleton().knownClasses.size(); + public int count() { + return knownClasses.size(); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index 838e72de54050..b1381625abe53 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -1464,7 +1464,7 @@ private static Class forName(String name, boolean initialize, ClassLoader loa } Class result; try { - result = ClassForNameSupport.forName(name, loader); + result = ClassForNameSupport.singleton().forName(name, loader); } catch (ClassNotFoundException e) { if (loader != null && PredefinedClassesSupport.hasBytecodeClasses()) { result = loader.loadClass(name); // may throw @@ -1919,6 +1919,10 @@ public Object getJfrEventConfiguration() { return companion.getJfrEventConfiguration(); } + public boolean isReached() { + return classInitializationInfo.isTypeReached(); + } + private static class ReflectionDataAccessors { @SuppressWarnings("unused") private static SoftReference> getReflectionData(DynamicHub that) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java index 69859b4239649..22f6047210a77 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java @@ -620,14 +620,14 @@ public static Stream packages() { @Substitute private static Class loadClassOrNull(String name) { - return ClassForNameSupport.forNameOrNull(name, null); + return ClassForNameSupport.singleton().forNameOrNull(name, null); } @SuppressWarnings("unused") @Substitute private static Class loadClass(Module module, String name) { /* The module system is not supported for now, therefore the module parameter is ignored. */ - return ClassForNameSupport.forNameOrNull(name, null); + return ClassForNameSupport.singleton().forNameOrNull(name, null); } @SuppressWarnings("unused") diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java index 5e70822a2da36..efe9e399d95fd 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_ClassLoader.java @@ -174,7 +174,7 @@ Class loadClass(Module module, String name) { @Substitute // @SuppressWarnings({"unused"}) // private Class findLoadedClass0(String name) { - return ClassForNameSupport.forNameOrNull(name, SubstrateUtil.cast(this, ClassLoader.class)); + return ClassForNameSupport.singleton().forNameOrNull(name, SubstrateUtil.cast(this, ClassLoader.class)); } /** diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java index f04c82e3a20d8..6994b98652d58 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompiledMethodSupport.java @@ -350,9 +350,8 @@ public boolean isFinalField(ResolvedJavaField f, ConstantFieldTool tool) { static class RuntimeCompilationReflectionProvider extends AnalysisConstantReflectionProvider { - @SuppressWarnings("unused") RuntimeCompilationReflectionProvider(BigBang bb, ClassInitializationSupport classInitializationSupport) { - super(bb.getUniverse(), bb.getMetaAccess()); + super(bb.getUniverse(), bb.getMetaAccess(), classInitializationSupport); } @Override diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/meta/SubstrateRuntimeConfigurationBuilder.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/meta/SubstrateRuntimeConfigurationBuilder.java index 5ced65ffd1b12..dd257bb08192d 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/meta/SubstrateRuntimeConfigurationBuilder.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/meta/SubstrateRuntimeConfigurationBuilder.java @@ -84,7 +84,7 @@ protected Providers createProviders(CodeCacheProvider codeCache, ConstantReflect @Override protected ConstantReflectionProvider createConstantReflectionProvider() { - return new AnalysisConstantReflectionProvider(aUniverse, metaAccess); + return new AnalysisConstantReflectionProvider(aUniverse, metaAccess, classInitializationSupport); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ConditionalConfigurationRegistry.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ConditionalConfigurationRegistry.java index 371cba6838164..f8bd664caceac 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ConditionalConfigurationRegistry.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ConditionalConfigurationRegistry.java @@ -45,7 +45,7 @@ protected void registerConditionalConfiguration(ConfigurationCondition condition consumer.accept(condition); } else { Collection handlers = pendingReachabilityHandlers.computeIfAbsent(condition.getType(), key -> new ConcurrentLinkedQueue<>()); - ConfigurationCondition runtimeCondition = ConfigurationCondition.alwaysTrue(); + ConfigurationCondition runtimeCondition = condition.isRuntimeChecked() ? condition : ConfigurationCondition.alwaysTrue(); handlers.add(() -> consumer.accept(runtimeCondition)); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index ba2f3206f1854..edbbf1670fd4e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -977,7 +977,7 @@ protected void setupNativeImage(OptionValues options, Map, InitKind> classInitKinds = new ConcurrentHashMap<>(); + /** + * We need always-reached types to avoid users injecting class initialization checks in our VM + * implementation and hot paths and to prevent users from making the whole class hierarchy + * require initialization nodes. + */ + @SuppressWarnings("DataFlowIssue")// + private static final Set> alwaysReachedTypes = Set.of( + Object.class, Class.class, String.class, + Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, + Enum.class, Cloneable.class, Formattable.class, Throwable.class, Serializable.class, AutoCloseable.class, Runnable.class, + Iterable.class, Collection.class, Set.class, List.class, Map.class, + System.class, Thread.class, + Reference.class, SoftReference.class, StackWalker.class, ReferenceQueue.class); + + final Set> typesRequiringReachability = ConcurrentHashMap.newKeySet(); + boolean configurationSealed; final ImageClassLoader loader; @@ -133,7 +156,7 @@ Set> classesWithKind(InitKind kind) { /** * Returns true if the provided type is initialized at image build time. - * + *

* If the return value is true, then the class is also guaranteed to be initialized already. * This means that calling this method might trigger class initialization, i.e., execute * arbitrary user code. @@ -144,7 +167,7 @@ public boolean maybeInitializeAtBuildTime(ResolvedJavaType type) { /** * Returns true if the provided type is initialized at image build time. - * + *

* If the return value is true, then the class is also guaranteed to be initialized already. * This means that calling this method might trigger class initialization, i.e., execute * arbitrary user code. @@ -328,7 +351,7 @@ InitKind computeInitKindAndMaybeInitializeClass(Class clazz) { * Computes the class initialization kind of the provided class, all superclasses, and all * interfaces that the provided class depends on (i.e., interfaces implemented by the provided * class that declare default methods). - * + *

* Also defines class initialization based on a policy of the subclass. */ InitKind computeInitKindAndMaybeInitializeClass(Class clazz, boolean memoize) { @@ -450,4 +473,55 @@ private static void addAllInterfaces(Class clazz, EconomicSet> resul } } } + + public void addForTypeReachedTracking(Class clazz) { + if (!isAlwaysReached(clazz)) { + UserError.guarantee(!configurationSealed, "It is not possible to register types for reachability tracking after the analysis has started."); + typesRequiringReachability.add(clazz); + } + } + + public boolean isAlwaysReached(Class jClass) { + Set systemModules = Set.of("org.graalvm.nativeimage.builder", "org.graalvm.nativeimage", "org.graalvm.nativeimage.base", "com.oracle.svm.svm_enterprise", + "org.graalvm.word", "jdk.internal.vm.ci", "jdk.graal.compiler", "com.oracle.graal.graal_enterprise"); + Set jdkModules = Set.of("java.base", "jdk.management", "java.management", "org.graalvm.collections"); + + String classModuleName = jClass.getModule().getName(); + boolean alwaysReachedModule = classModuleName != null && (systemModules.contains(classModuleName) || jdkModules.contains(classModuleName)); + return jClass.isPrimitive() || + jClass.isArray() || + alwaysReachedModule || + alwaysReachedTypes.contains(jClass); + } + + /** + * If any type in the type hierarchy was marked as "type reached", we have to track + * initialization for all its subtypes. Otherwise, marking the supertype as reached could be + * missed when the initializer of the subtype is computed at build time. + */ + public boolean requiresInitializationNodeForTypeReached(ResolvedJavaType type) { + if (type == null) { + return false; + } + var jClass = OriginalClassProvider.getJavaClass(type); + if (isAlwaysReached(jClass)) { + return false; + } + + if (TreatAllUserSpaceTypesAsTrackedForTypeReached.getValue()) { + return true; + } + + if (typesRequiringReachability.contains(jClass) || + requiresInitializationNodeForTypeReached(type.getSuperclass())) { + return true; + } + + for (ResolvedJavaType anInterface : type.getInterfaces()) { + if (requiresInitializationNodeForTypeReached(anInterface)) { + return true; + } + } + return false; + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java index bc5a2dc3e32f5..bac1bcdfaec7d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java @@ -64,6 +64,7 @@ import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; import com.oracle.svm.core.config.ObjectLayout; import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.ameta.AnalysisConstantReflectionProvider; import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerPolicy.SimulateClassInitializerInlineScope; import com.oracle.svm.hosted.fieldfolding.IsStaticFinalFieldInitializedNode; import com.oracle.svm.hosted.fieldfolding.MarkStaticFinalFieldInitializedNode; @@ -374,9 +375,10 @@ protected boolean handleArrayCopy(ImageHeapArray source, int sourcePos, ImageHea } private Node handleEnsureClassInitializedNode(EnsureClassInitializedNode node) { - var classInitType = (AnalysisType) node.constantTypeOrNull(providers.getConstantReflection()); + var aConstantReflection = (AnalysisConstantReflectionProvider) providers.getConstantReflection(); + var classInitType = (AnalysisType) node.constantTypeOrNull(aConstantReflection); if (classInitType != null) { - if (support.trySimulateClassInitializer(graph.getDebug(), classInitType, clusterMember)) { + if (support.trySimulateClassInitializer(graph.getDebug(), classInitType, clusterMember) && !aConstantReflection.initializationCheckRequired(classInitType)) { /* Class is already simulated initialized, no need for a run-time check. */ return null; } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java index 205699b3bce8e..57b481c722d6e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java @@ -607,4 +607,8 @@ private static String reasonToString(HostedProviders providers, Object reason) { return String.valueOf(reason); } } + + public ClassInitializationSupport getClassInitializationSupport() { + return classInitializationSupport; + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/HostedRuntimeConfigurationBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/HostedRuntimeConfigurationBuilder.java index fa3e9a3269b75..1b448c8f6e77d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/HostedRuntimeConfigurationBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/HostedRuntimeConfigurationBuilder.java @@ -79,7 +79,7 @@ protected Providers createProviders(CodeCacheProvider codeCache, ConstantReflect @Override protected ConstantReflectionProvider createConstantReflectionProvider() { - return new HostedConstantReflectionProvider(universe, (HostedMetaAccess) metaAccess); + return new HostedConstantReflectionProvider(universe, (HostedMetaAccess) metaAccess, classInitializationSupport); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/UninterruptibleAnnotationChecker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/UninterruptibleAnnotationChecker.java index cf38b9a86f0b6..f5ccebf382302 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/UninterruptibleAnnotationChecker.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/UninterruptibleAnnotationChecker.java @@ -43,12 +43,14 @@ import jdk.graal.compiler.graph.Node; import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.ValueNode; import jdk.graal.compiler.nodes.java.AbstractNewObjectNode; import jdk.graal.compiler.nodes.java.MonitorEnterNode; import jdk.graal.compiler.nodes.java.NewMultiArrayNode; import jdk.graal.compiler.nodes.virtual.CommitAllocationNode; import jdk.graal.compiler.options.Option; import jdk.graal.compiler.options.OptionsParser; +import jdk.vm.ci.meta.ConstantReflectionProvider; import jdk.vm.ci.meta.ResolvedJavaMethod; /** Checks that {@linkplain Uninterruptible} has been used consistently. */ @@ -69,9 +71,9 @@ private static UninterruptibleAnnotationChecker singleton() { UninterruptibleAnnotationChecker() { } - public static void checkAfterParsing(ResolvedJavaMethod method, StructuredGraph graph) { + public static void checkAfterParsing(ResolvedJavaMethod method, StructuredGraph graph, ConstantReflectionProvider constantReflectionProvider) { if (Uninterruptible.Utils.isUninterruptible(method) && graph != null) { - singleton().checkGraph(method, graph); + singleton().checkGraph(method, graph, constantReflectionProvider); } } @@ -252,7 +254,7 @@ private void checkCallers(HostedMethod caller, Uninterruptible callerAnnotation, } } - private void checkGraph(ResolvedJavaMethod method, StructuredGraph graph) { + private void checkGraph(ResolvedJavaMethod method, StructuredGraph graph, ConstantReflectionProvider constantReflectionProvider) { Uninterruptible annotation = Uninterruptible.Utils.getAnnotation(method); for (Node node : graph.getNodes()) { if (isAllocationNode(node)) { @@ -265,7 +267,10 @@ private void checkGraph(ResolvedJavaMethod method, StructuredGraph graph) { * It is therefore safe to have class initialization nodes in methods that are * annotated with calleeMustBe = false. */ - violations.add("Uninterruptible method " + method.format("%H.%n(%p)") + " is not allowed to do class initialization."); + ValueNode hub = ((EnsureClassInitializedNode) node).getHub(); + + var culprit = hub.isConstant() ? constantReflectionProvider.asJavaType(hub.asConstant()).toClassName() : "unknown"; + violations.add("Uninterruptible method " + method.format("%H.%n(%p)") + " is not allowed to do class initialization. Initialized type: " + culprit); } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java index 5a1d4dcb35525..d9d162ee83a34 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java @@ -31,6 +31,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.hosted.ameta.AnalysisConstantReflectionProvider; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import jdk.vm.ci.meta.Constant; import jdk.vm.ci.meta.JavaConstant; @@ -46,8 +47,8 @@ public class HostedConstantReflectionProvider extends AnalysisConstantReflection private final HostedMemoryAccessProvider hMemoryAccess; @SuppressWarnings("this-escape") - public HostedConstantReflectionProvider(HostedUniverse hUniverse, HostedMetaAccess hMetaAccess) { - super(hUniverse.getBigBang().getUniverse(), hUniverse.getBigBang().getMetaAccess()); + public HostedConstantReflectionProvider(HostedUniverse hUniverse, HostedMetaAccess hMetaAccess, ClassInitializationSupport classInitializationSupport) { + super(hUniverse.getBigBang().getUniverse(), hUniverse.getBigBang().getMetaAccess(), classInitializationSupport); this.hUniverse = hUniverse; this.hMetaAccess = hMetaAccess; this.hMemoryAccess = new HostedMemoryAccessProvider(hMetaAccess, this); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java index 67b9b8328a5a5..5964d47f7701c 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java @@ -44,6 +44,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.c.function.CEntryPointLiteral; import org.graalvm.nativeimage.c.function.CFunction; @@ -68,6 +69,7 @@ import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.c.BoxedRelocatedPointer; import com.oracle.svm.core.c.function.CFunctionOptions; +import com.oracle.svm.core.classinitialization.ClassInitializationInfo; import com.oracle.svm.core.config.ConfigurationValues; import com.oracle.svm.core.config.ObjectLayout; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; @@ -146,6 +148,9 @@ public void build(DebugContext debug) { for (AnalysisType aType : aUniverse.getTypes()) { makeType(aType); } + for (AnalysisType aType : aUniverse.getTypes()) { + checkHierarchyForTypeReachedConstraints(aType); + } for (AnalysisField aField : aUniverse.getFields()) { makeField(aField); } @@ -278,9 +283,49 @@ private HostedType makeType(AnalysisType aType) { " -> " + hTypeChecked + " @ " + Integer.toHexString(System.identityHashCode(hTypeChecked)) + " / " + aTypeChecked + " @ " + Integer.toHexString(System.identityHashCode(aTypeChecked))); } + + /* + * Mark all types whose subtype is marked as --initialize-at-build-time types as reached. We + * need this as interfaces without default methods are not transitively initialized at build + * time by their subtypes. + */ + if (hType.wrapped.isReachable() && + ClassInitializationSupport.singleton().maybeInitializeAtBuildTime(hostedJavaClass) && + hub.getClassInitializationInfo().getTypeReached() == ClassInitializationInfo.TypeReached.NOT_REACHED) { + hType.wrapped.forAllSuperTypes(t -> { + var superHub = hUniverse.hostVM().dynamicHub(t); + if (superHub.getClassInitializationInfo().getTypeReached() == ClassInitializationInfo.TypeReached.NOT_REACHED) { + superHub.getClassInitializationInfo().setTypeReached(); + } + }); + } return hType; } + /** + * The {@link ClassInitializationInfo#getTypeReached()} for each super-type hub must have a + * value whose ordinal is greater or equal to its own value. + */ + private void checkHierarchyForTypeReachedConstraints(AnalysisType type) { + if (type.isReachable()) { + var hub = hUniverse.hostVM().dynamicHub(type); + if (type.getSuperclass() != null) { + checkSuperHub(hub, hub.getSuperHub()); + } + + for (AnalysisType superInterface : type.getInterfaces()) { + checkSuperHub(hub, hUniverse.hostVM().dynamicHub(superInterface)); + } + } + } + + private static void checkSuperHub(DynamicHub hub, DynamicHub superTypeHub) { + ClassInitializationInfo.TypeReached typeReached = hub.getClassInitializationInfo().getTypeReached(); + ClassInitializationInfo.TypeReached superTypeReached = superTypeHub.getClassInitializationInfo().getTypeReached(); + VMError.guarantee(superTypeReached.ordinal() >= typeReached.ordinal(), + "Super type of a type must have type reached >= than the type: %s is %s but %s is %s", hub.getName(), typeReached, superTypeHub.getName(), superTypeReached); + } + /* * Normally types need to be compared with equals, and there is a gate check enforcing this. * Using a separate method hides the comparison from the checker. diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/HostedGraphKit.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/HostedGraphKit.java index 84ffed0d53e94..2ee25aa645732 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/HostedGraphKit.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/HostedGraphKit.java @@ -35,6 +35,7 @@ import com.oracle.svm.core.graal.code.SubstrateCompilationIdentifier; import com.oracle.svm.core.graal.replacements.SubstrateGraphKit; import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; import jdk.graal.compiler.core.common.type.StampFactory; @@ -73,7 +74,9 @@ public AnalysisMetaAccess getMetaAccess() { } public void emitEnsureInitializedCall(ResolvedJavaType type) { - if (EnsureClassInitializedNode.needsRuntimeInitialization(graph.method().getDeclaringClass(), type)) { + boolean requiresInitializationForTypeReached = ClassInitializationSupport.singleton().requiresInitializationNodeForTypeReached(type); + if (requiresInitializationForTypeReached || + EnsureClassInitializedNode.needsRuntimeInitialization(graph.method().getDeclaringClass(), type)) { ValueNode hub = createConstant(getConstantReflection().asJavaClass(type), JavaKind.Object); appendWithUnwind(new EnsureClassInitializedNode(hub)); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java index d9ee3c359e51f..be4a6399b2428 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java @@ -32,6 +32,7 @@ import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisPolicy; import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; import com.oracle.svm.hosted.fieldfolding.IsStaticFinalFieldInitializedNode; @@ -83,7 +84,7 @@ private Node handleEnsureClassInitializedNode(EnsureClassInitializedNode node) { */ simulateClassInitializerSupport.trySimulateClassInitializer(bb, type); } - if (simulateClassInitializerSupport.isClassInitializerSimulated(type)) { + if (simulateClassInitializerSupport.isClassInitializerSimulated(type) && !ClassInitializationSupport.singleton().requiresInitializationNodeForTypeReached(type)) { return null; } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/SubstrateClassInitializationPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/SubstrateClassInitializationPlugin.java index bc4dcd1fe2ea9..5b8caada369c3 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/SubstrateClassInitializationPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/SubstrateClassInitializationPlugin.java @@ -28,7 +28,9 @@ import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; import jdk.graal.compiler.nodes.ConstantNode; import jdk.graal.compiler.nodes.FrameState; import jdk.graal.compiler.nodes.ValueNode; @@ -58,8 +60,12 @@ public void loadReferencedType(GraphBuilderContext builder, ConstantPool constan @Override public boolean apply(GraphBuilderContext builder, ResolvedJavaType type, Supplier frameState) { - if (EnsureClassInitializedNode.needsRuntimeInitialization(builder.getMethod().getDeclaringClass(), type)) { - emitEnsureClassInitialized(builder, builder.getSnippetReflection().forObject(host.dynamicHub(type)), frameState.get()); + var requiredForTypeReached = ClassInitializationSupport.singleton().requiresInitializationNodeForTypeReached(type); + if (requiredForTypeReached || + EnsureClassInitializedNode.needsRuntimeInitialization(builder.getMethod().getDeclaringClass(), type)) { + assert !type.isArray() : "Array types must not have initialization nodes: " + type.getName(); + SnippetReflectionProvider snippetReflection = builder.getSnippetReflection(); + emitEnsureClassInitialized(builder, snippetReflection.forObject(host.dynamicHub(type)), frameState.get()); return true; } return false; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/NativeImageConditionResolver.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/NativeImageConditionResolver.java index 99736dc84a7d5..cdf0ae9c0624f 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/NativeImageConditionResolver.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/NativeImageConditionResolver.java @@ -46,7 +46,17 @@ public NativeImageConditionResolver(ImageClassLoader classLoader, ClassInitializ public TypeResult resolveCondition(UnresolvedConfigurationCondition unresolvedCondition) { String canonicalizedName = RegistryAdapter.canonicalizeTypeName(unresolvedCondition.getTypeName()); TypeResult> clazz = classLoader.findClass(canonicalizedName); - return clazz.map(ConfigurationCondition::create); + return clazz.map(type -> { + /* + * We don't want to track always reached types: we convert them into build-time + * reachability checks. + */ + var runtimeChecked = !classInitializationSupport.isAlwaysReached(type) && unresolvedCondition.isRuntimeChecked(); + if (runtimeChecked) { + classInitializationSupport.addForTypeReachedTracking(type); + } + return ConfigurationCondition.create(type, runtimeChecked); + }); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java index ff60d493eda0c..f525f9bedf1af 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java @@ -61,6 +61,7 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.graalvm.nativeimage.ImageSingletons; @@ -141,7 +142,7 @@ public class ReflectionDataBuilder extends ConditionalConfigurationRegistry impl private final Map> processedTypes = new ConcurrentHashMap<>(); private final Map, Set> pendingRecordClasses; - record ConditionalTask(ConfigurationCondition condition, Runnable task) { + record ConditionalTask(ConfigurationCondition condition, Consumer task) { } private final Set pendingConditionalTasks = ConcurrentHashMap.newKeySet(); @@ -160,7 +161,7 @@ public void duringSetup(AnalysisMetaAccess analysisMetaAccess, AnalysisUniverse this.metaAccess = analysisMetaAccess; this.universe = analysisUniverse; for (var conditionalTask : pendingConditionalTasks) { - registerConditionalConfiguration(conditionalTask.condition, (cnd) -> universe.getBigbang().postTask(debug -> conditionalTask.task.run())); + registerConditionalConfiguration(conditionalTask.condition, (cnd) -> universe.getBigbang().postTask(debug -> conditionalTask.task.accept(cnd))); } pendingConditionalTasks.clear(); } @@ -169,13 +170,13 @@ public void beforeAnalysis(BeforeAnalysisAccessImpl beforeAnalysisAccess) { this.analysisAccess = beforeAnalysisAccess; } - private void runConditionalInAnalysisTask(ConfigurationCondition condition, Runnable task) { + private void runConditionalInAnalysisTask(ConfigurationCondition condition, Consumer task) { if (sealed) { throw UserError.abort("Too late to add classes, methods, and fields for reflective access. Registration must happen in a Feature before the analysis has finished."); } if (universe != null) { - registerConditionalConfiguration(condition, (cnd) -> universe.getBigbang().postTask(debug -> task.run())); + registerConditionalConfiguration(condition, (cnd) -> universe.getBigbang().postTask(debug -> task.accept(cnd))); } else { pendingConditionalTasks.add(new ConditionalTask(condition, task)); VMError.guarantee(universe == null, "There shouldn't be a race condition on Feature.duringSetup."); @@ -189,17 +190,17 @@ private void setQueryFlag(Class clazz, int flag) { @Override public void register(ConfigurationCondition condition, boolean unsafeInstantiated, Class clazz) { Objects.requireNonNull(clazz, () -> nullErrorMessage("class")); - runConditionalInAnalysisTask(condition, () -> registerClass(clazz, unsafeInstantiated, true)); + runConditionalInAnalysisTask(condition, (cnd) -> registerClass(cnd, clazz, unsafeInstantiated, true)); } @Override public void registerAllClassesQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_CLASSES_FLAG); try { for (Class innerClass : clazz.getClasses()) { innerClasses.computeIfAbsent(innerClass.getDeclaringClass(), c -> ConcurrentHashMap.newKeySet()).add(innerClass); - registerClass(innerClass, false, !MissingRegistrationUtils.throwMissingRegistrationErrors()); + registerClass(cnd, innerClass, false, !MissingRegistrationUtils.throwMissingRegistrationErrors()); } } catch (LinkageError e) { registerLinkageError(clazz, e, classLookupExceptions); @@ -209,12 +210,12 @@ public void registerAllClassesQuery(ConfigurationCondition condition, Class c @Override public void registerAllDeclaredClassesQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_DECLARED_CLASSES_FLAG); try { for (Class innerClass : clazz.getDeclaredClasses()) { innerClasses.computeIfAbsent(clazz, c -> ConcurrentHashMap.newKeySet()).add(innerClass); - registerClass(innerClass, false, !MissingRegistrationUtils.throwMissingRegistrationErrors()); + registerClass(cnd, innerClass, false, !MissingRegistrationUtils.throwMissingRegistrationErrors()); } } catch (LinkageError e) { registerLinkageError(clazz, e, classLookupExceptions); @@ -222,7 +223,7 @@ public void registerAllDeclaredClassesQuery(ConfigurationCondition condition, Cl }); } - private void registerClass(Class clazz, boolean unsafeInstantiated, boolean allowForName) { + private void registerClass(ConfigurationCondition condition, Class clazz, boolean unsafeInstantiated, boolean allowForName) { if (shouldExcludeClass(clazz)) { return; } @@ -234,7 +235,7 @@ private void registerClass(Class clazz, boolean unsafeInstantiated, boolean a } if (allowForName) { - ClassForNameSupport.registerClass(clazz); + ClassForNameSupport.singleton().registerClass(condition, clazz); if (!MissingRegistrationUtils.throwMissingRegistrationErrors()) { /* @@ -258,25 +259,25 @@ private void registerClass(Class clazz, boolean unsafeInstantiated, boolean a @Override public void registerClassLookupException(ConfigurationCondition condition, String typeName, Throwable t) { - runConditionalInAnalysisTask(condition, () -> ClassForNameSupport.registerExceptionForClass(typeName, t)); + runConditionalInAnalysisTask(condition, (cnd) -> ClassForNameSupport.singleton().registerExceptionForClass(cnd, typeName, t)); } @Override public void registerClassLookup(ConfigurationCondition condition, String typeName) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { try { - registerClass(Class.forName(typeName, false, ClassLoader.getSystemClassLoader()), false, true); + registerClass(cnd, Class.forName(typeName, false, ClassLoader.getSystemClassLoader()), false, true); } catch (ClassNotFoundException e) { - ClassForNameSupport.registerNegativeQuery(typeName); + ClassForNameSupport.singleton().registerNegativeQuery(cnd, typeName); } catch (Throwable t) { - ClassForNameSupport.registerExceptionForClass(typeName, t); + ClassForNameSupport.singleton().registerExceptionForClass(cnd, typeName, t); } }); } @Override public void registerAllRecordComponentsQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_RECORD_COMPONENTS_FLAG); registerRecordComponents(clazz); }); @@ -284,11 +285,11 @@ public void registerAllRecordComponentsQuery(ConfigurationCondition condition, C @Override public void registerAllPermittedSubclassesQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_PERMITTED_SUBCLASSES_FLAG); if (clazz.isSealed()) { for (Class permittedSubclass : clazz.getPermittedSubclasses()) { - registerClass(permittedSubclass, false, false); + registerClass(condition, permittedSubclass, false, false); } } }); @@ -296,11 +297,11 @@ public void registerAllPermittedSubclassesQuery(ConfigurationCondition condition @Override public void registerAllNestMembersQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_NEST_MEMBERS_FLAG); for (Class nestMember : clazz.getNestMembers()) { if (nestMember != clazz) { - registerClass(nestMember, false, false); + registerClass(condition, nestMember, false, false); } } }); @@ -308,7 +309,7 @@ public void registerAllNestMembersQuery(ConfigurationCondition condition, Class< @Override public void registerAllSignersQuery(ConfigurationCondition condition, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_SIGNERS_FLAG); Object[] signers = clazz.getSigners(); if (signers != null) { @@ -322,12 +323,12 @@ public void registerAllSignersQuery(ConfigurationCondition condition, Class c @Override public void register(ConfigurationCondition condition, boolean queriedOnly, Executable... executables) { requireNonNull(executables, "executable"); - runConditionalInAnalysisTask(condition, () -> registerMethods(queriedOnly, executables)); + runConditionalInAnalysisTask(condition, (cnd) -> registerMethods(queriedOnly, executables)); } @Override public void registerAllMethodsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { for (Class current = clazz; current != null; current = current.getSuperclass()) { setQueryFlag(current, ALL_METHODS_FLAG); } @@ -341,7 +342,7 @@ public void registerAllMethodsQuery(ConfigurationCondition condition, boolean qu @Override public void registerAllDeclaredMethodsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_DECLARED_METHODS_FLAG); try { registerMethods(queriedOnly, clazz.getDeclaredMethods()); @@ -353,7 +354,7 @@ public void registerAllDeclaredMethodsQuery(ConfigurationCondition condition, bo @Override public void registerAllConstructorsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { for (Class current = clazz; current != null; current = current.getSuperclass()) { setQueryFlag(current, ALL_CONSTRUCTORS_FLAG); } @@ -367,7 +368,7 @@ public void registerAllConstructorsQuery(ConfigurationCondition condition, boole @Override public void registerAllDeclaredConstructorsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_DECLARED_CONSTRUCTORS_FLAG); try { registerMethods(queriedOnly, clazz.getDeclaredConstructors()); @@ -430,7 +431,7 @@ private void registerMethod(boolean queriedOnly, Executable reflectExecutable) { @Override public void registerMethodLookup(ConfigurationCondition condition, Class declaringClass, String methodName, Class... parameterTypes) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { try { registerMethod(true, declaringClass.getDeclaredMethod(methodName, parameterTypes)); } catch (NoSuchMethodException e) { @@ -442,7 +443,7 @@ public void registerMethodLookup(ConfigurationCondition condition, Class decl @Override public void registerConstructorLookup(ConfigurationCondition condition, Class declaringClass, Class... parameterTypes) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { try { registerMethod(true, declaringClass.getDeclaredConstructor(parameterTypes)); } catch (NoSuchMethodException e) { @@ -455,7 +456,7 @@ public void registerConstructorLookup(ConfigurationCondition condition, Class @Override public void register(ConfigurationCondition condition, boolean finalIsWritable, Field... fields) { requireNonNull(fields, "field"); - runConditionalInAnalysisTask(condition, () -> registerFields(false, fields)); + runConditionalInAnalysisTask(condition, (cnd) -> registerFields(false, fields)); } @Override @@ -464,7 +465,7 @@ public void registerAllFieldsQuery(ConfigurationCondition condition, Class cl } private void registerAllFieldsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { for (Class current = clazz; current != null; current = current.getSuperclass()) { setQueryFlag(current, ALL_FIELDS_FLAG); } @@ -482,7 +483,7 @@ public void registerAllDeclaredFieldsQuery(ConfigurationCondition condition, Cla } private void registerAllDeclaredFieldsQuery(ConfigurationCondition condition, boolean queriedOnly, Class clazz) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { setQueryFlag(clazz, ALL_DECLARED_FIELDS_FLAG); try { registerFields(queriedOnly, clazz.getDeclaredFields()); @@ -532,7 +533,7 @@ private void registerField(boolean queriedOnly, Field reflectField) { @Override public void registerFieldLookup(ConfigurationCondition condition, Class declaringClass, String fieldName) { - runConditionalInAnalysisTask(condition, () -> { + runConditionalInAnalysisTask(condition, (cnd) -> { try { registerField(false, declaringClass.getDeclaredField(fieldName)); } catch (NoSuchFieldException e) { @@ -549,7 +550,7 @@ private void processAnnotationMethod(boolean queriedOnly, Method method) { Class annotationClass = method.getDeclaringClass(); Class proxyClass = Proxy.getProxyClass(annotationClass.getClassLoader(), annotationClass); try { - var condition = ConfigurationCondition.create(proxyClass); + var condition = ConfigurationCondition.create(proxyClass, true); register(condition, queriedOnly, proxyClass.getDeclaredMethod(method.getName(), method.getParameterTypes())); } catch (NoSuchMethodException e) { /* @@ -563,7 +564,7 @@ private void processAnnotationField(Field field) { Class annotationClass = field.getDeclaringClass(); Class proxyClass = Proxy.getProxyClass(annotationClass.getClassLoader(), annotationClass); try { - var condition = ConfigurationCondition.create(proxyClass); + var condition = ConfigurationCondition.create(proxyClass, true); register(condition, false, proxyClass.getDeclaredField(field.getName())); } catch (NoSuchFieldException e) { /* @@ -597,12 +598,12 @@ private void checkSubtypeForOverridingField(AnalysisField field, AnalysisType su * {@link Class#getDeclaredMethods()} internally, instead of * {@link AnalysisType#resolveConcreteMethod(ResolvedJavaMethod)} which gives different results * in at least two scenarios: - * + *

* 1) When resolving a static method, resolveConcreteMethod does not return a subclass method * with the same signature, since they are actually fully distinct methods. However, these * methods need to be included in the hiding list because them showing up in a reflection query * would be wrong. - * + *

* 2) When resolving an interface method from an abstract class, resolveConcreteMethod returns * an undeclared method with the abstract subclass as declaring class, which is not the * reflection API behavior. @@ -794,7 +795,7 @@ private void registerTypesForGenericSignature(Type type, int dimension) { /* * Reflection signature parsing will try to instantiate classes via Class.forName(). */ - ClassForNameSupport.registerClass(clazz); + ClassForNameSupport.singleton().registerClass(clazz); } else if (type instanceof TypeVariable) { /* Bounds are reified lazily. */ registerTypesForGenericSignature(queryGenericInfo(((TypeVariable) type)::getBounds), dimension); @@ -1178,7 +1179,7 @@ private static String nullErrorMessage(String kind) { public static class TestBackdoor { public static void registerField(ReflectionDataBuilder reflectionDataBuilder, boolean queriedOnly, Field field) { - reflectionDataBuilder.runConditionalInAnalysisTask(ConfigurationCondition.alwaysTrue(), () -> reflectionDataBuilder.registerField(queriedOnly, field)); + reflectionDataBuilder.runConditionalInAnalysisTask(ConfigurationCondition.alwaysTrue(), (cnd) -> reflectionDataBuilder.registerField(queriedOnly, field)); } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java index bd25a0dc6acf1..02fb129b2034a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java @@ -277,7 +277,7 @@ public void duringSetup(DuringSetupAccess a) { /* Primitive classes cannot be accessed through Class.forName() */ for (Class primitiveClass : PRIMITIVE_CLASSES) { - ClassForNameSupport.registerNegativeQuery(primitiveClass.getName()); + ClassForNameSupport.singleton().registerNegativeQuery(ConfigurationCondition.alwaysTrue(), primitiveClass.getName()); } access.registerObjectReachableCallback(SubstrateAccessor.class, ReflectionFeature::onAccessorReachable);