From b7fbbd16e386040d45768eeb0e2cad3dcb6bb0e1 Mon Sep 17 00:00:00 2001 From: Dominik Lano Date: Thu, 20 Aug 2026 20:00:04 +0200 Subject: [PATCH] Verify all objects reachable through build-time-initialized static fields --- .../compiler/libgraal/LibGraalFeature.java | 5 +- .../native-image/ClassInitialization.md | 12 + .../org.graalvm.nativeimage/snapshot.sigtest | 10 +- .../graalvm/nativeimage/hosted/Feature.java | 41 +- .../hosted/FieldValueTransformer.java | 11 +- ...FieldValueTransformerRequiresUniverse.java | 67 + .../clinit/ClassInitializationTracking.java | 6 +- substratevm/CHANGELOG.md | 2 + .../mx_substratevm_benchmark.py | 16 +- .../pointsto/standalone/StandaloneHost.java | 4 + .../StandaloneAnalysisFeatureImpl.java | 6 +- ...andaloneFieldValueAvailabilitySupport.java | 2 +- .../StandaloneConstantReflectionProvider.java | 2 +- .../com/oracle/graal/pointsto/api/HostVM.java | 8 + .../pointsto/heap/HostedValuesProvider.java | 4 +- .../graal/pointsto/meta/AnalysisField.java | 21 +- .../graal/pointsto/meta/AnalysisType.java | 15 +- .../graal/pointsto/meta/AnalysisUniverse.java | 73 +- .../objectfile/debugentry/ClassEntry.java | 8 +- ...internal_foreign_abi_NativeEntryPoint.java | 5 +- .../svm/core/FutureDefaultsOptions.java | 14 +- .../svm/core/doc-files/FutureDefaultsHelp.txt | 4 +- .../svm/core/feature/JVMCIFeatureAccess.java | 19 +- .../FieldOffsetFieldValueTransformer.java | 6 +- .../StaticFieldBaseFieldValueTransformer.java | 6 +- .../src/com/oracle/svm/core/heap/GCCause.java | 14 +- .../svm/core/jdk/ServiceCatalogSupport.java | 37 +- ...get_jdk_internal_module_ModuleReaders.java | 2 +- .../oracle/svm/core/jdk/VarHandleSupport.java | 5 +- .../svm/core/locks/ClassInstanceReplacer.java | 47 +- .../oracle/svm/core/locks/VMLockSupport.java | 16 +- .../target/ExecutableAccessorComputer.java | 6 +- .../target/ReflectionFieldValueComputers.java | 8 +- .../graal/hosted/FieldsOffsetsFeature.java | 7 +- .../GraalGraphObjectReplacer.java | 77 +- .../RuntimeCompilationFeature.java | 2 +- .../Target_java_lang_reflect_Field.java | 2 +- .../svm/guest/hosted/GuestFeatureHandler.java | 5 + .../svm/guest/hosted/GuestFeatureImpl.java | 5 + .../test/VerifyJavaTypeGetNameUsage.java | 3 +- .../hosted/test/VerifyReflectionUsage.java | 1 + .../oracle/svm/hosted/ClassLoaderFeature.java | 33 +- .../com/oracle/svm/hosted/FeatureImpl.java | 73 +- .../hosted/GuestFeatureDispatchFeature.java | 18 + .../svm/hosted/HeapObjectReplacerAdapter.java | 57 + .../oracle/svm/hosted/ModuleLayerFeature.java | 25 +- .../svm/hosted/NativeImageGenerator.java | 11 + .../src/com/oracle/svm/hosted/SVMHost.java | 31 +- .../AnalysisConstantReflectionProvider.java | 7 + .../hosted/ameta/CustomTypeFieldHandler.java | 8 +- .../ameta/FieldValueInterceptionSupport.java | 251 ++- .../hosted/ameta/HostedDynamicHubFeature.java | 30 +- .../hosted/ameta/SVMHostedValueProvider.java | 13 +- .../svm/hosted/c/CGlobalDataFeature.java | 31 +- .../ClassInitializationSupport.java | 25 +- .../hosted/classinitialization/InitKind.java | 4 +- ...edictableBuildTimeInitializationError.java | 299 +++ ...ictableBuildTimeInitializationFeature.java | 1950 +++++++++++++++++ ...edictableBuildTimeInitializationField.java | 173 ++ ...bleBuildTimeInitializationLookupLayer.java | 232 ++ ...redictableBuildTimeInitializationType.java | 500 +++++ .../code/CEntryPointLiteralFeature.java | 12 +- .../heap/ImageHeapCollectionFeature.java | 5 +- .../heap/ImageHeapCollectionSupport.java | 57 +- .../svm/hosted/image/sources/SourceCache.java | 15 +- .../HostedClassLoaderPackageManagement.java | 10 +- .../hosted/jdk/JDKInitializationFeature.java | 4 + .../svm/hosted/jfr/JfrEventFeature.java | 67 +- .../svm/hosted/jfr/JfrEventSubstitution.java | 77 +- .../AutomaticUnsafeTransformationSupport.java | 107 +- .../substitute/FieldValueTransformation.java | 13 +- .../thread/HostedJavaThreadsFeature.java | 78 +- .../thread/LayeredVMThreadLocalCollector.java | 15 +- .../hosted/thread/VMThreadLocalCollector.java | 8 +- .../option/FutureDefaultsOptionsSupport.java | 2 + .../ImageSingletonsSupportImpl.java | 10 +- .../truffle/api/SubstrateTruffleRuntime.java | 8 +- .../util/HeapConstantToConstantReplacer.java | 67 + .../svm/util/HeapObjectConstantReplacer.java | 124 ++ .../oracle/svm/util/HeapObjectReplacer.java | 77 + .../com/oracle/svm/util/InterceptedField.java | 49 + .../oracle/svm/util/InterceptedFieldType.java | 56 + .../system/WebImageJavaLangSubstitutions.java | 5 + 83 files changed, 4898 insertions(+), 343 deletions(-) create mode 100644 sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapObjectReplacerAdapter.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationError.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationFeature.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java diff --git a/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java b/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java index 212b96c37c694..673acaf7fc5b4 100644 --- a/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java +++ b/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -52,6 +52,7 @@ import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.FieldValueTransformer; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; import org.graalvm.nativeimage.libgraal.hosted.LibGraalLoader; @@ -261,7 +262,7 @@ void afterAnalysis(AfterAnalysisAccess access) { * Transformer for {@code Fields.offsets} and {@code Edges.iterationMask} which need to be * recomputed to use SVM field offsets instead of HotSpot field offsets. */ - class FieldOffsetsTransformer implements FieldValueTransformer { + class FieldOffsetsTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse { /** * Map from {@link Fields} objects to a (newOffsets, newIterationMask) tuple represented as * a {@link java.util.Map.Entry} value. diff --git a/docs/reference-manual/native-image/ClassInitialization.md b/docs/reference-manual/native-image/ClassInitialization.md index 5288a276d1b97..9f60e0dc5a6ca 100644 --- a/docs/reference-manual/native-image/ClassInitialization.md +++ b/docs/reference-manual/native-image/ClassInitialization.md @@ -26,6 +26,8 @@ To accommodate this requirement, a short-term "relevant supertype" is used, as w * Relevant supertypes of types initialized at build time must also be initialized at build time. * Relevant subtypes of types initialized at runtime must also be initialized at runtime. * No instances of classes that are initialized at runtime must be present in the executable. +* With `--future-defaults=predictable-build-time-initialization`, a class initialized at build time must not reference instances of run-time initialized classes, directly or indirectly, through its static fields, + even if reachability analysis does not mark those instances as reachable. To enjoy the complete out-of-the-box experience of Native Image and still get the benefits of build-time initialization, Native Image does two things: * [Build-Time Initialization](#build-time-initialization) @@ -41,6 +43,16 @@ Native Image initializes most JDK classes at build time, including the garbage c For all of the classes that are initialized at build time, Native Image gives proper support so that the semantics remain consistent despite class initialization occurring at build time. If you discover an issue with a JDK class behaving incorrectly because of class initialization at build time, please [report an issue](https://github.com/oracle/graal/issues/new). +### Verification of Objects Referenced by Static Fields + +By default, Native Image verifies that objects included in the executable's image heap are instances of build-time initialized classes and are allowed in the image heap. + +With `--future-defaults=predictable-build-time-initialization`, this verification extends to any object referenced directly +or indirectly by static fields of build-time initialized classes that could become part of the image heap. +This makes reporting of class initialization violations more reliable, as these errors are no longer masked by reachability. +If an object is found to be an instance of a run-time initialized class, the build fails with an error showing a path from a +build-time initialized class to the offending object and suggesting how to resolve the violation. + ## Automatic Initialization of Safe Classes For application classes, Native Image tries to find classes that can be safely initialized at build time. diff --git a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest index 647fa0ec0d4c3..c10b27396d10d 100644 --- a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest +++ b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest @@ -996,6 +996,7 @@ innr public abstract interface static AfterImageWriteAccess innr public abstract interface static AfterRegistrationAccess innr public abstract interface static BeforeAnalysisAccess innr public abstract interface static BeforeCompilationAccess +innr public abstract interface static BeforeFieldValueAccess innr public abstract interface static BeforeHeapLayoutAccess innr public abstract interface static BeforeImageWriteAccess innr public abstract interface static BeforeUniverseBuildingAccess @@ -1054,6 +1055,7 @@ meth public abstract org.graalvm.nativeimage.dynamicaccess.ResourceAccess getRes CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess outer org.graalvm.nativeimage.hosted.Feature +intf org.graalvm.nativeimage.hosted.Feature$BeforeFieldValueAccess intf org.graalvm.nativeimage.hosted.Feature$FeatureAccess meth public abstract !varargs void registerReachabilityHandler(java.util.function.Consumer,java.lang.Object[]) meth public abstract void registerAsAccessed(java.lang.reflect.Field) @@ -1061,8 +1063,8 @@ meth public abstract void registerAsInHeap(java.lang.Class) meth public abstract void registerAsUnsafeAccessed(java.lang.reflect.Field) meth public abstract void registerAsUnsafeAllocated(java.lang.Class) meth public abstract void registerAsUsed(java.lang.Class) +meth public abstract void registerBeforeFieldValueAccessHandler(java.util.function.BiConsumer>,java.lang.Class) meth public abstract void registerClassInitializerReachabilityHandler(java.util.function.Consumer,java.lang.Class) -meth public abstract void registerFieldValueTransformer(java.lang.reflect.Field,org.graalvm.nativeimage.hosted.FieldValueTransformer) meth public abstract void registerMethodOverrideReachabilityHandler(java.util.function.BiConsumer,java.lang.reflect.Executable) meth public abstract void registerSubtypeReachabilityHandler(java.util.function.BiConsumer>,java.lang.Class) @@ -1070,6 +1072,10 @@ CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$Bef outer org.graalvm.nativeimage.hosted.Feature intf org.graalvm.nativeimage.hosted.Feature$CompilationAccess +CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeFieldValueAccess + outer org.graalvm.nativeimage.hosted.Feature +meth public abstract void registerFieldValueTransformer(java.lang.reflect.Field,org.graalvm.nativeimage.hosted.FieldValueTransformer) + CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeHeapLayoutAccess outer org.graalvm.nativeimage.hosted.Feature intf org.graalvm.nativeimage.hosted.Feature$CompilationAccess @@ -1135,6 +1141,8 @@ CLSS public abstract interface org.graalvm.nativeimage.hosted.FieldValueTransfor meth public abstract java.lang.Object transform(java.lang.Object,java.lang.Object) meth public boolean isAvailable() +CLSS public abstract interface org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse + CLSS public final org.graalvm.nativeimage.hosted.RuntimeClassInitialization meth public !varargs static void initializeAtBuildTime(java.lang.Class[]) meth public !varargs static void initializeAtBuildTime(java.lang.String[]) diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java index 6aa6a9e6cf139..1a6db312afaec 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java @@ -267,13 +267,36 @@ interface DuringSetupAccess extends FeatureAccess { void registerBuildTimeBootstrapCondy(Executable method); } + /** + * Interface that encapsulates registration of field value transformers. Under + * {@code --future-defaults=predictable-build-time-initialization}, registration of transformers + * will only be possible before analysis starts or in a + * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}, + * which is triggered before the first field of the selected class is accessed. + */ + @Platforms(Platform.HOSTED_ONLY.class) + interface BeforeFieldValueAccess { + /** + * Registers a field value transformer for the provided field. + * Under {@code --future-defaults=predictable-build-time-initialization}, registration of + * transformers will only be possible before analysis starts or in a + * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}. + * + *

+ * See the JavaDoc of + * {@link FieldValueTransformer} for details. + *

+ */ + void registerFieldValueTransformer(Field field, FieldValueTransformer transformer); + } + /** * Access methods available for {@link Feature#beforeAnalysis}. * * @since 19.0 */ @Platforms(Platform.HOSTED_ONLY.class) - interface BeforeAnalysisAccess extends FeatureAccess { + interface BeforeAnalysisAccess extends FeatureAccess, BeforeFieldValueAccess { /** * Registers the provided type a used, i.e., metadata for the type is put into the native @@ -380,12 +403,18 @@ interface BeforeAnalysisAccess extends FeatureAccess { void registerClassInitializerReachabilityHandler(Consumer callback, Class clazz); /** - * Registers a field value transformer for the provided field. See the JavaDoc of - * {@link FieldValueTransformer} for details. - * - * @since 22.3 + * Registers a callback that allows registration of field value transformers on demand. The + * callback is triggered for every subclass of {@code baseClass}, including + * {@code baseClass} itself, before the first field value of the class is accessed. The class + * is passed as the callback's second parameter. + * + *

+ * This is a replacement for registration done in type/subtype reachability handlers, where + * this behavior can no longer be supported. Handlers must be registered in a + * {@link Feature#beforeAnalysis beforeAnalysis} callback. + *

*/ - void registerFieldValueTransformer(Field field, FieldValueTransformer transformer); + void registerBeforeFieldValueAccessHandler(BiConsumer> callback, Class baseClass); } /** diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java index 0996bfa460480..1cc4d2a73e3dd 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java @@ -44,9 +44,12 @@ import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess; +import java.util.function.BiConsumer; + /** * A transformer for a field value that can be registered using - * {@link BeforeAnalysisAccess#registerFieldValueTransformer}. + * {@link BeforeAnalysisAccess#registerFieldValueTransformer} or in a + * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}. *

* At image build time, the field value transformer provides the value of the field for the image * heap. Without a transformer, the value of the field in the image heap is the same as the hosted @@ -70,6 +73,12 @@ * reachability handler} for the declaring class of the field, or a * {@link BeforeAnalysisAccess#registerSubtypeReachabilityHandler subtype reachability handler} for * a super-type of the declaring class of the field. + *

+ * Under {@code --future-defaults=predictable-build-time-initialization}, + * registration of transformers will only be possible before static analysis starts + * or in a {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}. + * The handler is triggered for each subclass, including the base class, before its first field + * value is accessed. * * @since 22.3 */ diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java new file mode 100644 index 0000000000000..bfe3a0a5e0a86 --- /dev/null +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.nativeimage.hosted; + +/** + * Marker interface to denote that a field value transformer + * requires the analysis/hosted universe or internal operations that expand the universe or image heap, + * such as creating new universe types, methods or fields, modifying existing ones, + * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList} + * or {@code ImageHeapMap}. + * + *

+ * For example, {@code com.oracle.svm.core.reflect.target.ExecutableAccessorComputer} + * may create an accessor for a method or a constructor, which requires the analysis. + *

+ * + *

+ * This marker interface is used by {@code PredictableBuildTimeInitializationFeature} + * to differentiate between fields that can be read outside the universe and + * those that cannot be read are processed only if the field is read and its receiver is reachable. + *

+ * + *

+ * This marker interface can be used on external code, but is mostly relevant for internal transformers. + * It is placed in this package in order to be available for {@code jdk.graal.compiler.libgraal.LibGraalFeature}. + *

+ */ +public interface FieldValueTransformerRequiresUniverse { +} diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java index 79fe21dcc11a7..5ba194357af51 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -47,6 +47,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +@Platforms(Platform.HOSTED_ONLY.class) public class ClassInitializationTracking { public static final Map, StackTraceElement[]> initializedClasses = new ConcurrentHashMap<>(); diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 5958f1a206823..ec124afe5d847 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -14,6 +14,8 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-77491) Bundle format version is now 2.0, with explicit path root kinds, build argument groups that retain their source platform, and persistent markers for unavailable inputs. Older bundle formats remain readable. * (GR-79366) Add auxiliary images: a mechanism for persisting runtime-allocated objects and runtime-compiled code, and loading them in later executions of the same native image. +* (GR-51046) Introduced `--future-defaults=predictable-build-time-initialization`, which verifies that the classes of all objects stored in or reachable from static fields of build-time-initialized classes are also initialized at build time, even if those fields are not reachable. It also forbids registration of `FieldValueTransformer`s during analysis or in a reachability handler. +* (GR-51046) Added `BeforeAnalysis#registerBeforeFieldValueAccessHandler`, which registers a handler that allows field value transformer registration on demand, before field values are accessed. ## GraalVM 25.4 (Internal Version 25.4.4) * (GR-75824) When native executables are built with `-H:+StrictRuntimeJavaOptions`, runtime assertion options (for example, `-ea`, `-da`, `-esa`, and `-dsa`) are supported and configure the assertion status of runtime-loaded classes and runtime-initialized image classes. They do not affect build-time-initialized classes whose assertion status is *only* configured by `native-image -ea ...`. diff --git a/substratevm/mx.substratevm/mx_substratevm_benchmark.py b/substratevm/mx.substratevm/mx_substratevm_benchmark.py index 94d9174e824bb..2d6e8e198271a 100644 --- a/substratevm/mx.substratevm/mx_substratevm_benchmark.py +++ b/substratevm/mx.substratevm/mx_substratevm_benchmark.py @@ -726,11 +726,11 @@ def _empty_file(): _DACAPO_EXTRA_IMAGE_BUILD_ARGS = { 'h2' : [], - 'pmd': [], + 'pmd': ['--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'], # org.apache.crimson.parser.Parser2 is force initialized at build-time due to non-determinism in class initialization # order that can lead to runtime issues. See GR-26324. 'xalan': ['--initialize-at-build-time=org.apache.crimson.parser.Parser2,org.apache.crimson.parser.Parser2$Catalog,org.apache.crimson.parser.Parser2$NullHandler,org.apache.xml.utils.res.CharArrayWrapper'], - # There are two main issues with fop: + # There are three main issues with fop: # 1. LoggingFeature is enabled by default, causing the LogManager configuration to be parsed at build-time. However # DaCapo Harness sets the `java.util.logging.config.file` property at run-time. Therefore, we set # `java.util.logging.config.file` to an empty file to avoid incorrectly parsing the default log configuration, @@ -738,8 +738,10 @@ def _empty_file(): # not exist and would fail the benchmark when assertions are enabled. # 2. Native-image picks a different service provider than the JVM for javax.xml.transform.TransformerFactory. # We can simply remove the jar containing that provider as it is not required for the benchmark to run. + # 3. The Xerces DatatypeFactory provider is reachable from build-time initialized JAXB state. 'fop': [f"-Djava.util.logging.config.file={_empty_file()}", - '--initialize-at-run-time=org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList'], + '--initialize-at-run-time=org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList', + '--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'], 'batik': [] } @@ -952,6 +954,10 @@ def successPatterns(self): _scala_daCapo_additional_lib = { } +_scala_daCapo_extra_image_build_args = { + 'scalac': ['--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'], +} + class ScalaDaCapoNativeImageBenchmarkSuite(mx_sdk_benchmark.ScalaDaCapoBenchmarkSuite, BaseDaCapoNativeImageBenchmarkSuite, mx_sdk_benchmark.NativeImageBenchmarkMixin): #pylint: disable=too-many-ancestors def name(self): @@ -996,6 +1002,10 @@ def skip_agent_assertions(self, benchmark, args): else: return [] + def extra_image_build_argument(self, benchmark, args): + default_args = _scala_daCapo_extra_image_build_args.get(benchmark, []) + return default_args + super().extra_image_build_argument(benchmark, args) + def createCommandLineArgs(self, benchmarks, bmSuiteArgs): if benchmarks is None: mx.abort("Suite can only run a single benchmark per VM instance.") diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java index 91cf4b67f616d..d364c1042b87e 100644 --- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java +++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java @@ -181,6 +181,10 @@ public Optional handleForeignCall(ForeignCallDescriptor foreignC throw AnalysisError.shouldNotReachHere("StandaloneHost.handleForeignCall"); } + @Override + public void beforeFieldValueAccess(ResolvedJavaType type) { + } + @Override public boolean isClosedTypeWorld() { return closedTypeWorld; diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java index 2643265bf0ccb..f6629adb820ff 100644 --- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java +++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2022, 2022, Alibaba Group Holding Limited. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -248,6 +248,10 @@ public void registerClassInitializerReachabilityHandler(Consumer> callback, Class subclass) { + } } public static class DuringAnalysisAccessImpl extends BeforeAnalysisAccessImpl implements Feature.DuringAnalysisAccess { diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java index 2465c07c38fbd..a0cca49a8e65b 100644 --- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java +++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java @@ -117,7 +117,7 @@ private boolean isValueAvailable(AnalysisField field, Supplier raw } private boolean staticFieldSnapshottingAllowed(AnalysisField field) { - field.beforeFieldValueAccess(); + field.markDeclaringClassAsReachable(); if (!field.isStatic()) { return true; } diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java index e914ee9f0f85d..647c9c5cac075 100644 --- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java +++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java @@ -166,7 +166,7 @@ public JavaConstant readArrayElement(JavaConstant array, int index) { public final JavaConstant readFieldValue(ResolvedJavaField f, JavaConstant receiver) { AnalysisField field = (AnalysisField) f; - field.beforeFieldValueAccess(); + field.markDeclaringClassAsReachable(); if (field.equals(commonPoolField)) { /* diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java index 39ac3616dc6ff..ce8bae3917f05 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java @@ -171,6 +171,14 @@ public void registerType(AnalysisType newValue, int identityHashCode) { public void checkType(ResolvedJavaType type, AnalysisUniverse universe) { } + /** + * Register all field value transformers before fields of this type + * are accessed. + * + * @param type the hosted type whose fields are being accessed + */ + public abstract void beforeFieldValueAccess(ResolvedJavaType type); + /** * Run initialization tasks for a newly created {@link AnalysisType}. * diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java index 1929873201b66..1df51f21383ab 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,7 +51,7 @@ public JavaConstant readFieldValueWithReplacement(AnalysisField field, JavaConst } private JavaConstant doReadValue(AnalysisField field, JavaConstant receiver) { - field.beforeFieldValueAccess(); + field.markDeclaringClassAsReachable(); return interceptHosted(GuestAccess.get().getProviders().getConstantReflection().readFieldValue(field.wrapped, receiver)); } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java index f187ec75bc92e..93ecca4b4cb05 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java @@ -38,6 +38,7 @@ import com.oracle.svm.shared.meta.GuaranteeFolded; import com.oracle.svm.util.GuestAccess; import com.oracle.svm.util.GuestAnnotationAccess; +import com.oracle.svm.util.InterceptedField; import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.OriginalFieldProvider; @@ -48,7 +49,7 @@ import jdk.vm.ci.meta.ResolvedJavaField; import jdk.vm.ci.meta.ResolvedJavaType; -public abstract class AnalysisField extends AnalysisElement implements WrappedJavaField, OriginalFieldProvider { +public abstract class AnalysisField extends AnalysisElement implements WrappedJavaField, OriginalFieldProvider, InterceptedField { static final AnalysisField[] EMPTY_ARRAY = new AnalysisField[0]; @@ -203,6 +204,7 @@ public int hashCode() { return id; } + @Override public JavaKind getStorageKind() { return fieldType.getStorageKind(); @@ -396,10 +398,12 @@ protected void onTrackedAcrossLayers(Object reason) { AnalysisError.guarantee(!getUniverse().sealed(), "Field %s was marked as tracked after the universe was sealed", this); } + @Override public Object getFieldValueInterceptor() { return fieldValueInterceptor; } + @Override public void setFieldValueInterceptor(Object fieldValueInterceptor) { this.fieldValueInterceptor = fieldValueInterceptor; } @@ -476,11 +480,13 @@ public JavaConstant getConstantValue() { } /** - * Ensure that all reachability handlers that were present at the time the declaring type was - * marked as reachable are executed before accessing field values. This allows a field value - * transformer to be installed reliably in a reachability handler. + * Mark the declaring class and its super types as reachable. + * This was the default behavior when reading a field, but + * registration of field value transformers in reachability + * handlers, under "--future-defaults=predictable-build-time-initialization" + * is forbidden to not affect the universe. */ - public void beforeFieldValueAccess() { + public void markDeclaringClassAsReachable() { declaringClass.registerAsReachable(this); declaringClass.forAllSuperTypes(type -> { @@ -498,4 +504,9 @@ public void beforeFieldValueAccess() { } }); } + + @Override + public void beforeFieldValueAccess() { + getUniverse().hostVM().beforeFieldValueAccess(declaringClass); + } } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java index ad1b396e25724..e25d6d4a34a25 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java @@ -60,6 +60,7 @@ import com.oracle.graal.pointsto.util.ConcurrentLightHashSet; import com.oracle.svm.shared.util.LogUtils; import com.oracle.svm.util.GuestAccess; +import com.oracle.svm.util.InterceptedFieldType; import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.OriginalMethodProvider; @@ -79,7 +80,7 @@ import jdk.vm.ci.meta.Signature; import jdk.vm.ci.meta.UnresolvedJavaType; -public abstract class AnalysisType extends AnalysisElement implements WrappedJavaType, OriginalClassProvider, Comparable { +public abstract class AnalysisType extends AnalysisElement implements WrappedJavaType, OriginalClassProvider, Comparable, InterceptedFieldType { private static final AtomicReferenceFieldUpdater UNSAFE_ACCESS_FIELDS_UPDATER = // AtomicReferenceFieldUpdater.newUpdater(AnalysisType.class, Object.class, "unsafeAccessedFields"); @@ -912,21 +913,11 @@ public Object getReachableReason() { * the field on the Java type system level). For example {@link WordBase word types} have a * {@link #getJavaKind} of {@link JavaKind#Object}, but a primitive {@link #storageKind}. */ + @Override public final JavaKind getStorageKind() { return storageKind; } - /** - * Returns true if this type is part of the word type hierarchy, i.e, implements - * {@link WordBase}. - */ - public boolean isWordType() { - /* Word types are currently the only types where kind and storageKind differ. */ - boolean wordType = getJavaKind() != getStorageKind(); - assert !wordType || getJavaKind().isObject() : Assertions.errorMessage("Only words are expected to have a discrepancy between java kind and storage kind", this); - return wordType; - } - @Override public ResolvedJavaType getWrapped() { return wrapped; diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java index 9aa6d75354ded..9d303eb1b7f6b 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java @@ -61,6 +61,8 @@ import com.oracle.graal.pointsto.meta.AnalysisElement.MethodOverrideReachableNotification; import com.oracle.graal.pointsto.util.AnalysisError; import com.oracle.graal.pointsto.util.ConcurrentLightHashSet; +import com.oracle.svm.util.HeapConstantToConstantReplacer; +import com.oracle.svm.util.HeapObjectConstantReplacer; import com.oracle.svm.util.OriginalClassProvider; import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; @@ -712,13 +714,80 @@ private JavaConstant replaceConstant(JavaConstant source, boolean allowObjectToC JavaConstant destination = source; for (Function replacer : objectReplacers) { - destination = Objects.requireNonNull(replacer.apply(destination)); + if (replacer instanceof HeapObjectConstantReplacer heapObjectReplacer) { + destination = Objects.requireNonNull(heapObjectReplacer.replaceConstant(destination, true)); + } else { + destination = Objects.requireNonNull(replacer.apply(destination)); + } + assert !HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(destination); assert destination.getJavaKind() == JavaKind.Object : destination; } + return finishObjectReplacement(destination, allowObjectToConstantReplacement); + } + + /** + * Applies registered replacers without using internal operations described in + * {@link com.oracle.svm.util.HeapObjectReplacer} that expand the analysis/hosted universe or image heap. + * This supports verification of objects that are not otherwise reachable by the image heap. + * + * @return the replaced constant, {@link HeapObjectConstantReplacer#CANNOT_REPLACE_MARKER}, or + * {@code source} if no replacer changes it + */ + public JavaConstant replaceConstantNoHeap(JavaConstant source) { + assert !(source instanceof ImageHeapConstant) : "ImageHeapConstants were already processed and must not reach object replacement again: " + source; + if (source == null || source.getJavaKind() != JavaKind.Object || source.isNull()) { + return source; + } + + JavaConstant destination = source; + for (Function replacer : objectReplacers) { + if (replacer instanceof HeapObjectConstantReplacer heapObjectReplacer) { + destination = Objects.requireNonNull(heapObjectReplacer.replaceConstant(destination, false)); + if (HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(destination)) { + return destination; + } + } else { + destination = Objects.requireNonNull(replacer.apply(destination)); + } + assert destination.getJavaKind() == JavaKind.Object : destination; + } + + return finishObjectReplacement(destination, true, false); + } + + private JavaConstant finishObjectReplacement(JavaConstant destination, boolean allowObjectToConstantReplacement) { + return finishObjectReplacement(destination, allowObjectToConstantReplacement, true); + } + + /** + * Applies object-to-constant replacers. + * + *

+ * When {@code reachable} is {@code false}, then these replacers cannot + * use internal operations that expand the analysis/hosted universe or image heap, + * described in {@link HeapConstantToConstantReplacer}. + *

+ * + * @return Replacement constant, or {@link HeapConstantToConstantReplacer#CANNOT_REPLACE_MARKER}, + * if the replacer could not replace the constant without using an internal operation + * that expands the analysis/hosted universe or image heap. + */ + private JavaConstant finishObjectReplacement(JavaConstant destination, boolean allowObjectToConstantReplacement, boolean reachable) { ImageHeapConstant imageHeapConstant = null; for (Function replacer : objectToConstantSnapshotReplacers) { - ImageHeapConstant result = replacer.apply(destination); + ImageHeapConstant result; + if (replacer instanceof HeapConstantToConstantReplacer heapObjectReplacer) { + var heapResult = heapObjectReplacer.replaceConstant(destination, reachable); + if (HeapConstantToConstantReplacer.CANNOT_REPLACE_MARKER.equals(heapResult)) { + return heapResult; + } + AnalysisError.guarantee(heapResult == null || heapResult instanceof ImageHeapConstant, + "Heap object to constant replacer must always return null, ImageHeapConstant, or the marker constant: %s", heapResult); + result = (ImageHeapConstant) heapResult; + } else { + result = replacer.apply(destination); + } if (result != null) { AnalysisError.guarantee(allowObjectToConstantReplacement, "Object to constant replacement has been triggered from an unsupported location"); AnalysisError.guarantee(imageHeapConstant == null, "Multiple object to constant replacers have been triggered on a single object %s %s %s", destination, imageHeapConstant, result); diff --git a/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java b/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java index 51f4d44601c24..ba0bdd4f6eac9 100644 --- a/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java +++ b/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2020, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -102,7 +102,11 @@ public void seal() { assert methods instanceof ArrayList && compiledMethods instanceof ArrayList && files instanceof ArrayList && indexedFiles == null && dirs instanceof ArrayList && indexedDirs == null : "ClassEntry should only be sealed once"; - methods = List.copyOf(methods); + methods = methods.stream() + .sorted(Comparator.comparing(MethodEntry::isDeopt) + .thenComparing(MethodEntry::getMethodName) + .thenComparing(MethodEntry::getSymbolName)) + .toList(); methods.forEach(MethodEntry::seal); compiledMethods = compiledMethods.stream().sorted(Comparator.comparing(CompiledMethodEntry::primary)).toList(); diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java index edcb51c9013ca..face06a9c33f4 100644 --- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java +++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java @@ -31,6 +31,7 @@ import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.c.function.CFunctionPointer; import org.graalvm.nativeimage.hosted.FieldValueTransformer; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; import com.oracle.svm.core.annotate.Inject; import com.oracle.svm.core.annotate.RecomputeFieldValue; @@ -128,7 +129,7 @@ public Object transform(Object receiver, Object originalValue) { } @Platforms(Platform.HOSTED_ONLY.class) - static final class DowncallAddressTransformer implements FieldValueTransformer { + static final class DowncallAddressTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse { private final ForeignFunctionsRuntime foreignFunctionsRuntime = ForeignFunctionsRuntime.singleton(); @Override @@ -155,7 +156,7 @@ static UserException rethrowMissingForeignRegistrationError(MissingForeignRegist } @Platforms(Platform.HOSTED_ONLY.class) - static final class DowncallInvokerAddressTransformer implements FieldValueTransformer { + static final class DowncallInvokerAddressTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse { private final ForeignFunctionsRuntime foreignFunctionsRuntime = ForeignFunctionsRuntime.singleton(); @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java index e0e1541b4cdc6..f67e145a7731c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -96,8 +96,9 @@ public class FutureDefaultsOptions { private static final String CLASS_FOR_NAME_RESPECTS_CLASS_LOADER = "class-for-name-respects-class-loader"; private static final String EXACT_REFLECTION = "exact-reflection"; public static final String EXPLICIT_FEATURE_SINGLETON_REGISTRATION = "explicit-feature-singleton-registration"; + public static final String PREDICTABLE_BUILD_TIME_INITIALIZATION = "predictable-build-time-initialization"; private static final List ALL_FUTURE_DEFAULTS = List.of(CLASS_FOR_NAME_RESPECTS_CLASS_LOADER, EXACT_REFLECTION, RUN_TIME_INITIALIZE_FILE_SYSTEM_PROVIDERS, - RUN_TIME_INITIALIZE_SECURITY_PROVIDERS, RUN_TIME_INITIALIZE_RESOURCE_BUNDLES, EXPLICIT_FEATURE_SINGLETON_REGISTRATION); + RUN_TIME_INITIALIZE_SECURITY_PROVIDERS, RUN_TIME_INITIALIZE_RESOURCE_BUNDLES, EXPLICIT_FEATURE_SINGLETON_REGISTRATION, PREDICTABLE_BUILD_TIME_INITIALIZATION); private static final String COMPLETE_REFLECTION_TYPES = "complete-reflection-types"; private static final List RETIRED_FUTURE_DEFAULTS = List.of(COMPLETE_REFLECTION_TYPES); @@ -281,6 +282,10 @@ public static boolean explicitFeatureSingletonRegistration() { return getFutureDefaults().contains(EXPLICIT_FEATURE_SINGLETON_REGISTRATION); } + public static boolean predictableBuildTimeInitialization() { + return getFutureDefaults().contains(PREDICTABLE_BUILD_TIME_INITIALIZATION); + } + /** Keep in sync with the static query methods from {@link #allFutureDefaults()} to the end of this class. */ @SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class) @Platforms(Platform.HOSTED_ONLY.class) @@ -323,5 +328,10 @@ public boolean exactReflection() { public boolean explicitFeatureSingletonRegistration() { return FutureDefaultsOptions.explicitFeatureSingletonRegistration(); } + + @Override + public boolean predictableBuildTimeInitialization() { + return FutureDefaultsOptions.predictableBuildTimeInitialization(); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt index d80815266a737..ceb75bda78834 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt @@ -1,4 +1,4 @@ -Enable options that are planned to become defaults in future releases. Comma-separated list can contain 'all', 'none', 'run-time-initialize-jdk', 'class-for-name-respects-class-loader', 'exact-reflection', 'run-time-initialize-file-system-providers', 'run-time-initialize-security-providers', 'run-time-initialize-resource-bundles', 'explicit-feature-singleton-registration'. The preferred usage is '--future-defaults=all'. +Enable options that are planned to become defaults in future releases. Comma-separated list can contain 'all', 'none', 'run-time-initialize-jdk', 'class-for-name-respects-class-loader', 'exact-reflection', 'run-time-initialize-file-system-providers', 'run-time-initialize-security-providers', 'run-time-initialize-resource-bundles', 'explicit-feature-singleton-registration', 'predictable-build-time-initialization'. The preferred usage is '--future-defaults=all'. The meaning of each possible option is as follows: 'all' - is the preferred option, and it enables all other behaviors. @@ -18,3 +18,5 @@ The meaning of each possible option is as follows: 'run-time-initialize-resource-bundles' - shifts away from build-time initialization for 'java.util.ResourceBundle'. Unless you store 'ResourceBundle'-related classes in the image heap, this option should not affect you. In case this option breaks your build, follow the suggestions in the error messages. 'explicit-feature-singleton-registration' - stops automatically publishing user features reached from '--features' as 'ImageSingletons'. If code needs feature-singleton lookup compatibility, the feature must register its feature object explicitly. + + 'predictable-build-time-initialization' - verifies that the classes of all objects stored in or reachable from static fields of build-time-initialized classes are also initialized at build time, even if those fields are not reachable. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java index 8693004b57564..610675939540d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java @@ -142,11 +142,23 @@ public interface DuringSetupAccess extends FeatureAccess { void registerBuildTimeBootstrapCondy(ResolvedJavaMethod method); } + /** + * JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeFieldValueAccess}. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public interface BeforeFieldValueAccess { + /** + * JVMCI-based counterpart of + * {@link org.graalvm.nativeimage.hosted.Feature.BeforeFieldValueAccess#registerFieldValueTransformer(java.lang.reflect.Field, org.graalvm.nativeimage.hosted.FieldValueTransformer)}. + */ + void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer); + } + /** * JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess}. */ @Platforms(Platform.HOSTED_ONLY.class) - public interface BeforeAnalysisAccess extends FeatureAccess { + public interface BeforeAnalysisAccess extends FeatureAccess, BeforeFieldValueAccess { /** * JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerAsUsed(Class)}. */ @@ -205,9 +217,10 @@ void registerClassInitializerReachabilityHandler(Consumer /** * JVMCI-based counterpart of - * {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerFieldValueTransformer(java.lang.reflect.Field, org.graalvm.nativeimage.hosted.FieldValueTransformer)}. + * {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class)}. */ - void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer); + void registerBeforeFieldValueAccessHandler(BiConsumer callback, + ResolvedJavaType baseType); } /** diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java index 0d711ac5cef27..23e2420e1706c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.fieldvaluetransformer; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; + import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.core.StaticFieldsSupport; import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; @@ -40,7 +42,7 @@ /** * Implements the field value transformation semantics of {@link Kind#FieldOffset}. */ -public record FieldOffsetFieldValueTransformer(ResolvedJavaField targetField, JavaKind returnKind) implements JVMCIFieldValueTransformerWithAvailability { +public record FieldOffsetFieldValueTransformer(ResolvedJavaField targetField, JavaKind returnKind) implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java index 35b919dbedabd..dbba06d4f99bc 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,6 +28,8 @@ import com.oracle.svm.core.StaticFieldsSupport; import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; + import jdk.graal.compiler.nodes.ValueNode; import jdk.graal.compiler.nodes.spi.CoreProviders; import jdk.vm.ci.meta.JavaConstant; @@ -36,7 +38,7 @@ /** * Implements the field value transformation semantics of {@link Kind#StaticFieldBase}. */ -public record StaticFieldBaseFieldValueTransformer(ResolvedJavaField targetField) implements JVMCIFieldValueTransformerWithAvailability { +public record StaticFieldBaseFieldValueTransformer(ResolvedJavaField targetField) implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java index f59e11bd1ae24..a60c0f1a42032 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,6 +51,7 @@ import com.oracle.svm.shared.singletons.traits.SingletonLayeredInstallationKind.InitialLayerOnly; import com.oracle.svm.shared.singletons.traits.SingletonTraits; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectReplacer; import jdk.graal.compiler.debug.Assertions; @@ -159,7 +160,16 @@ public void duringSetup(DuringSetupAccess access) { * In traditional builds we lazily register GCCauses as they become visible to the * native-image generator. */ - access.registerObjectReplacer(ImageSingletons.lookup(GCCauseSupport.class)::collectGCCauses); + GCCauseSupport support = ImageSingletons.lookup(GCCauseSupport.class); + access.registerObjectReplacer(new HeapObjectReplacer() { + @Override + public Object replaceObject(Object source, boolean reachable) { + if (reachable) { + support.collectGCCauses(source); + } + return source; + } + }); } else { /* * For layered builds we eagerly register all GCCauses in the initial layer. In all diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java index f6f2871bf5609..3c71dd78e574a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,6 +34,7 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.FieldValueTransformer; import com.oracle.svm.core.encoder.SymbolEncoder; import com.oracle.svm.shared.singletons.AutomaticallyRegisteredImageSingleton; @@ -67,9 +68,10 @@ public void removeServicesFromServicesCatalog(String serviceProvider, Set { + class ServiceCatalogMapFieldValueTransformer implements FieldValueTransformer { + @Override + @SuppressWarnings("unchecked") + public Object transform(Object receiver, Object original) { VMError.guarantee(sealed); ConcurrentHashMap> map = (ConcurrentHashMap>) original; final ConcurrentHashMap> res = new ConcurrentHashMap<>(); @@ -83,8 +85,19 @@ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess acce } }); return res; - }); - access.registerFieldValueTransformer(ReflectionUtil.lookupField(ModuleDescriptor.Provides.class, "providers"), (receiver, original) -> { + } + + @Override + public boolean isAvailable() { + /* Available after it is sealed, because it can be read before analysis by PBTI */ + return sealed; + } + } + + class ModuleDescriptorProvidersFieldValueTransformer implements FieldValueTransformer { + @Override + @SuppressWarnings("unchecked") + public Object transform(Object receiver, Object original) { VMError.guarantee(sealed, "Service provider detector must be registered before the analysis starts"); List providers = (List) original; String service = ((ModuleDescriptor.Provides) receiver).service(); @@ -94,7 +107,17 @@ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess acce } SymbolEncoder encoder = SymbolEncoder.singleton(); return providers.stream().map(encoder::encodeClass).toList(); - }); + } + + @Override + public boolean isAvailable() { + return sealed; + } + } + + public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess access) { + access.registerFieldValueTransformer(ReflectionUtil.lookupField(ServicesCatalog.class, "map"), new ServiceCatalogMapFieldValueTransformer()); + access.registerFieldValueTransformer(ReflectionUtil.lookupField(ModuleDescriptor.Provides.class, "providers"), new ModuleDescriptorProvidersFieldValueTransformer()); registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "requires"); registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "exports"); registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "opens"); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java index 3b56431daf0c6..ba691c1a0c7b7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java @@ -144,7 +144,7 @@ final class Target_jdk_internal_module_ModuleReferences_JarModuleReader { @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.None, isFinal = false) JarFile jf; @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.None, isFinal = false) URI uri; - @Inject String runtimeModuleName; + @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset, isFinal = false) String runtimeModuleName; @Alias @TargetElement(name = "newJarFile") diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java index c203b8b38cd4d..e76e9c45d9c4d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,7 @@ package com.oracle.svm.core.jdk; import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.core.StaticFieldsSupport; @@ -109,7 +110,7 @@ class VarHandleFieldOffsetAsLongComputer extends VarHandleFieldOffsetComputer { } } -class VarHandleStaticBaseComputer implements JVMCIFieldValueTransformerWithAvailability { +class VarHandleStaticBaseComputer implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java index 351319617b521..c9ee13b0094f6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,41 +26,58 @@ import java.util.Collections; import java.util.IdentityHashMap; -import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.util.HeapObjectReplacer; + @Platforms(Platform.HOSTED_ONLY.class) -public final class ClassInstanceReplacer implements Function { +public final class ClassInstanceReplacer extends HeapObjectReplacer { private final Class sourceClass; - private final List imageHeapList; private final Function createReplacement; + private final Consumer objectReachableCallback; private final Map replacements = Collections.synchronizedMap(new IdentityHashMap<>()); + private final Map reachableEntries = Collections.synchronizedMap(new IdentityHashMap<>()); - public ClassInstanceReplacer(Class sourceClass, List imageHeapList, Function createReplacement) { + public ClassInstanceReplacer(Class sourceClass, Function createReplacement, Consumer objectReachableCallback) { this.sourceClass = sourceClass; - this.imageHeapList = imageHeapList; this.createReplacement = createReplacement; + this.objectReachableCallback = objectReachableCallback; } + /** + * Create a replacement for the source object, or access it from a cache. + */ @Override - public Object apply(Object object) { + public Object replaceObject(Object object, boolean reachable) { if (object == null || object.getClass() != sourceClass) { return object; } - return replacements.computeIfAbsent(sourceClass.cast(object), this::doReplace); - } - - private T doReplace(S object) { - T replacement = createReplacement.apply(object); - assert replacement.getClass() != sourceClass : "leads to recursive replacement"; - if (imageHeapList != null) { - imageHeapList.add(replacement); + S source = sourceClass.cast(object); + T replacement = getOrCreateReplacement(source); + if (reachable) { + onObjectReachable(replacement); } return replacement; } + + public void onObjectReachable(T replacement) { + reachableEntries.computeIfAbsent(replacement, _ -> { + objectReachableCallback.accept(replacement); + return true; + }); + } + + public T getOrCreateReplacement(S object) { + return replacements.computeIfAbsent(object, source -> { + T replacement = createReplacement.apply(source); + assert replacement.getClass() != sourceClass : "leads to recursive replacement"; + return replacement; + }); + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/VMLockSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/VMLockSupport.java index 89070af13cb5f..169f0532087ba 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/VMLockSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/VMLockSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -74,11 +74,11 @@ public static VMLockSupport singleton() { @Platforms(Platform.HOSTED_ONLY.class) public void registerObjectReplacers(InternalFeature.DuringSetupAccess access) { - mutexReplacer = new ClassInstanceReplacer<>(VMMutex.class, mutexes, this::replaceVMMutex); + mutexReplacer = new ClassInstanceReplacer<>(VMMutex.class, this::replaceVMMutex, mutexes::add); access.registerObjectReplacer(mutexReplacer); - access.registerObjectReplacer(new ClassInstanceReplacer<>(VMCondition.class, conditions, this::replaceVMCondition)); - access.registerObjectReplacer(new ClassInstanceReplacer<>(VMSemaphore.class, semaphores, this::replaceSemaphore)); + access.registerObjectReplacer(new ClassInstanceReplacer<>(VMCondition.class, this::replaceVMCondition, this::onVMConditionReachable)); + access.registerObjectReplacer(new ClassInstanceReplacer<>(VMSemaphore.class, this::replaceSemaphore, semaphores::add)); } @Platforms(Platform.HOSTED_ONLY.class) @@ -88,7 +88,7 @@ private VMMutex replaceVMMutex(VMMutex source) { @Platforms(Platform.HOSTED_ONLY.class) private VMCondition replaceVMCondition(VMCondition source) { - return new RuntimeVMCondition((RuntimeVMMutex) mutexReplacer.apply(source.getMutex()), source.getName()); + return new RuntimeVMCondition((RuntimeVMMutex) mutexReplacer.getOrCreateReplacement(source.getMutex()), source.getName()); } @Platforms(Platform.HOSTED_ONLY.class) @@ -96,6 +96,12 @@ private VMSemaphore replaceSemaphore(VMSemaphore source) { return new RuntimeVMSemaphore(source.getName()); } + @Platforms(Platform.HOSTED_ONLY.class) + private void onVMConditionReachable(VMCondition condition) { + conditions.add(condition); + mutexReplacer.onObjectReachable(condition.getMutex()); + } + /** * Initializes all {@link VMMutex}, {@link VMCondition}, and {@link VMSemaphore} objects. * diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ExecutableAccessorComputer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ExecutableAccessorComputer.java index ed2846eb8a7ae..14c8cdb88f647 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ExecutableAccessorComputer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ExecutableAccessorComputer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,8 @@ import java.lang.reflect.Executable; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; + import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.reflect.SubstrateAccessor; import com.oracle.svm.util.GuestAccess; @@ -39,7 +41,7 @@ * * @see RecomputeFieldValue */ -public final class ExecutableAccessorComputer implements JVMCIFieldValueTransformer { +public final class ExecutableAccessorComputer implements JVMCIFieldValueTransformer, FieldValueTransformerRequiresUniverse { @Override public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) { GuestAccess access = GuestAccess.get(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionFieldValueComputers.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionFieldValueComputers.java index 6a3d4b619df8e..9f5a6a3761e48 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionFieldValueComputers.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionFieldValueComputers.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.reflect.target; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; + import com.oracle.svm.core.fieldvaluetransformer.JVMCIFieldValueTransformerWithAvailability; import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; import com.oracle.svm.shared.BuildPhaseProvider; @@ -41,7 +43,7 @@ public final class ReflectionFieldValueComputers { private ReflectionFieldValueComputers() { } - public static final class FieldDeletionReasonComputer implements JVMCIFieldValueTransformerWithAvailability { + public static final class FieldDeletionReasonComputer implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { return BuildPhaseProvider.isHostedUniverseBuilt(); @@ -55,7 +57,7 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) } } - public static final class FieldOffsetComputer implements JVMCIFieldValueTransformerWithAvailability { + public static final class FieldOffsetComputer implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { return BuildPhaseProvider.isHostedUniverseBuilt(); @@ -68,7 +70,7 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) } } - public static final class LayerNumberComputer implements JVMCIFieldValueTransformerWithAvailability { + public static final class LayerNumberComputer implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { @Override public boolean isAvailable() { return BuildPhaseProvider.isHostedUniverseBuilt(); diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/FieldsOffsetsFeature.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/FieldsOffsetsFeature.java index 6bbbd2b6690ea..f2b41970490cb 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/FieldsOffsetsFeature.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/FieldsOffsetsFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.graal.pointsto.meta.AnalysisType; @@ -70,7 +71,7 @@ public class FieldsOffsetsFeature implements Feature { /** Recomputes the {@code Fields.offsets} value after the image field layout is available. */ - private static final class OffsetsRecomputation implements FieldValueTransformerWithAvailability { + private static final class OffsetsRecomputation implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { // JVMCI migration blocked by GR-72589: Migrate GraalCompilerFeature to terminus @Override public boolean isAvailable() { @@ -88,7 +89,7 @@ public Object transform(Object receiver, Object originalValue) { } } - private static final class IterationMaskRecomputation implements FieldValueTransformerWithAvailability { + private static final class IterationMaskRecomputation implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { // JVMCI migration blocked by GR-72589: Migrate GraalCompilerFeature to terminus @Override public boolean isAvailable() { diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/GraalGraphObjectReplacer.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/GraalGraphObjectReplacer.java index 31e8777c7a301..ae1f59e5314ac 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/GraalGraphObjectReplacer.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/GraalGraphObjectReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; import org.graalvm.nativeimage.c.function.RelocatedPointer; import org.graalvm.nativeimage.hosted.Feature.BeforeHeapLayoutAccess; @@ -71,6 +70,7 @@ import com.oracle.svm.shared.util.LogUtils; import com.oracle.svm.shared.util.ReflectionUtil; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectReplacer; import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.OriginalFieldProvider; import com.oracle.svm.util.OriginalMethodProvider; @@ -105,7 +105,7 @@ * * It is mainly used to replace the Hosted* meta data with the Substrate* meta data. */ -public class GraalGraphObjectReplacer implements Function { +public class GraalGraphObjectReplacer extends HeapObjectReplacer { public static class Options { @Option(help = "Ensure all created SubstrateTypes are linked")// public static final HostedOptionKey GuaranteeSubstrateTypesLinked = new HostedOptionKey<>(false); @@ -149,9 +149,18 @@ public void setAnalysisAccess(BeforeAnalysisAccessImpl beforeAnalysisAccess) { this.beforeAnalysisAccess = beforeAnalysisAccess; } + /** + * Replaces Graal related objects during analysis in the universe. + * It is mainly used to replace the Hosted* meta data with the Substrate* meta data. + * + * @param source Source object to be replaced + * @param reachable If true, then the universe can be affected (new type created, image heap constant scan, etc.), + * otherwise the universe is untouched. + * @return Replacement object + */ @SuppressWarnings("unchecked") @Override - public Object apply(Object source) { + public Object replaceObject(Object source, boolean reachable) { if (source == null) { return null; @@ -170,13 +179,13 @@ public Object apply(Object source) { if (source instanceof MetaAccessProvider) { dest = sProviders.getMetaAccessProvider(); } else if (source instanceof JVMCIRuntime) { - throw new UnsupportedFeatureException("JVMCIRuntime should not appear in the image: " + source); + return unsupportedFeature(reachable, "JVMCIRuntime should not appear in the image: " + source); } else if (source instanceof GraalHotSpotVMConfig) { - throw new UnsupportedFeatureException("GraalHotSpotVMConfig should not appear in the image: " + source); + return unsupportedFeature(reachable, "GraalHotSpotVMConfig should not appear in the image: " + source); } else if (source instanceof HotSpotBackendFactory factory) { Architecture hostArch = HotSpotJVMCIRuntime.runtime().getHostJVMCIBackend().getTarget().arch; if (!factory.getArchitecture().equals(hostArch.getName())) { - throw new UnsupportedFeatureException("Non-host architecture HotSpotBackendFactory should not appear in the image: " + source); + return unsupportedFeature(reachable, "Non-host architecture HotSpotBackendFactory should not appear in the image: " + source); } } else if (source instanceof GraalRuntime) { dest = sGraalRuntime; @@ -195,27 +204,39 @@ public Object apply(Object source) { } else if (source instanceof ResolvedJavaMethod) { if (source instanceof OriginalMethodProvider) { - dest = createMethod((ResolvedJavaMethod) source); + if (reachable) { + dest = createMethod((ResolvedJavaMethod) source); + } else { + return CANNOT_REPLACE_MARKER; + } } else if (source instanceof SubstrateMetadata) { // do nothing, metadata needed by the compiler at runtime } else if (!(source instanceof SharedRuntimeMethod)) { - throw new UnsupportedFeatureException("ResolvedJavaMethod that is not a subtype of SharedRuntimeMethod is not allowed at runtime, method=" + source); + return unsupportedFeature(reachable, "ResolvedJavaMethod that is not a subtype of SharedRuntimeMethod is not allowed at runtime, method=" + source); } } else if (source instanceof ResolvedJavaField) { if (source instanceof OriginalFieldProvider) { - dest = createField((ResolvedJavaField) source); + if (reachable) { + dest = createField((ResolvedJavaField) source); + } else { + return CANNOT_REPLACE_MARKER; + } } else if (source instanceof SubstrateMetadata) { // do nothing, metadata needed by the compiler at runtime } else if (!(source instanceof SubstrateField)) { - throw new UnsupportedFeatureException("ResolvedJavaField that is not a subtype of SubstrateField is not allowed at runtime, method=" + source); + return unsupportedFeature(reachable, "ResolvedJavaField that is not a subtype of SubstrateField is not allowed at runtime, method=" + source); } } else if (source instanceof ResolvedJavaType) { if (source instanceof OriginalClassProvider) { - dest = createType((ResolvedJavaType) source); + if (reachable) { + dest = createType((ResolvedJavaType) source); + } else { + return CANNOT_REPLACE_MARKER; + } } else if (source instanceof SubstrateMetadata) { // do nothing, metadata needed by the compiler at runtime } else if (!(source instanceof SubstrateType)) { - throw new UnsupportedFeatureException("ResolvedJavaType that is not a subtype of SubstrateType is not allowed at runtime, method=" + source); + return unsupportedFeature(reachable, "ResolvedJavaType that is not a subtype of SubstrateType is not allowed at runtime, method=" + source); } } else if (source instanceof Signature) { if (source instanceof ResolvedSignature) { @@ -223,7 +244,7 @@ public Object apply(Object source) { } else if (source instanceof SubstrateMetadata) { // do nothing, metadata needed by the compiler at runtime } else if (!(source instanceof SubstrateSignature)) { - throw new UnsupportedFeatureException(source.toString()); + return unsupportedFeature(reachable, source.toString()); } } else if (source instanceof JavaConstant cst) { if (source instanceof ImageHeapConstant heapConstant) { @@ -231,26 +252,40 @@ public Object apply(Object source) { } else if (source instanceof SubstrateMetadata) { // do nothing, the interpreter runtime needs this meta data at runtime } else if (cst.getJavaKind().isObject() && !cst.isDefaultForKind() && !(cst instanceof SubstrateObjectConstant)) { - throw new UnsupportedFeatureException("Unknown subtype of JavaConstant for inclusion in runtime heap " + source); + return unsupportedFeature(reachable, "Unknown subtype of JavaConstant for inclusion in runtime heap " + source); } } else if (source instanceof FieldLocationIdentity && !(source instanceof SubstrateFieldLocationIdentity)) { - dest = createFieldLocationIdentity((FieldLocationIdentity) source); + if (reachable) { + dest = createFieldLocationIdentity((FieldLocationIdentity) source); + } else { + return CANNOT_REPLACE_MARKER; + } } assert dest != null; Class destClass = dest.getClass(); if (jvmciCleanerClass.isAssignableFrom(destClass)) { - throw new UnsupportedFeatureException(jvmciCleanerClass.getName() + " objects should not appear in the image: " + source); + return unsupportedFeature(reachable, jvmciCleanerClass.getName() + " objects should not appear in the image: " + source); } String className = destClass.getName(); - assert !className.contains(".hotspot.") || className.contains(".svm.jtt.hotspot.") : "HotSpot object in image " + className; - assert !className.contains(".graal.reachability") : "Analysis meta object in image " + className; - assert !className.contains(".pointsto.meta.") : "Analysis meta object in image " + className; - assert !className.contains(".hosted.meta.") : "Hosted meta object in image " + className; + + if (reachable) { + assert !className.contains(".hotspot.") || className.contains(".svm.jtt.hotspot.") : "HotSpot object in image " + className; + assert !className.contains(".graal.reachability") : "Analysis meta object in image " + className; + assert !className.contains(".pointsto.meta.") : "Analysis meta object in image " + className; + assert !className.contains(".hosted.meta.") : "Hosted meta object in image " + className; + } return dest; } + private static Object unsupportedFeature(boolean reachable, String message) { + if (reachable) { + throw new UnsupportedFeatureException(message); + } + return CANNOT_REPLACE_MARKER; + } + public synchronized SubstrateMethod createMethod(ResolvedJavaMethod original) { assert !(original instanceof SubstrateMethod) : original; diff --git a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompilationFeature.java b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompilationFeature.java index 5829ed9809364..ba0bcaa6073b7 100644 --- a/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompilationFeature.java +++ b/substratevm/src/com.oracle.svm.graal/src/com/oracle/svm/graal/hosted/runtimecompilation/RuntimeCompilationFeature.java @@ -535,7 +535,7 @@ public void beforeAnalysis(BeforeAnalysisAccess c) { * replacer, to avoid corner cases later when writing the native image. */ for (ResolvedJavaMethod method : replacements.getSnippetMethods()) { - objectReplacer.apply(method); + objectReplacer.replaceObject(method, true); } /* diff --git a/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/reflect/target/Target_java_lang_reflect_Field.java b/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/reflect/target/Target_java_lang_reflect_Field.java index 9fcf94fac98d7..575b52737246c 100644 --- a/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/reflect/target/Target_java_lang_reflect_Field.java +++ b/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/reflect/target/Target_java_lang_reflect_Field.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureHandler.java b/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureHandler.java index 8ff2bfdc6a4c0..48ed5c3e2e282 100644 --- a/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureHandler.java +++ b/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureHandler.java @@ -152,6 +152,11 @@ public static void invokeSubtypeReachabilityHandler(BiConsumer> callback, Feature.BeforeFieldValueAccess beforeFieldValueAccess, Class type) { + callback.accept(beforeFieldValueAccess, type); + } + @BuilderInvoked public void afterAnalysisForEachFeature(Feature.QueryReachabilityAccess queryReachabilityAccess) { GuestFeatureImpl.AfterAnalysisAccessImpl access = new GuestFeatureImpl.AfterAnalysisAccessImpl(queryReachabilityAccess); diff --git a/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureImpl.java b/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureImpl.java index 9b7dc8264fcdd..064ccac2929ef 100644 --- a/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureImpl.java +++ b/substratevm/src/com.oracle.svm.guest/src/com/oracle/svm/guest/hosted/GuestFeatureImpl.java @@ -277,6 +277,11 @@ public void registerFieldValueTransformer(Field field, FieldValueTransformer tra // A guest wrapper backed by a host proxy is required because the field transformer is a guest callback. beforeAnalysisAccess.registerFieldValueTransformer(field, transformer); } + + @Override + public void registerBeforeFieldValueAccessHandler(BiConsumer> callback, Class subclass) { + beforeAnalysisAccess.registerBeforeFieldValueAccessHandler(callback, subclass); + } } public static class DuringAnalysisAccessImpl extends BeforeAnalysisAccessImpl implements Feature.DuringAnalysisAccess { diff --git a/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyJavaTypeGetNameUsage.java b/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyJavaTypeGetNameUsage.java index 16e48952af3d8..eedc735977bd6 100644 --- a/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyJavaTypeGetNameUsage.java +++ b/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyJavaTypeGetNameUsage.java @@ -130,7 +130,8 @@ public interface Provider { "com.oracle.svm.truffle.tck.AbstractMethodListParser$SignaturePredicate", "com.oracle.svm.util.JVMCIReflectionUtil", "com.oracle.svm.interpreter.metadata.CremaResolvedJavaMethodImpl", - "com.oracle.svm.interpreter.metadata.CremaResolvedJavaMethodImpl$CremaResolvedNativeJavaMethod"); + "com.oracle.svm.interpreter.metadata.CremaResolvedJavaMethodImpl$CremaResolvedNativeJavaMethod", + "com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationType"); protected VerifyJavaTypeGetNameUsage(Set additionalExcludes) { this.exclude = Stream.concat(DEFAULT_EXCLUDE.stream(), additionalExcludes.stream()).collect(Collectors.toUnmodifiableSet()); diff --git a/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyReflectionUsage.java b/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyReflectionUsage.java index b774f6b951c64..6fd09791f1111 100644 --- a/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyReflectionUsage.java +++ b/substratevm/src/com.oracle.svm.hosted.test/src/com/oracle/svm/hosted/test/VerifyReflectionUsage.java @@ -253,6 +253,7 @@ public interface Provider { clazz("com.oracle.svm.hosted.jdk.localization.CharsetSubstitutionsFeature"), clazz("com.oracle.svm.hosted.jdk.localization.LocalizationFeature"), clazz("com.oracle.svm.hosted.jdk.VarHandleFeature"), + clazz("com.oracle.svm.hosted.jfr.JfrEventFeature"), clazz("com.oracle.svm.hosted.jfr.JfrEventSubstitution"), clazz("com.oracle.svm.hosted.jni.JNIAccessFeature"), clazz("com.oracle.svm.hosted.jni.JNIAccessFeature$JNIRuntimeAccessibilitySupportImpl"), diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderFeature.java index d8039af5cf54f..9867d7c73a605 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,10 +26,11 @@ import java.util.concurrent.ConcurrentHashMap; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; + import com.oracle.graal.pointsto.ObjectScanner.OtherReason; import com.oracle.graal.pointsto.ObjectScanner.ScanReason; import com.oracle.graal.pointsto.heap.ImageHeapConstant; -import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.fieldvaluetransformer.FieldValueTransformerWithAvailability; import com.oracle.svm.core.fieldvaluetransformer.JavaConstantWrapper; @@ -37,9 +38,11 @@ import com.oracle.svm.core.jdk.ResourceLoaderKeys; import com.oracle.svm.hosted.imagelayer.CrossLayerConstantRegistry; import com.oracle.svm.hosted.jdk.HostedClassLoaderPackageManagement; +import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.shared.util.VMError; import com.oracle.svm.util.GuestAccess; +import com.oracle.svm.util.HeapConstantToConstantReplacer; import com.oracle.svm.util.JVMCIFieldValueTransformer; import com.oracle.svm.util.JVMCIReflectionUtil; @@ -87,8 +90,11 @@ private Object runtimeClassLoaderObjectReplacer(Object replaceCandidate) { return replaceCandidate; } - JavaConstant replaceClassLoadersWithLayerConstant(CrossLayerConstantRegistry registry, Object object) { + JavaConstant replaceClassLoadersWithLayerConstant(CrossLayerConstantRegistry registry, Object object, boolean reachable) { if (object instanceof ClassLoader loader) { + if (!reachable) { + return HeapConstantToConstantReplacer.CANNOT_REPLACE_MARKER; + } if (replaceWithAppClassLoader(loader) || loader == appClassLoader) { return registry.getConstant(ResourceLoaderKeys.APP); } else if (loader == platformClassLoader) { @@ -130,12 +136,21 @@ public void duringSetup(DuringSetupAccess access) { } } else { var hostedValuesProvider = accessImpl.getUniverse().getHostedValuesProvider(); - // JVMCI migration blocked by GR-72593: Migrate ClassLoaderFeature to terminus - accessImpl.registerJVMCIObjectToConstantReplacer(constant -> (ImageHeapConstant) replaceClassLoadersWithLayerConstant(registry, - hostedValuesProvider.asObject(Object.class, constant))); + accessImpl.registerJVMCIObjectToConstantReplacer(new HeapConstantToConstantReplacer() { + @Override + public JavaConstant replaceConstant(JavaConstant constant, boolean reachable) { + Object object = hostedValuesProvider.asObject(Object.class, constant); + return replaceClassLoadersWithLayerConstant(registry, object, reachable); + } + }); // relink packages defined in the prior layers - accessImpl.registerJVMCIObjectToConstantReplacer(constant -> packageManager.replaceWithPriorLayerPackage( - hostedValuesProvider.asObject(Object.class, constant))); + accessImpl.registerJVMCIObjectToConstantReplacer(new HeapConstantToConstantReplacer() { + @Override + public JavaConstant replaceConstant(JavaConstant constant, boolean reachable) { + Object object = hostedValuesProvider.asObject(Object.class, constant); + return packageManager.replaceWithPriorLayerPackage(object, reachable); + } + }); } } @@ -230,7 +245,7 @@ public Object transform(Object receiver, Object originalValue) { } } - static class ExtensionLayerPackageMapTransformer implements JVMCIFieldValueTransformer { + static class ExtensionLayerPackageMapTransformer implements JVMCIFieldValueTransformer, FieldValueTransformerRequiresUniverse { @Override public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/FeatureImpl.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/FeatureImpl.java index e1f363f16b4c6..d91c019b5aecc 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/FeatureImpl.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/FeatureImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -77,6 +77,7 @@ import com.oracle.graal.pointsto.meta.ObjectReachableCallback; import com.oracle.graal.pointsto.util.AnalysisError; import com.oracle.svm.common.meta.MethodVariant; +import com.oracle.svm.core.FutureDefaultsOptions; import com.oracle.svm.core.LinkerInvocation; import com.oracle.svm.core.annotate.Delete; import com.oracle.svm.core.feature.InternalFeature; @@ -107,11 +108,13 @@ import com.oracle.svm.hosted.meta.HostedUniverse; import com.oracle.svm.hosted.option.HostedOptionProvider; import com.oracle.svm.hosted.reflect.ReflectionDataBuilder; +import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.shared.util.ReflectionUtil; import com.oracle.svm.shared.util.VMError; import com.oracle.svm.util.GuestAccess; import com.oracle.svm.util.GuestAnnotationAccess; import com.oracle.svm.util.GuestInvoked; +import com.oracle.svm.util.HeapObjectReplacer; import com.oracle.svm.util.JVMCIFieldValueTransformer; import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.OriginalFieldProvider; @@ -533,7 +536,11 @@ public DuringSetupAccessImpl(FeatureHandler featureHandler, ImageClassLoader ima @Override public void registerObjectReplacer(Function replacer) { // GR-78997: migrate builder-side clients to JVMCI or guest features. - getUniverse().registerObjectReplacer(new LegacyObjectReplacerAdapter(replacer, getUniverse().getHostedValuesProvider())); + if (replacer instanceof HeapObjectReplacer heapObjectReplacer) { + getUniverse().registerObjectReplacer(new HeapObjectReplacerAdapter(heapObjectReplacer, getUniverse().getHostedValuesProvider())); + } else { + getUniverse().registerObjectReplacer(new LegacyObjectReplacerAdapter(replacer, getUniverse().getHostedValuesProvider())); + } } /** @@ -929,14 +936,22 @@ public void registerClassInitializerReachabilityHandler(Consumeroriginal (Host VM) field. See * {@link OriginalFieldProvider#getOriginalField}. @@ -944,6 +959,7 @@ public void registerFieldValueTransformer(Field field, FieldValueTransformer tra */ @Override public void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer) { + checkFieldValueTransformerRegistrationAllowed(); VMError.guarantee(!(field instanceof OriginalFieldProvider), "The ResolvedJavaField %s must be the original (Host VM) field. You can use OriginalFieldProvider.getOriginalField() to retrieve that", field); FieldValueInterceptionSupport.singleton().registerFieldValueTransformer(field, transformer); @@ -1100,15 +1116,27 @@ public void registerClassInitializerReachabilityHandler(JavaConstant callback, R * forward from an initial layer and updated by an extension layer. Unlike * {@link #registerFieldValueTransformer(Field, FieldValueTransformer)}, the transformer is * represented as an image-layer-aware JVMCI transformer so that updatable prior-layer field - * values can be tracked and patched when a later layer supplies a replacement value. + * values can be tracked and patched when a later layer supplies a replacement value. Under + * {@code --future-defaults=predictable-build-time-initialization}, registration of + * transformers will only be possible before analysis starts. * * @param field the field whose value should be transformed * @param transformer the layered transformer instance to apply to the field */ public void registerLayeredFieldValueTransformer(Field field, LayeredFieldValueTransformer transformer) { + checkFieldValueTransformerRegistrationAllowed(); FieldValueInterceptionSupport.singleton().registerLayeredFieldValueTransformer(getMetaAccess().lookupJavaField(field), transformer); } + private static void checkFieldValueTransformerRegistrationAllowed() { + if (FutureDefaultsOptions.predictableBuildTimeInitialization() && BuildPhaseProvider.isAnalysisStarted()) { + throw UserError.abort( + "Field value transformers must be registered before analysis" + + "or in a beforeFieldValueAccess callback when " + + "--future-defaults=predictable-build-time-initialization is enabled."); + } + } + /** * Registers a method as having an analysis-opaque return value. This designation limits the * type-flow analysis performed on the method's return value. @@ -1141,6 +1169,43 @@ public void allowStableFieldFoldingBeforeAnalysis(Field field) { public void allowStableFieldFoldingBeforeAnalysis(ResolvedJavaField field) { getHostVM().allowStableFieldFoldingBeforeAnalysis(field instanceof AnalysisField analysisField ? analysisField : getUniverse().lookup(field)); } + + /** + * Guest-invoked method for the public before field value access handler registration method. + * + * @param callback a {@link JavaConstant} representing the guest + * {@code BiConsumer>} + * @param baseType a JVMCI type representing the guest {@code Class} whose field value access triggers the callback + * @see org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) + */ + @GuestInvoked + public void registerBeforeFieldValueAccessHandler(JavaConstant callback, ResolvedJavaType baseType) { + if (callback == null || callback.isNull()) { + throw UserError.abort("'registerBeforeFieldValueAccessHandler' called with a null callback"); + } + BiConsumer wrappedCallback = (access, type) -> { + GuestFeatureDispatchFeature dispatch = ImageSingletons.lookup(GuestFeatureDispatchFeature.class); + dispatch.invokeGuestBeforeFieldValueAccessHandler(callback, (Feature.BeforeFieldValueAccess) access, type); + }; + registerBeforeFieldValueAccessHandler(wrappedCallback, baseType); + } + + /** + * JVMCI-based counterpart of + * {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class)}. + */ + @Override + public void registerBeforeFieldValueAccessHandler(BiConsumer callback, ResolvedJavaType baseType) { + FieldValueInterceptionSupport.singleton().registerBeforeFieldValueAccessHandler(callback, baseType); + } + + @Override + public void registerBeforeFieldValueAccessHandler(BiConsumer> callback, Class baseClass) { + AnalysisType baseType = getMetaAccess().lookupJavaType(baseClass); + FieldValueInterceptionSupport.singleton().registerBeforeFieldValueAccessHandler((access, type) -> { + callback.accept((Feature.BeforeFieldValueAccess) access, OriginalClassProvider.getJavaClass(type)); + }, baseType); + } } public static class DuringAnalysisAccessImpl extends BeforeAnalysisAccessImpl implements Feature.DuringAnalysisAccess, JVMCIFeatureAccess.DuringAnalysisAccess { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/GuestFeatureDispatchFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/GuestFeatureDispatchFeature.java index 9c9e7d6e46104..3650683d6fe94 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/GuestFeatureDispatchFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/GuestFeatureDispatchFeature.java @@ -79,6 +79,7 @@ final class GuestFeatureDispatchFeature implements InternalFeature { private ResolvedJavaType resourceAccessType; private ResolvedJavaType jniAccessType; private ResolvedJavaType foreignAccessType; + private ResolvedJavaType beforeFieldValueAccessType; private ResolvedJavaType beforeAnalysisAccessType; private ResolvedJavaType duringSetupAccessType; private ResolvedJavaType duringAnalysisAccessType; @@ -89,6 +90,7 @@ final class GuestFeatureDispatchFeature implements InternalFeature { private ResolvedJavaMethod invokeReachabilityHandler; private ResolvedJavaMethod invokeMethodOverrideReachabilityHandler; private ResolvedJavaMethod invokeSubtypeReachabilityHandler; + private ResolvedJavaMethod invokeBeforeFieldValueAccessHandler; private ResolvedJavaMethod afterRegistrationForEachFeature; private ResolvedJavaMethod duringSetupForEachFeature; private ResolvedJavaMethod beforeAnalysisForEachFeature; @@ -161,6 +163,7 @@ private void initializeGuestFeatureHandler(ImageClassLoader loader) { ResolvedJavaType biConsumerType = guestAccess.lookupType(BiConsumer.class); ResolvedJavaType executableType = guestAccess.lookupType(Executable.class); ResolvedJavaType classType = guestAccess.lookupType(Class.class); + beforeFieldValueAccessType = guestAccess.lookupType(Feature.BeforeFieldValueAccess.class); beforeAnalysisAccessType = guestAccess.lookupType(Feature.BeforeAnalysisAccess.class); duringSetupAccessType = guestAccess.lookupType(Feature.DuringSetupAccess.class); duringAnalysisAccessType = guestAccess.lookupType(Feature.DuringAnalysisAccess.class); @@ -185,6 +188,8 @@ private void initializeGuestFeatureHandler(ImageClassLoader loader) { invokeMethodOverrideReachabilityHandler = JVMCIReflectionUtil.getUniqueDeclaredMethod(handlerType, "invokeMethodOverrideReachabilityHandler", biConsumerType, duringAnalysisAccessType, executableType); invokeSubtypeReachabilityHandler = JVMCIReflectionUtil.getUniqueDeclaredMethod(handlerType, "invokeSubtypeReachabilityHandler", biConsumerType, duringAnalysisAccessType, classType); + invokeBeforeFieldValueAccessHandler = JVMCIReflectionUtil.getUniqueDeclaredMethod(handlerType, "invokeBeforeFieldValueAccessHandler", biConsumerType, beforeFieldValueAccessType, + classType); afterRegistrationForEachFeature = JVMCIReflectionUtil.getUniqueDeclaredMethod(handlerType, "afterRegistrationForEachFeature", reflectiveAccessType, resourceAccessType, jniAccessType, foreignAccessType); duringSetupForEachFeature = JVMCIReflectionUtil.getUniqueDeclaredMethod(handlerType, "duringSetupForEachFeature", duringSetupAccessType); @@ -252,6 +257,19 @@ void invokeGuestSubtypeReachabilityHandler(JavaConstant callback, JVMCIFeatureAc } } + void invokeGuestBeforeFieldValueAccessHandler(JavaConstant callback, Feature.BeforeFieldValueAccess access, ResolvedJavaType type) { + JavaConstant guestClass = guestAccess.getProviders().getConstantReflection().asJavaClass(OriginalClassProvider.getOriginalType(type)); + if (guestClass == null || guestClass.isNull()) { + return; + } + JavaConstant beforeFieldValueAccess = guestAccess.createHostProxy(access, beforeFieldValueAccessType); + try { + guestAccess.invokeStatic(invokeBeforeFieldValueAccessHandler, callback, beforeFieldValueAccess, guestClass); + } catch (InvocationException invocationException) { + throw handleFeatureError(invocationException); + } + } + /** * Converts the path returned by the host-side {@link org.graalvm.nativeimage.hosted.Feature.AfterImageWriteAccess#getImagePath()} * to the string representation consumed in the guest context. The guest-side diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapObjectReplacerAdapter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapObjectReplacerAdapter.java new file mode 100644 index 0000000000000..cba627f7ce27c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapObjectReplacerAdapter.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted; + +import java.util.Objects; + +import com.oracle.graal.pointsto.heap.HostedValuesProvider; +import com.oracle.svm.util.HeapObjectConstantReplacer; +import com.oracle.svm.util.HeapObjectReplacer; + +import jdk.vm.ci.meta.JavaConstant; + +/** + * Adapts a heap object replacer to the constant-based analysis pipeline. + * Temporary solution until GR-78997: Migrate builder-side object replacers to JavaConstants or guest features + */ +final class HeapObjectReplacerAdapter extends HeapObjectConstantReplacer { + private final HeapObjectReplacer replacer; + private final HostedValuesProvider hostedValuesProvider; + + HeapObjectReplacerAdapter(HeapObjectReplacer replacer, HostedValuesProvider hostedValuesProvider) { + this.replacer = Objects.requireNonNull(replacer); + this.hostedValuesProvider = Objects.requireNonNull(hostedValuesProvider); + } + + @Override + public JavaConstant replaceConstant(JavaConstant constant, boolean reachable) { + Object object = hostedValuesProvider.asObject(Object.class, constant); + Object replacement = replacer.replaceObject(object, reachable); + if (replacement == HeapObjectReplacer.CANNOT_REPLACE_MARKER) { + return CANNOT_REPLACE_MARKER; + } + return hostedValuesProvider.forObjectReplacerResult(replacement); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ModuleLayerFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ModuleLayerFeature.java index 2543e298c2926..294baadee5659 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ModuleLayerFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ModuleLayerFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -98,6 +98,7 @@ import com.oracle.svm.shared.util.ModuleSupport; import com.oracle.svm.shared.util.ReflectionUtil; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectReplacer; import com.oracle.svm.util.HostedModuleSupport; import com.oracle.svm.util.OriginalClassProvider; @@ -167,7 +168,27 @@ public void duringSetup(DuringSetupAccess access) { * Register an object replacer that will ensure all references to hosted module instances * are replaced with the appropriate runtime module instance. */ - access.registerObjectReplacer(source -> replaceHostedModules(source, accessImpl)); + access.registerObjectReplacer(new ModuleLayerObjectReplacer(accessImpl)); + } + + private final class ModuleLayerObjectReplacer extends HeapObjectReplacer { + private final AnalysisAccessBase access; + + private ModuleLayerObjectReplacer(AnalysisAccessBase access) { + this.access = access; + } + + @Override + public Object replaceObject(Object source, boolean reachable) { + if (!reachable) { + if (source instanceof Module module) { + return CANNOT_REPLACE_MARKER; + } + return source; + } + + return replaceHostedModules(source, access); + } } private Object replaceHostedModules(Object source, AnalysisAccessBase access) { 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 861d97affcde2..02617ccf9102a 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 @@ -227,6 +227,7 @@ import com.oracle.svm.hosted.code.NativeMethodSubstitutionProcessor; import com.oracle.svm.hosted.code.RestrictHeapAccessCalleesImpl; import com.oracle.svm.hosted.code.SubstrateGraphMakerFactory; +import com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature; import com.oracle.svm.hosted.diagnostic.HostedHeapDumpHandler; import com.oracle.svm.hosted.heap.ObservableImageHeapMapProviderImpl; import com.oracle.svm.hosted.heap.SVMImageHeapScanner; @@ -782,6 +783,11 @@ protected void doRun(Map entryPoints, Resol runtimeConfiguration, nativeLibraries); featureHandler.forEachFeature(feature -> feature.afterCompilation(config)); BuildPhaseProviderImpl.markCompilationFinished(); + + // if (FutureDefaultsOptions.predictableBuildTimeInitialization()) { + /* Handle values available after compilation finishes, before the heap verifier. */ + ImageSingletons.lookup(PredictableBuildTimeInitializationFeature.class).afterCompilationFinishes(); + // } } /* Re-run shadow heap verification after compilation. */ @@ -953,6 +959,11 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De bb.getHostVM().checkWellKnownStableFieldsBeforeAnalysis(bb); ServiceCatalogSupport.singleton().seal(); bb.getHostVM().getClassInitializationSupport().sealConfiguration(); + FieldValueInterceptionSupport.singleton().sealHandlerRegistration(); + // if (FutureDefaultsOptions.predictableBuildTimeInitialization()) { + /* At this point, class initialization configuration is sealed. */ + ImageSingletons.lookup(PredictableBuildTimeInitializationFeature.class).afterBeforeAnalysis(); + // } if (ImageLayerBuildingSupport.buildingImageLayer()) { ImageSingletons.lookup(AccessImageSingletonFeature.class).processRegisteredSingletons(aUniverse); if (ImageLayerBuildingSupport.buildingSharedLayer()) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java index 87c9f9542a1e9..ae199e3e362d2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java @@ -487,6 +487,24 @@ private void registerType(AnalysisType analysisType, DynamicHub hub) { assert existing == null; } + @Override + public void beforeFieldValueAccess(ResolvedJavaType type) { + var oType = OriginalClassProvider.getOriginalType(type); + + /* + * These transformations are required to be available before field's value is + * accessed. Both beforeFieldValueAccess and onTypeReachable can be called + * simultaneously and their behavior around field value transformers has to be + * synchronized. + * + * Additionally, when the type becomes reachable, fields that + * were accessed by the initializer, need to be marked as unsafely accessed. + */ + automaticUnsafeTransformations + .getOrComputeTransformations(this, oType) + .ensureDone(); + } + @Override public void onTypeReachable(BigBang bb, AnalysisType analysisType) { if (!analysisType.isReachable()) { @@ -500,8 +518,17 @@ public void onTypeReachable(BigBang bb, AnalysisType analysisType) { /* Decide when the type should be initialized. */ classInitializationSupport.maybeInitializeAtBuildTime(analysisType); - /* Compute the automatic substitutions. */ - automaticUnsafeTransformations.computeTransformations(bb, this, OriginalClassProvider.getOriginalType(analysisType)); + /* Compute the automatic substitutions. See beforeFieldValueAccess for reference. */ + ResolvedJavaType type = OriginalClassProvider.getOriginalType(analysisType); + var pendingFieldAccesses = automaticUnsafeTransformations + .getOrComputeTransformations(this, type) + .ensureDone(); + + for (var pendingAccess : pendingFieldAccesses) { + var field = bb.getUniverse().lookup(pendingAccess.targetField()); + + bb.postTask(_ -> field.registerAsUnsafeAccessed(pendingAccess.reason())); + } } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/AnalysisConstantReflectionProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/AnalysisConstantReflectionProvider.java index 16dbc473d9652..4f71ba3effb85 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/AnalysisConstantReflectionProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/AnalysisConstantReflectionProvider.java @@ -314,6 +314,13 @@ public JavaConstant readValue(AnalysisField field, JavaConstant receiver, boolea return null; } + /* + * For TypeData to be retrieved using getOrComputeData, the declaring class must be marked as reachable. + * Previously, this was performed by fieldValueInterceptionSupport.isValueAvailable, but that is no longer + * the case. + */ + field.markDeclaringClassAsReachable(); + VMError.guarantee(receiver == null || receiver instanceof ImageHeapConstant, "Expected ImageHeapConstant, found: %s", receiver); JavaConstant value = null; if (returnSimulatedValues) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/CustomTypeFieldHandler.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/CustomTypeFieldHandler.java index d74a548263086..10a769e171d89 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/CustomTypeFieldHandler.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/CustomTypeFieldHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -88,17 +88,13 @@ private List transformTypes(AnalysisField field, List fieldValueInterceptors = new ConcurrentHashMap<>(); private final LayeredFieldValueTransformerSupport layeredSupport = HostedImageLayerBuildingSupport.buildingImageLayer() ? LayeredFieldValueTransformerSupport.singleton() : null; + /** + * Callbacks for types before their field is read. + * This is used for on-demand field value transformer registration. + */ + private final Map>> notifications = new ConcurrentHashMap<>(); + private boolean handlerRegistrationSealed; + + /** + * Map of original types that were already accessed, the future represents + * operations that have to happen before a field is accessed. + */ + private final Map> accessed = new ConcurrentHashMap<>(); + private final JVMCIFeatureAccess.BeforeFieldValueAccess access = new BeforeFieldValueAccessImpl(); + public static FieldValueInterceptionSupport singleton() { return ImageSingletons.lookup(FieldValueInterceptionSupport.class); } @@ -116,6 +139,23 @@ public FieldValueInterceptionSupport(AnnotationSubstitutionProcessor annotationS this.annotationSubstitutions = annotationSubstitutions; } + /** + * Registration of before-field-value-access handlers must be done in a {@code beforeAnalysis} callback + * at the latest. Otherwise, an exception will be thrown. + * + *

+ * These before-field-value-access handlers are a replacement for the dynamic registration of transformers in a + * reachability handler. These reachability handlers can no longer be used, because + * {@code PredictableBuildTimeInitializationFeature} would not have access to their results + * without marking the declaring class of a field as reachable. Therefore, it is also + * not desirable to allow before-field-value-access handlers to be registered dynamically, + * during analysis. + *

+ */ + public void sealHandlerRegistration() { + handlerRegistrationSealed = true; + } + /** * Returns a {@link JVMCIFieldValueTransformer} if one was already registered for the field. In * contrast to most other methods of this class, invoking this method does not prevent a future @@ -160,6 +200,10 @@ private WrappedFieldValueTransformer(JavaConstant fieldValueTransformer) { this.fieldValueTransformer = Objects.requireNonNull(fieldValueTransformer); } + public FieldValueTransformer getFieldValueTransformer() { + return GuestAccess.get().getSnippetReflection().asObject(FieldValueTransformer.class, fieldValueTransformer); + } + @Override public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) { GuestAccess access = GuestAccess.get(); @@ -247,7 +291,7 @@ public void registerLayeredFieldValueTransformer(AnalysisField aField, LayeredFi registerFieldValueTransformer(oField, OriginalClassProvider.getOriginalType(oField.getType()), layeredSupport.createTransformer(aField, transformer)); } - Object lookupFieldValueInterceptor(AnalysisField field) { + Object lookupFieldValueInterceptor(InterceptedField field) { var result = field.getFieldValueInterceptor(); if (result == null) { result = computeAndCacheFieldValueInterceptor(field); @@ -255,13 +299,18 @@ Object lookupFieldValueInterceptor(AnalysisField field) { return result == INTERCEPTOR_ACCESSED_MARKER ? null : result; } - private Object computeAndCacheFieldValueInterceptor(AnalysisField field) { + private Object computeAndCacheFieldValueInterceptor(InterceptedField field) { /* * Trigger computation of automatic substitutions. There might be an automatic substitution * for the current field, and we must register it before checking if a field value * transformer exists. */ - field.beforeFieldValueAccess(); + if (field instanceof AnalysisField aField && !FutureDefaultsOptions.predictableBuildTimeInitialization()) { + /* Preserve legacy registration from reachability handlers. */ + aField.markDeclaringClassAsReachable(); + } + + beforeFieldValueAccess(field); ResolvedJavaField oField = OriginalFieldProvider.getOriginalField(field); Object result = null; @@ -324,16 +373,70 @@ private Object computeAndCacheFieldValueInterceptor(AnalysisField field) { return result; } + /** + * Whether the field value transformer requires the analysis. + * A layered transformer cannot be installed without an AnalysisField, or + * executing the transformer requires universe elements or internal operations that expand + * the analysis/hosted universe or image heap, as described in {@link FieldValueTransformerRequiresUniverse}. + */ + public boolean requiresAnalysis(InterceptedField field) { + if (layeredSupport != null && GuestAnnotationAccess.getAnnotation(field, LayeredFieldValue.class) != null) { + /* Check for the annotation before the field value transformer can be installed. */ + return true; + } + + Object interceptor = lookupFieldValueInterceptor(field); + if (interceptor instanceof FieldValueTransformation transformation) { + interceptor = transformation.getFieldValueTransformer(); + } + + if (interceptor instanceof WrappedFieldValueTransformer wrappedFieldValueTransformer) { + interceptor = wrappedFieldValueTransformer.getFieldValueTransformer(); + } + + if (interceptor instanceof LayeredFieldValueTransformerImpl) { + return true; + } + + return interceptor instanceof FieldValueTransformerRequiresUniverse; + } + + /** + * Check if a field value is intentionally never available + * when {@link ValueNeverAvailableFieldValueTransformer} is used. + */ + public boolean isValueNeverAvailable(InterceptedField field) { + Object interceptor = lookupFieldValueInterceptor(field); + if (interceptor instanceof FieldValueTransformation transformation) { + interceptor = transformation.getFieldValueTransformer(); + } + return interceptor instanceof ValueNeverAvailableFieldValueTransformer; + } + + /** + * Returns whether the field uses a layered field value transformer. + */ + public boolean isLayeredFieldValue(InterceptedField field) { + Object interceptor = lookupFieldValueInterceptor(field); + if (interceptor instanceof FieldValueTransformation transformation) { + interceptor = transformation.getFieldValueTransformer(); + } + if (interceptor instanceof WrappedFieldValueTransformer wrappedFieldValueTransformer) { + interceptor = wrappedFieldValueTransformer.getFieldValueTransformer(); + } + return interceptor instanceof LayeredFieldValueTransformerImpl; + } + /** * Check if the value of the provided field is currently available. This does not register a * notification or retry. After this method has been called, it is not possible to install a * transformer anymore. */ - public boolean isValueAvailable(AnalysisField field, JavaConstant receiver) { + public boolean isValueAvailable(InterceptedField field, JavaConstant receiver) { return isValueAvailable(field, receiver, false); } - boolean isValueAvailable(AnalysisField field, JavaConstant receiver, boolean unknownReceiver) { + boolean isValueAvailable(InterceptedField field, JavaConstant receiver, boolean unknownReceiver) { var interceptor = lookupFieldValueInterceptor(field); if (interceptor instanceof FieldValueTransformation transformation) { var transformer = transformation.getFieldValueTransformer(); @@ -357,10 +460,16 @@ boolean isValueAvailable(AnalysisField field, JavaConstant receiver, boolean unk /** * Returns true if a field value interceptor ({@link FieldValueTransformation} or * {@link FieldValueComputer}) has been registered for this field. Unlike - * {@link #hasFieldValueTransformer(AnalysisField)}, calling this method is side effect free. + * {@link #hasFieldValueTransformer(InterceptedField)}, calling this method is side effect free. */ - public static boolean hasFieldValueInterceptor(AnalysisField field) { + public static boolean hasFieldValueInterceptor(InterceptedField field) { var interceptor = field.getFieldValueInterceptor(); + if (interceptor == null) { + /* The interceptor might not be cached on the field. */ + var oField = OriginalFieldProvider.getOriginalField(field); + interceptor = singleton().fieldValueInterceptors.get(oField); + } + if (interceptor != null && interceptor != INTERCEPTOR_ACCESSED_MARKER) { VMError.guarantee(interceptor instanceof FieldValueTransformation || interceptor instanceof FieldValueComputer); return true; @@ -372,7 +481,7 @@ public static boolean hasFieldValueInterceptor(AnalysisField field) { * Returns true if a field value transformer has been registered for this field. After this * method has been called, it is not possible to install a transformer anymore. */ - public boolean hasFieldValueTransformer(AnalysisField field) { + public boolean hasFieldValueTransformer(InterceptedField field) { return lookupFieldValueInterceptor(field) instanceof FieldValueTransformation; } @@ -411,8 +520,10 @@ public ValueNode tryIntrinsifyFieldLoad(CoreProviders providers, LoadFieldNode n return transformerWithAvailability.intrinsify(providers, receiver); } - JavaConstant readFieldValue(AnalysisField field, JavaConstant receiver) { + JavaConstant readFieldValue(InterceptedField field, JavaConstant receiver) { assert isValueAvailable(field, receiver) : field; + /* Cannot read a value that requires analysis, without an analysis field */ + assert !requiresAnalysis(field) || field instanceof AnalysisField; JavaConstant value; var interceptor = lookupFieldValueInterceptor(field); if (interceptor instanceof FieldValueTransformation transformation) { @@ -457,7 +568,7 @@ JavaConstant readFieldValue(AnalysisField field, JavaConstant receiver) { return interceptValue(field, value); } - private static JavaConstant interceptValue(AnalysisField field, JavaConstant value) { + private static JavaConstant interceptValue(InterceptedField field, JavaConstant value) { JavaConstant result = value; if (result != null) { result = filterInjectedAccessor(field, result); @@ -472,9 +583,9 @@ private static JavaConstant interceptValue(AnalysisField field, JavaConstant val * image. Ideally they should never be read, but there are corner cases where this happens. We * intercept the value and return 0 / null. */ - private static JavaConstant filterInjectedAccessor(AnalysisField field, JavaConstant value) { + private static JavaConstant filterInjectedAccessor(InterceptedField field, JavaConstant value) { if (GuestAnnotationAccess.isAnnotationPresent(field, InjectAccessors.class)) { - assert !field.isAccessed(); + assert !(field instanceof AnalysisField aField) || !aField.isAccessed(); return JavaConstant.defaultForKind(value.getJavaKind()); } return value; @@ -487,7 +598,7 @@ private static JavaConstant filterInjectedAccessor(AnalysisField field, JavaCons * {@link com.oracle.svm.hosted.phases.EarlyConstantFoldLoadFieldPlugin}, but we could still see * such a field here if user code, e.g., accesses it via reflection. */ - private static JavaConstant interceptAssertionStatus(AnalysisField field, JavaConstant value) { + private static JavaConstant interceptAssertionStatus(InterceptedField field, JavaConstant value) { Boolean enabled = ClassInitializationSupport.foldedAssertionStatus(field); if (enabled != null) { return JavaConstant.forBoolean(!enabled); @@ -499,23 +610,25 @@ private static JavaConstant interceptAssertionStatus(AnalysisField field, JavaCo * Intercept {@link Word} fields. {@link Word} values are boxed objects in the hosted world, but * primitive values in the runtime world, so the default value of {@link Word} fields is 0. */ - private static JavaConstant interceptWordField(AnalysisField field, JavaConstant value) { + private static JavaConstant interceptWordField(InterceptedField field, JavaConstant value) { if (value.getJavaKind() == JavaKind.Object && value.isNull() && field.getType().isWordType()) { return JavaConstant.forIntegerKind(SubstrateTarget.getWordKind(), 0); } return value; } - private FieldValueTransformation createLayeredFieldValueTransformation(ResolvedJavaField oField, AnalysisField aField) { - LayeredFieldValueGuestValue layeredFieldValue = LayeredFieldValueGuestValue.get(aField); + private FieldValueTransformation createLayeredFieldValueTransformation(ResolvedJavaField oField, InterceptedField field) { + LayeredFieldValueGuestValue layeredFieldValue = LayeredFieldValueGuestValue.get(oField); if (layeredFieldValue != null) { - var transformer = layeredSupport.createTransformer(aField, layeredFieldValue); + assert field instanceof AnalysisField : "Cannot create layered field value transformer without analysis field"; + + var transformer = layeredSupport.createTransformer((AnalysisField) field, layeredFieldValue); return new FieldValueTransformation(OriginalClassProvider.getOriginalType(oField.getType()), transformer); } return null; } - private static FieldValueComputer createFieldValueComputer(AnalysisField field) { + private static FieldValueComputer createFieldValueComputer(InterceptedField field) { UnknownObjectFieldGuestValue unknownObjectField = UnknownObjectFieldGuestValue.get(field); if (unknownObjectField != null) { checkMisplacedAnnotation(field.getStorageKind().isObject(), field); @@ -541,7 +654,7 @@ private static FieldValueComputer createFieldValueComputer(AnalysisField field) * developers if the annotation is misplaced, e.g. {@link UnknownObjectField} is used on a * primitive field and vice versa. */ - private static void checkMisplacedAnnotation(boolean condition, AnalysisField field) { + private static void checkMisplacedAnnotation(boolean condition, InterceptedField field) { if (!condition) { String fieldType; Class expectedAnnotationType; @@ -560,7 +673,7 @@ private static void checkMisplacedAnnotation(boolean condition, AnalysisField fi } } - private static List extractAnnotationTypes(AnalysisField field, List types, List fullyQualifiedTypes) { + private static List extractAnnotationTypes(InterceptedField field, List types, List fullyQualifiedTypes) { List annotationTypes = new ArrayList<>(types); for (String annotationTypeName : fullyQualifiedTypes) { ResolvedJavaType annotationType = GuestAccess.get().lookupType(annotationTypeName); @@ -570,11 +683,99 @@ private static List extractAnnotationTypes(AnalysisField field if (annotationTypes.isEmpty()) { /* If no types are specified, fall back to the field declared type. */ - AnalysisType fieldType = field.getType(); - UserError.guarantee(CustomTypeFieldHandler.isConcreteType(fieldType), "Illegal use of @UnknownObjectField annotation on field %s. " + + var fieldType = field.getType(); + UserError.guarantee(fieldType.isConcreteType(), "Illegal use of @UnknownObjectField annotation on field %s. " + "The field type must be concrete or the annotation must declare a concrete type.", field); - annotationTypes.add(fieldType); + /* + * CustomTypeFieldHandler.transformTypes could mix different + * InterceptedFieldType implementations, so original has to be used here. + */ + annotationTypes.add(OriginalClassProvider.getOriginalType(fieldType)); } return annotationTypes; } + + private final class BeforeFieldValueAccessImpl implements BeforeFieldValueAccess, JVMCIFeatureAccess.BeforeFieldValueAccess { + FieldValueInterceptionSupport support = FieldValueInterceptionSupport.this; + + @Override + public void registerFieldValueTransformer(Field field, FieldValueTransformer transformer) { + support.registerLegacyFieldValueTransformer(field, transformer); + } + + /** + * Guest-invoked method for on-demand field value transformer registration. + * + * @param field a JVMCI field representing the guest {@link Field} to transform + * @param transformer a {@link JavaConstant} representing the guest {@link FieldValueTransformer} + * @see BeforeFieldValueAccess#registerFieldValueTransformer(Field, FieldValueTransformer) + */ + @GuestInvoked + public void registerFieldValueTransformer(ResolvedJavaField field, JavaConstant transformer) { + registerFieldValueTransformer(field, WrappedFieldValueTransformer.create(transformer)); + } + + @Override + public void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer) { + ResolvedJavaField originalField = OriginalFieldProvider.getOriginalField(field); + VMError.guarantee(originalField != null, "Cannot register a field value transformer for synthetic field %s", field); + support.registerFieldValueTransformer(originalField, transformer); + } + } + + /** + * Register a before field value access handler to allow on-demand registration + * of field value transformers. If a subtype was already accessed before, then + * the callback is called for it. + * + * @param callback Callback for field value transformer on-demand registration + * @param baseType Base type for which the callback is called for + */ + public void registerBeforeFieldValueAccessHandler(BiConsumer callback, ResolvedJavaType baseType) { + ResolvedJavaType originalBaseType = OriginalClassProvider.getOriginalType(baseType); + + UserError.guarantee(!handlerRegistrationSealed, "Handlers for beforeFieldValueAccess must be registered before analysis."); + /* + * Registration is only expected to happen synchronously in beforeAnalysis callback and + * beforeAnalysis callbacks are also called synchronously. + */ + notifications.computeIfAbsent(originalBaseType, _ -> new ArrayList<>()).add(callback); + + /* Run the callback for already seen types */ + accessed.forEach((originalType, _) -> { + if (originalBaseType.isAssignableFrom(originalType)) { + callback.accept(access, originalType); + } + }); + } + + /** + * Register all field value transformer before a field value is accessed. + * Including callbacks from notifications and automatic transformer registration + * {@link AutomaticUnsafeTransformationSupport}. + * + * @param field Field that is being read + */ + public void beforeFieldValueAccess(InterceptedField field) { + var type = field.getDeclaringClass(); + ResolvedJavaType originalType = OriginalClassProvider.getOriginalType(type); + + Callable task = () -> { + field.beforeFieldValueAccess(); + + notifications.forEach((baseType, typeNotifications) -> { + if (!baseType.isAssignableFrom(originalType)) { + return; + } + + typeNotifications.forEach(callback -> { + callback.accept(access, originalType); + }); + }); + + return null; + }; + + accessed.computeIfAbsent(originalType, _ -> new AnalysisFuture<>(task)).ensureDone(); + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/HostedDynamicHubFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/HostedDynamicHubFeature.java index eec20b4d7f7a5..472b4c1e7ee09 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/HostedDynamicHubFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/HostedDynamicHubFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,9 @@ import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.hosted.FeatureImpl.DuringSetupAccessImpl; +import com.oracle.svm.hosted.substitute.DeletedElementException; import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.util.HeapObjectReplacer; @AutomaticallyRegisteredFeature public class HostedDynamicHubFeature implements InternalFeature { @@ -42,11 +44,29 @@ public void duringSetup(DuringSetupAccess a) { hostVM = access.getHostVM(); // Replace all instances of Class with the corresponding DynamicHub obtained from SVMHost - access.registerObjectReplacer(source -> { - if (source instanceof Class clazz) { - return hostVM.dynamicHub(metaAccess.lookupJavaType(clazz)); + access.registerObjectReplacer(new HeapObjectReplacer() { + @Override + public Object replaceObject(Object source, boolean reachable) { + if (source instanceof Class clazz) { + if (reachable) { + return hostVM.dynamicHub(metaAccess.lookupJavaType(clazz)); + } + + try { + /* Only provide hub when available for PBTI, fail before image heap. */ + var type = metaAccess.optionalLookupJavaType(clazz); + + if (type.isEmpty()) { + return CANNOT_REPLACE_MARKER; + } + + return hostVM.dynamicHub(type.get()); + } catch (DeletedElementException e) { + return CANNOT_REPLACE_MARKER; + } + } + return source; } - return source; }); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/SVMHostedValueProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/SVMHostedValueProvider.java index 61df2dbbfe33a..1d63ee21cde73 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/SVMHostedValueProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/SVMHostedValueProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,6 +36,7 @@ import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; import com.oracle.graal.pointsto.meta.AnalysisUniverse; import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.util.InterceptedField; import com.oracle.svm.core.SubstrateTarget; import com.oracle.svm.core.meta.MethodOffset; import com.oracle.svm.core.meta.MethodRef; @@ -66,6 +67,14 @@ public SVMHostedValueProvider(AnalysisMetaAccess metaAccess, AnalysisUniverse un */ @Override public ValueSupplier readFieldValue(AnalysisField field, JavaConstant receiver) { + return readFieldValue((InterceptedField) field, receiver); + } + + /** + * Read value of a field which implements the {@link InterceptedField interception interface}. + * In general, this is the {@link AnalysisField} or {@code PredictableBuildTimeInitializationField}. + */ + public ValueSupplier readFieldValue(InterceptedField field, JavaConstant receiver) { if (fieldValueInterceptionSupport.isValueAvailable(field, receiver)) { /* Materialize and return the value. */ return ValueSupplier.eagerValue(doReadValue(field, receiver)); @@ -86,7 +95,7 @@ public JavaConstant readFieldValueWithReplacement(AnalysisField field, JavaConst return replaceObject(doReadValue(field, receiver)); } - private JavaConstant doReadValue(AnalysisField field, JavaConstant receiver) { + private JavaConstant doReadValue(InterceptedField field, JavaConstant receiver) { VMError.guarantee(!(receiver instanceof SubstrateObjectConstant)); return interceptHosted(fieldValueInterceptionSupport.readFieldValue(field, receiver)); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/CGlobalDataFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/CGlobalDataFeature.java index 97ce22e607e26..c6e78e1781bd0 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/CGlobalDataFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/CGlobalDataFeature.java @@ -68,6 +68,7 @@ import com.oracle.svm.shared.singletons.traits.SingletonTraits; import com.oracle.svm.shared.util.ReflectionUtil; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectReplacer; import jdk.graal.compiler.core.common.NumUtil; import jdk.graal.compiler.core.common.memory.BarrierType; @@ -146,10 +147,26 @@ private boolean isLaidOut() { @Override public void duringSetup(DuringSetupAccess a) { - a.registerObjectReplacer(this::replaceObject); + a.registerObjectReplacer(new CGlobalDataObjectReplacer()); ImageSingletons.add(CGlobalDataLoadPolicy.class, new CGlobalDataLoadPolicy()); } + private final class CGlobalDataObjectReplacer extends HeapObjectReplacer { + @Override + public Object replaceObject(Object object, boolean reachable) { + if (object instanceof CGlobalDataImpl cglobal) { + if (appLayerCGlobalTracking != null) { + cglobal = appLayerCGlobalTracking.getCanonicalRepresentation(cglobal); + } + if (reachable) { + registerAsAccessedOrGet(cglobal, false); + } + return cglobal; + } + return object; + } + } + @Override public void beforeAnalysis(BeforeAnalysisAccess access) { if (ImageLayerBuildingSupport.buildingImageLayer()) { @@ -387,18 +404,6 @@ public Collection getCGlobalDataInfos() { return Collections.unmodifiableCollection(map.values()); } - private Object replaceObject(Object obj) { - if (obj instanceof CGlobalDataImpl cglobal) { - if (appLayerCGlobalTracking != null) { - cglobal = appLayerCGlobalTracking.getCanonicalRepresentation(cglobal); - } - registerAsAccessedOrGet(cglobal, false); - return cglobal; - } else { - return obj; - } - } - private static CGlobalDataInfo assignCGlobalDataSize(Map.Entry, CGlobalDataInfo> entry, int wordSize) { CGlobalDataImpl data = entry.getKey(); CGlobalDataInfo info = entry.getValue(); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java index 11430de800612..e17df6e306c7d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -215,7 +215,8 @@ public synchronized void sealConfiguration() { ReportUtils.report("class initialization configuration", SubstrateOptions.reportsPath(), "class_initialization_configuration", "csv", writer -> { writer.println("Class or Package Name, Initialization Kind, Reasons"); for (ClassOrPackageConfig config : allConfigs) { - writer.append(config.getName()).append(", ").append(config.getKind().toString()).append(", ").append(String.join(" and ", config.getReasons())).append(System.lineSeparator()); + writer.append(config.getName()).append(", ").append(config.getKind().toString()).append(", ") + .append(String.join(" and ", config.getReasons())).append(System.lineSeparator()); } }); } @@ -258,7 +259,7 @@ InitKind specifiedInitKindFor(ResolvedJavaType type) { * Returns the computed init kind for {@code clazz}, which can differ from the configured init * kind returned by {@link #specifiedInitKindFor(Class)}. */ - InitKind computedInitKindFor(Class clazz) { + public InitKind computedInitKindFor(Class clazz) { return classInitKinds.get(clazz); } @@ -284,8 +285,11 @@ Boolean isStrictlyDefined(ResolvedJavaType type) { /** * Returns all classes of a single {@link InitKind}. */ - Set> classesWithKind(InitKind kind) { - return classInitKinds.entrySet().stream().filter(e -> e.getValue() == kind).map(Map.Entry::getKey).collect(Collectors.toSet()); + public Set> classesWithKind(InitKind kind) { + return classInitKinds.entrySet().stream() + .filter(e -> e.getValue() == kind) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); } /** @@ -339,6 +343,15 @@ public boolean shouldInitializeAtBuildTime(ResolvedJavaType type) { return specifiedInitKindFor(OriginalClassProvider.getJavaClass(type)) == InitKind.BUILD_TIME; } + /** + * Returns {@code true} if the provided type and all of its class-initialization dependencies + * can be initialized at image build time. An unsuccessful computation is not memoized, so this + * query does not assign run-time initialization to unspecified dependencies. + */ + public boolean canInitializeAtBuildTime(ResolvedJavaType type) { + return computeInitKindAndMaybeInitializeClass(OriginalClassProvider.getJavaClass(type), false) == InitKind.BUILD_TIME; + } + /** * Ensure class is initialized. Report class initialization errors in a user-friendly way if * class initialization fails. @@ -673,7 +686,7 @@ private InitKind processInterfaces(Class clazz, boolean memoizeEager) { return result; } - String reasonForClass(Class clazz) { + public String reasonForClass(Class clazz) { InitKind initKind = classInitKinds.get(clazz); String reason = classInitializationConfiguration.lookupReason(clazz.getTypeName()); if (initKind == InitKind.RUN_TIME) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java index 7e25d7c017ff8..e3f0fc62ab934 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,7 @@ * The initialization kind for a class. The order of the enum values matters, {@link #max} depends * on it. */ -enum InitKind { +public enum InitKind { /** Class is initialized during image building, so it is already initialized at runtime. */ BUILD_TIME, /** Class should be initialized at runtime and not during image building. */ diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationError.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationError.java new file mode 100644 index 0000000000000..9563c3a219b47 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationError.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.classinitialization.pbti; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.hosted.classinitialization.ClassInitializationOptions; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature.DisallowedObjectException; +import com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature.MaterializedPath; +import com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature.Options; +import com.oracle.svm.shared.option.SubstrateOptionsParser; +import com.oracle.svm.util.InterceptedFieldType; +import com.oracle.svm.util.OriginalClassProvider; + +/** + * Exception thrown when {@link PredictableBuildTimeInitializationFeature} finds a class instance + * that is either initialized at run-time, or is disallowed by {@link com.oracle.svm.hosted.image.DisallowedImageHeapObjectFeature}. + */ +@SuppressWarnings("serial") +public class PredictableBuildTimeInitializationError extends UserError.UserException { + protected final Map> buildTimeFailures; + protected final Map> disallowedObjectFailures; + + PredictableBuildTimeInitializationError( + Map> buildTimeFailures, + Map> disallowedObjectExceptions) { + this(buildTimeFailures, disallowedObjectExceptions, null); + } + + PredictableBuildTimeInitializationError( + Map> buildTimeFailures, + Map> disallowedObjectExceptions, + Path reportPath) { + super(createMessage(buildTimeFailures, disallowedObjectExceptions, reportPath)); + this.buildTimeFailures = buildTimeFailures; + this.disallowedObjectFailures = disallowedObjectExceptions; + } + + public Map> getBuildTimeFailures() { + return buildTimeFailures; + } + + public Map> getDisallowedObjectFailures() { + return disallowedObjectFailures; + } + + private static String createMessage(Map> buildTimeFailures, + Map> disallowedObjectFailures, Path reportPath) { + StringBuilder message = new StringBuilder( + "Objects were found accessible from static fields of build-time initialized classes, " + + "but cannot be persisted safely in the image heap.") + .append(System.lineSeparator()) + .append("This is not allowed for correctness reasons: all objects accessible from static " + + "fields of build-time initialized classes must be safe to persist in the image heap.") + .append(System.lineSeparator()).append(System.lineSeparator()); + + int violationNumber = 1; + + for (var entry : buildTimeFailures.entrySet()) { + InterceptedFieldType type = entry.getKey(); + List paths = entry.getValue(); + + String typeName = type.toJavaName(); + List rootTypeNames = paths.stream().map(path -> path.getRootType().toJavaName()).distinct().toList(); + boolean typeInstantiationTraced = isTypeInitializationTraced(type); + + message + .append('[').append(violationNumber++).append("] An object of type '").append(typeName) + .append("' was found accessible from static fields of build-time initialized classes.") + .append(System.lineSeparator()) + .append(" This type, however, is marked for initialization at image run time for the following reason: ") + .append(ClassInitializationSupport.singleton().reasonForClass(OriginalClassProvider.getJavaClass(type))) + .append('.').append(System.lineSeparator()); + + message.append(System.lineSeparator()); + if (paths.size() == 1) { + MaterializedPath path = paths.getFirst(); + appendPathDetails(message, path, 4, typeInstantiationTraced); + } else { + for (int i = 0; i < paths.size(); i++) { + MaterializedPath path = paths.get(i); + message.append(" ").append(i + 1).append(". Path:").append(System.lineSeparator()); + appendPathDetails(message, path, 6, typeInstantiationTraced); + if (i + 1 < paths.size()) { + message.append(System.lineSeparator()); + } + } + } + + message.append(System.lineSeparator()) + .append(" Possible resolutions:") + .append(System.lineSeparator()).append(" 1) If it is intended that objects of type '") + .append(typeName) + .append("' are persisted in the image heap, add the following to the native-image arguments:") + .append(System.lineSeparator()).append(System.lineSeparator()).append(" ") + .append(SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, typeName, + "initialize-at-build-time", true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" Note that initializing new types can store additional objects in the heap.") + .append(System.lineSeparator()) + .append(" It is advised to check the static fields of '") + .append(typeName) + .append("' to see if they are safe for build-time initialization, and that they do not contain any " + + "sensitive data that should not become part of the image.") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" 2) The object was found through the build-time initialized ") + .append(rootTypeNames.size() == 1 ? "class '" : "classes '") + .append(String.join("', '", rootTypeNames)).append("'.") + .append(System.lineSeparator()).append(" If ") + .append(rootTypeNames.size() == 1 ? "this class does" : "these classes do") + .append(" not need to be initialized at build time, the safer approach is to initialize ") + .append(rootTypeNames.size() == 1 ? "it" : "them").append( + " at run time with:") + .append(System.lineSeparator()).append(System.lineSeparator()).append(" ") + .append(SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + String.join(",", rootTypeNames), + "initialize-at-run-time", true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()).append(" This prevents the ") + .append(rootTypeNames.size() == 1 ? "root's" : "roots'") + .append(" static state from being persisted in the image heap."); + + if (!typeInstantiationTraced) { + appendTraceInstantiationSuggestion(message, 6, typeName); + } else { + message.append(System.lineSeparator()).append(System.lineSeparator()); + } + } + + for (List objectFailures : disallowedObjectFailures.values()) { + DisallowedObjectException failure = objectFailures.getFirst(); + List rootTypeNames = objectFailures.stream().map(objectFailure -> objectFailure.getPath().getRootType().toJavaName()).distinct().toList(); + boolean typeInstantiationTraced = isTypeInitializationTraced(failure.getType()); + + message + .append('[').append(violationNumber++).append("] ") + .append(failure.getOriginalMessage()) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + + if (objectFailures.size() == 1) { + appendPathDetails(message, failure.getPath(), 4, typeInstantiationTraced); + } else { + for (int i = 0; i < objectFailures.size(); i++) { + message.append(" ").append(i + 1).append(". Path:").append(System.lineSeparator()); + appendPathDetails(message, objectFailures.get(i).getPath(), 6, typeInstantiationTraced); + if (i + 1 < objectFailures.size()) { + message.append(System.lineSeparator()); + } + } + } + + message + .append(System.lineSeparator()) + .append(" Possible resolution:") + .append(System.lineSeparator()) + .append(" The object was found through the build-time initialized ") + .append(rootTypeNames.size() == 1 ? "class '" : "classes '") + .append(String.join("', '", rootTypeNames)).append("'.") + .append(System.lineSeparator()).append(" If ") + .append(rootTypeNames.size() == 1 ? "this class does" : "these classes do") + .append(" not need to be initialized at build time, the safer approach is to initialize ") + .append(rootTypeNames.size() == 1 ? "it" : "them").append(" at run time with:") + .append(System.lineSeparator()).append(System.lineSeparator()).append(" ") + .append(SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + String.join(",", rootTypeNames), "initialize-at-run-time", + true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" This prevents the ") + .append(rootTypeNames.size() == 1 ? "root's" : "roots'") + .append(" static state from being persisted in the image heap.") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" Recommended action:") + .append(System.lineSeparator()).append(" ").append(failure.getInitializerAction()); + + if (!typeInstantiationTraced) { + appendTraceInstantiationSuggestion(message, 4, failure.getType().toJavaName()); + } else { + message.append(System.lineSeparator()).append(System.lineSeparator()); + } + } + + if (reportPath == null) { + appendFullReportOptionMessage(message); + } else { + message + .append("JSON file report can be found at: ") + .append(reportPath.toAbsolutePath().normalize()); + } + + return message.toString(); + } + + private static void appendPathDetails(StringBuilder message, MaterializedPath path, int offset, boolean typeInstantiationTraced) { + String indentation = " ".repeat(offset); + if (Options.PBTIFullReport.getValue()) { + appendDetailedObjectPath(message, path, indentation); + } else { + message + .append(indentation) + .append("Found from the build-time initialized class '") + .append(path.getRootType().toJavaName()) + .append("' through the path:") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(indentation) + .append('\'').append(path.getUnqualifiedPathString()).append('\'') + .append(System.lineSeparator()); + + if (path.isRootDifferentFromHolder()) { + message + .append(System.lineSeparator()) + .append(indentation).append("Held by class '") + .append(path.getHolderType().toJavaName()).append("'.") + .append(System.lineSeparator()); + } + } + + if (Options.PBTIFullReport.getValue()) { + InterceptedFieldType rootType = path.getRootType(); + message + .append(System.lineSeparator()) + .append(indentation).append("The class '") + .append(rootType.toJavaName()) + .append("' is marked for initialization at image build time for the following reason: ") + .append(ClassInitializationSupport.singleton().reasonForClass(OriginalClassProvider.getJavaClass(rootType))) + .append('.').append(System.lineSeparator()); + } + + if (typeInstantiationTraced && path.hasInstantiationTrace()) { + message.append(System.lineSeparator()); + appendObjectInstantiationTrace(message, indentation, path.getInstantiationTrace()); + } + } + + private static void appendDetailedObjectPath(StringBuilder message, MaterializedPath path, String indentation) { + message + .append(indentation) + .append("The violating object was found from a build-time initialized class with the following path:") + .append(System.lineSeparator()); + + message.append(path.getDetailedObjectPathString(indentation + " ")); + } + + private static void appendTraceInstantiationSuggestion(StringBuilder message, int offset, String type) { + String indentation = " ".repeat(offset); + message + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(indentation).append("To trace where the offending objects are instantiated, re-run with:") + .append(System.lineSeparator()).append(System.lineSeparator()).append(indentation) + .append(SubstrateOptionsParser.commandArgument(SubstrateOptions.TraceObjectInstantiation, type, true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()); + } + + private static void appendObjectInstantiationTrace(StringBuilder message, String linePrefix, String trace) { + message + .append(linePrefix) + .append("Tracing showed that the offending object was instantiated with the following trace:") + .append(System.lineSeparator()).append(trace); + } + + private static void appendFullReportOptionMessage(StringBuilder message) { + message + .append("To show violations in more detail, re-run with:") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(SubstrateOptionsParser.commandArgument(Options.PBTIFullReport, "+", true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()); + } + + private static boolean isTypeInitializationTraced(InterceptedFieldType type) { + return SubstrateOptions.TraceObjectInstantiation.hasBeenSet() && + SubstrateOptions.TraceObjectInstantiation.getValue().values().contains(type.toJavaName()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationFeature.java new file mode 100644 index 0000000000000..972713a2419aa --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationFeature.java @@ -0,0 +1,1950 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.classinitialization.pbti; + +import org.graalvm.collections.Equivalence; +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; +import org.graalvm.nativeimage.impl.clinit.ClassInitializationTracking; + +import com.oracle.graal.pointsto.constraints.UnsupportedPlatformException; +import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; +import com.oracle.graal.pointsto.heap.ImageHeapArray; +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.graal.pointsto.heap.TypedConstant; +import com.oracle.graal.pointsto.heap.value.ValueSupplier; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.graal.pointsto.meta.BaseLayerType; +import com.oracle.graal.pointsto.reports.ReportUtils; +import com.oracle.graal.pointsto.util.CompletionExecutor; +import com.oracle.svm.util.InterceptedField; +import com.oracle.svm.util.InterceptedFieldType; +// import com.oracle.svm.core.FutureDefaultsOptions; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.fieldvaluetransformer.FieldValueTransformerWithAvailability; +import com.oracle.svm.core.image.DisallowedImageHeapObjects; +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.hosted.GuestTypes; +import com.oracle.svm.hosted.classinitialization.ClassInitializationOptions; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.InitKind; +import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport; +import com.oracle.svm.hosted.ameta.SVMHostedValueProvider; +import com.oracle.svm.hosted.methodhandles.InjectedInvokerRenamingSubstitutionProcessor; +import com.oracle.svm.hosted.methodhandles.MethodHandleInvokerRenamingSubstitutionProcessor; +import com.oracle.svm.hosted.reflect.proxy.ProxyRenamingSubstitutionProcessor; +import com.oracle.svm.hosted.substitute.DeletedElementException; +import com.oracle.svm.shared.BuildPhaseProvider; +import com.oracle.svm.shared.collections.ConcurrentIdentityHashMap; +import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.shared.option.HostedOptionKey; +import com.oracle.svm.shared.option.SubstrateOptionsParser; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits; +import com.oracle.svm.shared.singletons.traits.SingletonTraits; +import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectConstantReplacer; +import com.oracle.svm.util.OriginalClassProvider; + +import jdk.graal.compiler.core.common.type.CompressibleConstant; +import jdk.graal.compiler.java.LambdaUtils; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionStability; +import jdk.graal.compiler.util.EconomicHashMap; +import jdk.graal.compiler.util.EconomicHashSet; +import jdk.graal.compiler.util.json.JsonBuilder; +import jdk.graal.compiler.util.json.JsonPrettyWriter; +import jdk.graal.compiler.util.json.JsonWriter; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.MetaAccessProvider; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Predictable build-time initialization (pbti) feature ensures that all classes of objects + * that are stored in or accessible from static fields of build-time initialized classes + * are also build-time initialized, even if they are not accessed by the analysis. + * This feature attempts to prevent these types of issues being masked by reachability. + * + *

+ * Additionally, objects are also checked by {@link DisallowedImageHeapObjects#check(Object, DisallowedImageHeapObjects.DisallowedObjectReporter)}. + *

+ * + *

+ * The processing lifecycle goes as follows: + *

    + *
  • In {@link com.oracle.svm.hosted.NativeImageGenerator} after _beforeAnalysis_ feature calls are finished
  • + *
  • After analysis, to process roots found by analysis
  • + *
  • After compilation, to re-process delayed fields by {@link FieldValueTransformerWithAvailability}
  • + *
  • Before image write, to re-process any remaining delayed fields
  • + *
+ *

+ */ +@AutomaticallyRegisteredFeature +@SingletonTraits(access = BuiltinTraits.BuildtimeAccessOnly.class, layeredCallbacks = BuiltinTraits.NoLayeredCallbacks.class) +@SuppressWarnings("unused") +public class PredictableBuildTimeInitializationFeature implements InternalFeature { + private static final int FIELD_BATCH_SIZE = 32; + + public static class Options { + // @formatter:off + /** + * Full reports for failures discovered before analysis are generated and reported immediately. + * Failures discovered after analysis are retained and included in the final report before image writing. + */ + @Option(help = "Create a full report of predictable build-time initialization failures, including all reconstructed paths", stability = OptionStability.EXPERIMENTAL) + public static final HostedOptionKey PBTIFullReport = new HostedOptionKey<>(true); + + /** + * It is better to limit this number as the number of explored branches can be quite substantial in some cases. + * For {@link PredictableBuildTimeInitializationFeature pbti} purposes, + * one path per one root is enough to make decisions about class initialization configuration. + */ + @Option(help = "Maximum number of alternate path branches explored per offending type and root when reconstructing a full PBTI report. Use -1 for no limit.", stability = OptionStability.EXPERIMENTAL) + public static final HostedOptionKey PBTIMaximumPathBranches = new HostedOptionKey<>(100, optionKey -> { + int value = optionKey.getValue(); + if (value < -1) { + throw UserError.invalidOptionValue(optionKey, value, "The value must be -1 or greater."); + } + }); + + /** + * This option stops class initialization being gated by reachability. + * It can also interfere with {@link com.oracle.svm.hosted.classinitialization.ClassInitializationOptions#AssertInitializationSpecifiedForAllClasses}. + */ + @Option(help = "Initialize all possible roots for PBTI", stability = OptionStability.EXPERIMENTAL) + public static final HostedOptionKey InitializeNewRoots = new HostedOptionKey<>(true); + // @formatter:on + } + + protected ClassInitializationSupport classInitializationSupport; + private AnalysisUniverse universe; + private MetaAccessProvider origMetaAccess; + protected SVMHostedValueProvider hostedValueProvider; + private FieldValueInterceptionSupport fieldValueInterceptionSupport; + private GuestTypes guestTypes; + + /** + * Custom type layer for pbti representing types after substitution was performed, + * and to prevent type lookups from expanding the analysis universe. + */ + private PredictableBuildTimeInitializationLookupLayer pbtiLookupLayer; + + private CompletionExecutor executor; + + /** + * Set of all types that were already processed. + */ + protected final Set processedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Set of processed objects read from fields to prevent cyclic dependency between objects. + */ + protected final Set processedConstants = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Extra paths that have encountered the same constant. Used for path reconstruction. + */ + private final Map> extraPathsToConstant = new ConcurrentHashMap<>(); + + /** + * Map of run-time initialized types found at paths accessed from roots. + */ + protected final Map> buildTimeFailures = new ConcurrentHashMap<>(); + + /** + * Disallowed objects found while traversing the fields of build-time initialized objects. + */ + private final Map> disallowedObjectFailures = new ConcurrentIdentityHashMap<>(); + + /** + * Field reads delayed by {@link FieldValueTransformerWithAvailability#isAvailable()}. + */ + private List>> delayedFieldValues = Collections.synchronizedList(new ArrayList<>()); + + /** + * Layered field values that are not currently available. + * These are separate from normal delayed fields because unavailable layered values can be + * silently skipped in beforeImageWrite. + */ + private List>> delayedLayeredFieldValues = Collections.synchronizedList(new ArrayList<>()); + + /** + * Fields that require the analysis to be processed any further. + * This is done based on {@link FieldValueTransformerRequiresUniverse}, + * if a field is read and the receiver is reachable, then it can be checked further. + */ + private List> analysisFieldValues = Collections.synchronizedList(new ArrayList<>()); + + /** + * Values whose constant replacer can only run after analysis has finished. + */ + private List> analysisReplacementValues = Collections.synchronizedList(new ArrayList<>()); + + /** + * Values of renamed types deferred so PBTI cannot consume names before analysis does. + */ + private List> analysisDeferredValues = Collections.synchronizedList(new ArrayList<>()); + + /** + * Roots discovered by type creation callbacks, which can run concurrently. + */ + private final Queue createdTypes = new ConcurrentLinkedQueue<>(); + + protected static final String[] EXCLUDED_ROOTS = { + /* + * Field nativeInstrumentedFilterState contains ByteBuffer, + * which pbti can find, and is disallowed by DisallowedImageHeapObjects check. + */ + "com.oracle.truffle.runtime.debug.JFRListener", + }; + + /** + * Array of type names that are ignored from further processing, when an instance is found. + * This can only be used for internal classes that might be misconfigured. + */ + protected static final String[] IGNORED_TYPES = { + /* + * This class is not marked as build-time initialized, when Truffle's + * initialization configuration is missing. An instance can be found from: + * - "com.oracle.svm.truffle.nfi.ErrnoMirrorGen" + * - "com.oracle.svm.truffle.nfi.KeysArrayGen" + */ + "com.oracle.truffle.api.library.DynamicDispatchLibraryGen", + /* + * When InitializeNewRoots is enabled, this class instance can be found from these roots + * - org.graalvm.polyglot.Engine + * - com.oracle.truffle.polyglot.PolyglotLanguageContext + * - com.oracle.truffle.polyglot.PolyglotContextImpl + * - com.oracle.truffle.api.TruffleLogger$LoggerCache + * - com.oracle.truffle.sl.runtime.SLFunction + * And these violating objects can be found active: + * - jdk.internal.jimage.ImageBufferCache$1 + * - started Thread instance + * - ByteBuffer + * - instance of Cleanable or jdk.internal.ref.Cleaner + */ + "com.oracle.truffle.polyglot.PolyglotContextImpl" + }; + + private final class ProcessFieldsTask implements CompletionExecutor.DebugContextRunnable { + private final LinkedPath path; + private final InterceptedFieldType type; + private final JavaConstant receiver; + + ProcessFieldsTask(LinkedPath path, InterceptedFieldType type) { + this(path, type, null); + } + + ProcessFieldsTask(LinkedPath path, InterceptedFieldType type, JavaConstant receiver) { + this.path = path; + this.type = type; + this.receiver = receiver; + } + + @Override + public void run(DebugContext debug) { + var fields = receiver == null ? type.getStaticFields() : type.getInstanceFields(true); + for (var field : fields) { + processField(new LinkedPath(path, new Field(type, (InterceptedField) field)), receiver); + } + } + + @Override + public DebugContext.Description getDescription() { + if (receiver == null) { + return new DebugContext.Description(path, "PBTI static fields"); + } + + return new DebugContext.Description(path, "PBTI instance fields"); + } + } + + private final class ProcessDelayedFieldsTask implements CompletionExecutor.DebugContextRunnable { + private final List>> taskDelayedFields; + + ProcessDelayedFieldsTask(List>> taskDelayedFields) { + this.taskDelayedFields = List.copyOf(taskDelayedFields); + } + + @Override + public void run(DebugContext debug) { + for (Pair> delayedField : taskDelayedFields) { + if (!delayedField.getRight().isAvailable()) { + delayedFieldValues.add(delayedField); + continue; + } + + processConstant(delayedField.getLeft(), getValueFromSupplier(delayedField.getLeft(), delayedField.getRight())); + } + } + + @Override + public DebugContext.Description getDescription() { + return new DebugContext.Description(taskDelayedFields, "PBTI delayed fields"); + } + } + + private final class ProcessDelayedLayeredFieldsTask implements CompletionExecutor.DebugContextRunnable { + private final List>> taskDelayedFields; + + ProcessDelayedLayeredFieldsTask(List>> taskDelayedFields) { + this.taskDelayedFields = List.copyOf(taskDelayedFields); + } + + @Override + public void run(DebugContext debug) { + for (Pair> delayedField : taskDelayedFields) { + if (delayedField.getRight().isAvailable()) { + processConstant(delayedField.getLeft(), getValueFromSupplier(delayedField.getLeft(), delayedField.getRight())); + } + } + } + + @Override + public DebugContext.Description getDescription() { + return new DebugContext.Description(taskDelayedFields, "PBTI delayed layered fields"); + } + } + + private final class ProcessAnalysisFieldsTask implements CompletionExecutor.DebugContextRunnable { + private final List> taskAnalysisFields; + + ProcessAnalysisFieldsTask(List> taskAnalysisFields) { + this.taskAnalysisFields = List.copyOf(taskAnalysisFields); + } + + @Override + public void run(DebugContext debug) { + for (Pair analysisField : taskAnalysisFields) { + processField(analysisField.getLeft(), analysisField.getRight()); + } + } + + @Override + public DebugContext.Description getDescription() { + return new DebugContext.Description(taskAnalysisFields, "PBTI analysis fields"); + } + } + + private final class ProcessAnalysisConstantsTask implements CompletionExecutor.DebugContextRunnable { + private final List> taskAnalysisReplacementValues; + + ProcessAnalysisConstantsTask(List> taskAnalysisReplacementValues) { + this.taskAnalysisReplacementValues = List.copyOf(taskAnalysisReplacementValues); + } + + @Override + public void run(DebugContext debug) { + for (Pair analysisReplacementValue : taskAnalysisReplacementValues) { + processConstant(analysisReplacementValue.getLeft(), analysisReplacementValue.getRight()); + } + } + + @Override + public DebugContext.Description getDescription() { + return new DebugContext.Description(taskAnalysisReplacementValues, "PBTI analysis constants"); + } + } + + /** + * Result of a constant read from {@link #resolveObjectConstant(LinkedPath, JavaConstant)}. + * + * @param path Where was the constant read from + * @param type Resolved type of the constant + * @param constant Constant object + */ + private record ConstantInfo(LinkedPath path, InterceptedFieldType type, JavaConstant constant) { + } + + /** Data for one member of a path, independent of the linked path structure. */ + protected abstract static class PathMember implements Comparable { + private JavaConstant constant; + private InterceptedFieldType constantType; + + /** + * @return Get the constant that was read at this path member. + */ + public JavaConstant getConstant() { + assert constant != null; + return constant; + } + + /** + * @param constant Set the constant that was read from this path member, after replacement + */ + public void setConstant(JavaConstant constant, InterceptedFieldType constantType) { + this.constant = constant; + this.constantType = constantType; + } + + public InterceptedFieldType getConstantType() { + assert constantType != null; + return constantType; + } + + /** + * @return Whether there should be a dot between this and the previous member when formatting + * a path. + */ + public boolean requiresTrailingDot() { + return true; + } + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); + + @Override + public abstract String toString(); + + public String toString(boolean qualified) { + return toString(); + } + + @Override + public final int compareTo(PathMember other) { + if (this instanceof Index leftIndex && other instanceof Index rightIndex) { + return Integer.compare(leftIndex.value, rightIndex.value); + } + + int result = Integer.compare(sortKind(), other.sortKind()); + if (result != 0) { + return result; + } + return toString().compareTo(other.toString()); + } + + private int sortKind() { + return switch (this) { + case Root root -> 0; + case Field field -> 1; + case Index index -> 2; + default -> throw VMError.shouldNotReachHere("Unknown path member: " + this); + }; + } + } + + /** + * Path that was taken from {@link Root} to access a {@link Field} to check the build-time + * initialization status of the constant it holds. + */ + protected static final class LinkedPath { + public final LinkedPath prev; + public final PathMember member; + + /** + * The root at which this path starts at. + */ + private final Root root; + + LinkedPath(LinkedPath prev, PathMember member) { + this.prev = prev; + this.member = member; + this.root = prev == null ? (Root) member : prev.root; + } + + @Override + public String toString() { + return toString(true); + } + + public String toString(boolean qualified) { + return formatPath(getPathMembers(), qualified); + } + + public List getPathMembers() { + var result = new ArrayList(); + for (LinkedPath current = this; current != null; current = current.prev) { + result.add(current.member); + } + return result; + } + + public InterceptedFieldType getRootType() { + return root.type; + } + + /** + * @return The type of the constant that holds the field (or array) that + * the final constant, where an exception occurred, was read from. + */ + public InterceptedFieldType getHolderType() { + for (LinkedPath path = this; path != null; path = path.prev) { + if (path.member instanceof Field field) { + return field.type; + } + } + + throw VMError.shouldNotReachHere("No holder type found for path: " + this); + } + + private static String formatPath(List path) { + return formatPath(path, true); + } + + private static String formatPath(List path, boolean qualified) { + var result = new StringBuilder(); + var rootIdx = path.size() - 1; + for (int i = rootIdx; i >= 0; i--) { + PathMember member = path.get(i); + if (i < rootIdx && member.requiresTrailingDot()) { + result.append('.'); + } + result.append(member.toString(qualified)); + } + return result.toString(); + } + } + + public static final class Root extends PathMember { + public InterceptedFieldType type; + + Root(InterceptedFieldType type) { + this.type = type; + } + + @Override + public String toString() { + return toString(true); + } + + @Override + public String toString(boolean qualified) { + return type.toJavaName(qualified); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Root root && root.type.equals(type); + } + + @Override + public int hashCode() { + return type.hashCode(); + } + } + + public static final class Field extends PathMember { + public InterceptedFieldType type; + public InterceptedField field; + + Field(InterceptedFieldType type, InterceptedField field) { + this.type = type; + this.field = field; + } + + @Override + public String toString() { + return field.getName(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Field f && f.field.equals(field) && f.type.equals(type); + } + + @Override + public int hashCode() { + return field.hashCode(); + } + } + + public static final class Index extends PathMember { + public int value; + + Index(int value) { + this.value = value; + } + + @Override + public String toString() { + return "[" + value + "]"; + } + + @Override + public boolean requiresTrailingDot() { + return false; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Index idx && idx.value == value; + } + + @Override + public int hashCode() { + return value; + } + } + + @SuppressWarnings("serial") + public final class DisallowedObjectException extends RuntimeException { + private final Object object; + private final InterceptedFieldType type; + private final String originalMessage; + private final String initializerAction; + private final MaterializedPath path; + + private DisallowedObjectException(String message, Object object, String initializerAction, InterceptedFieldType type, MaterializedPath path) { + super(createMessage(message, object, initializerAction, type, path)); + this.object = object; + this.initializerAction = initializerAction; + this.originalMessage = message; + this.type = type; + this.path = path; + } + + public String getOriginalMessage() { + return originalMessage; + } + + public Object getObject() { + return object; + } + + public InterceptedFieldType getType() { + return type; + } + + public String getPathString() { + return path.getPathString(); + } + + public MaterializedPath getPath() { + return path; + } + + public String getInitializerAction() { + return initializerAction; + } + + private static String createMessage(String message, Object object, String initializerAction, InterceptedFieldType type, MaterializedPath path) { + return type.toJavaName() + " reachable from " + path.getRootType().toJavaName() + " via " + path.getUnqualifiedPathString() + ": " + message + " " + initializerAction; + } + } + + /** + * Exception thrown in an object replacer or a field value transformer. + * The exception trace is shown along with the traversal path and build-time initialized root. + * When the cause is a UserError instance, then a possible resolution is shown by initializing + * the root at run-time. + */ + @SuppressWarnings("serial") + public static final class TraversalException extends UnsupportedFeatureException { + private final MaterializedPath path; + + /** + * Different from {@code cause} to shorten the error message, + * because it is already shown there. + */ + private final Throwable source; + + private TraversalException(MaterializedPath path, Throwable cause) { + super(createMessage(path, cause)); + this.path = path; + this.source = cause; + } + + public String getPathString() { + return path.getPathString(); + } + + public Throwable getSourceException() { + return source; + } + + private static String createMessage(MaterializedPath path, Throwable cause) { + StringBuilder message = new StringBuilder( + "An exception occurred while traversing an object accessible from a static field of a build-time initialized class.") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append("Found from the build-time initialized class '") + .append(path.getRootType().toJavaName()) + .append("' through the path:") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" '").append(path.getUnqualifiedPathString()).append('\'') + .append(System.lineSeparator()).append(System.lineSeparator()); + + if (cause instanceof UserError.UserException) { + message + .append("Possible resolution:") + .append(System.lineSeparator()) + .append(" The object was found through the build-time initialized class '") + .append(path.getRootType().toJavaName()) + .append("'.") + .append(System.lineSeparator()) + .append(" If this class does not need to be initialized at build time, the safer approach is to initialize it at run time with:") + .append(System.lineSeparator()).append(System.lineSeparator()).append(" ") + .append(SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + path.getRootType().toJavaName(), "initialize-at-run-time", true, false)) + .append(System.lineSeparator()).append(System.lineSeparator()) + .append(" This prevents the root's static state from being persisted in the image heap.") + .append(System.lineSeparator()).append(System.lineSeparator()); + } + + return message.append("The traversal failed with:") + .append(System.lineSeparator()) + .append(cause) + .toString(); + } + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + // return FutureDefaultsOptions.predictableBuildTimeInitialization(); + return true; + } + + @Override + public void onRegistration(OnRegistrationAccess access) { + ImageSingletons.add(PredictableBuildTimeInitializationFeature.class, this); + } + + @Override + public void duringSetup(DuringSetupAccess a) { + var access = (FeatureImpl.DuringSetupAccessImpl) a; + + classInitializationSupport = ClassInitializationSupport.singleton(); + universe = access.getUniverse(); + hostedValueProvider = (SVMHostedValueProvider) universe.getHostedValuesProvider(); + fieldValueInterceptionSupport = FieldValueInterceptionSupport.singleton(); + origMetaAccess = universe.getOriginalMetaAccess(); + guestTypes = access.getImageClassLoader().guestTypes; + + pbtiLookupLayer = new PredictableBuildTimeInitializationLookupLayer( + access.getHostVM(), origMetaAccess, universe.getSubstitutions(), universe.getWordKind()); + + executor = new CompletionExecutor(access.getDebugContext(), access.getBigBang()); + + if (Options.InitializeNewRoots.getValue()) { + access.registerOnTypeCreatedCallback((type) -> { + if (type.getWrapped() instanceof BaseLayerType) { + /* + * It is assumed that the type from previous layer + * was checked by the feature the layer was built. + * There is no extra re-processing for these types. + */ + return; + } + + var interceptedType = lookupType(OriginalClassProvider.getJavaClass(type)); + if (processedTypes.contains(interceptedType)) { + return; + } + + /* Run the potential initialization later */ + createdTypes.add(type); + }); + } + } + + /** + * This method has to run after every beforeAnalysis feature method has run. + * Called in {@link com.oracle.svm.hosted.NativeImageGenerator}, after class + * initialization registration is sealed. + * + *

+ * The current processing goes as following: + *

    + *
  • Initialize new roots from {@link GuestTypes} in series, if -H:+InitializeNewRoots
  • + *
  • Process fields in parallel before analysis
  • + *
  • Process newly found roots by analysis after it completes
  • + *
  • + * Process delayed fields via {@link FieldValueTransformerWithAvailability} + * after compilation, before the post-compilation image heap verification. + *
  • + *
  • Process delayed fields before heap layout, before the final image heap verification.
  • + *
  • Process any remaining delayed fields before image write, where they must be available.
  • + *
+ *

+ */ + public void afterBeforeAnalysis() { + executor.init(); + + assert createdTypes.isEmpty(); + processBuildTimeRoots(); + + if (Options.InitializeNewRoots.getValue()) { + /* + * Concurrent initialization can deadlock when class initializers contain cyclic + * dependencies, which happens for Truffle's Assumption classes. + */ + for (ResolvedJavaType type : guestTypes.getApplicationTypes()) { + processUninitializedRoot(OriginalClassProvider.getJavaClass(type)); + } + } + + runExecutor(); + throwIfFailure(); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + executor.init(); + + processAnalysisFieldValues(); + processAnalysisReplacementValues(); + processDeferredValues(); + + processDelayedFieldValues(); + processBuildTimeRoots(); + + if (Options.InitializeNewRoots.getValue()) { + AnalysisType type; + while ((type = createdTypes.poll()) != null) { + processUninitializedRoot(OriginalClassProvider.getJavaClass(type)); + } + } + + runExecutor(); + if (!Options.PBTIFullReport.getValue()) { + throwIfFailure(); + } + } + + /** + * Retry values that become available once compilation is finished. This must run before the + * post-compilation image heap verification so PBTI failures retain their rooted paths and are + * reported before the general image heap check. + */ + public void afterCompilationFinishes() { + executor.init(); + + processDelayedFieldValues(); + processDelayedLayeredFieldValues(); + runExecutor(); + + throwIfFailure(); + } + + /** + * Run before the final heap verification is done and the heap is sealed. + */ + @Override + public void beforeHeapLayout(BeforeHeapLayoutAccess access) { + executor.init(); + + processDelayedFieldValues(); + processDelayedLayeredFieldValues(); + runExecutor(); + + throwIfFailure(); + } + + @Override + public void beforeImageWrite(BeforeImageWriteAccess access) { + /* + * Values that can appear in the image heap must already be available because the image is about to be written. + * Process delayed fields in beforeImageWrite so that failures are reported before the executable is written out. + */ + executor.init(); + + assert createdTypes.isEmpty(); + processDelayedFieldValues(); + processDelayedLayeredFieldValues(); + runExecutor(); + + /* All fields must be processed by now */ + VMError.guarantee(delayedFieldValues.isEmpty(), + "Field values are still unavailable before image writing: %n%s", + String.join("\n", delayedFieldValues.stream().map(Pair::getLeft).map(p -> " - " + p.toString()).toList())); + + throwIfFailure(); + } + + /** + * Process the current set of build-time initialized classes. + */ + protected void processBuildTimeRoots() { + for (Class clazz : classInitializationSupport.classesWithKind(InitKind.BUILD_TIME)) { + InterceptedFieldType type = lookupType(clazz); + if (type == null) { + continue; + } + + if (classInitializationSupport.computedInitKindFor(OriginalClassProvider.getJavaClass(type)) != InitKind.BUILD_TIME) { + /* The Target_ class is initialized, but the underlying class might not be. */ + if (Options.InitializeNewRoots.getValue()) { + processUninitializedRoot(type); + } + continue; + } + + processInitializedRoot(type); + } + } + + /** + * Process a root synchronously and add its static fields to the executor. + */ + protected final void processUninitializedRoot(Class clazz) { + InterceptedFieldType type = lookupType(clazz); + if (type == null) { + return; + } + + processUninitializedRoot(type); + } + + /** + * Add root's static fields to the executor, ready for processing. + * A root is a type that is initialized at build time. If an attempt + * to initialize the type fails, then it is ignored. + */ + private void processUninitializedRoot(InterceptedFieldType type) { + if (isExcludedRoot(type)) { + return; + } + + if (isFromSharedLayer(type)) { + /* + * Shared layer types were initialized and verified while building the previous layer. + * Do not initialize or inspect their current hosted versions again. Generated types, + * such as Weld proxies, can be recreated under the same name with different metadata, + * which can make the build fail when compared with the persisted shared layer type. + */ + return; + } + + if (processedTypes.contains(type)) { + return; + } + + if (ClassInitializationOptions.AssertInitializationSpecifiedForAllClasses.getValue() && !classInitializationSupport.canInitializeAtBuildTime(type)) { + /* Memoize the initialization result only if the class has been specified as BUILD_TIME */ + return; + } + + try { + var clazz = OriginalClassProvider.getJavaClass(type); + if (!classInitializationSupport.maybeInitializeAtBuildTime(clazz)) { + return; + } + } catch (UserError.UserException e) { + if (!e.toString().contains("Class initialization of ")) { + /* Re-throw other exceptions that are unrelated to class initialization */ + throw e; + } + + return; + } + + processInitializedRoot(type); + } + + private void processInitializedRoot(InterceptedFieldType type) { + if (isExcludedRoot(type)) { + return; + } + + if (isFromSharedLayer(type)) { + return; + } + + if (!processedTypes.add(type)) { + /* Using the substituted type for deduplication of processing. */ + return; + } + + var clazz = OriginalClassProvider.getJavaClass(type); + assert classInitializationSupport.computedInitKindFor(clazz) == InitKind.BUILD_TIME; + + var root = new LinkedPath(null, new Root(type)); + if (type.getStaticFields().length != 0) { + executor.execute(new ProcessFieldsTask(root, type)); + } + } + + private void runExecutor() { + executor.start(); + try { + executor.complete(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + executor.shutdown(); + } + + private void processField(LinkedPath path, JavaConstant receiver) { + Field fieldMember = (Field) path.member; + InterceptedField field = fieldMember.field; + if (!field.getStorageKind().isObject()) { + return; + } + + if (fieldValueInterceptionSupport.requiresAnalysis(field)) { + /* + * Cannot read this field without accessing the analysis. + * The field has to be read and the receiver must be reachable + * in order to proceed further. + * + * It is assumed that the SVMHostedValueProvider + * uses FieldValueInterceptionSupport to read field values. + */ + if (!BuildPhaseProvider.isAnalysisFinished()) { + analysisFieldValues.add(Pair.create(path, receiver)); + return; + } + + if (!canProcessAnalysisField(field, receiver)) { + return; + } + + field = getAnalysisField(field); + /* + * At this point, the field is known to be read and its receiver is reachable, + * therefore the value can be read using its field value transformer + * without expanding the image heap or the analysis/hosted universe through internal operations. + */ + } + + /* This must use the analysis field when a layered transformer needs to be installed. */ + if (fieldValueInterceptionSupport.isValueNeverAvailable(field)) { + /* Explicitly ignore fields, that can never be read */ + return; + } + + /* + * Field value transformers are expected to be registered either + * in beforeAnalysis or in a beforeFieldValueAccess callback. + * Otherwise, no further registration is expected. + */ + JavaConstant receiverToRead = receiver; + if (receiver instanceof ImageHeapConstant imageHeapConstant) { + assert imageHeapConstant.isBackedByHostedObject(); + /* Cannot read the value from the VM if using heap constant */ + receiverToRead = imageHeapConstant.getHostedObject(); + } + + ValueSupplier valueSupplier = hostedValueProvider.readFieldValue(field, receiverToRead); + if (!valueSupplier.isAvailable()) { + if (fieldValueInterceptionSupport.isLayeredFieldValue(field)) { + /* Retried before image write */ + delayedLayeredFieldValues.add(Pair.create(path, valueSupplier)); + } else { + delayedFieldValues.add(Pair.create(path, valueSupplier)); + } + return; + } + + JavaConstant constant = getValueFromSupplier(path, valueSupplier); + processConstant(path, constant); + } + + private JavaConstant getValueFromSupplier(LinkedPath path, ValueSupplier valueSupplier) { + try { + return valueSupplier.get(); + } catch (Throwable cause) { + throw new TraversalException(new MaterializedPath(path), cause); + } + } + + /** + * All fields that were not available at the time of the read are re-processed. Fields that + * are still unavailable, as well as fields delayed while processing the newly available + * values, are retained for the next attempt. + */ + private void processDelayedFieldValues() { + var previouslyDelayedFieldValues = delayedFieldValues; + delayedFieldValues = Collections.synchronizedList(new ArrayList<>()); + + processCollectionBatched(previouslyDelayedFieldValues, ProcessDelayedFieldsTask::new); + } + + /** + * Retry layered field values once, ignoring values for which this layer has no update. Install + * a new list first so layered fields discovered transitively during processing can be skipped + * safely as well. + */ + private void processDelayedLayeredFieldValues() { + var previouslyDelayedLayeredFieldValues = delayedLayeredFieldValues; + delayedLayeredFieldValues = Collections.synchronizedList(new ArrayList<>()); + + processCollectionBatched(previouslyDelayedLayeredFieldValues, ProcessDelayedLayeredFieldsTask::new); + } + + /** + * Process fields whose transformers need analysis elements, after analysis has finished. + */ + private void processAnalysisFieldValues() { + processCollectionBatched(analysisFieldValues, ProcessAnalysisFieldsTask::new); + analysisFieldValues = null; /* No longer needed */ + } + + /** + * Retry values whose constant replacement requires analysis reachability. + */ + private void processAnalysisReplacementValues() { + processCollectionBatched(analysisReplacementValues, ProcessAnalysisConstantsTask::new); + analysisReplacementValues = null; /* No longer needed */ + } + + /** + * Retry values of renamed types, after analysis has finished. + */ + private void processDeferredValues() { + processCollectionBatched(analysisDeferredValues, ProcessAnalysisConstantsTask::new); + analysisDeferredValues = null; /* No longer needed */ + } + + private void processCollectionBatched(List collection, Function, CompletionExecutor.DebugContextRunnable> taskCreator) { + for (int start = 0; start < collection.size(); start += FIELD_BATCH_SIZE) { + int end = Math.min(start + FIELD_BATCH_SIZE, collection.size()); + executor.execute(taskCreator.apply(collection.subList(start, end))); + } + } + + /** + * Process a constant by either turning it into a constant + * and calling {@link #processObjectConstant(LinkedPath, JavaConstant, InterceptedFieldType)}, or if + * said constant represents an array, then iterate until all + * objects are read from the array and processed. + * + * @param rootPath Path where the constant was read + * @param root Constant to be processed + */ + private void processConstant(LinkedPath rootPath, JavaConstant root) { + /* Process the constant first, before allocating a deque */ + ConstantInfo rootInfo = resolveObjectConstant(rootPath, root); + if (rootInfo == null) { + return; + } + + if (!rootInfo.type().isArray()) { + processObjectConstant(rootInfo.path(), rootInfo.constant(), rootInfo.type()); + return; + } + + if (rootInfo.type().getComponentType().isPrimitive()) { + return; + } + + var constWorklist = new ArrayDeque>(); + addArrayElementsToWorklist(constWorklist, rootInfo.path(), rootInfo.constant()); + + while (!constWorklist.isEmpty()) { + var cp = constWorklist.poll(); + ConstantInfo info = resolveObjectConstant(cp.getLeft(), cp.getRight()); + if (info == null) { + continue; + } + + if (!info.type().isArray()) { + processObjectConstant(info.path(), info.constant(), info.type()); + continue; + } + + if (info.type().getComponentType().isPrimitive()) { + continue; + } + + addArrayElementsToWorklist(constWorklist, info.path(), info.constant()); + } + } + + private ConstantInfo resolveObjectConstant(LinkedPath path, JavaConstant constant) { + if (constant.isNull() || !constant.getJavaKind().isObject()) { + return null; + } + + JavaConstant replacement; + if (constant instanceof ImageHeapConstant imageHeapConstant) { + replacement = imageHeapConstant; + } else { + replacement = replaceConstantNoHeap(path, constant); + if (HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(replacement)) { + if (!BuildPhaseProvider.isAnalysisFinished()) { + analysisReplacementValues.add(Pair.create(path, constant)); + return null; + } + + if (!canProcessAnalysisReplacement(path)) { + return null; + } + + /* + * The constant cannot be replaced without internal operations that expand the analysis/hosted universe or image heap, + * so it is checked whether the nearest field that the constant was read from is read and its receiver is reachable. + * This increases the coverage of pbti without triggering those operations for unreachable objects. + */ + replacement = replaceConstantWithAllReplacers(path, constant); + } + } + + if (replacement.isNull()) { + return null; + } + + if (replacement instanceof ImageHeapConstant imageHeapConstant && !imageHeapConstant.isBackedByHostedObject()) { + /* It is assumed that un-backed constants were already analyzed in the prior layer. */ + return null; + } + + if (!BuildPhaseProvider.isAnalysisFinished() && !(constant instanceof TypedConstant) && isDeferredType(origMetaAccess.lookupJavaType(constant))) { + /* Defer before substitution lookup and constant deduplication so the value is retried. */ + analysisDeferredValues.add(Pair.create(path, replacement)); + return null; + } + + var type = lookupType(replacement); + if (type == null || !type.getStorageKind().isObject()) { + /* + * Platform unsupported and deleted types are ignored, these + * exceptions are thrown when their instances are visible to analysis. + */ + return null; + } + + /* For path reconstruction, the constant needs to be noted. */ + path.member.setConstant(replacement, type); + + JavaConstant constantIdentity = getConstantIdentity(replacement); + if (!processedConstants.add(constantIdentity)) { + if (Options.PBTIFullReport.getValue()) { + /* Skip extra processing, only take note of the path, if full report is done. */ + extraPathsToConstant.computeIfAbsent(constantIdentity, _ -> new ConcurrentLinkedQueue<>()).add(path); + } + return null; + } + + return new ConstantInfo(path, type, replacement); + } + + private JavaConstant replaceConstantNoHeap(LinkedPath path, JavaConstant constant) { + try { + return universe.replaceConstantNoHeap(constant); + } catch (Throwable cause) { + throw new TraversalException(new MaterializedPath(path), cause); + } + } + + private JavaConstant replaceConstantWithAllReplacers(LinkedPath path, JavaConstant constant) { + try { + return universe.replaceConstantWithAllReplacers(constant); + } catch (Throwable cause) { + throw new TraversalException(new MaterializedPath(path), cause); + } + } + + private void addArrayElementsToWorklist(ArrayDeque> constWorklist, LinkedPath path, JavaConstant inputConstant) { + JavaConstant array = inputConstant; + if (array instanceof ImageHeapConstant imageHeapConstant) { + assert imageHeapConstant.isBackedByHostedObject(); + + array = imageHeapConstant.getHostedObject(); + } + + int length = hostedValueProvider.readArrayLength(array); + for (int i = 0; i < length; i++) { + JavaConstant element = hostedValueProvider.readArrayElement(array, i); + constWorklist.add(Pair.create(new LinkedPath(path, new Index(i)), element)); + } + } + + private int readArrayLength(JavaConstant inputConstant) { + JavaConstant array = inputConstant; + if (array instanceof ImageHeapConstant imageHeapConstant) { + if (imageHeapConstant instanceof ImageHeapArray imageHeapArray) { + return imageHeapArray.getLength(); + } + + array = imageHeapConstant.getHostedObject(); + } + return hostedValueProvider.readArrayLength(array); + } + + /** + * Check if constant's class is build-time initialized. + * If not, report a failure. If it is, read all its + * fields and schedule them for execution. + * + * @param path Where the constant was read from + * @param constant Constant to be processed + * @param type of the constant + */ + private void processObjectConstant(LinkedPath path, JavaConstant constant, InterceptedFieldType type) { + assert !constant.isNull(); + + if (isIgnoredType(type)) { + return; + } + + if (!maybeInitializeAtBuildTime(type)) { + buildTimeFailures + .computeIfAbsent(type, _ -> Collections.synchronizedList(new ArrayList<>())) + .add(path); + return; + } + + var backingObject = getBackingObject(constant); + if (backingObject != null) { + DisallowedImageHeapObjects.DisallowedObjectReporter reporter = (msg, disallowedObject, initializerAction) -> { + return new DisallowedObjectException(msg, disallowedObject, initializerAction, type, new MaterializedPath(path)); + }; + + try { + DisallowedImageHeapObjects.check(backingObject, reporter); + } catch (DisallowedObjectException exception) { + disallowedObjectFailures + .computeIfAbsent(exception.object, _ -> Collections.synchronizedList(new ArrayList<>())) + .add(exception); + return; + } + } + + if (type.getInstanceFields(true).length != 0) { + executor.execute(new ProcessFieldsTask(path, type, constant)); + } + } + + private Object getBackingObject(JavaConstant constant) { + if (constant instanceof ImageHeapConstant imageHeapConstant) { + assert imageHeapConstant.isBackedByHostedObject(); + + return hostedValueProvider.asObject(Object.class, imageHeapConstant.getHostedObject()); + } + + return hostedValueProvider.asObject(Object.class, constant); + } + + protected static JavaConstant getConstantIdentity(JavaConstant constant) { + JavaConstant uncompressed = CompressibleConstant.uncompress(constant); + if (uncompressed instanceof ImageHeapConstant imageHeapConstant) { + /* Assumption that un-backed hosted object has been skipped/processed in previous steps. */ + assert imageHeapConstant.isBackedByHostedObject(); + + return imageHeapConstant.getHostedObject(); + } + return uncompressed; + } + + /** + * @return Whether this field that requires the universe can be further processed without + * internal operations expanding the analysis/hosted universe or image heap - the field is read + * and its receiver is reachable. + */ + private boolean canProcessAnalysisField(InterceptedField field, JavaConstant receiver) { + var analysisField = getAnalysisField(field); + if (analysisField == null) { + return false; + } + + if (!analysisField.isRead()) { + return false; + } + + if (receiver == null) { + return true; + } + + if (receiver instanceof ImageHeapConstant imageHeapConstant) { + return imageHeapConstant.isReachable(); + } + + Object receiverObject = hostedValueProvider.asObject(Object.class, receiver); + return universe.getHeapScanner().isObjectReachable(receiverObject); + } + + private AnalysisField getAnalysisField(InterceptedField field) { + var pbtiField = (PredictableBuildTimeInitializationField) field; + return universe.getField(pbtiField.getWrapped()); + } + + /** + * @return Whether the nearest field is read and its receiver is reachable. + */ + private boolean canProcessAnalysisReplacement(LinkedPath path) { + for (LinkedPath current = path; current != null; current = current.prev) { + /* Find the last field and check if it is read and its receiver is reachable. */ + if (current.member instanceof Field fieldMember) { + JavaConstant receiver = null; + if (!fieldMember.field.isStatic()) { + assert current.prev != null : current; + receiver = current.prev.member.getConstant(); + } + return canProcessAnalysisField(fieldMember.field, receiver); + } + } + throw VMError.shouldNotReachHere("No field found for path: " + path); + } + + private static final int JSON_OUTPUT_VERSION = 1; + + /** + * Write a versioned, machine-readable report. Run-time initialization failures are grouped by + * type and contain all paths to that type. Disallowed object failures have one entry per object. + * + *
{@code
+     * {
+     *   "version": 1,
+     *   "failures": [
+     *     {
+     *       "kind": "run-time-initialized-type",
+     *       "type": "example.RunTimeType",
+     *       "runTimeInitializationReason": "classes are initialized at run time by default",
+     *       "paths": [
+     *         {
+     *           "buildTimeRootType": "example.BuildTimeRoot",
+     *           "objectHolderType": "example.Holder",
+     *           "path": "example.BuildTimeRoot.value"
+     *         }
+     *       ],
+     *       "recommendedActions": [
+     *         {
+     *           "action": "initialize-type-at-build-time",
+     *           "command": "--initialize-at-build-time=example.RunTimeType"
+     *         },
+     *         {
+     *           "action": "initialize-roots-at-run-time",
+     *           "command": "--initialize-at-run-time=example.BuildTimeRoot"
+     *         }
+     *       ]
+     *     },
+     *     {
+     *       "kind": "disallowed-image-heap-object",
+     *       "type": "java.util.Random",
+     *       "message": "Reason the object is disallowed.",
+     *       "initializerAction": "Suggested action.",
+     *       "paths": [
+     *         {
+     *           "buildTimeRootType": "example.BuildTimeRoot",
+     *           "objectHolderType": "example.BuildTimeRoot",
+     *           "path": "example.BuildTimeRoot.random"
+     *         }
+     *       ],
+     *       "recommendedActions": [
+     *         {
+     *           "action": "initialize-roots-at-run-time",
+     *           "command": "--initialize-at-run-time=example.BuildTimeRoot"
+     *         }
+     *       ]
+     *     }
+     *   ]
+     * }
+     * }
+ */ + private static Path printStructuredFailures( + Map> buildTimeFailures, + Map> disallowedObjectFailures) { + Consumer consumer = writer -> { + try (JsonWriter out = new JsonPrettyWriter(writer); JsonBuilder.ObjectBuilder report = out.objectBuilder()) { + report.append("version", JSON_OUTPUT_VERSION); + try (JsonBuilder.ArrayBuilder failures = report.append("failures").array()) { + for (var entry : buildTimeFailures.entrySet()) { + List paths = entry.getValue(); + List rootTypeNames = paths.stream().map(path -> path.getRootType().toJavaName()).distinct().toList(); + try (JsonBuilder.ObjectBuilder failure = failures.nextEntry().object()) { + var initializationReason = ClassInitializationSupport.singleton().reasonForClass(OriginalClassProvider.getJavaClass(entry.getKey())); + + failure.append("kind", "run-time-initialized-type"); + failure.append("type", entry.getKey().toJavaName()); + failure.append("runTimeInitializationReason", initializationReason); + try (JsonBuilder.ArrayBuilder jsonPaths = failure.append("paths").array()) { + for (MaterializedPath path : paths) { + try (JsonBuilder.ObjectBuilder jsonPath = jsonPaths.nextEntry().object()) { + jsonPath.append("buildTimeRootType", path.getRootType().toJavaName()); + jsonPath.append("objectHolderType", path.getHolderType().toJavaName()); + jsonPath.append("path", path.getPathString()); + } + } + } + + try (JsonBuilder.ArrayBuilder recommendedActions = failure.append("recommendedActions").array()) { + try (JsonBuilder.ObjectBuilder action = recommendedActions.nextEntry().object()) { + action.append("action", "initialize-type-at-build-time"); + action.append("command", SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + entry.getKey().toJavaName(), "initialize-at-build-time")); + } + try (JsonBuilder.ObjectBuilder action = recommendedActions.nextEntry().object()) { + action.append("action", "initialize-roots-at-run-time"); + action.append("command", SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + String.join(",", rootTypeNames), "initialize-at-run-time")); + } + } + } + } + for (List objectFailures : disallowedObjectFailures.values()) { + DisallowedObjectException disallowed = objectFailures.getFirst(); + List rootTypeNames = objectFailures.stream().map(objectFailure -> objectFailure.path.getRootType().toJavaName()).distinct().toList(); + try (JsonBuilder.ObjectBuilder failure = failures.nextEntry().object()) { + failure.append("kind", "disallowed-image-heap-object"); + failure.append("type", disallowed.type.toJavaName()); + failure.append("message", disallowed.getOriginalMessage()); + failure.append("initializerAction", disallowed.getInitializerAction()); + try (JsonBuilder.ArrayBuilder jsonPaths = failure.append("paths").array()) { + for (DisallowedObjectException objectFailure : objectFailures) { + try (JsonBuilder.ObjectBuilder jsonPath = jsonPaths.nextEntry().object()) { + jsonPath.append("buildTimeRootType", objectFailure.path.getRootType().toJavaName()); + jsonPath.append("objectHolderType", objectFailure.path.getHolderType().toJavaName()); + jsonPath.append("path", objectFailure.getPathString()); + } + } + } + try (JsonBuilder.ArrayBuilder recommendedActions = failure.append("recommendedActions").array(); + JsonBuilder.ObjectBuilder action = recommendedActions.nextEntry().object()) { + action.append("action", "initialize-roots-at-run-time"); + action.append("command", SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization, + String.join(",", rootTypeNames), "initialize-at-run-time")); + } + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + return ReportUtils.report("predictable build-time initialization failures", SubstrateOptions.reportsPath(), + "predictable_build_time_initialization_failures", "json", consumer, false); + } + + private void throwIfFailure() { + if (!hasFailures()) { + return; + } + + if (!Options.PBTIFullReport.getValue()) { + throw new PredictableBuildTimeInitializationError( + sortFailureMap(materializeFailureMap(this.buildTimeFailures)), + sortDisallowedObjectFailureMap(disallowedObjectFailures)); + } + + var sortedBuildTimeFailures = reconstructAllFailurePaths(); + var sortedDisallowedObjectFailures = sortDisallowedObjectFailureMap(this.disallowedObjectFailures); + Path reportPath = printStructuredFailures(sortedBuildTimeFailures, sortedDisallowedObjectFailures); + + throw new PredictableBuildTimeInitializationError(sortedBuildTimeFailures, sortedDisallowedObjectFailures, reportPath); + } + + private boolean hasFailures() { + return !buildTimeFailures.isEmpty() || !disallowedObjectFailures.isEmpty(); + } + + private boolean maybeInitializeAtBuildTime(InterceptedFieldType type) { + var clazz = OriginalClassProvider.getJavaClass(type); + var result = classInitializationSupport.maybeInitializeAtBuildTime(clazz); + if (result) { + /* Newly initialized roots should also be processed */ + processInitializedRoot(type); + } + return result; + } + + /** + * Lookup a {@link InterceptedFieldType} without expanding the analysis/hosted universe, + * skipping deleted types and types unsupported on this platform. + * + * @param clazz Class to lookup type for + * @return Resolved platform supported type or null. + */ + protected final InterceptedFieldType lookupType(Class clazz) { + try { + return pbtiLookupLayer.lookup(clazz); + } catch (DeletedElementException | UnsupportedPlatformException e) { + /* Deleted types and types unsupported on the current platform are skipped. */ + return null; + } + } + + private InterceptedFieldType lookupType(JavaConstant constant) { + try { + return pbtiLookupLayer.lookup(constant); + } catch (DeletedElementException | UnsupportedPlatformException e) { + return null; + } + } + + private static boolean isDeferredType(ResolvedJavaType type) { + ResolvedJavaType originalType = OriginalClassProvider.getOriginalType(type); + return LambdaUtils.isLambdaType(originalType) || + InjectedInvokerRenamingSubstitutionProcessor.isInjectedInvokerType(originalType) || + MethodHandleInvokerRenamingSubstitutionProcessor.isMethodHandleType(originalType) || + ProxyRenamingSubstitutionProcessor.isProxyType(originalType); + } + + protected String[] getExcludedRoots() { + return EXCLUDED_ROOTS; + } + + private boolean isExcludedRoot(InterceptedFieldType type) { + return Arrays.stream(getExcludedRoots()).anyMatch(root -> type.toJavaName().startsWith(root)); + } + + protected String[] getIgnoredTypes() { + return IGNORED_TYPES; + } + + private boolean isIgnoredType(InterceptedFieldType type) { + return Arrays.stream(getIgnoredTypes()).anyMatch(ignoredType -> type.toJavaName().startsWith(ignoredType)); + } + + private boolean isFromSharedLayer(InterceptedFieldType type) { + var analysisType = universe.optionalLookup(OriginalClassProvider.getOriginalType(type)); + return analysisType != null && analysisType.isInSharedLayer(); + } + + /** + * Materialized path from the original {@link LinkedPath linked structure path}. + * Used for sorting and printing. + */ + public class MaterializedPath implements Comparable { + /** + * The type holding the last field before the constant was read. + * + *

+ * For example, in Root.field1.field2[0][1], the holder type + * is the type of constant that "field1" contains. + *

+ */ + private final InterceptedFieldType holderType; + + /** + * List of path members in a list. The order is reversed (root is the last element). + */ + private final List members; + + private final String pathString; + + MaterializedPath(LinkedPath path) { + this(path.getHolderType(), path.getPathMembers()); + } + + MaterializedPath(InterceptedFieldType holderType, List members) { + this.holderType = holderType; + this.members = members; + this.pathString = LinkedPath.formatPath(members); + } + + @Override + public String toString() { + return pathString; + } + + public String getPathString() { + return pathString; + } + + public String getUnqualifiedPathString() { + return LinkedPath.formatPath(members, false); + } + + public InterceptedFieldType getRootType() { + var root = (Root) this.members.getLast(); + return root.type; + } + + public InterceptedFieldType getHolderType() { + return holderType; + } + + public boolean isRootDifferentFromHolder() { + return !holderType.equals(getRootType()); + } + + protected Object getOffendingObject() { + return getBackingObject(members.getFirst().getConstant()); + } + + boolean hasInstantiationTrace() { + Object object = getOffendingObject(); + return ClassInitializationTracking.instantiatedObjects.get(object) != null; + } + + String getInstantiationTrace() { + Object object = getOffendingObject(); + if (object == null) { + return null; + } + + StackTraceElement[] trace = ClassInitializationTracking.instantiatedObjects.get(object); + if (trace == null) { + return null; + } + + StringBuilder result = new StringBuilder(); + for (StackTraceElement element : trace) { + result.append("\tat ").append(element).append(System.lineSeparator()); + } + return result.toString(); + } + + /** + * Show the path in more detail, with types and array sizes. + * + *

+ * The path {@code Root.field[1]} is shown as follows: + *

{@code
+         * Root
+         *   - field: Object[2]
+         *     - [1]: RuntimeInitialized
+         * }
+ * + * Where, {@code Root} is the build-time initialized root, {@code Object[2]} is the type of the array that + * was read from {@code field} field of {@code Root}. + *

+ * + * @param indentation Prefix for each line of the formatted path + * @return Formatted path + */ + String getDetailedObjectPathString(String indentation) { + StringBuilder message = new StringBuilder(); + for (int i = members.size() - 1; i >= 0; i--) { + PathMember member = members.get(i); + message.append(indentation).repeat(" ", members.size() - (i + 1)); + + switch (member) { + case Root root -> message.append(root.type.toJavaName()); + case Field field -> message.append("- ").append(field.field.getName()).append(": ").append(getConstantTypeName(field)); + case Index index -> message.append("- [").append(index.value).append("]: ").append(getConstantTypeName(index)); + default -> { + } + } + + message.append(System.lineSeparator()); + } + + return message.toString(); + } + + private String getConstantTypeName(PathMember member) { + InterceptedFieldType constantType = member.getConstantType(); + String typeName = constantType.toJavaName(); + if (!constantType.isArray()) { + return typeName; + } + + int firstArrayDimension = typeName.indexOf("[]"); + assert firstArrayDimension >= 0 : typeName; + return typeName.substring(0, firstArrayDimension) + '[' + readArrayLength(member.getConstant()) + ']' + typeName.substring(firstArrayDimension + 2); + } + + @Override + public int compareTo(MaterializedPath other) { + int result = Integer.compare(members.size(), other.members.size()); + if (result != 0) { + return result; + } + + /* Members are in the reverse order */ + for (int i = members.size() - 1; i >= 0; i--) { + result = members.get(i).compareTo(other.members.get(i)); + if (result != 0) { + return result; + } + } + return 0; + } + } + + private Map> materializeFailureMap(Map> failures) { + var result = new EconomicHashMap>(); + failures.forEach((type, paths) -> result.put(type, paths.stream().map(MaterializedPath::new).toList())); + return result; + } + + /** + * Sort failure map by offending type to make the errors (somewhat) stable across different builds. + */ + private static Map> sortFailureMap(Map> failures) { + var result = new LinkedHashMap>(failures.size()); + failures.entrySet().stream() + .sorted(Comparator.comparing(entry -> entry.getKey().toJavaName())) + .forEachOrdered(entry -> result.put(entry.getKey(), sortedPaths(entry.getValue()))); + return Collections.unmodifiableMap(result); + } + + /** + * Sort paths by their number of members and then by each member from root to leaf. + * To have access to the shortest path where a violation for certain type has occurred. + */ + private static List sortedPaths(List paths) { + return paths.stream().sorted().toList(); + } + + private static Map> sortDisallowedObjectFailureMap(Map> failures) { + var result = new EconomicHashMap>(Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE, failures.size()); + + // Compare by type name and sort individual paths + var comparator = Comparator.comparing((Pair> entry) -> entry.getRight().getFirst().type.toJavaName()); + failures.entrySet().stream() + .map(entry -> Pair.create(entry.getKey(), sortedDisallowedObjectFailures(entry.getValue()))) + .sorted(comparator) + .forEachOrdered(entry -> result.put(entry.getLeft(), entry.getRight())); + + return Collections.unmodifiableMap(result); + } + + private static List sortedDisallowedObjectFailures(List failures) { + var comparator = Comparator.comparing((DisallowedObjectException failure) -> failure.type.toJavaName()) + .thenComparing(failure -> failure.path); + + return failures.stream().sorted(comparator).toList(); + } + + private record BranchCounterKey(InterceptedFieldType offendingType, InterceptedFieldType rootType) { + } + + /** Entry for path reconstruction from {@link #extraPathsToConstant}. */ + private class PathWorklistEntry { + private InterceptedFieldType holderType; + private final List members; + private final Set visitedConstants; + private final InterceptedFieldType offendingType; + private LinkedPath path; + + PathWorklistEntry(LinkedPath path, InterceptedFieldType offendingType) { + this.members = new ArrayList<>(); + this.visitedConstants = new EconomicHashSet<>(); + this.path = path; + this.offendingType = offendingType; + } + + PathWorklistEntry(PathWorklistEntry previous, LinkedPath path) { + this.holderType = previous.holderType; + this.members = new ArrayList<>(previous.members); + this.visitedConstants = new EconomicHashSet<>(previous.visitedConstants); + this.path = path; + this.offendingType = previous.offendingType; + } + + private InterceptedFieldType getRoot() { + return path.getRootType(); + } + + private boolean hasNext() { + return path.prev != null; + } + + private boolean next() { + if (!visitedConstants.add(getConstantIdentity(path.member.getConstant()))) { + return false; + } + + if (holderType == null && path.member instanceof Field field) { + holderType = field.type; + } + + members.add(path.member); + path = path.prev; + return true; + } + + private MaterializedPath toResult() { + assert holderType != null; + + members.add(path.member); + path = null; + return new MaterializedPath(holderType, members); + } + } + + /** + * Reconstruct paths from the {@link LinkedPath} that have shared the same constant, via + * {@link #extraPathsToConstant}. Retain the shortest path from each root to an offending type, + * and sort the resulting paths so the shortest path is first. + */ + private Map> reconstructAllFailurePaths() { + Map> pathsByRoot = new EconomicHashMap<>(); + Map branchCounters = new EconomicHashMap<>(); + Queue worklist = new ArrayDeque<>(); + for (var failure : buildTimeFailures.entrySet()) { + for (var path : failure.getValue()) { + worklist.add(new PathWorklistEntry(path, failure.getKey())); + } + } + + while (!worklist.isEmpty()) { + var entry = worklist.poll(); + + if (!entry.hasNext()) { + InterceptedFieldType root = entry.getRoot(); + MaterializedPath reconstructedPath = entry.toResult(); + pathsByRoot.computeIfAbsent(entry.offendingType, _ -> new EconomicHashMap<>()) + .merge(root, reconstructedPath, (current, candidate) -> candidate.compareTo(current) < 0 ? candidate : current); + continue; + } + + var extraPaths = extraPathsToConstant.get(getConstantIdentity(entry.path.member.getConstant())); + if (extraPaths != null) { + for (var branch : extraPaths) { + BranchCounterKey counterKey = new BranchCounterKey(entry.offendingType, branch.getRootType()); + if (!canExerciseBranch(branchCounters, counterKey)) { + continue; + } + var branchEntry = new PathWorklistEntry(entry, branch); + if (branchEntry.next()) { + worklist.add(branchEntry); + } + } + } + + if (entry.next()) { + worklist.add(entry); + } + } + + Map> result = new EconomicHashMap<>(); + pathsByRoot.forEach((type, rootPaths) -> result.put(type, new ArrayList<>(rootPaths.values()))); + return sortFailureMap(result); + } + + private static boolean canExerciseBranch(Map branchCounters, BranchCounterKey counterKey) { + if (Options.PBTIMaximumPathBranches.getValue() < 0) { + return true; + } + + int branchCount = branchCounters.getOrDefault(counterKey, 0); + if (branchCount >= Options.PBTIMaximumPathBranches.getValue()) { + return false; + } + + branchCounters.put(counterKey, branchCount + 1); + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java new file mode 100644 index 0000000000000..2b3f08d56bbe4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.classinitialization.pbti; + +import java.lang.reflect.Modifier; + +import com.oracle.svm.util.InterceptedField; +import com.oracle.graal.pointsto.infrastructure.WrappedJavaField; +import com.oracle.svm.util.OriginalClassProvider; +import com.oracle.svm.util.OriginalFieldProvider; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.annotation.Annotated; + +/** + * Field implementing {@link InterceptedField} to be able to read field without + * accessing analysis elements in {@link com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport}. + */ +public final class PredictableBuildTimeInitializationField implements ResolvedJavaField, WrappedJavaField, OriginalFieldProvider, InterceptedField { + + private final PredictableBuildTimeInitializationLookupLayer lookupLayer; + private final ResolvedJavaField wrapped; + private final PredictableBuildTimeInitializationType declaringClass; + private final PredictableBuildTimeInitializationType fieldType; + + private volatile Object fieldValueInterceptor; + + PredictableBuildTimeInitializationField(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaField wrapped) { + this.lookupLayer = lookupLayer; + this.wrapped = wrapped; + this.declaringClass = lookupLayer.lookup(wrapped.getDeclaringClass()); + this.fieldType = resolveFieldType(lookupLayer, wrapped); + } + + PredictableBuildTimeInitializationLookupLayer getLookupLayer() { + return lookupLayer; + } + + @Override + public ResolvedJavaField getWrapped() { + return wrapped; + } + + @Override + public ResolvedJavaField unwrapTowardsOriginalField() { + return wrapped; + } + + @Override + public Object getFieldValueInterceptor() { + return fieldValueInterceptor; + } + + @Override + public void setFieldValueInterceptor(Object fieldValueInterceptor) { + this.fieldValueInterceptor = fieldValueInterceptor; + } + + @Override + public String getName() { + return wrapped.getName(); + } + + @Override + public PredictableBuildTimeInitializationType getType() { + return fieldType; + } + + @Override + public int getModifiers() { + return wrapped.getModifiers(); + } + + @Override + public int getOffset() { + return wrapped.getOffset(); + } + + @Override + public boolean isInternal() { + return wrapped.isInternal(); + } + + @Override + public boolean isSynthetic() { + return wrapped.isSynthetic(); + } + + @Override + public PredictableBuildTimeInitializationType getDeclaringClass() { + return declaringClass; + } + + @Override + public JavaConstant getConstantValue() { + return wrapped.getConstantValue(); + } + + @Override + public JavaKind getStorageKind() { + return fieldType.getStorageKind(); + } + + @Override + public boolean isStatic() { + return Modifier.isStatic(getModifiers()); + } + + @Override + public void beforeFieldValueAccess() { + lookupLayer.hostVM().beforeFieldValueAccess(declaringClass); + } + + @Override + public String toString() { + return "PBTIField<" + format("%h.%n") + " -> " + wrapped + ">"; + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public int hashCode() { + return wrapped.hashCode(); + } + + @Override + public Annotated getWrappedAnnotated() { + return wrapped; + } + + /** + * Resolve field type similarly to {@code com.oracle.graal.pointsto.meta.AnalysisField#getDeclaredType}. + */ + private static PredictableBuildTimeInitializationType resolveFieldType(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaField field) { + try { + JavaType type = field.getType(); + ResolvedJavaType resolvedType = type.resolve(OriginalClassProvider.getOriginalType(field.getDeclaringClass())); + PredictableBuildTimeInitializationType fieldType = lookupLayer.lookup(resolvedType); + return fieldType != null ? fieldType : lookupLayer.objectType(); + } catch (LinkageError e) { + return lookupLayer.objectType(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java new file mode 100644 index 0000000000000..6feb05855952a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.classinitialization.pbti; + +import java.lang.reflect.Field; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.oracle.svm.shared.util.VMError; +import org.graalvm.word.WordBase; + +import com.oracle.graal.pointsto.api.HostVM; +import com.oracle.graal.pointsto.constraints.UnsupportedPlatformException; +import com.oracle.graal.pointsto.heap.TypedConstant; +import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor; +import com.oracle.svm.util.OriginalClassProvider; +import com.oracle.svm.util.OriginalFieldProvider; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Lookup layer used by {@link PredictableBuildTimeInitializationFeature} to access substituted + * fields and types without touching the analysis. The pbti feature can find new types, that + * the analysis has not yet seen, but accessing them via the {@link com.oracle.graal.pointsto.meta.AnalysisUniverse} + * can result in an exception being thrown, after the universe is sealed. + */ +public final class PredictableBuildTimeInitializationLookupLayer { + + private final HostVM hostVM; + private final SubstitutionProcessor substitutions; + private final MetaAccessProvider wrappedMetaAccess; + + private final ConcurrentMap types = new ConcurrentHashMap<>(); + private final ConcurrentMap fields = new ConcurrentHashMap<>(); + + private final JavaKind wordKind; + private final ResolvedJavaType wordType; + private final PredictableBuildTimeInitializationType objectType; + + public PredictableBuildTimeInitializationLookupLayer(HostVM hostVM, MetaAccessProvider wrappedMetaAccess, SubstitutionProcessor substitutions, JavaKind wordKind) { + this.hostVM = hostVM; + this.substitutions = substitutions; + this.wrappedMetaAccess = wrappedMetaAccess; + + this.wordKind = wordKind; + this.wordType = wrappedMetaAccess.lookupJavaType(WordBase.class); + this.objectType = lookup(wrappedMetaAccess.lookupJavaType(Object.class)); + } + + public HostVM hostVM() { + return hostVM; + } + + public SubstitutionProcessor getSubstitutions() { + return substitutions; + } + + public MetaAccessProvider getOriginalMetaAccess() { + return wrappedMetaAccess; + } + + public JavaKind getWordKind() { + return wordKind; + } + + public PredictableBuildTimeInitializationType lookup(JavaConstant constant) { + if (constant == null || constant.getJavaKind() != JavaKind.Object || constant.isNull()) { + return null; + } + ResolvedJavaType type = constant instanceof TypedConstant typedConstant ? typedConstant.getType() : wrappedMetaAccess.lookupJavaType(constant); + return lookup(type); + } + + public PredictableBuildTimeInitializationType lookup(JavaType type) { + JavaType result = lookupAllowUnresolved(type); + if (result == null) { + return null; + } else if (result instanceof PredictableBuildTimeInitializationType pbtiType) { + return pbtiType; + } + throw VMError.shouldNotReachHere("Unresolved type found: " + type.toJavaName(true)); + } + + public PredictableBuildTimeInitializationType lookup(ResolvedJavaType type) { + if (type == null) { + return null; + } + if (type instanceof PredictableBuildTimeInitializationType pbtiType) { + return pbtiType; + } + + ResolvedJavaType originalType = OriginalClassProvider.getOriginalType(type); + PredictableBuildTimeInitializationType result = types.get(originalType); + if (result == null) { + ResolvedJavaType substitutedType = substitutions.lookup(originalType); + result = createType(originalType, substitutedType); + } + return result; + } + + private PredictableBuildTimeInitializationType createType(ResolvedJavaType originalType, ResolvedJavaType substitutedType) { + if (!isPlatformSupported(originalType)) { + throw new UnsupportedPlatformException("Type is not available in this platform: " + originalType.toJavaName(true)); + } + + JavaKind storageKind; + if (wordType.isAssignableFrom(originalType)) { + storageKind = wordKind; + } else { + storageKind = substitutedType.getJavaKind(); + } + + var newType = new PredictableBuildTimeInitializationType(this, substitutedType, storageKind); + return types.computeIfAbsent(originalType, _ -> newType); + } + + /** + * Check a type and its interface hierarchy for platform support. The superclass is eagerly + * looked up by {@link PredictableBuildTimeInitializationType} and checked separately. + */ + private boolean isPlatformSupported(ResolvedJavaType type) { + if (!hostVM.platformSupported(type)) { + return false; + } + + var interfaces = type.getInterfaces(); + var typeWorklist = new ArrayDeque<>(Arrays.asList(interfaces)); + + while (!typeWorklist.isEmpty()) { + var iface = typeWorklist.poll(); + if (!hostVM.platformSupported(iface)) { + return false; + } + + typeWorklist.addAll(Arrays.asList(iface.getInterfaces())); + } + + return true; + } + + public JavaType lookupAllowUnresolved(JavaType type) { + if (!(type instanceof ResolvedJavaType resolvedType)) { + return type; + } + return lookup(resolvedType); + } + + public List lookup(List inputTypes) { + if (inputTypes == null) { + return null; + } + return inputTypes.stream().map(this::lookupAllowUnresolved).toList(); + } + + public ResolvedJavaType[] lookup(ResolvedJavaType[] inputTypes) { + if (inputTypes == null) { + return null; + } + ResolvedJavaType[] result = new ResolvedJavaType[inputTypes.length]; + for (int i = 0; i < inputTypes.length; i++) { + result[i] = lookup(inputTypes[i]); + } + return result; + } + + public PredictableBuildTimeInitializationType lookup(Class clazz) { + return lookup(wrappedMetaAccess.lookupJavaType(clazz)); + } + + public PredictableBuildTimeInitializationField lookup(ResolvedJavaField field) { + if (field == null) { + return null; + } + if (field instanceof PredictableBuildTimeInitializationField pbtiField) { + assert pbtiField.getLookupLayer() == this : pbtiField; + return pbtiField; + } + + ResolvedJavaField originalField = OriginalFieldProvider.getOriginalField(field); + ResolvedJavaField substitutedField = substitutions.lookup(originalField); + return fields.computeIfAbsent(originalField, _ -> new PredictableBuildTimeInitializationField(this, substitutedField)); + } + + public ResolvedJavaField[] lookup(ResolvedJavaField[] inputFields) { + if (inputFields == null) { + return null; + } + ResolvedJavaField[] result = new ResolvedJavaField[inputFields.length]; + for (int i = 0; i < inputFields.length; i++) { + result[i] = lookup(inputFields[i]); + } + return result; + } + + public PredictableBuildTimeInitializationField lookup(Field field) { + return lookup(wrappedMetaAccess.lookupJavaField(field)); + } + + public PredictableBuildTimeInitializationType objectType() { + return objectType; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java new file mode 100644 index 0000000000000..f74b8d1bb4c2f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java @@ -0,0 +1,500 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.classinitialization.pbti; + +import java.util.Arrays; +import java.util.List; + +import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; +import com.oracle.svm.util.InterceptedFieldType; +import com.oracle.graal.pointsto.infrastructure.WrappedJavaType; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.InitKind; +import com.oracle.svm.util.OriginalClassProvider; + +import jdk.vm.ci.meta.Assumptions.AssumptionResult; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaRecordComponent; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; +import jdk.vm.ci.meta.UnresolvedJavaField; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Type after {@link com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor substitution}. + * This is intentionally separate from {@link com.oracle.graal.pointsto.meta.AnalysisType} to + * prevent type and field lookups from expanding the analysis/hosted universe. + * + *

Implements {@link InterceptedFieldType shared interface} to access fields via the {@link com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport}

+ */ +public final class PredictableBuildTimeInitializationType implements ResolvedJavaType, WrappedJavaType, OriginalClassProvider, InterceptedFieldType { + + private final PredictableBuildTimeInitializationLookupLayer lookupLayer; + private final ResolvedJavaType wrapped; + private final JavaKind storageKind; + + private final ResolvedJavaType superType; + private PredictableBuildTimeInitializationField[] instanceObjectFields; + private PredictableBuildTimeInitializationField[] staticObjectFields; + private final Class javaClass; + private final String qualifiedName; + + PredictableBuildTimeInitializationType(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaType wrapped, JavaKind storageKind) { + this.lookupLayer = lookupLayer; + this.wrapped = wrapped; + + this.superType = lookupLayer.lookup(wrapped.getSuperclass()); + this.javaClass = OriginalClassProvider.getJavaClass(this); + this.qualifiedName = wrapped.toJavaName(); + + this.storageKind = storageKind; + } + + @Override + public ResolvedJavaType getWrapped() { + return wrapped; + } + + @Override + public ResolvedJavaType unwrapTowardsOriginalType() { + return wrapped; + } + + @Override + public String getName() { + return wrapped.getName(); + } + + @Override + public String toJavaName() { + return qualifiedName; + } + + @Override + public String toJavaName(boolean qualified) { + /* Only qualified is cached, unqualified is not expected. */ + return qualified ? qualifiedName : wrapped.toJavaName(false); + } + + @Override + public boolean hasFinalizer() { + return wrapped.hasFinalizer(); + } + + @Override + public AssumptionResult hasFinalizableSubclass() { + return wrapped.hasFinalizableSubclass(); + } + + @Override + public boolean isInterface() { + return wrapped.isInterface(); + } + + @Override + public boolean isInstanceClass() { + return wrapped.isInstanceClass(); + } + + @Override + public boolean isPrimitive() { + return wrapped.isPrimitive(); + } + + @Override + public boolean isLeaf() { + return wrapped.isLeaf(); + } + + @Override + public boolean isEnum() { + return wrapped.isEnum(); + } + + @Override + public boolean isRecord() { + return wrapped.isRecord(); + } + + @Override + public List getRecordComponents() { + return wrapped.getRecordComponents(); + } + + @Override + public boolean isInitialized() { + InitKind kind = ClassInitializationSupport.singleton().computedInitKindFor(javaClass); + /* Check that when isInitialized is called, class is already initialized. */ + assert kind != null; + return kind == InitKind.BUILD_TIME; + } + + @Override + public void initialize() { + assert ClassInitializationSupport.singleton().maybeInitializeAtBuildTime(this); + } + + @Override + public boolean isLinked() { + return wrapped.isLinked(); + } + + @Override + public void link() { + wrapped.link(); + } + + @Override + public boolean hasDefaultMethods() { + return wrapped.hasDefaultMethods(); + } + + @Override + public boolean declaresDefaultMethods() { + return wrapped.declaresDefaultMethods(); + } + + @Override + public boolean isAssignableFrom(ResolvedJavaType other) { + return wrapped.isAssignableFrom(OriginalClassProvider.getOriginalType(other)); + } + + @Override + public boolean isJavaLangObject() { + return wrapped.isJavaLangObject(); + } + + @Override + public boolean isInstance(JavaConstant obj) { + return wrapped.isInstance(obj); + } + + @Override + public ResolvedJavaType getSuperclass() { + return superType; + } + + @Override + public ResolvedJavaType[] getInterfaces() { + return lookupLayer.lookup(wrapped.getInterfaces()); + } + + @Override + public ResolvedJavaType[] getDeclaredTypes() { + return lookupLayer.lookup(wrapped.getDeclaredTypes()); + } + + @Override + public ResolvedJavaType getSingleImplementor() { + return lookupLayer.lookup(wrapped.getSingleImplementor()); + } + + @Override + public ResolvedJavaType findLeastCommonAncestor(ResolvedJavaType otherType) { + return lookupLayer.lookup(wrapped.findLeastCommonAncestor(OriginalClassProvider.getOriginalType(otherType))); + } + + @Override + public AssumptionResult findLeafConcreteSubtype() { + AssumptionResult wrappedResult = wrapped.findLeafConcreteSubtype(); + if (wrappedResult != null && wrappedResult.isAssumptionFree()) { + return new AssumptionResult<>(lookupLayer.lookup(wrappedResult.getResult())); + } + return null; + } + + @Override + public ResolvedJavaType getComponentType() { + return lookupLayer.lookup(wrapped.getComponentType()); + } + + @Override + public ResolvedJavaType getElementalType() { + return lookupLayer.lookup(wrapped.getElementalType()); + } + + @Override + public ResolvedJavaType getArrayClass() { + return lookupLayer.lookup(wrapped.getArrayClass()); + } + + @Override + public boolean isHidden() { + return wrapped.isHidden(); + } + + @Override + public List getPermittedSubclasses() { + return lookupLayer.lookup(wrapped.getPermittedSubclasses()); + } + + @Override + public ResolvedJavaMethod resolveMethod(ResolvedJavaMethod method, ResolvedJavaType callerType) { + return wrapped.resolveMethod(method, callerType); + } + + @Override + public ResolvedJavaMethod resolveConcreteMethod(ResolvedJavaMethod method, ResolvedJavaType callerType) { + return wrapped.resolveConcreteMethod(method, callerType); + } + + @Override + public AssumptionResult findUniqueConcreteMethod(ResolvedJavaMethod method) { + return wrapped.findUniqueConcreteMethod(method); + } + + @Override + public PredictableBuildTimeInitializationField[] getInstanceFields(boolean includeSuperclasses) { + assert includeSuperclasses; /* There is no need to filter these out */ + if (instanceObjectFields == null) { + instanceObjectFields = convertFields(wrapped.getInstanceFields(true)); + } + return instanceObjectFields; + } + + @Override + public PredictableBuildTimeInitializationField[] getStaticFields() { + if (staticObjectFields == null) { + staticObjectFields = convertFields(wrapped.getStaticFields()); + } + return staticObjectFields; + } + + /** + * Similar implementation to {@code com.oracle.graal.pointsto.meta.AnalysisType#convertFields}. + */ + private PredictableBuildTimeInitializationField[] convertFields(ResolvedJavaField[] rawFields) { + PredictableBuildTimeInitializationField[] result = new PredictableBuildTimeInitializationField[rawFields.length]; + + int index = 0; + for (ResolvedJavaField rawField : rawFields) { + if (!rawField.isInternal() && lookupLayer.hostVM().platformSupported(rawField)) { + try { + PredictableBuildTimeInitializationField field = lookupLayer.lookup(rawField); + if (field != null && field.getStorageKind().isObject()) { + /* PBTI only cares about objects */ + result[index++] = field; + } + } catch (UnsupportedFeatureException ex) { + // Ignore deleted fields and fields of deleted types. + } + } + } + return index == result.length ? result : Arrays.copyOf(result, index); + } + + @Override + public ResolvedJavaField findInstanceFieldWithOffset(long offset, JavaKind expectedKind) { + ResolvedJavaField field = wrapped.findInstanceFieldWithOffset(offset, expectedKind); + return lookupLayer.lookup(field); + } + + @Override + public String getSourceFileName() { + return wrapped.getSourceFileName(); + } + + @Override + public boolean isLocal() { + return wrapped.isLocal(); + } + + @Override + public boolean isMember() { + return wrapped.isMember(); + } + + @Override + public ResolvedJavaType getEnclosingType() { + return lookupLayer.lookup(wrapped.getEnclosingType()); + } + + @Override + public ResolvedJavaMethod getEnclosingMethod() { + return wrapped.getEnclosingMethod(); + } + + @Override + public ResolvedJavaMethod[] getDeclaredConstructors() { + return wrapped.getDeclaredConstructors(); + } + + @Override + public ResolvedJavaMethod[] getDeclaredConstructors(boolean forceLink) { + return wrapped.getDeclaredConstructors(forceLink); + } + + @Override + public ResolvedJavaMethod[] getDeclaredMethods() { + return wrapped.getDeclaredMethods(); + } + + @Override + public ResolvedJavaMethod[] getDeclaredMethods(boolean forceLink) { + return wrapped.getDeclaredMethods(forceLink); + } + + @Override + public List getAllMethods(boolean forceLink) { + return wrapped.getAllMethods(forceLink); + } + + @Override + public ResolvedJavaMethod getClassInitializer() { + return wrapped.getClassInitializer(); + } + + @Override + public ResolvedJavaMethod findMethod(String name, Signature signature) { + return wrapped.findMethod(name, signature); + } + + @Override + public boolean isCloneableWithAllocation() { + return wrapped.isCloneableWithAllocation(); + } + + @Override + public ResolvedJavaType lookupType(UnresolvedJavaType unresolvedJavaType, boolean resolve) { + return lookupLayer.lookup(wrapped.lookupType(unresolvedJavaType, resolve)); + } + + @Override + public ResolvedJavaField resolveField(UnresolvedJavaField unresolvedJavaField, ResolvedJavaType accessingClass) { + return wrapped.resolveField(unresolvedJavaField, accessingClass); + } + + @Override + public boolean isArray() { + return wrapped.isArray(); + } + + @Override + public JavaKind getJavaKind() { + return wrapped.getJavaKind(); + } + + @Override + public JavaKind getStorageKind() { + return storageKind; + } + + @Override + public ResolvedJavaType resolve(ResolvedJavaType accessingClass) { + return this; + } + + @Override + public int getModifiers() { + return wrapped.getModifiers(); + } + + @Override + public boolean isSynchronized() { + return wrapped.isSynchronized(); + } + + @Override + public boolean isStatic() { + return wrapped.isStatic(); + } + + @Override + public boolean isFinalFlagSet() { + return wrapped.isFinalFlagSet(); + } + + @Override + public boolean isPublic() { + return wrapped.isPublic(); + } + + @Override + public boolean isPackagePrivate() { + return wrapped.isPackagePrivate(); + } + + @Override + public boolean isPrivate() { + return wrapped.isPrivate(); + } + + @Override + public boolean isProtected() { + return wrapped.isProtected(); + } + + @Override + public boolean isTransient() { + return wrapped.isTransient(); + } + + @Override + public boolean isStrict() { + return wrapped.isStrict(); + } + + @Override + public boolean isVolatile() { + return wrapped.isVolatile(); + } + + @Override + public boolean isNative() { + return wrapped.isNative(); + } + + @Override + public boolean isAbstract() { + return wrapped.isAbstract(); + } + + @Override + public boolean isConcrete() { + return wrapped.isConcrete(); + } + + @Override + public String toString() { + return "PBTIType<" + wrapped.toJavaName(true) + ">"; + } + + @Override + public int hashCode() { + return wrapped.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PredictableBuildTimeInitializationType pbtiType) { + return pbtiType.wrapped == wrapped; + } + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java index 0dedb593f0373..b658416079e26 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ import static com.oracle.svm.shared.util.VMError.shouldNotReachHere; import java.lang.reflect.Method; -import java.util.function.Function; import org.graalvm.nativeimage.c.function.CEntryPoint; import org.graalvm.nativeimage.c.function.CEntryPointLiteral; @@ -46,6 +45,7 @@ import com.oracle.svm.hosted.meta.PatchedWordConstant; import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.util.GuestAnnotationAccess; +import com.oracle.svm.util.HeapObjectConstantReplacer; import jdk.vm.ci.meta.JavaConstant; import jdk.vm.ci.meta.ResolvedJavaMethod; @@ -53,7 +53,7 @@ @AutomaticallyRegisteredFeature public class CEntryPointLiteralFeature implements InternalFeature { - class CEntryPointLiteralObjectReplacer implements Function { + class CEntryPointLiteralObjectReplacer extends HeapObjectConstantReplacer { private final HostedValuesProvider hostedValuesProvider; /** Creates a replacer that canonicalizes changed word values with {@code hostedValuesProvider}. */ @@ -62,7 +62,7 @@ class CEntryPointLiteralObjectReplacer implements Function> allMaps = ConcurrentHashMap.newKeySet(); private final Set> allLists = ConcurrentHashMap.newKeySet(); private final ScanReason scanReason = new OtherReason("Manual rescan triggered from " + ImageHeapCollectionSupport.class); + final Replacer replacer = new Replacer(); static ImageHeapCollectionSupport singleton() { return ImageSingletons.lookup(ImageHeapCollectionSupport.class); } - /** - * Rewrites hosted image-heap collections to their runtime storage objects and records the - * hosted collections that need later update propagation. During analysis, first reachability of - * a collection registers it for subsequent update/rescan handling; after analysis, encountering - * a collection that was never captured is treated as an invariant violation. - */ - Object replaceHostedWithRuntime(Object obj) { - if (obj instanceof HostedImageHeapMap) { - HostedImageHeapMap hostedImageHeapMap = (HostedImageHeapMap) obj; - if (BuildPhaseProvider.isAnalysisFinished()) { - VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis"); - } else { - allMaps.add(hostedImageHeapMap); - if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap layeredImageHeapMap) { - LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap); + final class Replacer extends HeapObjectReplacer { + /** + * Rewrites hosted image-heap collections to their runtime storage objects and records the + * hosted collections that need later update propagation. During analysis, first reachability of + * a collection registers it for subsequent update/rescan handling; after analysis, encountering + * a collection that was never captured is treated as an invariant violation. + */ + @Override + public Object replaceObject(Object obj, boolean reachable) { + if (obj instanceof HostedImageHeapMap) { + HostedImageHeapMap hostedImageHeapMap = (HostedImageHeapMap) obj; + if (reachable) { + if (BuildPhaseProvider.isAnalysisFinished()) { + VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis"); + } else { + allMaps.add(hostedImageHeapMap); + if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap layeredImageHeapMap) { + LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap); + } + } } + return hostedImageHeapMap.getRuntimeMap(); + } else if (obj instanceof HostedImageHeapList hostedImageHeapList) { + if (reachable) { + if (BuildPhaseProvider.isAnalysisFinished()) { + VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis"); + } else { + allLists.add(hostedImageHeapList); + } + } + return hostedImageHeapList.getRuntimeList(); } - return hostedImageHeapMap.getRuntimeMap(); - } else if (obj instanceof HostedImageHeapList hostedImageHeapList) { - if (BuildPhaseProvider.isAnalysisFinished()) { - VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis"); - } else { - allLists.add(hostedImageHeapList); - } - return hostedImageHeapList.getRuntimeList(); + return obj; } - return obj; } /** diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java index a9971faa63d5a..d1a256b362189 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2020, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -40,6 +40,8 @@ import java.util.List; import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; @@ -62,8 +64,17 @@ * the local cache or cached when not already present. Subclasses are responsible for providing * behaviours that identify an original source for addition to the cache and for verifying that a * cached file is not out of date with respect to its original. + * + *

+ * Marked as HOSTED_ONLY so + * {@link com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature} + * does not mark it as a failure that can appear in the generated image. In particular, the + * SourceRoots can contain {@code sun.nio.ch.FileChannelImpl}, which is initialized at run-time, and + * available from {@code SourceCache$SourceRoots.srcRoots.elementData[0].path.zfs.ch}, held + * by {@code jdk.nio.zipfs.ZipFileSystem}. + *

*/ - +@Platforms(Platform.HOSTED_ONLY.class) public class SourceCache { private static final class SourceRoots { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java index 9ccd5c709ad7d..8cee79657b54d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java @@ -36,7 +36,6 @@ import org.graalvm.nativeimage.ImageSingletons; -import com.oracle.graal.pointsto.heap.ImageHeapConstant; import com.oracle.svm.shared.BuildPhaseProvider; import com.oracle.svm.shared.singletons.AutomaticallyRegisteredImageSingleton; import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; @@ -56,10 +55,12 @@ import com.oracle.svm.shared.singletons.traits.SingletonTraits; import com.oracle.svm.shared.util.VMError; import com.oracle.svm.shared.util.ReflectionUtil; +import com.oracle.svm.util.HeapConstantToConstantReplacer; import jdk.graal.compiler.debug.Assertions; import jdk.graal.compiler.debug.GraalError; import jdk.internal.loader.ClassLoaders; +import jdk.vm.ci.meta.JavaConstant; /** * This class stores information about which packages need to be stored within each ClassLoader. @@ -229,8 +230,11 @@ public void registerPackage(ClassLoader runtimeClassLoader, String packageName, } } - public ImageHeapConstant replaceWithPriorLayerPackage(Object obj) { + public JavaConstant replaceWithPriorLayerPackage(Object obj, boolean reachable) { if (obj instanceof Package hostedPackage) { + if (!reachable) { + return HeapConstantToConstantReplacer.CANNOT_REPLACE_MARKER; + } /* * Determine the package's ClassLoader */ @@ -240,7 +244,7 @@ public ImageHeapConstant replaceWithPriorLayerPackage(Object obj) { var keyName = generateKeyName(hostedPackage.getName()); if (registry.constantExists(keyName)) { - return (ImageHeapConstant) registry.getConstant(keyName); + return registry.getConstant(keyName); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java index 3fcc4a775bd87..be2f181e6a4bd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java @@ -93,6 +93,10 @@ public void afterRegistration(AfterRegistrationAccess access) { if (FutureDefaultsOptions.resourceBundlesInitializedAtRunTime()) { rci.initializeAtRunTime("java.text.BreakIterator", FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON); rci.initializeAtRunTime("java.text.BreakIterator$BreakIteratorCache", FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON); + + /* PBTI will find its static fields when resource bundles are initialized at run-time */ + rci.initializeAtRunTime("com.oracle.svm.core.jdk.localization.substitutions.Util_java_text_BreakIterator", + FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON); } rci.initializeAtBuildTime("java.time", JDK_CLASS_REASON); // see HijrahChronologyFeature for more details diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java index 8e143984dd25e..a65f489822bbb 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java @@ -33,8 +33,8 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; -import com.oracle.svm.shared.BuildPhaseProvider; -import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.graal.pointsto.ObjectScanner; +import com.oracle.graal.pointsto.heap.ImageHeapScanner; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.fieldvaluetransformer.JVMCIFieldValueTransformerWithAvailability; import com.oracle.svm.core.hub.DynamicHub; @@ -45,18 +45,26 @@ import com.oracle.svm.core.jfr.JfrJavaEvents; import com.oracle.svm.core.jfr.traceid.JfrTraceId; import com.oracle.svm.core.jfr.traceid.JfrTraceIdMap; -import com.oracle.svm.jvmci.shared.meta.SharedType; import com.oracle.svm.hosted.FeatureImpl; import com.oracle.svm.hosted.GuestTypes; import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport; import com.oracle.svm.hosted.reflect.ReflectionFeature; +import com.oracle.svm.jvmci.shared.meta.SharedType; +import com.oracle.svm.shared.BuildPhaseProvider; +import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.shared.util.VMError; import com.oracle.svm.util.GuestAccess; import com.oracle.svm.util.JVMCIReflectionUtil; import com.oracle.svm.util.JVMCIRuntimeClassInitializationSupport; +import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.dynamicaccess.JVMCIRuntimeReflection; import jdk.internal.event.Event; import jdk.jfr.internal.JVM; +import jdk.jfr.internal.JVMSupport; +import jdk.jfr.internal.MetadataRepository; +import jdk.jfr.internal.TypeLibrary; +import jdk.jfr.internal.event.EventConfiguration; import jdk.vm.ci.meta.JavaConstant; import jdk.vm.ci.meta.MetaAccessProvider; import jdk.vm.ci.meta.ResolvedJavaType; @@ -68,6 +76,8 @@ */ @AutomaticallyRegisteredFeature public class JfrEventFeature implements InternalFeature { + private JfrEventSubstitution eventSubstitution; + @Override public boolean isInConfiguration(IsInConfigurationAccess access) { return JfrFeature.isInConfiguration(false); @@ -97,7 +107,9 @@ public void duringSetup(DuringSetupAccess c) { String reason = "from feature " + JfrEventFeature.class + " with '" + eventSubClass.toJavaName(false) + "'"; JVMCIRuntimeClassInitializationSupport.singleton().initializeAtBuildTime(eventSubClass, reason); } - config.registerSubstitutionProcessor(new JfrEventSubstitution(metaAccess, config.getUniverse().getHeapScanner())); + + eventSubstitution = new JfrEventSubstitution(metaAccess); + config.registerSubstitutionProcessor(eventSubstitution); /* * The value of this field is set later in the beforeCompilation method after analysis @@ -116,6 +128,53 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) return originalValue; } }); + + ImageHeapScanner heapScanner = config.getUniverse().getHeapScanner(); + config.registerOnTypeCreatedCallback(type -> { + if (!eventSubstitution.needsClassRedefinition(type.getWrapped())) { + return; + } + VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished()); + + Class eventClass = OriginalClassProvider.getJavaClass(type).asSubclass(Event.class); + + EventConfiguration configuration = eventSubstitution.takeEventConfiguration(eventClass); + if (configuration != null) { + /* + * Set the previously created configuration back for this event class. + * This process allows JfrEventSubstitution be used outside the analysis, + * without affecting the image heap with extra configuration and type objects. + * These objects are only used, if an analysis type is created for said event class. + */ + JVMSupport.setConfiguration(eventClass, configuration); + MetadataRepository.getInstance().register(eventClass); + } + + /* + * The rest of the code was previously present in JfrEventSubstitution, + * but also required the analysis to be used for heap scanning. + * Now, registration and heap scanning is only performed after AnalysisType is created. + */ + JfrJavaEvents.registerEventClass(eventClass); + + configuration = JVMSupport.getConfiguration(eventClass); + if (!configuration.isRegistered()) { + /* Keep previous behavior, register events that are not registered*/ + configuration.platformEventType().setRegistered(true); + } + + ObjectScanner.ScanReason reason = new ObjectScanner.OtherReason("Manual rescan for " + eventClass + " triggered from " + JfrEventFeature.class); + heapScanner.rescanObject(configuration, reason); + }); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + /* Prune unregistered types that were added from + * the substitution processor, but did not access onTypeCreated part. + * This can for example be custom metadata type. + */ + TypeLibrary.clearUnregistered(); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java index 9fcc2ce2e104a..f74cae44e2f02 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java @@ -29,17 +29,15 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; -import com.oracle.graal.pointsto.ObjectScanner.OtherReason; -import com.oracle.graal.pointsto.ObjectScanner.ScanReason; -import com.oracle.graal.pointsto.heap.ImageHeapScanner; -import com.oracle.svm.util.OriginalClassProvider; import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor; -import com.oracle.svm.shared.BuildPhaseProvider; -import com.oracle.svm.core.jfr.JfrJavaEvents; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.OriginalClassProvider; import jdk.jfr.internal.JVM; +import jdk.jfr.internal.JVMSupport; import jdk.jfr.internal.MetadataRepository; +import jdk.jfr.internal.TypeLibrary; +import jdk.jfr.internal.event.EventConfiguration; import jdk.vm.ci.meta.MetaAccessProvider; import jdk.vm.ci.meta.ResolvedJavaField; import jdk.vm.ci.meta.ResolvedJavaMethod; @@ -52,20 +50,24 @@ */ @Platforms(Platform.HOSTED_ONLY.class) public class JfrEventSubstitution extends SubstitutionProcessor { - - private final ImageHeapScanner heapScanner; - private final ResolvedJavaType baseEventType; private final ConcurrentHashMap typeSubstitution; private final ConcurrentHashMap methodSubstitutions; private final ConcurrentHashMap fieldSubstitutions; - JfrEventSubstitution(MetaAccessProvider metaAccess, ImageHeapScanner heapScanner) { - this.heapScanner = heapScanner; + /** + * Event configurations stored for events that do not yet have + * the associated analysis type. {@link #takeEventConfiguration(Class)} + * is called in {@link JfrEventFeature} when analysis type is created. + */ + private final ConcurrentHashMap, EventConfiguration> eventConfigurations; + + JfrEventSubstitution(MetaAccessProvider metaAccess) { baseEventType = metaAccess.lookupJavaType(jdk.internal.event.Event.class); typeSubstitution = new ConcurrentHashMap<>(); methodSubstitutions = new ConcurrentHashMap<>(); fieldSubstitutions = new ConcurrentHashMap<>(); + eventConfigurations = new ConcurrentHashMap<>(); } @Override @@ -138,21 +140,52 @@ private static ResolvedJavaMethod initEventMethod(ResolvedJavaMethod oldMethod) } private Boolean initEventClass(ResolvedJavaType eventType) throws RuntimeException { - VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished()); try { Class newEventClass = OriginalClassProvider.getJavaClass(eventType).asSubclass(jdk.internal.event.Event.class); eventType.initialize(); - MetadataRepository.getInstance().register(newEventClass); + MetadataRepository repository = MetadataRepository.getInstance(); - JfrJavaEvents.registerEventClass(newEventClass); - // the reflection registration for the event handler field is delayed to the JfrFeature - // duringAnalysis callback so it does not race/interfere with other retransforms + EventConfiguration previousConfiguration = JVMSupport.getConfiguration(newEventClass); + boolean previouslyUnregistered = previousConfiguration != null && !previousConfiguration.isRegistered(); + repository.register(newEventClass); + + /* + * Retransformation requires the event to have a configuration, that is why register() is called. + * The instrumentation is required for the substitution processor, but instrumented methods + * reference the configuration that is cleared until AnalysisType is created to reduce number configurations + * included in the image heap when the processor is called outside of analysis. + */ JVM.retransformClasses(new Class[]{newEventClass}); - // make sure the EventConfiguration object is fully scanned - ScanReason reason = new OtherReason("Manual rescan for " + newEventClass + " triggered from " + JfrEventSubstitution.class); - heapScanner.rescanObject(JVM.getConfiguration(newEventClass), reason); + /* JDK internal events can still be emitted by the builder VM after retransformation. */ + if (previousConfiguration == null && jdk.jfr.Event.class.isAssignableFrom(newEventClass)) { + /* + * If the event configuration was not previously present, then the external + * effects are reverted - type is unregistered, removed and configuration is reset. + * + * This allows the substitution processor be used outside of analysis, but + * have correct field/method substitutions after instrumentation without + * affecting the image size with configurations for unused types. + * The only "side effect" of this approach, is that the class is still instrumented + * in the builder/host vm. + * + * When analysis type is created for this class, then configuration and type + * information (TypeLibrary) are re-added back in JfrEventFeature in a onTypeCreated event handler. + */ + EventConfiguration configuration = JVMSupport.getConfiguration(newEventClass); + eventConfigurations.put(newEventClass, configuration); + + repository.unregister(newEventClass.asSubclass(jdk.jfr.Event.class)); + TypeLibrary.removeType(configuration.platformEventType().getId()); + JVMSupport.setConfiguration(newEventClass, null); + } else if (previouslyUnregistered) { + /* + * Previous configuration was not registered, mark the current one as + * unregistered and only mark it as registered in JfrEventFeature after analysis type has been created. + */ + JVMSupport.getConfiguration(newEventClass).platformEventType().setRegistered(false); + } return Boolean.TRUE; } catch (Throwable ex) { @@ -160,7 +193,11 @@ private Boolean initEventClass(ResolvedJavaType eventType) throws RuntimeExcepti } } - private boolean needsClassRedefinition(ResolvedJavaType type) { + EventConfiguration takeEventConfiguration(Class eventClass) { + return eventConfigurations.remove(eventClass); + } + + boolean needsClassRedefinition(ResolvedJavaType type) { return !type.isAbstract() && baseEventType.isAssignableFrom(type) && !baseEventType.equals(type); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java index 1008c046f779c..7917171493bc6 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,14 +38,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.stream.Collectors; import org.graalvm.collections.EconomicSet; -import com.oracle.graal.pointsto.BigBang; -import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.graal.pointsto.phases.NoClassInitializationPlugin; +import com.oracle.graal.pointsto.util.AnalysisFuture; import com.oracle.svm.core.ParsingReason; import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; @@ -110,6 +110,14 @@ * scale and array index shift unsafe computations. */ public class AutomaticUnsafeTransformationSupport { + /** + * Field that was accessed by the initializer. + * + * @param targetField Field that was unsafely accessed during initialization + * @param reason Field that stores result of the unsafe access + */ + public record AccessedField(ResolvedJavaField targetField, ResolvedJavaField reason) { + } private static final int BASIC_LEVEL = 1; private static final int INFO_LEVEL = 2; @@ -139,6 +147,12 @@ static class Options { private final EconomicSet neverInlineSet = EconomicSet.create(); private final EconomicSet noCheckedExceptionsSet = EconomicSet.create(); + /** + * Map of unsafe transformations being computed asynchronously. + * The result is a list of fields that were accessed unsafely. + */ + private final ConcurrentHashMap>> transformationTasks = new ConcurrentHashMap<>(); + private final Plugins plugins; private final OptionValues options; @@ -268,10 +282,25 @@ public AutomaticUnsafeTransformationSupport(OptionValues options, AnnotationSubs suppressWarnings = List.of(originalMetaAccess.lookupJavaType(ReflectionUtil.lookupClass(false, "sun.security.provider.ByteArrayAccess"))); } - public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType hostType) { + /** + * Returns the task that installs automatic field value transformers for {@code hostType}. + * + * @return Fields that were unsafely accessed by the initializer. + */ + public AnalysisFuture> getOrComputeTransformations(SVMHost hostVM, ResolvedJavaType hostType) { + return transformationTasks.computeIfAbsent(hostType, + type -> new AnalysisFuture<>(() -> computeTransformations(hostVM, type))); + } + + /** + * Computes field transformations based on the initializer and returns a list of + * fields that were unsafely accessed. + */ + private List computeTransformations(SVMHost hostVM, ResolvedJavaType hostType) { if (hostType.isArray()) { - return; + return List.of(); } + if (!hostVM.getClassInitializationSupport().maybeInitializeAtBuildTime(hostType)) { /* * The class initializer of this type is executed at run time. The methods in Unsafe are @@ -285,15 +314,17 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType * perform the unsafe access registration. And if not, the user needs to provide a * proper manual configuration file. */ - return; + return List.of(); } if (annotationSubstitutions.findFullSubstitution(hostType).isPresent()) { /* If the class is substituted clinit will be eliminated, so bail early. */ reportSkippedTransformation(hostType); - return; + return List.of(); } + List accessedFields = new ArrayList<>(); + /* Detect field offset computation in static initializers. */ ResolvedJavaMethod clinit = hostType.getClassInitializer(); @@ -309,16 +340,16 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType for (Invoke invoke : clinitGraph.getInvokes()) { if (invoke.callTarget() instanceof MethodCallTargetNode) { if (isInvokeTo(invoke, unsafeStaticFieldBaseMethod)) { - processUnsafeFieldComputation(bb, hostType, invoke, StaticFieldBase); + processUnsafeFieldComputation(hostType, invoke, StaticFieldBase, accessedFields); } else if (isInvokeTo(invoke, unsafeObjectFieldOffsetFieldMethod) || isInvokeTo(invoke, sunMiscUnsafeObjectFieldOffsetMethod) || isInvokeTo(invoke, unsafeStaticFieldOffsetMethod)) { - processUnsafeFieldComputation(bb, hostType, invoke, FieldOffset); + processUnsafeFieldComputation(hostType, invoke, FieldOffset, accessedFields); } else if (isInvokeTo(invoke, unsafeObjectFieldOffsetClassStringMethod)) { - processUnsafeObjectFieldOffsetClassStringInvoke(bb, hostType, invoke); + processUnsafeObjectFieldOffsetClassStringInvoke(hostType, invoke, accessedFields); } else if (isInvokeTo(invoke, unsafeArrayBaseOffsetMethod)) { - processUnsafeArrayBaseOffsetInvoke(bb, hostType, invoke); + processUnsafeArrayBaseOffsetInvoke(hostType, invoke); } else if (isInvokeTo(invoke, unsafeArrayIndexScaleMethod)) { - processUnsafeArrayIndexScaleInvoke(bb, hostType, invoke, clinitGraph); + processUnsafeArrayIndexScaleInvoke(hostType, invoke, clinitGraph); } } } @@ -328,6 +359,7 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType } } + return accessedFields; } /** @@ -341,7 +373,7 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType *

* static final long fieldOffset = Unsafe.getUnsafe().staticFieldBase(X.class.getDeclaredField("f")); */ - private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, Invoke invoke, Kind kind) { + private void processUnsafeFieldComputation(ResolvedJavaType type, Invoke invoke, Kind kind, List accessedFields) { List> unsuccessfulReasons = new ArrayList<>(); ResolvedJavaField targetField = null; @@ -357,7 +389,7 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In } else { unsuccessfulReasons.add(() -> "The argument of " + methodFormat + " is not a constant value or a field load that can be constant-folded."); } - processUnsafeFieldComputation(bb, type, invoke, kind, unsuccessfulReasons, targetField); + processUnsafeFieldComputation(type, invoke, kind, unsuccessfulReasons, targetField, accessedFields); } /** @@ -417,7 +449,7 @@ private boolean isValidField(Invoke invoke, ResolvedJavaField field, List static final long fieldOffset = Unsafe.getUnsafe().objectFieldOffset(X.class, "f"); */ - private void processUnsafeObjectFieldOffsetClassStringInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeObjectFieldOffsetInvoke) { + private void processUnsafeObjectFieldOffsetClassStringInvoke(ResolvedJavaType type, Invoke unsafeObjectFieldOffsetInvoke, List accessedFields) { List> unsuccessfulReasons = new ArrayList<>(); ResolvedJavaType targetFieldHolder = null; @@ -452,10 +484,11 @@ private void processUnsafeObjectFieldOffsetClassStringInvoke(BigBang bb, Resolve unsuccessfulReasons.add(() -> "The arguments of Unsafe.objectFieldOffset(Class, String) do not reference an existing field."); } } - processUnsafeFieldComputation(bb, type, unsafeObjectFieldOffsetInvoke, FieldOffset, unsuccessfulReasons, targetField); + processUnsafeFieldComputation(type, unsafeObjectFieldOffsetInvoke, FieldOffset, unsuccessfulReasons, targetField, accessedFields); } - private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, Invoke invoke, Kind kind, List> unsuccessfulReasons, ResolvedJavaField targetField) { + private void processUnsafeFieldComputation(ResolvedJavaType type, Invoke invoke, Kind kind, List> unsuccessfulReasons, ResolvedJavaField targetField, + List accessedFields) { assert kind == FieldOffset || kind == StaticFieldBase; /* * If the value returned by the call to Unsafe.objectFieldOffset() is stored into a field @@ -474,7 +507,11 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In * transformation. */ if (targetField != null && valueStoreField != null) { - if (tryAutomaticTransformation(bb, valueStoreField, kind, null, targetField)) { + if (tryAutomaticTransformation(valueStoreField, kind, null, targetField)) { + if (kind == FieldOffset) { + accessedFields.add(new AccessedField(targetField, valueStoreField)); + } + reportSuccessfulAutomaticRecomputation(kind, valueStoreField, targetField.getDeclaringClass().getName() + "." + targetField.getName()); } } else { @@ -488,7 +525,7 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In * * static final long arrayBaseOffsets = Unsafe.getUnsafe().arrayBaseOffset(byte[].class); */ - private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayBaseOffsetInvoke) { + private void processUnsafeArrayBaseOffsetInvoke(ResolvedJavaType type, Invoke unsafeArrayBaseOffsetInvoke) { List> unsuccessfulReasons = new ArrayList<>(); ResolvedJavaType arrayType = null; @@ -509,7 +546,7 @@ private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType typ ResolvedJavaField offsetField = result.valueStoreField; if (arrayType != null && offsetField != null) { ResolvedJavaType finalArrayType = arrayType; - if (tryAutomaticTransformation(bb, offsetField, ArrayBaseOffset, finalArrayType, null)) { + if (tryAutomaticTransformation(offsetField, ArrayBaseOffset, finalArrayType, null)) { reportSuccessfulAutomaticRecomputation(ArrayBaseOffset, offsetField, arrayType.toClassName()); } } else { @@ -526,7 +563,7 @@ private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType typ * * static final long byteArrayIndexScale = Unsafe.getUnsafe().arrayIndexScale(byte[].class); */ - private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayIndexScale, StructuredGraph clinitGraph) { + private void processUnsafeArrayIndexScaleInvoke(ResolvedJavaType type, Invoke unsafeArrayIndexScale, StructuredGraph clinitGraph) { List> unsuccessfulReasons = new ArrayList<>(); ResolvedJavaType arrayType = null; @@ -550,18 +587,18 @@ private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType typ if (arrayType != null) { if (indexScaleField != null) { - if (tryAutomaticTransformation(bb, indexScaleField, ArrayIndexScale, arrayType, null)) { + if (tryAutomaticTransformation(indexScaleField, ArrayIndexScale, arrayType, null)) { reportSuccessfulAutomaticRecomputation(ArrayIndexScale, indexScaleField, arrayType.toClassName()); indexScaleComputed = true; /* Try transformation for the array index shift computation if present. */ - indexShiftComputed = processArrayIndexShiftFromField(bb, type, indexScaleField, arrayType, clinitGraph); + indexShiftComputed = processArrayIndexShiftFromField(type, indexScaleField, arrayType, clinitGraph); } } else { /* * The index scale is not stored into a field, it might be used to compute the index * shift. */ - indexShiftComputed = processArrayIndexShiftFromLocal(bb, type, unsafeArrayIndexScale, arrayType); + indexShiftComputed = processArrayIndexShiftFromLocal(type, unsafeArrayIndexScale, arrayType); } } if (!indexScaleComputed && !indexShiftComputed) { @@ -591,7 +628,7 @@ private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType typ * It is important that constant folding is not enabled for the byteArrayIndexScale load because * it would break the link between the scale and shift computations. */ - private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType type, ResolvedJavaField indexScaleField, ResolvedJavaType arrayType, StructuredGraph clinitGraph) { + private boolean processArrayIndexShiftFromField(ResolvedJavaType type, ResolvedJavaField indexScaleField, ResolvedJavaType arrayType, StructuredGraph clinitGraph) { for (LoadFieldNode load : clinitGraph.getNodes().filter(LoadFieldNode.class)) { if (load.field().equals(indexScaleField)) { /* @@ -599,7 +636,7 @@ private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType typ * field was already found. The case where both scale and shift are computed is * uncommon. */ - if (processArrayIndexShift(bb, type, arrayType, load, true)) { + if (processArrayIndexShift(type, arrayType, load, true)) { /* * Return true as soon as an index shift computed from the index scale field is * found. It is very unlikely that there are multiple shift computations from @@ -628,15 +665,15 @@ private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType typ * } * */ - private boolean processArrayIndexShiftFromLocal(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayIndexScale, ResolvedJavaType arrayType) { + private boolean processArrayIndexShiftFromLocal(ResolvedJavaType type, Invoke unsafeArrayIndexScale, ResolvedJavaType arrayType) { /* Try to compute index shift. Report errors since the index scale field was not found. */ - return processArrayIndexShift(bb, type, arrayType, unsafeArrayIndexScale.asNode(), false); + return processArrayIndexShift(type, arrayType, unsafeArrayIndexScale.asNode(), false); } /** * Try to compute the arrayIndexShift. Return true if successful, false otherwise. */ - private boolean processArrayIndexShift(BigBang bb, ResolvedJavaType type, ResolvedJavaType arrayType, ValueNode indexScaleValue, boolean silentFailure) { + private boolean processArrayIndexShift(ResolvedJavaType type, ResolvedJavaType arrayType, ValueNode indexScaleValue, boolean silentFailure) { NodeIterable loadMethodCallTargetUsages = indexScaleValue.usages().filter(MethodCallTargetNode.class); for (MethodCallTargetNode methodCallTarget : loadMethodCallTargetUsages) { /* Iterate over all the calls that use the index scale value. */ @@ -669,7 +706,7 @@ private boolean processArrayIndexShift(BigBang bb, ResolvedJavaType type, Resolv } if (indexShiftField != null) { - if (tryAutomaticTransformation(bb, indexShiftField, ArrayIndexShift, arrayType, null)) { + if (tryAutomaticTransformation(indexShiftField, ArrayIndexShift, arrayType, null)) { reportSuccessfulAutomaticRecomputation(ArrayIndexShift, indexShiftField, arrayType.toClassName()); return true; } @@ -880,7 +917,7 @@ private boolean isAllowedUnsafeValueSink(Node valueNodeUsage) { * Try to register the automatic transformation for a field. Bail if the field was deleted or a * conflicting substitution is detected. */ - private boolean tryAutomaticTransformation(BigBang bb, ResolvedJavaField field, RecomputeFieldValue.Kind kind, ResolvedJavaType targetType, ResolvedJavaField targetField) { + private boolean tryAutomaticTransformation(ResolvedJavaField field, RecomputeFieldValue.Kind kind, ResolvedJavaType targetType, ResolvedJavaField targetField) { if (annotationSubstitutions.isDeleted(field)) { String conflictingSubstitution = "The field " + field.format("%H.%n") + " is marked as deleted. "; reportConflictingSubstitution(field, kind, conflictingSubstitution); @@ -931,19 +968,11 @@ private boolean tryAutomaticTransformation(BigBang bb, ResolvedJavaField field, } } - if (kind == FieldOffset) { - bb.postTask(_ -> toAnalysisField(bb, targetField).registerAsUnsafeAccessed(field)); - } singleton().registerFieldValueTransformer(field, newTransformer); return true; } } - private static AnalysisField toAnalysisField(BigBang bb, ResolvedJavaField targetField) { - VMError.guarantee(!(targetField instanceof AnalysisField), "AnalysisField not allowed: %s", targetField); - return bb.getUniverse().lookup(targetField); - } - private static void reportSkippedTransformation(ResolvedJavaType type) { if (Options.AutomaticUnsafeTransformationLogLevel.getValue() >= DEBUG_LEVEL) { LogUtils.warning("Skipped automatic unsafe transformation analysis for type " + type.getName() + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java index 76fab69267c33..78bf8ba2e1567 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,7 +32,6 @@ import org.graalvm.nativeimage.hosted.FieldValueTransformer; import com.oracle.graal.pointsto.heap.TypedConstant; -import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.svm.core.fieldvaluetransformer.JavaConstantWrapper; import com.oracle.svm.core.util.UserError; import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport; @@ -41,6 +40,7 @@ import com.oracle.svm.util.GuestAccess; import com.oracle.svm.util.JVMCIFieldValueTransformer; import com.oracle.svm.util.JVMCIReflectionUtil; +import com.oracle.svm.util.InterceptedField; import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.OriginalFieldProvider; @@ -72,7 +72,7 @@ public JVMCIFieldValueTransformer getFieldValueTransformer() { return fieldValueTransformer; } - public JavaConstant readValue(AnalysisField field, JavaConstant receiver) { + public JavaConstant readValue(InterceptedField field, JavaConstant receiver) { ReadLock readLock = valueCacheLock.readLock(); try { readLock.lock(); @@ -107,7 +107,7 @@ public JavaConstant readValue(AnalysisField field, JavaConstant receiver) { } } - private JavaConstant computeValue(AnalysisField field, JavaConstant receiver) { + private JavaConstant computeValue(InterceptedField field, JavaConstant receiver) { var originalValue = fetchOriginalValue(field, receiver); VMError.guarantee(originalValue != null, "Original value must not be `null`. Use `JavaConstant.NULL_POINTER`instead"); @@ -119,7 +119,7 @@ private JavaConstant computeValue(AnalysisField field, JavaConstant receiver) { return result; } - private void checkValue(JavaConstant newValue, AnalysisField field) { + private void checkValue(JavaConstant newValue, InterceptedField field) { if (newValue == null) { throw UserError.abort("JVMCIFieldValueTransformer must not return `null`. Use `JavaConstant.NULL_POINTER`instead"); } @@ -181,7 +181,8 @@ private JavaConstant getUnboxedConstant(JavaConstant newValue) { return newValue; } - protected JavaConstant fetchOriginalValue(AnalysisField aField, JavaConstant receiver) { + protected JavaConstant fetchOriginalValue(InterceptedField aField, JavaConstant receiver) { + /* InterceptedField can either be AnalysisField or PredictableBuildTimeInitializationField */ ResolvedJavaField oField = OriginalFieldProvider.getOriginalField(aField); if (oField == null) { return JavaConstant.NULL_POINTER; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java index 9b9e2aaa318f9..7413e9adbb609 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java @@ -29,6 +29,7 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse; import com.oracle.graal.pointsto.ObjectScanner.OtherReason; import com.oracle.graal.pointsto.ObjectScanner.ScanReason; @@ -89,6 +90,44 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue) } } + private final class NGroupsFieldValueTransformer implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { + // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus + + /* + * We must wait until reachableThreadGroups stabilizes after analysis to replace this + * value. + */ + @Override + public boolean isAvailable() { + return BuildPhaseProvider.isHostedUniverseBuilt(); + } + + @Override + public Object transform(Object receiver, Object originalValue) { + ThreadGroup group = (ThreadGroup) receiver; + return reachableThreadGroups.get(group).ngroups; + } + } + + private final class GroupsFieldValueTransformer implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse { + // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus + + /* + * We must wait until reachableThreadGroups stabilizes after analysis to replace this + * value. + */ + @Override + public boolean isAvailable() { + return BuildPhaseProvider.isHostedUniverseBuilt(); + } + + @Override + public Object transform(Object receiver, Object originalValue) { + ThreadGroup group = (ThreadGroup) receiver; + return reachableThreadGroups.get(group).groups; + } + } + /** * All {@link Thread} objects that are reachable in the image heap. Only unstarted threads, * i.e., threads in state NEW, are allowed. @@ -118,43 +157,8 @@ public void duringSetup(DuringSetupAccess a) { @Override public void beforeAnalysis(BeforeAnalysisAccess a) { - a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "ngroups"), new FieldValueTransformerWithAvailability() { - // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus - - /* - * We must wait until reachableThreadGroups stabilizes after analysis to replace this - * value. - */ - @Override - public boolean isAvailable() { - return BuildPhaseProvider.isHostedUniverseBuilt(); - } - - @Override - public Object transform(Object receiver, Object originalValue) { - ThreadGroup group = (ThreadGroup) receiver; - return reachableThreadGroups.get(group).ngroups; - } - }); - - a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "groups"), new FieldValueTransformerWithAvailability() { - // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus - - /* - * We must wait until reachableThreadGroups stabilizes after analysis to replace this - * value. - */ - @Override - public boolean isAvailable() { - return BuildPhaseProvider.isHostedUniverseBuilt(); - } - - @Override - public Object transform(Object receiver, Object originalValue) { - ThreadGroup group = (ThreadGroup) receiver; - return reachableThreadGroups.get(group).groups; - } - }); + a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "ngroups"), new NGroupsFieldValueTransformer()); + a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "groups"), new GroupsFieldValueTransformer()); FeatureImpl.BeforeAnalysisAccessImpl access = (FeatureImpl.BeforeAnalysisAccessImpl) a; registerDeferredThreadField(access, "java.util.concurrent.locks.AbstractOwnableSynchronizer", "exclusiveOwnerThread"); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java index 7d2bfeb221db2..402375a954b5a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -81,15 +81,22 @@ private LayeredVMThreadLocalCollector(Map threadLocalAssignm } @Override - public Object apply(Object source) { - /* Make sure all names have been assigned in the prior layers. */ + public Object replaceObject(Object source, boolean reachable) { + /* + * Make sure all names have been assigned in the prior layers + */ if (!initialLayer) { if (source instanceof FastThreadLocal threadLocal) { + if (!reachable) { + /* Unreachable, current layer fast thread local can be found by pbti */ + return CANNOT_REPLACE_MARKER; + } + var name = threadLocal.getName(); VMError.guarantee(threadLocalAssignmentMap.containsKey(name), "Found thread local which was not created in the initial layer %s", name); } } - return super.apply(source); + return super.replaceObject(source, reachable); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java index cc61dab813da8..f2702ce359996 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java @@ -33,7 +33,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; import com.oracle.svm.shared.config.ObjectLayout; import com.oracle.svm.core.config.ObjectLayoutCompilerSupport; @@ -47,6 +46,7 @@ import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks; import com.oracle.svm.shared.singletons.traits.SingletonTraits; import com.oracle.svm.shared.util.VMError; +import com.oracle.svm.util.HeapObjectReplacer; import jdk.graal.compiler.core.common.NumUtil; import jdk.graal.compiler.nodes.PiNode; @@ -59,7 +59,7 @@ * Collects all {@link FastThreadLocal} instances that are actually used by the application. */ @SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class) -public class VMThreadLocalCollector implements Function, VMThreadLocalOffsetProvider { +public class VMThreadLocalCollector extends HeapObjectReplacer implements VMThreadLocalOffsetProvider { private static final int LARGE_ENTRY_SIZE = 16; @@ -115,8 +115,8 @@ public VMThreadLocalInfo forFastThreadLocal(FastThreadLocal threadLocal) { } @Override - public Object apply(Object source) { - if (source instanceof FastThreadLocal fastThreadLocal) { + public Object replaceObject(Object source, boolean reachable) { + if (reachable && source instanceof FastThreadLocal fastThreadLocal) { forFastThreadLocal(fastThreadLocal); } /* diff --git a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java index 725dcdacae7e2..7bf9ac7eb463d 100644 --- a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java +++ b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java @@ -51,4 +51,6 @@ static FutureDefaultsOptionsSupport singleton() { boolean exactReflection(); boolean explicitFeatureSingletonRegistration(); + + boolean predictableBuildTimeInitialization(); } diff --git a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java index 667972b9f6018..313f1578c06f3 100644 --- a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java +++ b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -44,6 +44,8 @@ import org.graalvm.collections.EconomicSet; import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.impl.AnnotationExtractor; import org.graalvm.nativeimage.impl.ImageSingletonsSupport; @@ -298,7 +300,13 @@ static SingletonTraitMap getAnnotatedTraits(Class singletonClass, AnnotationE /** * Management of the {@link ImageSingletons} registry during image generation. + * + *

+ * The {@code PredictableBuildTimeInitializationFeature} can find + * a violation inside this class, marking it as HOSTED_ONLY skips it. + *

*/ + @Platforms(Platform.HOSTED_ONLY.class) public static final class HostedManagement { /** diff --git a/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java b/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java index b938320ba1008..ea6a884d32b83 100644 --- a/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java +++ b/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java @@ -118,7 +118,13 @@ public final class SubstrateTruffleRuntime extends OptimizedTruffleRuntime { private static final int DEBUG_TEAR_DOWN_TIMEOUT = 2_000; private static final int PRODUCTION_TEAR_DOWN_TIMEOUT = 10_000; - private KnownMethods hostedCallMethods; + /** + * Can be found via com.oracle.truffle.api.Truffle.RUNTIME.hostedCallMethods + * in pbti and an exception thrown via {@link com.oracle.svm.graal.hosted.runtimecompilation.GraalGraphObjectReplacer}: + * "ResolvedJavaMethod that is not a subtype of SharedRuntimeMethod is not allowed at runtime". + */ + @Platforms(Platform.HOSTED_ONLY.class) private KnownMethods hostedCallMethods; + private volatile BackgroundCompileQueue compileQueue; private volatile boolean initialized; private volatile Boolean profilingEnabled; diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java new file mode 100644 index 0000000000000..5d9257ec30ddf --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.util; + +import java.util.function.Function; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.shared.util.VMError; + +import jdk.vm.ci.meta.JavaConstant; + +/** + * A constant-to-constant replacer with restrictions on internal operations that expand the + * analysis/hosted universe or image heap, + * such as creating new universe types, methods or fields, modifying existing ones, + * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList} + * or {@code ImageHeapMap}. + * + * @param always must be ImageHeapConstant, the {@code com.oracle.svm.util} cannot access it directly. + */ +@Platforms(Platform.HOSTED_ONLY.class) +public abstract class HeapConstantToConstantReplacer implements Function { + /** + * Marker returned when replacement requires a reachable object. + * Same as {@link HeapObjectConstantReplacer#CANNOT_REPLACE_MARKER}, because + * further processing can use the same condition. + */ + public static final JavaConstant CANNOT_REPLACE_MARKER = HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER; + + @Override + public final T apply(JavaConstant constant) { + throw VMError.intentionallyUnimplemented(); + } + + /** + * @param constant constant to replace + * @param reachable whether the object is reachable and is being replaced for the image heap; + * when false, the internal operations described above must be avoided + * @return the replacement, {@code null} if this replacer does not apply, or + * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object + */ + public abstract JavaConstant replaceConstant(JavaConstant constant, boolean reachable); +} diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java new file mode 100644 index 0000000000000..d4a8058f4380b --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.util; + +import java.util.function.Function; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.shared.util.VMError; + +/** + * Extends functionality of object replacers to specify different + * replacement if the output of the replacer is meant to be stored + * in the image heap, or expand the analysis/hosted universe through internal operations. + * + *

+ * Expected behavior of {@link #replaceConstant(JavaConstant, boolean)} when {@code reachable} is {@code false} is + * that it does not expand the analysis/hosted universe or image heap through internal operations, + * such as creating new universe types, methods or fields, modifying existing ones, + * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList} + * or {@code ImageHeapMap}. + *

+ * + *

+ * Returns {@link #CANNOT_REPLACE_MARKER}, if {@code reachable} is false and the replacer cannot + * replace the object without using aforementioned internal operations. + *

+ */ +@Platforms(Platform.HOSTED_ONLY.class) +public abstract class HeapObjectConstantReplacer implements Function { + /** + * This marker is used when the replacer cannot reliably replace + * the object without using aforementioned internal operations. + */ + public static final JavaConstant CANNOT_REPLACE_MARKER = new JavaConstant() { + @Override + public JavaKind getJavaKind() { + return null; + } + + @Override + public boolean isNull() { + return false; + } + + @Override + public boolean isDefaultForKind() { + return false; + } + + @Override + public Object asBoxedPrimitive() { + return null; + } + + @Override + public int asInt() { + return 0; + } + + @Override + public boolean asBoolean() { + return false; + } + + @Override + public long asLong() { + return 0; + } + + @Override + public float asFloat() { + return 0; + } + + @Override + public double asDouble() { + return 0; + } + }; + + @Override + public final JavaConstant apply(JavaConstant constant) { + throw VMError.intentionallyUnimplemented(); + } + + /** + * Replace an object, optionally before it reaches the image heap. When {@code reachable} is + * true, this method may perform internal operations that expand the universe or image heap, + * such as adding entries to an {@code ImageHeapList}. + * + * @param constant that is replaced + * @param reachable whether the object is reachable and is being replaced for the image heap, + * when false, the internal operations described above must be avoided + * @return new object if the old one is replaced, the old object if it is unchanged, or + * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object + */ + public abstract JavaConstant replaceConstant(JavaConstant constant, boolean reachable); +} diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java new file mode 100644 index 0000000000000..23006c4191b6a --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.util; + +import java.util.function.Function; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.shared.util.VMError; + +/** + * Extends functionality of object replacers to specify different + * replacement if the output of the replacer is meant to be stored + * in the image heap, or expand the analysis/hosted universe through internal operations. + * + *

+ * Expected behavior of {@link #replaceObject(Object, boolean)} when {@code reachable} is {@code false} is + * that it does not expand the analysis/hosted universe or image heap through internal operations, + * such as creating new universe types, methods or fields, modifying existing ones, + * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList} + * or {@code ImageHeapMap}. + *

+ * + *

+ * Returns {@link #CANNOT_REPLACE_MARKER}, if {@code reachable} is false and the replacer cannot + * replace the object without using aforementioned mentioned internal operations. + *

+ */ +@Platforms(Platform.HOSTED_ONLY.class) +public abstract class HeapObjectReplacer implements Function { + /** + * This marker is used when the replacer cannot reliably replace + * the object without using aforementioned internal operations. + */ + public static final Object CANNOT_REPLACE_MARKER = new Object(); + + @Override + public final Object apply(Object object) { + throw VMError.intentionallyUnimplemented(); + } + + /** + * Replace an object, optionally before it reaches the image heap. When {@code reachable} is + * true, this method may perform internal operations that expand the universe or image heap, + * such as adding entries to an {@code ImageHeapList}. + * + * @param object that is replaced + * @param reachable whether the object is reachable and is being replaced for the image heap, + * when false, the internal operations described above must be avoided + * @return new object if the old one is replaced, the old object if it is unchanged, or + * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object + */ + public abstract Object replaceObject(Object object, boolean reachable); +} diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java new file mode 100644 index 0000000000000..62cfada986457 --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.util; + +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ModifiersProvider; +import jdk.vm.ci.meta.ResolvedJavaField; + +/** + * Common methods required by the {@code FieldValueInterceptionSupport} for a field. + */ +public interface InterceptedField extends ResolvedJavaField, OriginalFieldProvider, ModifiersProvider { + + Object getFieldValueInterceptor(); + + void setFieldValueInterceptor(Object fieldValueInterceptor); + + void beforeFieldValueAccess(); + + JavaKind getStorageKind(); + + @Override + InterceptedFieldType getType(); + + @Override + InterceptedFieldType getDeclaringClass(); +} diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java new file mode 100644 index 0000000000000..4e870069521bf --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.util; + +import jdk.graal.compiler.debug.Assertions; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.word.WordBase; + +/** + * Common methods required by the {@code FieldValueInterceptionSupport} for the type of field. + */ +public interface InterceptedFieldType extends ResolvedJavaType, OriginalClassProvider { + JavaKind getStorageKind(); + + /** + * Expands the {@code isConcrete} method, while maintaining + * previous compatibility, taken from {@code CustomSubstitutionType}. + */ + default boolean isConcreteType() { + return isArray() || (isInstanceClass() && !isAbstract()); + } + + /** + * Returns true if this type is part of the word type hierarchy, i.e, implements + * {@link WordBase}. + */ + default boolean isWordType() { + /* Word types are currently the only types where kind and storageKind differ. */ + boolean wordType = getJavaKind() != getStorageKind(); + assert !wordType || getJavaKind().isObject() : Assertions.errorMessage("Only words are expected to have a discrepancy between java kind and storage kind", this); + return wordType; + } +} diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java index d1ad86d7996d2..735b5b9d5eecd 100644 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java +++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java @@ -48,8 +48,10 @@ import com.oracle.svm.shared.NeverInline; import com.oracle.svm.core.OS; import com.oracle.svm.shared.util.SubstrateUtil; +import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.AnnotateOriginal; import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.jdk.Resources; @@ -291,6 +293,9 @@ private void runContinuation() { @SuppressWarnings("unused") final class Target_java_lang_System_Web { + /* Do not retain a Console instance connected to the image builder. */ + @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) private static volatile Console cons; + @Substitute @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static long currentTimeMillis() {