From e69b8a71be11b1ec0f7fc7aed969ae82db48bf54 Mon Sep 17 00:00:00 2001 From: Christian Wimmer Date: Thu, 22 Jun 2023 09:28:18 -0700 Subject: [PATCH] Simulate class initializer --- .../compiler/core/test/VerifyDebugUsage.java | 2 + substratevm/CHANGELOG.md | 1 + substratevm/mx.substratevm/mx_substratevm.py | 34 +- .../oracle/graal/pointsto/ObjectScanner.java | 2 +- .../heap/ImageHeapPrimitiveArray.java | 33 +- .../graal/pointsto/heap/ImageHeapScanner.java | 6 +- .../graal/pointsto/meta/AnalysisType.java | 7 + .../graal/pointsto/meta/AnalysisUniverse.java | 4 + .../InlineBeforeAnalysisGraphDecoder.java | 2 +- .../svm/hosted/NativeImageGenerator.java | 3 + .../src/com/oracle/svm/hosted/SVMHost.java | 17 +- .../AnalysisConstantReflectionProvider.java | 48 +- .../svm/hosted/ameta/ReadableJavaField.java | 2 +- .../analysis/DynamicHubInitializer.java | 11 +- .../analysis/NativeImagePointsToAnalysis.java | 2 +- ...NativeImageReachabilityAnalysisEngine.java | 2 +- .../flow/SVMMethodTypeFlowBuilder.java | 2 +- .../ClassInitializationFeature.java | 47 +- .../ClassInitializationOptions.java | 15 + .../ClassInitializationSupport.java | 33 +- .../ProvenSafeClassInitializationSupport.java | 2 +- ...imulateClassInitializerAbortException.java | 55 + .../SimulateClassInitializerCluster.java | 48 + ...SimulateClassInitializerClusterMember.java | 62 ++ ...ClassInitializerConstantFieldProvider.java | 53 + .../SimulateClassInitializerGraphDecoder.java | 761 ++++++++++++++ .../SimulateClassInitializerPolicy.java | 160 +++ .../SimulateClassInitializerResult.java | 57 + .../SimulateClassInitializerStatus.java | 56 + .../SimulateClassInitializerSupport.java | 614 +++++++++++ .../TypeInitializerGraph.java | 2 +- .../svm/hosted/heap/SVMImageHeapScanner.java | 8 +- .../HostedConstantReflectionProvider.java | 2 +- .../oracle/svm/hosted/meta/HostedType.java | 13 +- .../meta/SharedConstantFieldProvider.java | 6 +- .../InlineBeforeAnalysisGraphDecoderImpl.java | 106 ++ .../hosted/snippets/ReflectionPlugins.java | 4 +- .../AnnotationSubstitutionProcessor.java | 2 +- .../UnsafeAutomaticSubstitutionProcessor.java | 2 +- .../test/clinit/TestClassInitialization.java | 988 ++++++++++++++++++ ...estClassInitializationMustBeSafeEarly.java | 701 ------------- 41 files changed, 3173 insertions(+), 802 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerAbortException.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerCluster.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerClusterMember.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerConstantFieldProvider.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerPolicy.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerResult.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerStatus.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java create mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitialization.java delete mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitializationMustBeSafeEarly.java diff --git a/compiler/src/jdk.internal.vm.compiler.test/src/org/graalvm/compiler/core/test/VerifyDebugUsage.java b/compiler/src/jdk.internal.vm.compiler.test/src/org/graalvm/compiler/core/test/VerifyDebugUsage.java index 2cb2b4113c40..e9320a5e0954 100644 --- a/compiler/src/jdk.internal.vm.compiler.test/src/org/graalvm/compiler/core/test/VerifyDebugUsage.java +++ b/compiler/src/jdk.internal.vm.compiler.test/src/org/graalvm/compiler/core/test/VerifyDebugUsage.java @@ -122,6 +122,8 @@ protected void verify(StructuredGraph graph, CoreProviders context) { "org.graalvm.compiler.core.test.VerifyDebugUsageTest$InvalidDumpUsagePhase.run", "org.graalvm.compiler.hotspot.SymbolicSnippetEncoder.verifySnippetEncodeDecode", "com.oracle.graal.pointsto.phases.InlineBeforeAnalysis.decodeGraph", + "com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport.decodeGraph", + "com.oracle.svm.hosted.classinitialization.SimulateClassInitializerAbortException.doAbort", "org.graalvm.compiler.truffle.compiler.phases.inlining.CallTree.dumpBasic", "org.graalvm.compiler.truffle.compiler.phases.inlining.GraphManager.peRoot")); diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 2e21fb8ad38d..090044d8106f 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -6,6 +6,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-35746) Lower the default aligned chunk size from 1 MB to 512 KB for the serial and epsilon GCs, reducing memory usage and image size in many cases. * (GR-45841) BellSoft added support for the JFR event ThreadCPULoad. * (GR-45994) Removed the option `-H:EnableSignalAPI`. Please use the runtime option `EnableSignalHandling` if it is necessary to enable or disable signal handling explicitly. +* (GR-39406) Simulation of class initializer: Class initializer of classes that are not marked for initialization at image build time are simulated at image build time to avoid executing them at image run time. ## Version 23.0.0 * (GR-40187) Report invalid use of SVM specific classes on image class- or module-path as error. As a temporary workaround, `-H:+AllowDeprecatedBuilderClassesOnImageClasspath` allows turning the error into a warning. diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 6d70048e4776..6256226986f0 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -1397,7 +1397,11 @@ def cinterfacetutorial(args): @mx.command(suite.name, 'clinittest', 'Runs the ') def clinittest(args): - def build_and_test_clinittest_image(native_image, args=None): + def build_and_test_clinittest_images(native_image, args=None): + build_and_test_clinittest_image(native_image, args, True) + build_and_test_clinittest_image(native_image, args, False) + + def build_and_test_clinittest_image(native_image, args, new_class_init_policy): args = [] if args is None else args test_cp = classpath('com.oracle.svm.test') build_dir = join(svmbuild_dir(), 'clinittest') @@ -1407,11 +1411,17 @@ def build_and_test_clinittest_image(native_image, args=None): remove_tree(build_dir) mkpath(build_dir) + if new_class_init_policy: + policy_args = ['-H:+UseNewExperimentalClassInitialization', '-H:+SimulateClassInitializer', '-H:Features=com.oracle.svm.test.clinit.TestClassInitializationFeatureNewPolicyFeature'] + else: + policy_args = ['-H:-UseNewExperimentalClassInitialization', '-H:-SimulateClassInitializer', '-H:Features=com.oracle.svm.test.clinit.TestClassInitializationFeatureOldPolicyFeature'] + # Build and run the example native_image( - ['-H:Path=' + build_dir, '-cp', test_cp, '-H:Class=com.oracle.svm.test.clinit.TestClassInitializationMustBeSafeEarly', - '-H:Features=com.oracle.svm.test.clinit.TestClassInitializationMustBeSafeEarlyFeature', - '-H:+PrintClassInitialization', '-H:Name=clinittest', '-H:+ReportExceptionStackTraces'] + args) + ['-H:Path=' + build_dir, '-cp', test_cp, '-H:Class=com.oracle.svm.test.clinit.TestClassInitialization', + '-J--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '-J-ea', '-J-esa', + '-H:+PrintClassInitialization', '-H:Name=clinittest', '-H:+ReportExceptionStackTraces'] + policy_args + args) mx.run([join(build_dir, 'clinittest')]) # Check the reports for initialized classes @@ -1425,9 +1435,17 @@ def checkLine(line, marker, init_kind, msg, wrongly_initialized_lines): "Classes marked with " + marker + " must have init kind " + init_kind + " and message " + msg)] with open(classes_file) as f: for line in f: - checkLine(line, "MustBeDelayed", "RUN_TIME", "classes are initialized at run time by default", wrongly_initialized_lines) - checkLine(line, "MustBeSafeEarly", "BUILD_TIME", "class proven as side-effect free before analysis", wrongly_initialized_lines) - checkLine(line, "MustBeSafeLate", "BUILD_TIME", "class proven as side-effect free after analysis", wrongly_initialized_lines) + if new_class_init_policy: + checkLine(line, "MustBeSafeEarly", "SIMULATED", "classes are initialized at run time by default", wrongly_initialized_lines) + checkLine(line, "MustBeSafeLate", "SIMULATED", "classes are initialized at run time by default", wrongly_initialized_lines) + checkLine(line, "MustBeSimulated", "SIMULATED", "classes are initialized at run time by default", wrongly_initialized_lines) + checkLine(line, "MustBeDelayed", "RUN_TIME", "classes are initialized at run time by default", wrongly_initialized_lines) + else: + checkLine(line, "MustBeSafeEarly", "BUILD_TIME", "class proven as side-effect free before analysis", wrongly_initialized_lines) + checkLine(line, "MustBeSafeLate", "BUILD_TIME", "class proven as side-effect free after analysis", wrongly_initialized_lines) + checkLine(line, "MustBeSimulated", "RUN_TIME", "classes are initialized at run time by default", wrongly_initialized_lines) + checkLine(line, "MustBeDelayed", "RUN_TIME", "classes are initialized at run time by default", wrongly_initialized_lines) + if len(wrongly_initialized_lines) > 0: msg = "" for (line, error) in wrongly_initialized_lines: @@ -1439,7 +1457,7 @@ def checkLine(line, marker, init_kind, msg, wrongly_initialized_lines): check_class_initialization(all_classes_file) - native_image_context_run(build_and_test_clinittest_image, args) + native_image_context_run(build_and_test_clinittest_images, args) class SubstrateJvmFuncsFallbacksBuilder(mx.Project): diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java index 0f5efa2e0ef1..f3780aa32031 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ObjectScanner.java @@ -154,7 +154,7 @@ protected final void scanField(AnalysisField field, JavaConstant receiver, ScanR /* The value is not available yet. */ return; } - JavaConstant fieldValue = bb.getConstantReflectionProvider().readFieldValue(field, receiver); + JavaConstant fieldValue = bb.getUniverse().getHeapScanner().readFieldValue(field, receiver); if (fieldValue == null) { StringBuilder backtrace = new StringBuilder(); buildObjectBacktrace(bb, reason, backtrace); diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapPrimitiveArray.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapPrimitiveArray.java index b43e727383c1..66f1d9799a70 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapPrimitiveArray.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapPrimitiveArray.java @@ -27,8 +27,6 @@ import java.lang.reflect.Array; import java.util.function.Consumer; -import org.graalvm.compiler.debug.GraalError; - import com.oracle.graal.pointsto.ObjectScanner; import com.oracle.graal.pointsto.meta.AnalysisType; import com.oracle.graal.pointsto.util.AnalysisError; @@ -97,35 +95,10 @@ public JavaConstant readElementValue(int idx) { @Override public void setElement(int idx, JavaConstant value) { - /* - * Constants for sub-integer types are often just integer constants, i.e., we cannot rely on - * the JavaKind of the constant to match the type of the array. - */ - if (array instanceof boolean[] booleanArray) { - booleanArray[idx] = value.asInt() != 0; - } else if (array instanceof byte[] byteArray) { - byte v = (byte) value.asInt(); - GraalError.guarantee(v == value.asInt(), "type mismatch"); - byteArray[idx] = v; - } else if (array instanceof short[] shortArray) { - short v = (short) value.asInt(); - GraalError.guarantee(v == value.asInt(), "type mismatch"); - shortArray[idx] = v; - } else if (array instanceof char[] charArray) { - char v = (char) value.asInt(); - GraalError.guarantee(v == value.asInt(), "type mismatch"); - charArray[idx] = v; - } else if (array instanceof int[] intArray) { - intArray[idx] = value.asInt(); - } else if (array instanceof long[] longArray) { - longArray[idx] = value.asLong(); - } else if (array instanceof float[] floatArray) { - floatArray[idx] = value.asFloat(); - } else if (array instanceof double[] doubleArray) { - doubleArray[idx] = value.asDouble(); - } else { - throw AnalysisError.shouldNotReachHere("Unexpected array type: " + array.getClass()); + if (value.getJavaKind() != type.getComponentType().getJavaKind()) { + throw AnalysisError.shouldNotReachHere("Cannot store value of kind " + value.getJavaKind() + " into primitive array of type " + type); } + Array.set(array, idx, value.asBoxedPrimitive()); } @Override diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapScanner.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapScanner.java index 7658d7dd0963..16eebf834315 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapScanner.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/ImageHeapScanner.java @@ -181,7 +181,7 @@ void markTypeInstantiated(AnalysisType type, ScanReason reason) { universe.getBigbang().registerTypeAsInHeap(type, reason); } - JavaConstant createImageHeapConstant(JavaConstant constant, ScanReason reason) { + public JavaConstant createImageHeapConstant(JavaConstant constant, ScanReason reason) { if (isNonNullObjectConstant(constant)) { return getOrCreateImageHeapConstant(constant, reason); } @@ -490,6 +490,10 @@ protected ValueSupplier readHostedFieldValue(AnalysisField field, return ValueSupplier.eagerValue(value); } + public JavaConstant readFieldValue(AnalysisField field, JavaConstant receiver) { + return constantReflection.readFieldValue(field, receiver); + } + protected boolean skipScanning() { return false; } 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 953621c4a49f..67864c3fca64 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 @@ -183,6 +183,8 @@ public enum UsageKind { } private final AnalysisFuture onTypeReachableTask; + private final AnalysisFuture initializeMetaDataTask; + /** * Additional information that is only available for types that are marked as reachable. */ @@ -302,6 +304,7 @@ public AnalysisType(AnalysisUniverse universe, ResolvedJavaType javaType, JavaKi /* The registration task initializes the type. */ this.onTypeReachableTask = new AnalysisFuture<>(() -> universe.onTypeReachable(this), null); + this.initializeMetaDataTask = new AnalysisFuture<>(() -> universe.initializeMetaData(this), null); this.typeData = new AnalysisFuture<>(() -> { AnalysisError.guarantee(universe.getHeapScanner() != null, "Heap scanner is not available."); return universe.getHeapScanner().computeTypeData(this); @@ -689,6 +692,10 @@ public void ensureOnTypeReachableTaskDone() { onTypeReachableTask.ensureDone(); } + public AnalysisFuture getInitializeMetaDataTask() { + return initializeMetaDataTask; + } + public boolean getReachabilityListenerNotified() { return reachabilityListenerNotified; } 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 192e74512c19..405711c47a72 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 @@ -702,6 +702,10 @@ public void onTypeReachable(AnalysisType type) { } } + public void initializeMetaData(AnalysisType type) { + bb.initializeMetaData(type); + } + public SubstitutionProcessor getSubstitutions() { return substitutions; } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java index a04efd7a0acd..678a153ac79e 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/phases/InlineBeforeAnalysisGraphDecoder.java @@ -59,7 +59,7 @@ public class InlineBeforeAnalysisGraphDecoder extends PEGraphDecoder { public class InlineBeforeAnalysisMethodScope extends PEMethodScope { - private final InlineBeforeAnalysisPolicy.AbstractPolicyScope policyScope; + public final InlineBeforeAnalysisPolicy.AbstractPolicyScope policyScope; private boolean inliningAborted; 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 2d764621f02c..bbcb3b6bcc8d 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 @@ -260,6 +260,7 @@ import com.oracle.svm.hosted.cenum.CEnumCallWrapperSubstitutionProcessor; import com.oracle.svm.hosted.classinitialization.ClassInitializationFeature; import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; import com.oracle.svm.hosted.code.CEntryPointCallStubSupport; import com.oracle.svm.hosted.code.CEntryPointData; import com.oracle.svm.hosted.code.CFunctionSubstitutionProcessor; @@ -909,6 +910,8 @@ protected void setupNativeImage(OptionValues options, Map[] extractAnnotationTypes(AnalysisField field, Class[] } return annotationTypes.toArray(new Class[0]); } + + public SimulateClassInitializerSupport createSimulateClassInitializerSupport(AnalysisMetaAccess aMetaAccess) { + return new SimulateClassInitializerSupport(aMetaAccess, this); + } } 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 32de03cad622..fd185d062175 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 @@ -51,6 +51,7 @@ import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.SVMHost; import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; import com.oracle.svm.hosted.meta.HostedMetaAccess; import jdk.vm.ci.meta.Constant; @@ -65,6 +66,7 @@ public class AnalysisConstantReflectionProvider extends SharedConstantReflection private final AnalysisUniverse universe; private final UniverseMetaAccess metaAccess; private final ClassInitializationSupport classInitializationSupport; + private SimulateClassInitializerSupport simulateClassInitializerSupport; public AnalysisConstantReflectionProvider(AnalysisUniverse universe, UniverseMetaAccess metaAccess, ClassInitializationSupport classInitializationSupport) { this.universe = universe; @@ -165,10 +167,10 @@ public void forEachArrayElement(JavaConstant array, ObjIntConsumer @Override public final JavaConstant readFieldValue(ResolvedJavaField field, JavaConstant receiver) { - return readValue(metaAccess, (AnalysisField) field, receiver); + return readValue(metaAccess, (AnalysisField) field, receiver, false); } - public JavaConstant readValue(UniverseMetaAccess suppliedMetaAccess, AnalysisField field, JavaConstant receiver) { + public JavaConstant readValue(UniverseMetaAccess suppliedMetaAccess, AnalysisField field, JavaConstant receiver, boolean returnSimulatedValues) { if (!field.isStatic()) { if (receiver.isNull() || !field.getDeclaringClass().isAssignableFrom(((TypedConstant) receiver).getType(metaAccess))) { /* @@ -184,18 +186,29 @@ public JavaConstant readValue(UniverseMetaAccess suppliedMetaAccess, AnalysisFie } } - JavaConstant value; - if (receiver instanceof ImageHeapConstant) { + JavaConstant value = null; + if (returnSimulatedValues) { + value = readSimulatedValue(field); + } + if (value == null && receiver instanceof ImageHeapConstant) { ImageHeapInstance heapObject = (ImageHeapInstance) receiver; - return interceptValue(suppliedMetaAccess, field, heapObject.readFieldValue(field)); + value = heapObject.readFieldValue(field); + } + if (value == null) { + value = universe.lookup(ReadableJavaField.readFieldValue(suppliedMetaAccess, classInitializationSupport, field.wrapped, universe.toHosted(receiver))); } - value = universe.lookup(ReadableJavaField.readFieldValue(suppliedMetaAccess, classInitializationSupport, field.wrapped, universe.toHosted(receiver))); - return interceptValue(suppliedMetaAccess, field, value); } /** Read the field value and wrap it in a value supplier without performing any replacements. */ - public ValueSupplier readHostedFieldValue(AnalysisField field, HostedMetaAccess hMetaAccess, JavaConstant receiver) { + public ValueSupplier readHostedFieldValue(AnalysisField field, HostedMetaAccess hMetaAccess, JavaConstant receiver, boolean returnSimulatedValues) { + if (returnSimulatedValues) { + var simulatedValue = readSimulatedValue(field); + if (simulatedValue != null) { + return ValueSupplier.eagerValue(simulatedValue); + } + } + if (field.wrapped instanceof ReadableJavaField) { ReadableJavaField readableField = (ReadableJavaField) field.wrapped; if (readableField.isValueAvailableBeforeAnalysis()) { @@ -216,6 +229,25 @@ public ValueSupplier readHostedFieldValue(AnalysisField field, Hos return ValueSupplier.eagerValue(universe.lookup(ReadableJavaField.readFieldValue(metaAccess, classInitializationSupport, field.wrapped, receiver))); } + /** + * For classes that are simulated as initialized, provide the value of static fields to the + * static analysis so that they are seen properly as roots in the image heap. + * + * We cannot return such simulated field values for "normal" field value reads because then they + * would be seen during bytecode parsing too. Therefore, we only return such values when + * explicitly requested via a flag. + */ + private JavaConstant readSimulatedValue(AnalysisField field) { + if (!field.isStatic() || field.getDeclaringClass().isInitialized()) { + return null; + } + field.getDeclaringClass().getInitializeMetaDataTask().ensureDone(); + if (simulateClassInitializerSupport == null) { + simulateClassInitializerSupport = SimulateClassInitializerSupport.singleton(); + } + return simulateClassInitializerSupport.getSimulatedFieldValue(field); + } + public JavaConstant interceptValue(UniverseMetaAccess suppliedMetaAccess, AnalysisField field, JavaConstant value) { JavaConstant result = value; if (result != null) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/ReadableJavaField.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/ReadableJavaField.java index faa04224c354..6888e97cd445 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/ReadableJavaField.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/ReadableJavaField.java @@ -50,7 +50,7 @@ static JavaConstant readFieldValue(MetaAccessProvider metaAccess, ClassInitializ assert readableField.isValueAvailable() : "Field " + readableField.format("%H.%n") + " value not available for reading."; return readableField.readValue(metaAccess, classInitializationSupport, receiver); - } else if (classInitializationSupport.shouldInitializeAtRuntime(field.getDeclaringClass())) { + } else if (!classInitializationSupport.maybeInitializeAtBuildTime(field.getDeclaringClass())) { /* * The class is initialized at image run time. We must not use any field value from the * image builder VM, even if the class is already initialized there. We need to return diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/DynamicHubInitializer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/DynamicHubInitializer.java index 7b45ae7f5c9a..e25b0edda738 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/DynamicHubInitializer.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/DynamicHubInitializer.java @@ -48,6 +48,7 @@ import com.oracle.svm.core.meta.MethodPointer; import com.oracle.svm.hosted.ExceptionSynthesizer; import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; import com.oracle.svm.util.ReflectionUtil; import jdk.vm.ci.meta.ConstantReflectionProvider; @@ -113,7 +114,7 @@ public void initializeMetaData(ImageHeapScanner heapScanner, AnalysisType type) /* Support for Java enumerations. */ if (type.isEnum()) { AnalysisError.guarantee(hub.shouldInitEnumConstants(), "Enum constants already initialized for %s.", type.toJavaName(true)); - if (hostVM.getClassInitializationSupport().shouldInitializeAtRuntime(type)) { + if (!hostVM.getClassInitializationSupport().maybeInitializeAtBuildTime(type)) { hub.initEnumConstantsAtRuntime(javaClass); } else { hub.initEnumConstants(retrieveEnumConstantArray(type, javaClass)); @@ -164,12 +165,12 @@ private Enum[] retrieveEnumConstantArray(AnalysisType type, Class javaClas private void buildClassInitializationInfo(ImageHeapScanner heapScanner, AnalysisType type, DynamicHub hub) { AnalysisError.guarantee(hub.getClassInitializationInfo() == null, "Class initialization info already computed for %s.", type.toJavaName(true)); + boolean initializedAtBuildTime = SimulateClassInitializerSupport.singleton().trySimulateClassInitializer(bb, type); ClassInitializationInfo info; - if (hostVM.getClassInitializationSupport().shouldInitializeAtRuntime(type)) { - info = buildRuntimeInitializationInfo(type); - } else { - assert type.isInitialized(); + if (initializedAtBuildTime) { info = type.getClassInitializer() == null ? ClassInitializationInfo.NO_INITIALIZER_INFO_SINGLETON : ClassInitializationInfo.INITIALIZED_INFO_SINGLETON; + } else { + info = buildRuntimeInitializationInfo(type); } hub.setClassInitializationInfo(info); heapScanner.rescanField(hub, dynamicHubClassInitializationInfoField); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImagePointsToAnalysis.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImagePointsToAnalysis.java index c8022403df21..a463a602297e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImagePointsToAnalysis.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImagePointsToAnalysis.java @@ -116,7 +116,7 @@ public void onFieldAccessed(AnalysisField field) { @Override public void onTypeReachable(AnalysisType type) { - postTask(d -> initializeMetaData(type)); + postTask(d -> type.getInitializeMetaDataTask().ensureDone()); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImageReachabilityAnalysisEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImageReachabilityAnalysisEngine.java index a9b851e21b47..6147b8edb1d7 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImageReachabilityAnalysisEngine.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/NativeImageReachabilityAnalysisEngine.java @@ -91,7 +91,7 @@ public void onFieldAccessed(AnalysisField field) { @Override public void onTypeReachable(AnalysisType type) { - postTask(d -> initializeMetaData(type)); + postTask(d -> type.getInitializeMetaDataTask().ensureDone()); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/flow/SVMMethodTypeFlowBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/flow/SVMMethodTypeFlowBuilder.java index 6c10981646c2..761f6d045330 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/flow/SVMMethodTypeFlowBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/flow/SVMMethodTypeFlowBuilder.java @@ -149,7 +149,7 @@ protected void checkUnsafeOffset(ValueNode base, ValueNode offsetNode) { LoadFieldNode offsetLoadNode = (LoadFieldNode) offsetNode; AnalysisField field = (AnalysisField) offsetLoadNode.field(); if (field.isStatic() && - !getHostVM().getClassInitializationSupport().shouldInitializeAtRuntime(field.getDeclaringClass()) && + getHostVM().getClassInitializationSupport().maybeInitializeAtBuildTime(field.getDeclaringClass()) && !field.getDeclaringClass().unsafeFieldsRecomputed() && !(field.wrapped instanceof ComputedValueField) && !(base.isConstant() && base.asConstant().isDefaultForKind())) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationFeature.java index a8f6d3275819..0cb47bfbd608 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationFeature.java @@ -34,22 +34,26 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.graalvm.collections.Pair; import org.graalvm.compiler.graph.Node; import org.graalvm.compiler.options.OptionValues; import org.graalvm.compiler.phases.util.Providers; +import org.graalvm.nativeimage.impl.clinit.ClassInitializationTracking; import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; import com.oracle.graal.pointsto.meta.AnalysisUniverse; import com.oracle.graal.pointsto.reports.ReportUtils; import com.oracle.graal.pointsto.util.Timer; import com.oracle.graal.pointsto.util.TimerCollection; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.classinitialization.EnsureClassInitializedSnippets; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.graal.meta.RuntimeConfiguration; import com.oracle.svm.core.graal.meta.SubstrateForeignCallsProvider; @@ -57,11 +61,10 @@ import com.oracle.svm.core.option.OptionOrigin; import com.oracle.svm.core.option.SubstrateOptionsParser; import com.oracle.svm.core.snippets.SnippetRuntime; -import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.util.UserError; import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.FeatureImpl.AfterAnalysisAccessImpl; import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl; -import org.graalvm.nativeimage.impl.clinit.ClassInitializationTracking; @AutomaticallyRegisteredFeature public class ClassInitializationFeature implements InternalFeature { @@ -118,10 +121,10 @@ public void duringSetup(DuringSetupAccess a) { private Object checkImageHeapInstance(Object obj) { /* - * Note that computeInitKind also memoizes the class as InitKind.BUILD_TIME, which means - * that the user cannot later manually register it as RERUN or RUN_TIME. + * Note that initializeAtBuildTime also memoizes the class as InitKind.BUILD_TIME, which + * means that the user cannot later manually register it as RERUN or RUN_TIME. */ - if (obj != null && classInitializationSupport.shouldInitializeAtRuntime(obj.getClass())) { + if (obj != null && !classInitializationSupport.maybeInitializeAtBuildTime(obj.getClass())) { String msg = "No instances of " + obj.getClass().getTypeName() + " are allowed in the image heap as this class should be initialized at image runtime."; msg += classInitializationSupport.objectInstantiationTraceMessage(obj, " To fix the issue mark " + obj.getClass().getTypeName() + " for build-time initialization with " + @@ -169,16 +172,21 @@ public void duringAnalysis(DuringAnalysisAccess access) { */ @Override @SuppressWarnings("try") - public void afterAnalysis(AfterAnalysisAccess access) { + public void afterAnalysis(AfterAnalysisAccess a) { + AfterAnalysisAccessImpl access = (AfterAnalysisAccessImpl) a; try (Timer.StopTimer ignored = TimerCollection.createTimerAndStart(TimerCollection.Registry.CLINIT)) { classInitializationSupport.setUnsupportedFeatures(null); assert classInitializationSupport.checkDelayedInitialization(); - classInitializationSupport.doLateInitialization(universe, metaAccess); + if (SimulateClassInitializerSupport.singleton().isEnabled()) { + /* Simulation of class initializer replaces the "late initialization". */ + } else { + classInitializationSupport.doLateInitialization(universe, metaAccess); + } if (ClassInitializationOptions.PrintClassInitialization.getValue()) { - reportClassInitializationInfo(SubstrateOptions.reportsPath()); + reportClassInitializationInfo(access, SubstrateOptions.reportsPath()); } if (SubstrateOptions.TraceClassInitialization.hasBeenSet()) { reportTrackedClassInitializationTraces(SubstrateOptions.reportsPath()); @@ -202,21 +210,32 @@ public void afterAnalysis(AfterAnalysisAccess access) { * Prints a file for every type of class initialization. Each file contains a list of classes * that belong to it. */ - private void reportClassInitializationInfo(String path) { + private void reportClassInitializationInfo(AfterAnalysisAccessImpl access, String path) { ReportUtils.report("class initialization report", path, "class_initialization_report", "csv", writer -> { writer.println("Class Name, Initialization Kind, Reason for Initialization"); - reportKind(writer, BUILD_TIME); - reportKind(writer, RERUN); - reportKind(writer, RUN_TIME); + reportKind(access, writer, BUILD_TIME); + reportKind(access, writer, RERUN); + reportKind(access, writer, RUN_TIME); }); } - private void reportKind(PrintWriter writer, InitKind kind) { + private void reportKind(AfterAnalysisAccessImpl access, PrintWriter writer, InitKind kind) { List> allClasses = new ArrayList<>(classInitializationSupport.classesWithKind(kind)); allClasses.sort(Comparator.comparing(Class::getTypeName)); allClasses.forEach(clazz -> { writer.print(clazz.getTypeName() + ", "); - writer.print(kind + ", "); + boolean simulated = false; + if (kind != BUILD_TIME) { + Optional type = access.getMetaAccess().optionalLookupJavaType(clazz); + if (type.isPresent()) { + simulated = SimulateClassInitializerSupport.singleton().isClassInitializerSimulated(type.get()); + } + } + if (simulated) { + writer.print("SIMULATED, "); + } else { + writer.print(kind + ", "); + } writer.println(classInitializationSupport.reasonForClass(clazz)); }); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationOptions.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationOptions.java index 4f7ed1eabfb9..b6128313be24 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationOptions.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationOptions.java @@ -102,4 +102,19 @@ private static class InitializationValueEager extends InitializationValueTransfo @Option(help = "Use new class initialization strategy that allows all classes to be used at image build time.", type = OptionType.Expert)// public static final HostedOptionKey UseNewExperimentalClassInitialization = new HostedOptionKey<>(false); + + @Option(help = "Simulate the effects of class initializer at image build time, to avoid class initialization at run time.", type = OptionType.Expert)// + public static final HostedOptionKey SimulateClassInitializer = new HostedOptionKey<>(true); + + @Option(help = "Configuration for SimulateClassInitializer: Collect all reasons why a class initializer cannot be simulated.", type = OptionType.Expert)// + static final HostedOptionKey SimulateClassInitializerCollectAllReasons = new HostedOptionKey<>(false); + + @Option(help = "Configuration for SimulateClassInitializer: Maximum inlining depth during simulation.", type = OptionType.Expert)// + static final HostedOptionKey SimulateClassInitializerMaxInlineDepth = new HostedOptionKey<>(200); + + @Option(help = "Configuration for SimulateClassInitializer: Maximum number of loop iterations that are unrolled during simulation.", type = OptionType.Expert)// + static final HostedOptionKey SimulateClassInitializerMaxLoopIterations = new HostedOptionKey<>(2_000); + + @Option(help = "Configuration for SimulateClassInitializer: Maximum number of bytes allocated in the image heap for each class initializer.", type = OptionType.Expert)// + static final HostedOptionKey SimulateClassInitializerMaxAllocatedBytes = new HostedOptionKey<>(40_000); } 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 3e9ebf3eda6a..3405e25009f6 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 @@ -34,6 +34,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; import org.graalvm.nativeimage.impl.clinit.ClassInitializationTracking; @@ -93,6 +94,10 @@ public static ClassInitializationSupport create(MetaAccessProvider metaAccess, I return new ProvenSafeClassInitializationSupport(metaAccess, loader); } + public static ClassInitializationSupport singleton() { + return (ClassInitializationSupport) ImageSingletons.lookup(RuntimeClassInitializationSupport.class); + } + ClassInitializationSupport(MetaAccessProvider metaAccess, ImageClassLoader loader) { this.metaAccess = metaAccess; this.loader = loader; @@ -139,25 +144,25 @@ Set> classesWithKind(InitKind kind) { } /** - * Returns true if the provided type should be initialized at runtime. - */ - public boolean shouldInitializeAtRuntime(ResolvedJavaType type) { - return computeInitKindAndMaybeInitializeClass(OriginalClassProvider.getJavaClass(type)) != InitKind.BUILD_TIME; - } - - /** - * Returns true if the provided class should be initialized at runtime. + * Returns true if the provided type is initialized at image build time. + * + * If the return value is true, then the class is also guaranteed to be initialized already. + * This means that calling this method might trigger class initialization, i.e., execute + * arbitrary user code. */ - public boolean shouldInitializeAtRuntime(Class clazz) { - return computeInitKindAndMaybeInitializeClass(clazz) != InitKind.BUILD_TIME; + public boolean maybeInitializeAtBuildTime(ResolvedJavaType type) { + return maybeInitializeAtBuildTime(OriginalClassProvider.getJavaClass(type)); } /** - * Initializes the class during image building, unless initialization must be delayed to - * runtime. + * Returns true if the provided type is initialized at image build time. + * + * If the return value is true, then the class is also guaranteed to be initialized already. + * This means that calling this method might trigger class initialization, i.e., execute + * arbitrary user code. */ - public void maybeInitializeHosted(ResolvedJavaType type) { - computeInitKindAndMaybeInitializeClass(OriginalClassProvider.getJavaClass(type)); + public boolean maybeInitializeAtBuildTime(Class clazz) { + return computeInitKindAndMaybeInitializeClass(clazz) == InitKind.BUILD_TIME; } /** diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ProvenSafeClassInitializationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ProvenSafeClassInitializationSupport.java index 92f3e3c5041f..89cceba7c7bc 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ProvenSafeClassInitializationSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ProvenSafeClassInitializationSupport.java @@ -450,7 +450,7 @@ private Set> initializeSafeDelayedClasses(TypeInitializerGraph initGrap * See if initialization worked--it can fail due to implicit * exceptions. */ - if (!shouldInitializeAtRuntime(c)) { + if (maybeInitializeAtBuildTime(c)) { provenSafe.add(c); ClassInitializationInfo initializationInfo = type.getClassInitializer() == null ? ClassInitializationInfo.NO_INITIALIZER_INFO_SINGLETON : ClassInitializationInfo.INITIALIZED_INFO_SINGLETON; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerAbortException.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerAbortException.java new file mode 100644 index 000000000000..63e7a0175f66 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerAbortException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import org.graalvm.compiler.core.common.GraalBailoutException; +import org.graalvm.compiler.debug.DebugContext; +import org.graalvm.compiler.nodes.StructuredGraph; + +/** + * Exception used to abort a simulation when it is known that simulation cannot succeed. This avoids + * decoding further Graal IR nodes of the class initializer. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +public final class SimulateClassInitializerAbortException extends GraalBailoutException { + private static final long serialVersionUID = 1L; + + static RuntimeException doAbort(SimulateClassInitializerClusterMember clusterMember, StructuredGraph graph, Object reason) { + graph.getDebug().dump(DebugContext.BASIC_LEVEL, graph, "SimulateClassInitializer abort: %s", reason); + clusterMember.notInitializedReasons.add(reason); + throw new SimulateClassInitializerAbortException(); + } + + private SimulateClassInitializerAbortException() { + super(""); + } + + @Override + public synchronized Throwable fillInStackTrace() { + /* Exception is used to abort parsing, stack trace is not necessary. */ + return this; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerCluster.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerCluster.java new file mode 100644 index 000000000000..9f67aa97da48 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerCluster.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import org.graalvm.collections.EconomicMap; + +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.HostedProviders; +import com.oracle.svm.common.meta.MultiMethod; + +/** + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +public final class SimulateClassInitializerCluster { + final SimulateClassInitializerSupport support; + final BigBang bb; + final HostedProviders providers; + final EconomicMap clusterMembers = EconomicMap.create(); + + SimulateClassInitializerCluster(SimulateClassInitializerSupport support, BigBang bb) { + this.support = support; + this.bb = bb; + this.providers = bb.getProviders(MultiMethod.ORIGINAL_METHOD); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerClusterMember.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerClusterMember.java new file mode 100644 index 000000000000..f447f4ad11b4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerClusterMember.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import java.util.ArrayList; +import java.util.List; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.EconomicSet; + +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisType; + +import jdk.vm.ci.meta.JavaConstant; + +/** + * A type whose class initializer is analyzed as part of the same + * {@link SimulateClassInitializerCluster} because there is a possible cyclic dependency. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +public final class SimulateClassInitializerClusterMember { + final SimulateClassInitializerCluster cluster; + final AnalysisType type; + + final EconomicSet dependencies = EconomicSet.create(); + final List notInitializedReasons = new ArrayList<>(); + final EconomicMap staticFieldValues = EconomicMap.create(); + + /** The mutable status field of the cluster member. */ + SimulateClassInitializerStatus status; + + SimulateClassInitializerClusterMember(SimulateClassInitializerCluster cluster, AnalysisType type) { + this.cluster = cluster; + this.type = type; + cluster.clusterMembers.put(type, this); + + this.status = SimulateClassInitializerStatus.COLLECTING_DEPENDENCIES; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerConstantFieldProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerConstantFieldProvider.java new file mode 100644 index 000000000000..206cd145584c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerConstantFieldProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.hosted.ameta.AnalysisConstantFieldProvider; + +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ResolvedJavaField; + +/** + * Constant field provider that also folds fields of classes that are already simulated as + * initialized. The regular {@link AnalysisConstantFieldProvider} is not allowed to do that because + * otherwise simulation information would be used during bytecode parsing already. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +final class SimulateClassInitializerConstantFieldProvider extends AnalysisConstantFieldProvider { + final SimulateClassInitializerSupport support; + + SimulateClassInitializerConstantFieldProvider(MetaAccessProvider metaAccess, SVMHost hostVM, SimulateClassInitializerSupport support) { + super(metaAccess, hostVM); + this.support = support; + } + + @Override + protected boolean isClassInitialized(ResolvedJavaField field) { + return support.isClassInitializerSimulated((AnalysisType) field.getDeclaringClass()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java new file mode 100644 index 000000000000..11cc46472653 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerGraphDecoder.java @@ -0,0 +1,761 @@ +/* + * Copyright (c) 2023, 2023, 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. + */ +/* + * Copyright (c) 2023, 2023, 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; + +import java.util.List; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.EconomicSet; +import org.graalvm.compiler.core.common.type.TypedConstant; +import org.graalvm.compiler.debug.DebugContext; +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.nodes.AbstractBeginNode; +import org.graalvm.compiler.nodes.ConstantNode; +import org.graalvm.compiler.nodes.ControlSplitNode; +import org.graalvm.compiler.nodes.StructuredGraph; +import org.graalvm.compiler.nodes.ValueNode; +import org.graalvm.compiler.nodes.extended.BoxNode; +import org.graalvm.compiler.nodes.graphbuilderconf.LoopExplosionPlugin.LoopExplosionKind; +import org.graalvm.compiler.nodes.java.AccessMonitorNode; +import org.graalvm.compiler.nodes.java.LoadFieldNode; +import org.graalvm.compiler.nodes.java.LoadIndexedNode; +import org.graalvm.compiler.nodes.java.NewArrayNode; +import org.graalvm.compiler.nodes.java.NewInstanceNode; +import org.graalvm.compiler.nodes.java.NewMultiArrayNode; +import org.graalvm.compiler.nodes.java.StoreFieldNode; +import org.graalvm.compiler.nodes.java.StoreIndexedNode; +import org.graalvm.compiler.nodes.virtual.AllocatedObjectNode; +import org.graalvm.compiler.nodes.virtual.CommitAllocationNode; +import org.graalvm.compiler.nodes.virtual.VirtualArrayNode; +import org.graalvm.compiler.nodes.virtual.VirtualBoxingNode; +import org.graalvm.compiler.nodes.virtual.VirtualInstanceNode; +import org.graalvm.compiler.nodes.virtual.VirtualObjectNode; +import org.graalvm.compiler.replacements.arraycopy.ArrayCopyNode; +import org.graalvm.compiler.replacements.nodes.ObjectClone; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.ObjectScanner; +import com.oracle.graal.pointsto.heap.ImageHeapArray; +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.graal.pointsto.heap.ImageHeapInstance; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisGraphDecoder; +import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; +import com.oracle.svm.core.config.ObjectLayout; +import com.oracle.svm.core.meta.SubstrateObjectConstant; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerPolicy.SimulateClassInitializerInlineScope; +import com.oracle.svm.hosted.fieldfolding.IsStaticFinalFieldInitializedNode; +import com.oracle.svm.hosted.fieldfolding.MarkStaticFinalFieldInitializedNode; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * The graph decoder that performs the partial evaluation of a single class initializer and all + * methods invoked by that class initializer. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +public class SimulateClassInitializerGraphDecoder extends InlineBeforeAnalysisGraphDecoder { + + protected final SimulateClassInitializerSupport support; + protected final SimulateClassInitializerClusterMember clusterMember; + + /** + * Stored in a separate field because it is frequently accessed, so having a separate field + * makes the code more readable. + */ + protected final MetaAccessProvider metaAccess; + + protected final EconomicMap allVirtualObjects = EconomicMap.create(); + protected final EconomicMap currentStaticFields = EconomicMap.create(); + protected final EconomicSet currentActiveObjects = EconomicSet.create(); + protected final EconomicMap isStaticFinalFieldInitializedStates = EconomicMap.create(); + + protected SimulateClassInitializerGraphDecoder(BigBang bb, SimulateClassInitializerPolicy policy, SimulateClassInitializerClusterMember clusterMember, StructuredGraph graph) { + super(bb, policy, graph, clusterMember.cluster.providers, unused -> LoopExplosionKind.FULL_UNROLL); + + this.support = clusterMember.cluster.support; + this.clusterMember = clusterMember; + this.metaAccess = providers.getMetaAccess(); + } + + @Override + public void decode(ResolvedJavaMethod classInitializer) { + for (var f : classInitializer.getDeclaringClass().getStaticFields()) { + var field = (AnalysisField) f; + + /* + * The initial value (before any field store) of a static field in our own class is the + * value coming from the constant pool attribute. + */ + var initialValue = field.getConstantValue(); + if (initialValue == null) { + initialValue = JavaConstant.defaultForKind(field.getStorageKind()); + } + currentStaticFields.put(field, initialValue); + isStaticFinalFieldInitializedStates.put(field, Boolean.FALSE); + } + super.decode(classInitializer); + } + + @Override + protected void maybeAbortInlining(MethodScope ms, LoopScope loopScope, Node node) { + InlineBeforeAnalysisMethodScope methodScope = cast(ms); + + if (node instanceof ControlSplitNode || node instanceof AccessMonitorNode) { + if (support.collectAllReasons) { + if (methodScope.isInlinedMethod()) { + /* + * We want to collect more reasons in the class initializer itself, so we abort + * inlining of too complex methods. + */ + abortInlining(methodScope); + return; + } else if (loopScope.loopDepth == 0) { + /* + * We are in the class initializer itself, so we just continue decoding. Unless + * we are in an unrolled loop, in which case we need to abort because otherwise + * decoding can quickly explode the graph size. + */ + return; + } + } + + /* + * Any control flow split means that our resulting class initializer cannot be + * simulated, so we can do an early abort of the analysis. + */ + throw SimulateClassInitializerAbortException.doAbort(clusterMember, graph, node); + } + } + + @Override + protected void checkLoopExplosionIteration(MethodScope methodScope, LoopScope loopScope) { + if (loopScope.loopIteration > support.maxLoopIterations) { + /* + * Most loops that are not unrollable bail out of loop unrolling earlier when a + * ControlSplitNode is appended. But for example an empty endless loop triggers this + * check. + */ + throw SimulateClassInitializerAbortException.doAbort(clusterMember, graph, "Loop iteration count exceeding unrolling limit"); + } + } + + @Override + protected Node handleFloatingNodeBeforeAdd(MethodScope methodScope, LoopScope loopScope, Node n) { + Node node = n; + if (node instanceof AllocatedObjectNode allocatedObjectNode) { + node = handleAllocatedObjectNode(allocatedObjectNode); + } + return super.handleFloatingNodeBeforeAdd(methodScope, loopScope, node); + } + + @Override + protected Node doCanonicalizeFixedNode(InlineBeforeAnalysisMethodScope methodScope, LoopScope loopScope, Node initialNode) { + Node node = super.doCanonicalizeFixedNode(methodScope, loopScope, initialNode); + + var countersScope = (SimulateClassInitializerInlineScope) methodScope.policyScope; + if (node instanceof StoreFieldNode storeFieldNode) { + node = handleStoreFieldNode(storeFieldNode); + } else if (node instanceof LoadFieldNode loadFieldNode) { + node = handleLoadFieldNode(loadFieldNode); + } else if (node instanceof StoreIndexedNode storeIndexedNode) { + node = handleStoreIndexedNode(storeIndexedNode); + } else if (node instanceof LoadIndexedNode loadIndexedNode) { + node = handleLoadIndexedNode(loadIndexedNode); + } else if (node instanceof ArrayCopyNode arrayCopyNode) { + node = handleArrayCopyNode(arrayCopyNode); + } else if (node instanceof EnsureClassInitializedNode ensureClassInitializedNode) { + node = handleEnsureClassInitializedNode(ensureClassInitializedNode); + } else if (node instanceof IsStaticFinalFieldInitializedNode isStaticFinalFieldInitializedNode) { + node = handleIsStaticFinalFieldInitializedNode(isStaticFinalFieldInitializedNode); + } else if (node instanceof MarkStaticFinalFieldInitializedNode markStaticFinalFieldInitializedNode) { + node = handleMarkStaticFinalFieldInitializedNode(markStaticFinalFieldInitializedNode); + } else if (node instanceof AccessMonitorNode accessMonitorNode) { + node = handleAccessMonitorNode(accessMonitorNode); + } else if (node instanceof CommitAllocationNode commitAllocationNode) { + handleCommitAllocationNode(countersScope, commitAllocationNode); + } else if (node instanceof NewInstanceNode newInstanceNode) { + node = handleNewInstanceNode(countersScope, newInstanceNode); + } else if (node instanceof NewArrayNode newArrayNode) { + node = handleNewArrayNode(countersScope, newArrayNode); + } else if (node instanceof NewMultiArrayNode newMultiArrayNode) { + node = handleNewMultiArrayNode(countersScope, newMultiArrayNode); + } else if (node instanceof BoxNode boxNode) { + node = handleBoxNode(boxNode); + } else if (node instanceof ObjectClone objectClone) { + node = handleObjectClone(countersScope, objectClone); + } + + if (node instanceof AbstractBeginNode && node.predecessor() instanceof ControlSplitNode) { + /* + * It is not possible to do a flow-sensitive analysis during partial evaluation, so + * after every control flow split we need to re-set our information about fields and + * objects. But this is only necessary in the "collect all reasons" mode, i.e., normally + * we abort the analysis. + */ + if (!support.collectAllReasons) { + throw SimulateClassInitializerAbortException.doAbort(clusterMember, graph, node.predecessor()); + } + currentActiveObjects.clear(); + currentStaticFields.clear(); + isStaticFinalFieldInitializedStates.clear(); + } + + return node; + } + + private Node handleStoreFieldNode(StoreFieldNode node) { + var field = (AnalysisField) node.field(); + if (field.isStatic()) { + currentStaticFields.put(field, node.value()); + + } else { + var object = asActiveImageHeapInstance(node.object()); + var value = node.value().asJavaConstant(); + if (object != null && value != null) { + object.setFieldValue(field, adaptForImageHeap(value, field.getStorageKind())); + return null; + } + } + return node; + } + + private Node handleLoadFieldNode(LoadFieldNode node) { + var field = (AnalysisField) node.field(); + if (field.isStatic()) { + var currentValue = currentStaticFields.get(field); + if (currentValue instanceof ValueNode currentValueNode) { + return currentValueNode; + } else if (currentValue instanceof JavaConstant currentConstant) { + return ConstantNode.forConstant(currentConstant, metaAccess); + } + assert currentValue == null : "Unexpected static field value: " + currentValue; + + ConstantNode canonicalized = support.tryCanonicalize(bb, node); + if (canonicalized != null) { + return canonicalized; + } + + } else { + var object = asActiveImageHeapInstance(node.object()); + if (object != null) { + var currentValue = (JavaConstant) object.getFieldValue(field); + return ConstantNode.forConstant(currentValue, metaAccess); + } + } + return node; + } + + private Node handleStoreIndexedNode(StoreIndexedNode node) { + var array = asActiveImageHeapArray(node.array()); + var value = node.value().asJavaConstant(); + int idx = asIntegerOrMinusOne(node.index()); + + if (array != null && value != null && idx >= 0 && idx < array.getLength()) { + var componentType = (AnalysisType) array.getType(metaAccess).getComponentType(); + if (node.elementKind().isPrimitive() || value.isNull() || componentType.isAssignableFrom(((TypedConstant) value).getType(metaAccess))) { + array.setElement(idx, adaptForImageHeap(value, componentType.getStorageKind())); + return null; + } + } + return node; + } + + private Node handleLoadIndexedNode(LoadIndexedNode node) { + var array = asActiveImageHeapArray(node.array()); + int idx = asIntegerOrMinusOne(node.index()); + + if (array != null && idx >= 0 && idx < array.getLength()) { + var currentValue = (JavaConstant) array.getElement(idx); + return ConstantNode.forConstant(currentValue, metaAccess); + } + return node; + } + + private Node handleArrayCopyNode(ArrayCopyNode node) { + if (handleArrayCopy(asActiveImageHeapArray(node.getSource()), asIntegerOrMinusOne(node.getSourcePosition()), + asActiveImageHeapArray(node.getDestination()), asIntegerOrMinusOne(node.getDestinationPosition()), asIntegerOrMinusOne(node.getLength()))) { + return null; + } + return node; + } + + protected boolean handleArrayCopy(ImageHeapArray source, int sourcePos, ImageHeapArray dest, int destPos, int length) { + if (source == null || sourcePos < 0 || sourcePos >= source.getLength() || + dest == null || destPos < 0 || destPos >= dest.getLength() || + length < 0 || sourcePos > source.getLength() - length || destPos > dest.getLength() - length) { + return false; + } + + var sourceComponentType = (AnalysisType) source.getType(metaAccess).getComponentType(); + var destComponentType = (AnalysisType) dest.getType(metaAccess).getComponentType(); + if (sourceComponentType.getJavaKind() != destComponentType.getJavaKind()) { + return false; + } + if (destComponentType.getJavaKind() == JavaKind.Object && !destComponentType.isJavaLangObject() && !sourceComponentType.equals(destComponentType)) { + for (int i = 0; i < length; i++) { + var elementValueType = ((TypedConstant) source.getElement(sourcePos + i)).getType(metaAccess); + if (!destComponentType.isAssignableFrom(elementValueType)) { + return false; + } + } + } + + /* All checks passed, we can now copy array elements. */ + if (source == dest && sourcePos < destPos) { + /* Must copy backwards to avoid losing elements. */ + for (int i = length - 1; i >= 0; i--) { + dest.setElement(destPos + i, (JavaConstant) source.getElement(sourcePos + i)); + } + } else { + for (int i = 0; i < length; i++) { + dest.setElement(destPos + i, (JavaConstant) source.getElement(sourcePos + i)); + } + } + return true; + } + + private Node handleEnsureClassInitializedNode(EnsureClassInitializedNode node) { + var classInitType = (AnalysisType) node.constantTypeOrNull(providers.getConstantReflection()); + if (classInitType != null) { + if (support.trySimulateClassInitializer(graph.getDebug(), classInitType, clusterMember)) { + /* Class is already simulated initialized, no need for a run-time check. */ + return null; + } + var classInitTypeMember = clusterMember.cluster.clusterMembers.get(classInitType); + if (classInitTypeMember != null && !classInitTypeMember.status.published) { + /* + * The class is part of the same cycle as our class. We optimistically remove the + * initialization check, which is correct if the whole cycle can be simulated. If + * the cycle cannot be simulated, then this graph with the optimistic assumption + * will be discarded. + */ + clusterMember.dependencies.add(classInitTypeMember); + return null; + } + } + return node; + } + + private Node handleIsStaticFinalFieldInitializedNode(IsStaticFinalFieldInitializedNode node) { + var field = (AnalysisField) node.getField(); + var isStaticFinalFieldInitialized = isStaticFinalFieldInitializedStates.get(field); + if (isStaticFinalFieldInitialized != null) { + return ConstantNode.forBoolean(isStaticFinalFieldInitialized); + } + if (support.trySimulateClassInitializer(graph.getDebug(), field.getDeclaringClass(), clusterMember)) { + return ConstantNode.forBoolean(true); + } + return node; + } + + private Node handleMarkStaticFinalFieldInitializedNode(MarkStaticFinalFieldInitializedNode node) { + var field = (AnalysisField) node.getField(); + isStaticFinalFieldInitializedStates.put(field, Boolean.TRUE); + return node; + } + + private Node handleAccessMonitorNode(AccessMonitorNode node) { + var object = asActiveImageHeapConstant(node.object()); + if (object != null) { + /* + * Objects allocated within the class initializer are similar to escape analyzed + * objects, so we can eliminate such synchronization. + * + * Note that we cannot eliminate all synchronization in general: an object that was + * present before class initialization started could be permanently locked by another + * thread, in which case the class initializer must never complete. We cannot detect + * such cases during simulation. + */ + return null; + } + return node; + } + + private Node handleAllocatedObjectNode(AllocatedObjectNode node) { + var imageHeapConstant = allVirtualObjects.get(node.getVirtualObject()); + if (imageHeapConstant != null) { + return ConstantNode.forConstant(imageHeapConstant, metaAccess); + } + return node; + } + + private ValueNode handleNewInstanceNode(SimulateClassInitializerInlineScope countersScope, NewInstanceNode node) { + var type = (AnalysisType) node.instanceClass(); + if (accumulateNewInstanceSize(countersScope, type, node)) { + var instance = new ImageHeapInstance(type); + for (var field : type.getInstanceFields(true)) { + var aField = (AnalysisField) field; + instance.setFieldValue(aField, JavaConstant.defaultForKind(aField.getStorageKind())); + } + currentActiveObjects.add(instance); + return ConstantNode.forConstant(instance, metaAccess); + } + return node; + } + + private ValueNode handleNewArrayNode(SimulateClassInitializerInlineScope countersScope, NewArrayNode node) { + var arrayType = (AnalysisType) node.elementType().getArrayClass(); + int length = asIntegerOrMinusOne(node.length()); + if (accumulateNewArraySize(countersScope, arrayType, length, node)) { + var array = createNewArray(arrayType, length); + return ConstantNode.forConstant(array, metaAccess); + } + return node; + } + + protected ImageHeapArray createNewArray(AnalysisType arrayType, int length) { + var array = ImageHeapArray.create(arrayType, length); + var defaultValue = JavaConstant.defaultForKind(arrayType.getComponentType().getStorageKind()); + for (int i = 0; i < length; i++) { + array.setElement(i, defaultValue); + } + currentActiveObjects.add(array); + return array; + } + + private ValueNode handleNewMultiArrayNode(SimulateClassInitializerInlineScope countersScope, NewMultiArrayNode node) { + int[] dimensions = new int[node.dimensionCount()]; + + /* + * Check first that all array dimensions are valid and that the sum of all resulting array + * sizes is not too large. + */ + long totalLength = 1; + var curArrayType = (AnalysisType) node.type(); + for (int i = 0; i < dimensions.length; i++) { + int length = asIntegerOrMinusOne(node.dimension(i)); + totalLength = totalLength * length; + if (!accumulateNewArraySize(countersScope, curArrayType, totalLength, node)) { + return node; + } + dimensions[i] = length; + curArrayType = curArrayType.getComponentType(); + } + + var array = createNewMultiArray((AnalysisType) node.type(), 0, dimensions); + return ConstantNode.forConstant(array, metaAccess); + } + + private ImageHeapArray createNewMultiArray(AnalysisType curArrayType, int curDimension, int[] dimensions) { + int curLength = dimensions[curDimension]; + int nextDimension = curDimension + 1; + if (nextDimension == dimensions.length) { + return createNewArray(curArrayType, curLength); + } + var nextArrayType = curArrayType.getComponentType(); + + var array = ImageHeapArray.create(curArrayType, dimensions[curDimension]); + for (int i = 0; i < curLength; i++) { + array.setElement(i, createNewMultiArray(nextArrayType, nextDimension, dimensions)); + } + currentActiveObjects.add(array); + return array; + } + + private ValueNode handleBoxNode(BoxNode node) { + var value = node.getValue().asJavaConstant(); + if (value == null || node.hasIdentity()) { + return node; + } + + /* + * No need to produce a ImageHeapConstant, because we know all the boxing classes are always + * initialized at image build time. + */ + var boxedValue = switch (node.getBoxingKind()) { + case Byte -> Byte.valueOf((byte) value.asInt()); + case Boolean -> Boolean.valueOf(value.asInt() != 0); + case Short -> Short.valueOf((short) value.asInt()); + case Char -> Character.valueOf((char) value.asInt()); + case Int -> Integer.valueOf(value.asInt()); + case Long -> Long.valueOf(value.asLong()); + case Float -> Float.valueOf(value.asFloat()); + case Double -> Double.valueOf(value.asDouble()); + default -> throw VMError.shouldNotReachHere("Unexpected kind", node.getBoxingKind()); + }; + return ConstantNode.forConstant(providers.getSnippetReflection().forObject(boxedValue), metaAccess); + } + + private ValueNode handleObjectClone(SimulateClassInitializerInlineScope countersScope, ObjectClone node) { + var originalImageHeapConstant = asActiveImageHeapConstant(node.getObject()); + if (originalImageHeapConstant != null) { + var type = (AnalysisType) originalImageHeapConstant.getType(metaAccess); + if ((originalImageHeapConstant instanceof ImageHeapArray originalArray && accumulateNewArraySize(countersScope, type, originalArray.getLength(), node.asNode())) || + (type.isCloneableWithAllocation() && accumulateNewInstanceSize(countersScope, type, node.asNode()))) { + var cloned = originalImageHeapConstant.forObjectClone(); + currentActiveObjects.add(cloned); + return ConstantNode.forConstant(cloned, metaAccess); + } + } + + var original = node.getObject().asJavaConstant(); + if (original != null && ((ConstantNode) node.getObject()).getStableDimension() > 0) { + /* + * Cloning of an array with stable elements produces a new image heap array with all + * elements copied from the stable array. But the new array is not stable anymore. + */ + var arrayType = (AnalysisType) metaAccess.lookupJavaType(original); + Integer length = providers.getConstantReflection().readArrayLength(original); + if (length != null && accumulateNewArraySize(countersScope, arrayType, length, node.asNode())) { + var array = ImageHeapArray.create(arrayType, length); + for (int i = 0; i < length; i++) { + array.setElement(i, adaptForImageHeap(providers.getConstantReflection().readArrayElement(original, i), arrayType.getComponentType().getStorageKind())); + } + currentActiveObjects.add(array); + return ConstantNode.forConstant(array, metaAccess); + } + } + return node.asNode(); + } + + private void handleCommitAllocationNode(SimulateClassInitializerInlineScope countersScope, CommitAllocationNode node) { + boolean progress; + do { + /* + * To handle multi-dimensional arrays, we process the virtual objects multiple times as + * long as we make any progress: In the first iteration, the "inner" arrays are + * processed and put into the allVirtualObjects map. In the second iteration, these + * virtual objects can be put into the outer array. + */ + progress = false; + + int pos = 0; + for (int i = 0; i < node.getVirtualObjects().size(); i++) { + VirtualObjectNode virtualObject = node.getVirtualObjects().get(i); + int entryCount = virtualObject.entryCount(); + List entries = node.getValues().subList(pos, pos + entryCount); + pos += entryCount; + + if (!node.getLocks(i).isEmpty() || node.getEnsureVirtual().get(i)) { + /* + * Ignore unnecessary corner cases: we do not expect to see objects that are + * locked because constructors are not inlined when escape analysis runs, i.e., + * objects are always materialized before the constructor when they are also + * unlocked. Similarly, we do not see virtual objects that were already passed + * into a EnsureVirtualizedNode. + */ + } else if (allVirtualObjects.containsKey(virtualObject)) { + /* Already processed in previous round. */ + } else if (virtualObject instanceof VirtualBoxingNode) { + VMError.shouldNotReachHere("For testing: check if this is reachable"); + /* + * Could be handled the same way as a BoxNode, but does not occur in practice + * because escape analysis does not seem to materialize boxed objects as part of + * a CommitAllocationNode. + */ + } else if (virtualObject instanceof VirtualInstanceNode virtualInstance) { + progress |= handleVirtualInstance(countersScope, virtualInstance, entries, node); + } else if (virtualObject instanceof VirtualArrayNode virtualArray) { + progress |= handleVirtualArray(countersScope, virtualArray, entries, node); + } else { + throw VMError.shouldNotReachHere(virtualObject.toString()); + } + } + } while (progress); + } + + private boolean handleVirtualInstance(SimulateClassInitializerInlineScope countersScope, VirtualInstanceNode virtualInstance, List entries, Node reason) { + var type = (AnalysisType) virtualInstance.type(); + if (!accumulateNewInstanceSize(countersScope, type, reason)) { + return false; + } + var instance = new ImageHeapInstance(type); + for (int j = 0; j < virtualInstance.entryCount(); j++) { + var entry = lookupConstantEntry(j, entries); + if (entry == null) { + /* + * That happens only in corner cases since constructors are not inlined when escape + * analysis runs. + */ + return false; + } + var field = (AnalysisField) virtualInstance.field(j); + instance.setFieldValue(field, adaptForImageHeap(entry, field.getStorageKind())); + } + allVirtualObjects.put(virtualInstance, instance); + currentActiveObjects.add(instance); + return true; + } + + private boolean handleVirtualArray(SimulateClassInitializerInlineScope countersScope, VirtualArrayNode virtualArray, List entries, Node reason) { + var arrayType = (AnalysisType) virtualArray.type(); + int length = virtualArray.entryCount(); + if (!accumulateNewArraySize(countersScope, arrayType, length, reason)) { + return false; + } + var array = ImageHeapArray.create(arrayType, length); + for (int j = 0; j < length; j++) { + var entry = lookupConstantEntry(j, entries); + if (entry == null) { + /* + * Handling this would require emitting multiple StoreIndexed node, which is not + * possible as part of a canonicalization during partial evaluation. + */ + return false; + } + array.setElement(j, adaptForImageHeap(entry, arrayType.getComponentType().getStorageKind())); + } + allVirtualObjects.put(virtualArray, array); + currentActiveObjects.add(array); + return true; + } + + private JavaConstant lookupConstantEntry(int index, List entries) { + var entry = entries.get(index); + if (entry instanceof VirtualObjectNode virtualObjectNode) { + return allVirtualObjects.get(virtualObjectNode); + } else { + return entry.asJavaConstant(); + } + } + + protected boolean accumulateNewInstanceSize(SimulateClassInitializerInlineScope countersScope, AnalysisType type, Node reason) { + assert type.isInstanceClass() : type; + /* + * We do not know yet how large the object really will be, because we do not know yet which + * fields are reachable. So we estimate by just summing up field sizes. + */ + var objectLayout = ImageSingletons.lookup(ObjectLayout.class); + long allocationSize = objectLayout.getFirstFieldOffset(); + for (var field : type.getInstanceFields(true)) { + allocationSize += objectLayout.sizeInBytes(((AnalysisField) field).getStorageKind()); + } + return accumulatedNewObjectSize(countersScope, allocationSize, reason); + } + + protected boolean accumulateNewArraySize(SimulateClassInitializerInlineScope countersScope, AnalysisType arrayType, long length, Node reason) { + if (length < 0) { + return false; + } + var objectLayout = ImageSingletons.lookup(ObjectLayout.class); + long allocationSize = objectLayout.getArraySize(arrayType.getComponentType().getStorageKind(), (int) length, true); + return accumulatedNewObjectSize(countersScope, allocationSize, reason); + } + + private boolean accumulatedNewObjectSize(SimulateClassInitializerInlineScope countersScope, long allocationSize, Node reason) { + if (countersScope.accumulativeCounters.allocatedBytes + allocationSize > support.maxAllocatedBytes) { + if (debug.isLogEnabled(DebugContext.BASIC_LEVEL)) { + debug.log("object size %s too large since already %s allocated: %s %s", allocationSize, countersScope.accumulativeCounters.allocatedBytes, reason, reason.getNodeSourcePosition()); + } + if (support.collectAllReasons) { + return false; + } else { + throw SimulateClassInitializerAbortException.doAbort(clusterMember, graph, reason); + } + } + countersScope.accumulativeCounters.allocatedBytes += allocationSize; + countersScope.allocatedBytes += allocationSize; + return true; + } + + protected ImageHeapConstant asActiveImageHeapConstant(ValueNode node) { + var constant = node.asJavaConstant(); + if (constant instanceof ImageHeapConstant imageHeapConstant && currentActiveObjects.contains(imageHeapConstant)) { + return imageHeapConstant; + } + return null; + } + + protected ImageHeapInstance asActiveImageHeapInstance(ValueNode node) { + var constant = node.asJavaConstant(); + if (constant instanceof ImageHeapInstance imageHeapInstance && currentActiveObjects.contains(imageHeapInstance)) { + return imageHeapInstance; + } + return null; + } + + protected ImageHeapArray asActiveImageHeapArray(ValueNode node) { + var constant = node.asJavaConstant(); + if (constant instanceof ImageHeapArray imageHeapArray && currentActiveObjects.contains(imageHeapArray)) { + return imageHeapArray; + } + return null; + } + + protected static int asIntegerOrMinusOne(ValueNode node) { + var constant = node.asJavaConstant(); + if (constant != null) { + return constant.asInt(); + } + return -1; + } + + /** + * Make sure that constants added into the image heap have the correct kind. Constants for + * sub-integer types are often just integer constants in the Graal IR, i.e., we cannot rely on + * the JavaKind of the constant to match the type of the field or array. + * + * The second part is temporary until all constants embedded in graphs are ImageHeapConstant: + * While currently a graph can contain both {@link ImageHeapConstant} and + * {@link SubstrateObjectConstant} as roots, it is never allowed that a + * {@link ImageHeapConstant} references a {@link SubstrateObjectConstant}. + */ + private JavaConstant adaptForImageHeap(JavaConstant value, JavaKind storageKind) { + if (value.getJavaKind() != storageKind) { + assert value instanceof PrimitiveConstant && value.getJavaKind().getStackKind() == storageKind.getStackKind() : "only sub-int values can have a mismatch of the JavaKind: " + + value.getJavaKind() + ", " + storageKind; + return JavaConstant.forPrimitive(storageKind, value.asLong()); + + } else if (storageKind == JavaKind.Object && !(value instanceof ImageHeapConstant)) { + return bb.getUniverse().getHeapScanner().createImageHeapConstant(value, new ObjectScanner.OtherReason("SimulateClassInitializerGraphDecoder")); + + } else { + return value; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerPolicy.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerPolicy.java new file mode 100644 index 000000000000..dd079dd41408 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerPolicy.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.nodes.FixedWithNextNode; +import org.graalvm.compiler.nodes.ValueNode; +import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import org.graalvm.compiler.nodes.graphbuilderconf.InlineInvokePlugin; +import org.graalvm.compiler.nodes.graphbuilderconf.NodePlugin; +import org.graalvm.nativeimage.AnnotationAccess; + +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysis; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisPolicy; +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.core.code.FactoryMethodMarker; +import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.hosted.phases.ConstantFoldLoadFieldPlugin; +import com.oracle.svm.hosted.phases.InlineBeforeAnalysisPolicyUtils; + +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * This class is necessary because simulation of class initializer is based on + * {@link InlineBeforeAnalysis}. The scope keeps track of the number of bytes that were allocated in + * the image heap. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +public final class SimulateClassInitializerPolicy extends InlineBeforeAnalysisPolicy { + + static class AccumulativeCounters { + long allocatedBytes; + } + + public static final class SimulateClassInitializerInlineScope extends InlineBeforeAnalysisPolicy.AbstractPolicyScope { + final AccumulativeCounters accumulativeCounters; + + long allocatedBytes; + + SimulateClassInitializerInlineScope(AccumulativeCounters accumulativeCounters, int inliningDepth) { + super(inliningDepth); + this.accumulativeCounters = accumulativeCounters; + } + + @Override + public boolean allowAbort() { + return true; + } + + @Override + public void commitCalleeScope(InlineBeforeAnalysisPolicy.AbstractPolicyScope callee) { + SimulateClassInitializerInlineScope calleeScope = (SimulateClassInitializerInlineScope) callee; + assert accumulativeCounters == calleeScope.accumulativeCounters; + allocatedBytes += calleeScope.allocatedBytes; + } + + @Override + public void abortCalleeScope(InlineBeforeAnalysisPolicy.AbstractPolicyScope callee) { + SimulateClassInitializerInlineScope calleeScope = (SimulateClassInitializerInlineScope) callee; + assert accumulativeCounters == calleeScope.accumulativeCounters; + accumulativeCounters.allocatedBytes -= calleeScope.allocatedBytes; + } + + @Override + public boolean processNode(AnalysisMetaAccess metaAccess, ResolvedJavaMethod method, Node node) { + return true; + } + + @Override + public String toString() { + return "allocatedBytes: " + allocatedBytes + " (" + accumulativeCounters.allocatedBytes + ")"; + } + } + + private final SVMHost hostVM; + private final SimulateClassInitializerSupport support; + + SimulateClassInitializerPolicy(SVMHost hostVM, SimulateClassInitializerSupport support) { + super(new NodePlugin[]{new ConstantFoldLoadFieldPlugin(ParsingReason.PointsToAnalysis)}); + + this.hostVM = hostVM; + this.support = support; + } + + @Override + protected boolean shouldInlineInvoke(GraphBuilderContext b, ResolvedJavaMethod method, ValueNode[] args) { + if (b.getDepth() > support.maxInlineDepth) { + /* Safeguard against excessive inlining, for example endless recursion. */ + return false; + } + if (AnnotationAccess.isAnnotationPresent(method.getDeclaringClass(), FactoryMethodMarker.class)) { + /* + * Synthetic factory methods are annotated as "never inline before analysis" because + * they would all be inlined immediately. But for the class initializer analysis, we + * want them inlined so that we can simulate the allocations. + */ + return true; + } + return InlineBeforeAnalysisPolicyUtils.inliningAllowed(hostVM, b, method); + } + + @Override + protected boolean tryInvocationPlugins() { + return true; + } + + @Override + protected boolean needsExplicitExceptions() { + return true; + } + + @Override + protected InlineInvokePlugin.InlineInfo createInvokeInfo(ResolvedJavaMethod method) { + return InlineInvokePlugin.InlineInfo.createStandardInlineInfo(method); + } + + @Override + protected FixedWithNextNode processInvokeArgs(ResolvedJavaMethod targetMethod, FixedWithNextNode insertionPoint, ValueNode[] arguments) { + // No action is needed + return insertionPoint; + } + + @Override + protected AbstractPolicyScope createRootScope() { + /* The counters including all inlined methods. */ + var accumulated = new AccumulativeCounters(); + /* The counters just for the class initializer. */ + return new SimulateClassInitializerInlineScope(accumulated, 0); + } + + @Override + protected AbstractPolicyScope openCalleeScope(ResolvedJavaMethod method, AbstractPolicyScope o) { + var outer = (SimulateClassInitializerInlineScope) o; + return new SimulateClassInitializerInlineScope(outer.accumulativeCounters, outer.inliningDepth + 1); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerResult.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerResult.java new file mode 100644 index 000000000000..3039adf6b63e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerResult.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.UnmodifiableEconomicMap; + +import com.oracle.graal.pointsto.meta.AnalysisField; + +import jdk.vm.ci.meta.JavaConstant; + +/** + * The simulation result for a class. If a class was simulated as initialized, it also stores the + * static field values for the class, i.e., the initial values of the static fields that are later + * on written into the image heap. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +final class SimulateClassInitializerResult { + + static final SimulateClassInitializerResult NOT_SIMULATED_INITIALIZED = new SimulateClassInitializerResult(false, null); + static final SimulateClassInitializerResult INITIALIZED_HOSTED = SimulateClassInitializerResult.forInitialized(EconomicMap.emptyMap()); + + final boolean simulatedInitialized; + final UnmodifiableEconomicMap staticFieldValues; + + static SimulateClassInitializerResult forInitialized(EconomicMap staticFieldValues) { + return new SimulateClassInitializerResult(true, staticFieldValues); + } + + private SimulateClassInitializerResult(boolean simulatedInitialized, EconomicMap staticFieldValues) { + this.simulatedInitialized = simulatedInitialized; + this.staticFieldValues = staticFieldValues; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerStatus.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerStatus.java new file mode 100644 index 000000000000..3e5585735e7d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerStatus.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 2023, 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; + +/** + * The current status of a {@link SimulateClassInitializerClusterMember}. + * + * See {@link SimulateClassInitializerSupport} for an overview of class initializer simulation. + */ +enum SimulateClassInitializerStatus { + /** Dependencies of the cluster member are still being collected. */ + COLLECTING_DEPENDENCIES(false), + /** + * All dependencies of the cluster member itself have been collected, but cyclic dependencies + * are still being analyzed. So no analysis decision can be made yet for the cluster member. + */ + INIT_CANDIDATE(false), + /** + * Simulation of the cluster member was successful, and the result was published into + * {@link SimulateClassInitializerSupport#analyzedClasses}. + */ + PUBLISHED_AS_INITIALIZED(true), + /** + * Simulation of the cluster member was not successful, and the result was published into + * {@link SimulateClassInitializerSupport#analyzedClasses}. + */ + PUBLISHED_AS_NOT_INITIALIZED(true); + + final boolean published; + + SimulateClassInitializerStatus(boolean published) { + this.published = published; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java new file mode 100644 index 000000000000..345f54194859 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/SimulateClassInitializerSupport.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2023, 2023, 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; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import org.graalvm.collections.EconomicSet; +import org.graalvm.compiler.core.common.spi.ConstantFieldProvider; +import org.graalvm.compiler.debug.DebugContext; +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.nodes.BeginNode; +import org.graalvm.compiler.nodes.ConstantNode; +import org.graalvm.compiler.nodes.EndNode; +import org.graalvm.compiler.nodes.FullInfopointNode; +import org.graalvm.compiler.nodes.MergeNode; +import org.graalvm.compiler.nodes.ReturnNode; +import org.graalvm.compiler.nodes.StartNode; +import org.graalvm.compiler.nodes.StructuredGraph; +import org.graalvm.compiler.nodes.UnreachableBeginNode; +import org.graalvm.compiler.nodes.UnreachableControlSinkNode; +import org.graalvm.compiler.nodes.VirtualState; +import org.graalvm.compiler.nodes.java.ExceptionObjectNode; +import org.graalvm.compiler.nodes.java.FinalFieldBarrierNode; +import org.graalvm.compiler.nodes.java.LoadFieldNode; +import org.graalvm.compiler.nodes.java.StoreFieldNode; +import org.graalvm.compiler.nodes.virtual.VirtualObjectNode; +import org.graalvm.compiler.options.OptionValues; +import org.graalvm.compiler.phases.common.CanonicalizerPhase; +import org.graalvm.compiler.printer.GraalDebugHandlersFactory; +import org.graalvm.compiler.replacements.PEGraphDecoder; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.HostedProviders; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysis; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisGraphDecoder; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.SVMHost; +import com.oracle.svm.hosted.ameta.AnalysisConstantReflectionProvider; +import com.oracle.svm.hosted.fieldfolding.MarkStaticFinalFieldInitializedNode; +import com.oracle.svm.hosted.meta.HostedConstantReflectionProvider; +import com.oracle.svm.hosted.meta.HostedType; +import com.oracle.svm.hosted.phases.InlineBeforeAnalysisGraphDecoderImpl; +import com.oracle.svm.util.ClassUtil; + +import jdk.vm.ci.meta.JavaConstant; + +/** + * The main entry point for simulation of class initializer. + * + * A class initializer is executed exactly once before the first usage of a class. The specification + * requires that this happens as late as possible just before the first usage. But if executing a + * class initializer does not have any side effects, i.e., does not depend on any external state and + * does not modify any external state, then there is no observable difference when already starting + * out with the class pre-initialized at image run time. However, we do not want to really + * initialize the class in the image generator VM, because the class could be used by Feature code + * that runs at image build time, and we do not want to inherit any static state modified by a + * Feature. Therefore, we simulate the class initializer. + * + * A class with a simulated class initializer has the following initialization status: 1) Its + * initialization status in the hosting VM that runs the image generator does not matter. The class + * may or may not be initialized there. But in any case, static field values of the hosting VM are + * not visible at run time. 2) The class starts out as initialized at image run time, i.e., there + * are no run-time class initialization checks necessary and the class initializer itself is not AOT + * compiled. Static field values in the image heap reflect the status after the class initializer. + * + * Simulation determines whether a class initializer is side-effect free and does not depend on + * external state, i.e., if executing the class initializer before the first usage of the class does + * not lead to any observable differences (except timing differences). + * + * Examples of what can be simulated: + *
    + *
  • Allocations of instances, and allocations of arrays with a constant length. This is safe + * because a class initializer is a single-execute method, and all loops are unrolled during + * simulation.
  • + *
  • Reads and writes of objects (instances and arrays) that are allocated in the same class + * initializer.
  • + *
  • Reads of immutable state of classes that are simulated themselves or that are initialized at + * image build time.
  • + *
  • Cyclic class initializer dependencies where all members of the cycle can be simulated.
  • + *
  • Arithmetic or conditions that can be constant folded.
  • + *
+ * + * Examples of what prevents a class initializer from being simulated: + *
    + *
  • Reads or writes of mutable state of another class, i.e., accesses of non-final static fields + * or instance fields, or accesses of arrays that were read from final fields.
  • + *
  • Dependencies on another class that are not simulated themselves or that are not initialized + * at image build time.
  • + *
  • Invokes that cannot be de-virtualized
  • + *
  • Invokes of native methods.
  • + *
  • Loops that cannot be fully unrolled.
  • + *
  • Arithmetic or conditions that cannot be constant folded.
  • + *
+ * + * Simulation is implemented using partial evaluation of Graal IR, leveraging + * {@link PEGraphDecoder}. The class initializer itself and any method that partial evaluation + * reaches is parsed using {@link AnalysisParsedGraph}. The parsed graphs are shared with the static + * analysis, i.e., there is no special bytecode parsing (like special graph builder plugins). The + * simulation is similar to {@link InlineBeforeAnalysis}, but with significantly more + * canonicalizations done in {@link SimulateClassInitializerGraphDecoder#doCanonicalizeFixedNode}. + * + * To support cyclic class initializer dependencies, all not-yet-simulated classes referenced by a + * class initializer are handled as part of the same {@link SimulateClassInitializerCluster}. Each + * {@link SimulateClassInitializerClusterMember} goes through states defined in + * {@link SimulateClassInitializerStatus}: Only when all members of a cycle have been analyzed, the + * results for the whole cycle are published, i.e., either all or none of the members are marked as + * simulated. + * + * The graph decoder implementation {@link SimulateClassInitializerGraphDecoder} extends the + * {@link InlineBeforeAnalysisGraphDecoder}, which allows to abort inlining of a method. This "abort + * a particular inlining" is not necessary in "production mode" where such an abort immediately + * marks the class initializer as "simulation not possible". But for diagnostic purposes, a + * {@link #collectAllReasons} mode can be enabled. In this mode, as many reasons as possible are + * collected why simulation is not possible. When users try to make more class initializer + * simulateable, having as many reasons as possible is helpful. + * + * The results of the simulation are available before the static analysis, i.e., if a class + * initializer is simulated it is not seen as a root method by the static analysis. This is + * important because otherwise static final fields in the class would still be seen as written by + * the static analysis. However, it is not allowed to use simulation results already during bytecode + * parsing: since simulation relies on parsing arbitrary methods that might be reachable from a + * class initializer, using simulation results during parsing would lead to cyclic dependencies. In + * the best case that would be bytecode parsing deadlocks, in the worst case it would be + * non-deterministic usage of simulation information depending on the order in which classes are + * analyzed. Therefore, the simulation results are not used by + * {@link AnalysisConstantReflectionProvider} (only by {@link HostedConstantReflectionProvider}) and + * they are not used by {@link AnalysisType#isInitialized} ( only by + * {@link HostedType#isInitialized}). In order to use simulation results before the static analysis, + * the simulation results are used by {@link InlineBeforeAnalysis} (see the implementation of + * {@link InlineBeforeAnalysisGraphDecoderImpl}). + */ +public class SimulateClassInitializerSupport { + + protected final ClassInitializationSupport classInitializationSupport = ClassInitializationSupport.singleton(); + protected final SimulateClassInitializerPolicy simulateClassInitializerPolicy; + protected final SimulateClassInitializerConstantFieldProvider simulatedFieldValueConstantFieldProvider; + + /** The main data structure that stores all published results of the simulation. */ + protected final ConcurrentMap analyzedClasses = new ConcurrentHashMap<>(); + + /** + * Simulation of class initializer (like {@link InlineBeforeAnalysis}) requires ParseOnce, + * because otherwise graphs parsed for static analysis omits exception edges. + */ + protected final boolean enabled = ClassInitializationOptions.SimulateClassInitializer.getValue() && SubstrateOptions.parseOnce(); + + /* Cached value of options to avoid frequent lookup of option values. */ + protected final boolean collectAllReasons = ClassInitializationOptions.SimulateClassInitializerCollectAllReasons.getValue(); + protected final int maxInlineDepth = ClassInitializationOptions.SimulateClassInitializerMaxInlineDepth.getValue(); + protected final int maxLoopIterations = ClassInitializationOptions.SimulateClassInitializerMaxLoopIterations.getValue(); + protected final int maxAllocatedBytes = ClassInitializationOptions.SimulateClassInitializerMaxAllocatedBytes.getValue(); + + public static SimulateClassInitializerSupport singleton() { + return ImageSingletons.lookup(SimulateClassInitializerSupport.class); + } + + @SuppressWarnings("this-escape") + public SimulateClassInitializerSupport(AnalysisMetaAccess aMetaAccess, SVMHost hostVM) { + simulateClassInitializerPolicy = new SimulateClassInitializerPolicy(hostVM, this); + simulatedFieldValueConstantFieldProvider = new SimulateClassInitializerConstantFieldProvider(aMetaAccess, hostVM, this); + } + + public boolean isEnabled() { + return enabled; + } + + /** + * Initiate the simulation of the class initializer, unless there is already a published result + * available. + * + * The method returns true if the provided type is either initialized at build time or the + * simulation succeeded, i.e., if the type starts out as "initialized" at image run time. + */ + @SuppressWarnings("try") + public boolean trySimulateClassInitializer(BigBang bb, AnalysisType type) { + var existingResult = lookupPublishedSimulateClassInitializerResult(type); + if (existingResult != null) { + return existingResult.simulatedInitialized; + } + + var debug = new DebugContext.Builder(bb.getOptions()).build(); + try (var scope = debug.scope("SimulateClassInitializer", type)) { + /* Entry point to the analysis: start a new cluster of class initializers. */ + var cluster = new SimulateClassInitializerCluster(this, bb); + boolean result = trySimulateClassInitializer(debug, type, cluster, null); + + for (var clusterMember : cluster.clusterMembers.getValues()) { + VMError.guarantee(clusterMember.status.published, "All members of the cluster must be published at the end of the analysis"); + } + return result; + + } catch (Throwable ex) { + throw debug.handle(ex); + } + } + + boolean trySimulateClassInitializer(DebugContext debug, AnalysisType type, SimulateClassInitializerClusterMember dependant) { + var existingResult = lookupPublishedSimulateClassInitializerResult(type); + if (existingResult != null) { + return existingResult.simulatedInitialized; + } + return trySimulateClassInitializer(debug, type, dependant.cluster, dependant); + } + + /** + * Tries to constant-fold the field access based on results from the class initializer + * simulation. + */ + public ConstantNode tryCanonicalize(BigBang bb, LoadFieldNode node) { + var field = (AnalysisField) node.field(); + var simulatedFieldValue = getSimulatedFieldValue(field); + if (simulatedFieldValue == null) { + return null; + } + + /* + * We need to go via a ConstantFieldProvider to produce the ConstantNode so that stable + * array dimensions are properly computed. But we cannot use the ConstantFieldProvider from + * the providers, because our regular implementation only folds static fields for types + * where isInitialized() returns true. For our simulated types, AnalysisType.isInitialized() + * is still false because that method needs to return a stable value that does not change + * while the analysis is running. + */ + return simulatedFieldValueConstantFieldProvider.readConstantField(field, new ConstantFieldProvider.ConstantFieldTool<>() { + @Override + public JavaConstant readValue() { + return simulatedFieldValue; + } + + @Override + public JavaConstant getReceiver() { + /* We are only reading static fields. */ + return null; + } + + @Override + public Object getReason() { + return node.getNodeSourcePosition(); + } + + @Override + public ConstantNode foldConstant(JavaConstant constant) { + return constant == null ? null : ConstantNode.forConstant(constant, bb.getMetaAccess()); + } + + @Override + public ConstantNode foldStableArray(JavaConstant constant, int stableDimensions, boolean isDefaultStable) { + return constant == null ? null : ConstantNode.forConstant(constant, stableDimensions, isDefaultStable, bb.getMetaAccess()); + } + + @Override + public OptionValues getOptions() { + return bb.getOptions(); + } + }); + } + + /** + * Returns the simulated value of a static field, or null if no such value is available. + */ + public JavaConstant getSimulatedFieldValue(AnalysisField field) { + if (!enabled || !field.isStatic()) { + return null; + } + + var existingResult = analyzedClasses.get(field.getDeclaringClass()); + if (existingResult == null || !existingResult.simulatedInitialized) { + return null; + } + + var simulatedFieldValue = existingResult.staticFieldValues.get(field); + if (simulatedFieldValue != null) { + return simulatedFieldValue; + } else { + /* + * The JVM specification requires that all static final fields are explicitly + * initialized, even when they have the default value. So we should never hit this path + * for a static final field. But languages like Scala violate the specification and + * write static final fields outside the class initializer. Therefore, we must return + * null here to indicate that we do not know the value yet. + */ + return null; + } + } + + public boolean isClassInitializerSimulated(AnalysisType type) { + var existingResult = lookupPublishedSimulateClassInitializerResult(type); + if (existingResult != null) { + return existingResult.simulatedInitialized; + } + return false; + } + + private SimulateClassInitializerResult lookupPublishedSimulateClassInitializerResult(AnalysisType type) { + if (!type.isLinked()) { + return SimulateClassInitializerResult.NOT_SIMULATED_INITIALIZED; + } else if (type.isInitialized()) { + /* + * Type is registered as "initialize at build time", so class initializer is already + * executed by the host VM that runs the image generator. + */ + return SimulateClassInitializerResult.INITIALIZED_HOSTED; + } + VMError.guarantee(!type.isArray() && !type.isPrimitive(), "array and primitive types are always initialized at build time"); + + if (!enabled) { + return SimulateClassInitializerResult.NOT_SIMULATED_INITIALIZED; + } + return analyzedClasses.get(type); + } + + boolean trySimulateClassInitializer(DebugContext debug, AnalysisType type, SimulateClassInitializerCluster cluster, SimulateClassInitializerClusterMember dependant) { + var existingClusterMember = cluster.clusterMembers.get(type); + if (existingClusterMember != null) { + assert !existingClusterMember.status.published : type + ": " + existingClusterMember.status; + /* Cycle in class initializer dependencies. */ + dependant.dependencies.add(existingClusterMember); + return false; + } + + var clusterMember = new SimulateClassInitializerClusterMember(cluster, type); + if (dependant != null) { + dependant.dependencies.add(clusterMember); + } + + checkStrictlyInitializeAtRunTime(clusterMember); + if (clusterMember.notInitializedReasons.size() == 0 || collectAllReasons) { + addSuperDependencies(debug, clusterMember); + } + if (clusterMember.notInitializedReasons.size() == 0 || collectAllReasons) { + addClassInitializerDependencies(clusterMember); + } + + clusterMember.status = SimulateClassInitializerStatus.INIT_CANDIDATE; + + EconomicSet transitiveDependencies = EconomicSet.create(); + boolean dependenciesMissing = collectTransitiveDependencies(clusterMember, transitiveDependencies); + if (dependenciesMissing) { + /* Cycle is not fully processed yet, delay publishing results. */ + return false; + } + + boolean allDependenciesSimulated = true; + for (var dependency : transitiveDependencies) { + assert dependency.status != SimulateClassInitializerStatus.COLLECTING_DEPENDENCIES : dependency.type + ": " + dependency.status; + if (dependency.status == SimulateClassInitializerStatus.PUBLISHED_AS_NOT_INITIALIZED || dependency.notInitializedReasons.size() > 0) { + allDependenciesSimulated = false; + break; + } + } + + publishResults(debug, allDependenciesSimulated, transitiveDependencies); + return allDependenciesSimulated; + } + + private void checkStrictlyInitializeAtRunTime(SimulateClassInitializerClusterMember clusterMember) { + var clazz = clusterMember.type.getJavaClass(); + if (classInitializationSupport.specifiedInitKindFor(clazz) == InitKind.RUN_TIME && classInitializationSupport.isStrictlyDefined(clazz)) { + /* + * The class itself (not just the whole package) is registered as + * "initialize at run time", so we honor that registration. There was hopefully a good + * reason for the explicit registration. + */ + clusterMember.notInitializedReasons.add("Class is strictly defined as initialize at run time"); + } + } + + private void addSuperDependencies(DebugContext debug, SimulateClassInitializerClusterMember clusterMember) { + if (clusterMember.type.isInterface()) { + /* + * Initialization of an interface does not trigger initialization of any + * super-interface, even in the case of default methods. + */ + return; + } + var supertype = clusterMember.type.getSuperclass(); + if (supertype != null) { + addDependency(debug, clusterMember, supertype, supertype); + } + addInterfaceDependencies(debug, clusterMember.type, clusterMember); + } + + private void addInterfaceDependencies(DebugContext debug, AnalysisType type, SimulateClassInitializerClusterMember clusterMember) { + for (var iface : type.getInterfaces()) { + if (iface.declaresDefaultMethods()) { + /* + * An interface that declares default methods is initialized when a class + * implementing it is initialized. + */ + addDependency(debug, clusterMember, iface, iface); + } else { + /* + * An interface that does not declare default methods is independent from a class + * that implements it, i.e., the interface can still be uninitialized even when the + * class is initialized. + */ + addInterfaceDependencies(debug, iface, clusterMember); + } + } + } + + private void addDependency(DebugContext debug, SimulateClassInitializerClusterMember clusterMember, AnalysisType newDependency, Object reason) { + var dependencyResult = lookupPublishedSimulateClassInitializerResult(newDependency); + if (dependencyResult != null) { + if (!dependencyResult.simulatedInitialized) { + clusterMember.notInitializedReasons.add(reason); + } + return; + } + trySimulateClassInitializer(debug, newDependency, clusterMember.cluster, clusterMember); + } + + private void addClassInitializerDependencies(SimulateClassInitializerClusterMember clusterMember) { + var classInitializer = clusterMember.type.getClassInitializer(); + if (classInitializer == null) { + return; + } + + StructuredGraph graph; + try { + graph = decodeGraph(clusterMember, classInitializer); + } catch (SimulateClassInitializerAbortException ignored) { + VMError.guarantee(!clusterMember.notInitializedReasons.isEmpty(), "Reason must be added before throwing the abort-exception"); + return; + } + + for (Node node : graph.getNodes()) { + processEffectsOfNode(clusterMember, node); + } + } + + @SuppressWarnings("try") + private StructuredGraph decodeGraph(SimulateClassInitializerClusterMember clusterMember, AnalysisMethod classInitializer) { + var bb = clusterMember.cluster.bb; + var analysisParsedGraph = classInitializer.ensureGraphParsed(bb); + var description = new DebugContext.Description(classInitializer, ClassUtil.getUnqualifiedName(classInitializer.getClass()) + ":" + classInitializer.getId()); + var debug = new DebugContext.Builder(bb.getOptions(), new GraalDebugHandlersFactory(bb.getProviders(classInitializer).getSnippetReflection())).description(description).build(); + + var result = new StructuredGraph.Builder(bb.getOptions(), debug) + .method(classInitializer) + .recordInlinedMethods(false) + .trackNodeSourcePosition(analysisParsedGraph.getEncodedGraph().trackNodeSourcePosition()) + .build(); + + try (var scope = debug.scope("SimulateClassInitializerGraphDecoder", result)) { + + var decoder = createGraphDecoder(clusterMember, bb, result); + decoder.decode(classInitializer); + debug.dump(DebugContext.BASIC_LEVEL, result, "SimulateClassInitializer after decode"); + + CanonicalizerPhase.create().apply(result, clusterMember.cluster.providers); + return result; + + } catch (Throwable ex) { + throw debug.handle(ex); + } + } + + protected SimulateClassInitializerGraphDecoder createGraphDecoder(SimulateClassInitializerClusterMember clusterMember, BigBang bb, StructuredGraph result) { + return new SimulateClassInitializerGraphDecoder(bb, simulateClassInitializerPolicy, clusterMember, result); + } + + private static void processEffectsOfNode(SimulateClassInitializerClusterMember clusterMember, Node node) { + if (node instanceof StartNode || node instanceof BeginNode || node instanceof UnreachableBeginNode || node instanceof UnreachableControlSinkNode || node instanceof FullInfopointNode) { + /* Boilerplate nodes in a graph that do not lead to any machine code. */ + return; + } else if (node instanceof ReturnNode returnNode) { + assert returnNode.result() == null : "Class initializer always has return type void"; + return; + } else if (node instanceof VirtualState) { + /* All kinds of FrameState can be ignored. */ + return; + } else if (node instanceof VirtualObjectNode) { + /* + * Can still be referenced by a FrameState even when all allocations were partially + * evaluated into image heap constants. + */ + return; + } else if (node instanceof ConstantNode) { + return; + } else if (node instanceof FinalFieldBarrierNode) { + return; + } else if (node instanceof MarkStaticFinalFieldInitializedNode) { + return; + + } else if (node instanceof StoreFieldNode storeFieldNode) { + var field = (AnalysisField) storeFieldNode.field(); + if (field.isStatic() && field.getDeclaringClass().equals(clusterMember.type)) { + var constantValue = storeFieldNode.value().asJavaConstant(); + if (constantValue != null) { + clusterMember.staticFieldValues.put(field, constantValue); + return; + } + } + } + + clusterMember.notInitializedReasons.add(node); + } + + private void publishResults(DebugContext debug, boolean simulatedInitialized, EconomicSet transitiveDependencies) { + for (var clusterMember : transitiveDependencies) { + if (clusterMember.status.published) { + continue; + } + assert clusterMember.status == SimulateClassInitializerStatus.INIT_CANDIDATE : clusterMember.type + ": " + clusterMember.status; + + SimulateClassInitializerResult existingResult; + if (simulatedInitialized) { + assert clusterMember.notInitializedReasons.isEmpty() : clusterMember.type + ": " + clusterMember.notInitializedReasons; + if (debug.isLogEnabled(DebugContext.BASIC_LEVEL)) { + debug.log("simulated: %s", clusterMember.type.toJavaName(true)); + } + existingResult = analyzedClasses.putIfAbsent(clusterMember.type, SimulateClassInitializerResult.forInitialized(clusterMember.staticFieldValues)); + clusterMember.status = SimulateClassInitializerStatus.PUBLISHED_AS_INITIALIZED; + + } else { + if (debug.isLogEnabled(DebugContext.BASIC_LEVEL)) { + debug.log("not simulated: %s:%n %s", clusterMember.type.toJavaName(true), + clusterMember.notInitializedReasons.stream() + .map(reason -> reasonToString(clusterMember.cluster.providers, reason)) + .filter(s -> s != null && !s.isEmpty()) + .collect(Collectors.joining(System.lineSeparator() + " "))); + } + existingResult = analyzedClasses.putIfAbsent(clusterMember.type, SimulateClassInitializerResult.NOT_SIMULATED_INITIALIZED); + clusterMember.status = SimulateClassInitializerStatus.PUBLISHED_AS_NOT_INITIALIZED; + } + if (existingResult != null && simulatedInitialized != existingResult.simulatedInitialized) { + StringBuilder msg = new StringBuilder("mismatch with existing registration: ").append(clusterMember.type.toJavaName(true)) + .append(", existingResult: ").append(existingResult.simulatedInitialized) + .append(", new: ").append(simulatedInitialized) + .append(System.lineSeparator()).append("Cluster members:"); + for (var m : clusterMember.cluster.clusterMembers.getValues()) { + msg.append(System.lineSeparator()).append(" ").append(m.type.toJavaName(true)).append(": ").append(m.status) + .append(", ").append(m.staticFieldValues.size()) + .append(", ").append(m.notInitializedReasons.isEmpty() ? "(no reasons)" : reasonToString(clusterMember.cluster.providers, m.notInitializedReasons.get(0))); + } + throw VMError.shouldNotReachHere(msg.toString()); + } + } + } + + private boolean collectTransitiveDependencies(SimulateClassInitializerClusterMember clusterMember, EconomicSet transitiveDependencies) { + if (clusterMember.status == SimulateClassInitializerStatus.COLLECTING_DEPENDENCIES) { + return true; + } else if (clusterMember.status.published) { + transitiveDependencies.add(clusterMember); + /* + * No need to follow any transitive dependencies, that was done when the cluster member + * was published. + */ + } else { + assert clusterMember.status == SimulateClassInitializerStatus.INIT_CANDIDATE : clusterMember.type + ": " + clusterMember.status; + if (transitiveDependencies.add(clusterMember)) { + for (var dependency : clusterMember.dependencies) { + if (collectTransitiveDependencies(dependency, transitiveDependencies)) { + return true; + } + } + } + } + return false; + } + + private static String reasonToString(HostedProviders providers, Object reason) { + if (reason instanceof AnalysisType type) { + return "superclass/interface: " + type.toJavaName(true); + } else if (reason instanceof EnsureClassInitializedNode node && node.constantTypeOrNull(providers.getConstantReflection()) != null) { + return "class initializer dependency: " + + ((EnsureClassInitializedNode) reason).constantTypeOrNull(providers.getConstantReflection()).toJavaName(true) + + " " + node.getNodeSourcePosition(); + } else if (reason instanceof Node node) { + if (node instanceof BeginNode || node instanceof ExceptionObjectNode || node instanceof MergeNode || node instanceof EndNode) { + /* Filter out uninteresting nodes to keep the reason printing short. */ + return null; + } else { + return "node " + node + " " + node.getNodeSourcePosition(); + } + } else { + return String.valueOf(reason); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/TypeInitializerGraph.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/TypeInitializerGraph.java index f520e58aa87c..a7b06e24e683 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/TypeInitializerGraph.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/TypeInitializerGraph.java @@ -175,7 +175,7 @@ private Safety initialMethodSafety(AnalysisMethod m) { } private boolean isSubstitutedMethod(AnalysisMethod m) { - return !classInitializationSupport.shouldInitializeAtRuntime(m.getDeclaringClass()) && m.getWrapped() instanceof SubstitutionMethod; + return classInitializationSupport.maybeInitializeAtBuildTime(m.getDeclaringClass()) && m.getWrapped() instanceof SubstitutionMethod; } /** diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/SVMImageHeapScanner.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/SVMImageHeapScanner.java index f6fa4cb59847..ee7961fd0229 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/SVMImageHeapScanner.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/SVMImageHeapScanner.java @@ -107,7 +107,13 @@ public boolean isValueAvailable(AnalysisField field) { @Override protected ValueSupplier readHostedFieldValue(AnalysisField field, JavaConstant receiver) { AnalysisConstantReflectionProvider aConstantReflection = (AnalysisConstantReflectionProvider) this.constantReflection; - return aConstantReflection.readHostedFieldValue(field, hostedMetaAccess, receiver); + return aConstantReflection.readHostedFieldValue(field, hostedMetaAccess, receiver, true); + } + + @Override + public JavaConstant readFieldValue(AnalysisField field, JavaConstant receiver) { + AnalysisConstantReflectionProvider aConstantReflection = (AnalysisConstantReflectionProvider) this.constantReflection; + return aConstantReflection.readValue(metaAccess, field, receiver, true); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java index 75785187779f..7774fd048b9c 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedConstantReflectionProvider.java @@ -108,7 +108,7 @@ public void forEachArrayElement(JavaConstant array, ObjIntConsumer public JavaConstant readFieldValue(ResolvedJavaField field, JavaConstant receiver) { var hField = (HostedField) field; assert checkHub(receiver) : "Receiver " + receiver + " of field " + hField + " read should not be java.lang.Class. Expecting to see DynamicHub here."; - return hUniverse.lookup(aConstantReflection.readValue(hMetaAccess, hField.getWrapped(), receiver)); + return hUniverse.lookup(aConstantReflection.readValue(hMetaAccess, hField.getWrapped(), receiver, true)); } private boolean checkHub(JavaConstant constant) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedType.java index e395a492fdeb..8febc3022eba 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedType.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/HostedType.java @@ -238,7 +238,18 @@ public final AssumptionResult hasFinalizableSubclass() { @Override public final boolean isInitialized() { - return wrapped.isInitialized(); + if (!wrapped.isReachable()) { + /* Workaround until ParseOnce can always be enabled. */ + return wrapped.isInitialized(); + } + + /* + * Note that we do not delegate to wrapped.isInitialized here: when a class initializer is + * simulated at image build time, then AnalysisType.isInitialized() returns false but + * DynamicHub.isInitialized returns true. We want to treat such classes as initialized + * during AOT compilation. + */ + return getHub().isInitialized(); } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/SharedConstantFieldProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/SharedConstantFieldProvider.java index 26f83f9c02c8..f6a7a2c50470 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/SharedConstantFieldProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/SharedConstantFieldProvider.java @@ -85,7 +85,7 @@ public boolean isStableField(ResolvedJavaField field, ConstantFieldTool tool) } private boolean allowConstantFolding(ResolvedJavaField field) { - if (field.isStatic() && !field.getDeclaringClass().isInitialized() && !(asAnalysisField(field).getWrapped() instanceof ReadableJavaField)) { + if (field.isStatic() && !isClassInitialized(field) && !(asAnalysisField(field).getWrapped() instanceof ReadableJavaField)) { /* * The class is not initialized at image build time, so we do not have a static field * value to constant fold. Note that a ReadableJavaField is able to provide a field @@ -96,6 +96,10 @@ private boolean allowConstantFolding(ResolvedJavaField field) { return hostVM.allowConstantFolding(field); } + protected boolean isClassInitialized(ResolvedJavaField field) { + return field.getDeclaringClass().isInitialized(); + } + @Override protected boolean isFinalFieldValueConstant(ResolvedJavaField field, JavaConstant value, ConstantFieldTool tool) { if (value.getJavaKind() == JavaKind.Object && metaAccess.isInstanceOf(value, MethodPointer.class)) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java new file mode 100644 index 000000000000..35411f8a6410 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/InlineBeforeAnalysisGraphDecoderImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023, 2023, 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.phases; + +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.nodes.ConstantNode; +import org.graalvm.compiler.nodes.StructuredGraph; +import org.graalvm.compiler.nodes.java.LoadFieldNode; + +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.HostedProviders; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisGraphDecoder; +import com.oracle.graal.pointsto.phases.InlineBeforeAnalysisPolicy; +import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; +import com.oracle.svm.hosted.fieldfolding.IsStaticFinalFieldInitializedNode; + +public class InlineBeforeAnalysisGraphDecoderImpl extends InlineBeforeAnalysisGraphDecoder { + + private final SimulateClassInitializerSupport simulateClassInitializerSupport = SimulateClassInitializerSupport.singleton(); + + public InlineBeforeAnalysisGraphDecoderImpl(BigBang bb, InlineBeforeAnalysisPolicy policy, StructuredGraph graph, HostedProviders providers) { + super(bb, policy, graph, providers, null); + } + + @Override + protected Node doCanonicalizeFixedNode(InlineBeforeAnalysisMethodScope methodScope, LoopScope loopScope, Node initialNode) { + Node node = super.doCanonicalizeFixedNode(methodScope, loopScope, initialNode); + + if (node instanceof EnsureClassInitializedNode ensureClassInitializedNode) { + node = handleEnsureClassInitializedNode(ensureClassInitializedNode); + } else if (node instanceof LoadFieldNode loadFieldNode) { + node = handleLoadFieldNode(loadFieldNode); + } else if (node instanceof IsStaticFinalFieldInitializedNode isStaticFinalFieldInitializedNode) { + node = handleIsStaticFinalFieldInitializedNode(isStaticFinalFieldInitializedNode); + } + return node; + } + + private Node handleEnsureClassInitializedNode(EnsureClassInitializedNode node) { + AnalysisType type = (AnalysisType) node.constantTypeOrNull(bb.getConstantReflectionProvider()); + if (type != null) { + if (type.isReachable()) { + /* + * The class initializer is always analyzed for reachable types so that the + * DynamicHub can be properly initialized. Avoid starting a second concurrent + * analysis. + */ + type.getInitializeMetaDataTask().ensureDone(); + } else { + /* + * Even for types that are not yet reachable, we can analyze the class initializer. + * If the type gets reachable later, the analysis results are re-used. Or the type + * can remain unreachable throughout the whole analysis, because the Graal IR we are + * decoding here could actually be dead code that is removed before building the + * type flow graph. + */ + simulateClassInitializerSupport.trySimulateClassInitializer(bb, type); + } + if (simulateClassInitializerSupport.isClassInitializerSimulated(type)) { + return null; + } + } + return node; + } + + private Node handleLoadFieldNode(LoadFieldNode node) { + ConstantNode canonicalized = simulateClassInitializerSupport.tryCanonicalize(bb, node); + if (canonicalized != null) { + return canonicalized; + } + return node; + } + + private Node handleIsStaticFinalFieldInitializedNode(IsStaticFinalFieldInitializedNode node) { + var field = (AnalysisField) node.getField(); + if (simulateClassInitializerSupport.isClassInitializerSimulated(field.getDeclaringClass())) { + return ConstantNode.forBoolean(true); + } + return node; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java index 23513e435b74..a64cbec615f2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java @@ -224,7 +224,7 @@ private void registerConditionalFoldInvocationPlugins(InvocationPlugins plugins) /* VarHandles.makeFieldHandle() triggers init of receiver class (JDK-8291065). */ Object classArg = args[0]; if (classArg instanceof Class) { - if (classInitializationSupport.shouldInitializeAtRuntime((Class) classArg)) { + if (!classInitializationSupport.maybeInitializeAtBuildTime((Class) classArg)) { /* Skip the folding and register the field for run time reflection. */ if (reason.duringAnalysis()) { Field field = ReflectionUtil.lookupField(true, (Class) args[0], (String) args[1]); @@ -247,7 +247,7 @@ private void registerConditionalFoldInvocationPlugins(InvocationPlugins plugins) Object fieldArg = args[0]; if (fieldArg instanceof Field) { Field field = (Field) fieldArg; - if (isStatic(field) && classInitializationSupport.shouldInitializeAtRuntime(field.getDeclaringClass())) { + if (isStatic(field) && !classInitializationSupport.maybeInitializeAtBuildTime(field.getDeclaringClass())) { /* Skip the folding and register the field for run time reflection. */ if (reason.duringAnalysis()) { RuntimeReflection.register(field); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java index f731519246e1..72bd14bf79a1 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AnnotationSubstitutionProcessor.java @@ -120,7 +120,7 @@ public AnnotationSubstitutionProcessor(ImageClassLoader imageClassLoader, MetaAc public void registerFieldValueTransformer(Field reflectionField, FieldValueTransformer transformer) { ResolvedJavaField field = metaAccess.lookupJavaField(reflectionField); - if (!SubstrateOptions.parseOnce() && classInitializationSupport.shouldInitializeAtRuntime(reflectionField.getDeclaringClass())) { + if (!SubstrateOptions.parseOnce() && !classInitializationSupport.maybeInitializeAtBuildTime(reflectionField.getDeclaringClass())) { String parseOnce = SubstrateOptions.ParseOnce.getName(); String reason = "It was detected that " + parseOnce + " is disabled. " + "Registering a field value transformer for a field whose declaring class is marked for run time initialization is not supported in this configuration. " + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/UnsafeAutomaticSubstitutionProcessor.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/UnsafeAutomaticSubstitutionProcessor.java index fc6b2ed96dbc..ee5ae4f54abc 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/UnsafeAutomaticSubstitutionProcessor.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/UnsafeAutomaticSubstitutionProcessor.java @@ -353,7 +353,7 @@ public void computeSubstitutions(SVMHost hostVM, ResolvedJavaType hostType) { if (hostType.isArray()) { return; } - if (hostVM.getClassInitializationSupport().shouldInitializeAtRuntime(hostType)) { + if (!hostVM.getClassInitializationSupport().maybeInitializeAtBuildTime(hostType)) { /* * The class initializer of this type is executed at run time. The methods in Unsafe are * substituted to return the correct value at image runtime, or fail if the field was diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitialization.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitialization.java new file mode 100644 index 000000000000..92149d7ce60e --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitialization.java @@ -0,0 +1,988 @@ +/* + * Copyright (c) 2019, 2019, 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.test.clinit; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; + +import com.oracle.svm.core.NeverInline; +import com.oracle.svm.hosted.classinitialization.ClassInitializationOptions; +import com.oracle.svm.hosted.fieldfolding.IsStaticFinalFieldInitializedNode; + +import jdk.internal.misc.Unsafe; + +/* + * The suffix of class names indicates the time when the class initializer is determined to be safe + * for execution/simulation at image build time. The tests are run in two configurations: 1) with + * the "old" class initialization strategy, which uses the "early" and "late" class initializer + * analysis to initialize classes at image build time, and 2) with the "new" class initialization + * strategy where all classes can be used at image build time, but class initializer are simulated + * to avoid explicit initialization at image build time. This leads to the following 4 suffixes: + * + * - MustBeSafeEarly: The early class initializer analysis (before static analysis) finds this class + * as safe for initialization at image build time. The simulation of class initializer also must + * succeed, because it is more powerful than the early analysis. + * + * - MustBeSafeLate: The late class initializer analysis (ater static analysis) finds this class as + * safe for initialization at image build time. The * simulation of class initializer also must + * succeed, because it is more powerful than the late analysis. + * + * - MustBeSimulated: Neither the early nor the late analysis finds this class as safe for + * initialization. But the simulation of class initializer succeeded, i.e., with the "new" class + * initialization strategy the class starts out as initialized at run time. + * + * - MustBeDelayed: The class initializer has side effects, it must be executed at run time. + * + * The suffixes are checked in the feature at build time (code in this file), at run time in the + * main method (code in this file), and by an external script that parses the class initialization + * log output (code in mx_substratevm.py). + */ + +class PureMustBeSafeEarly { + static int v; + static { + v = 1; + v = 42; + } +} + +class InitializesPureMustBeDelayed { + static int v; + static { + v = PureMustBeSafeEarly.v; + } +} + +/** + * The class initializer of this type is actually never executed because static final int fields + * assigned directly at the field definition site are already constant folded by javac at all usage + * sites. + */ +class NonPureAccessedFinal { + static final int v = 1; + static { + if (alwaysTrue()) { + throw new RuntimeException("Must not be called at runtime or compile time."); + } + } + + static boolean alwaysTrue() { + return true; + } +} + +class PureCallMustBeSafeEarly { + static int v; + static { + v = TestClassInitialization.pure(); + } +} + +class NonPureMustBeDelayed { + static int v = 1; + static { + System.out.println("Delaying NonPureMustBeDelayed"); + } +} + +class InitializesNonPureMustBeDelayed { + static int v = NonPureMustBeDelayed.v; +} + +class SystemPropReadMustBeDelayed { + static int v = 1; + static { + System.getProperty("test"); + } +} + +class SystemPropWriteMustBeDelayed { + static int v = 1; + static { + System.setProperty("test", ""); + } +} + +class StartsAThreadMustBeDelayed { + static int v = 1; + static { + new Thread().start(); + } +} + +class CreatesAnExceptionMustBeDelayed { + static Exception e; + static { + e = new Exception("should fire at runtime"); + } +} + +class ThrowsAnExceptionUninitializedMustBeDelayed { + static int v = 1; + static { + if (PureMustBeSafeEarly.v == 42) { + throw new RuntimeException("should fire at runtime"); + } + } +} + +interface PureInterfaceMustBeSafeEarly { +} + +class PureSubclassMustBeDelayed extends SuperClassMustBeDelayed { + static int v = 1; +} + +class SuperClassMustBeDelayed implements PureInterfaceMustBeSafeEarly { + static { + System.out.println("Delaying SuperClassMustBeDelayed"); + } +} + +interface InterfaceNonPureMustBeDelayed { + int v = B.v; + + class B { + static int v = 1; + static { + System.out.println("Delaying InterfaceNonPureMustBeDelayed"); + } + } +} + +interface InterfaceNonPureDefaultMustBeDelayed { + int v = B.v; + + class B { + static int v = 1; + static { + System.out.println("Delaying InterfaceNonPureDefaultMustBeDelayed"); + } + } + + default int m() { + return v; + } +} + +class PureSubclassInheritsDelayedInterfaceMustBeSafeEarly implements InterfaceNonPureMustBeDelayed { + static int v = 1; +} + +class PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed implements InterfaceNonPureDefaultMustBeDelayed { + static int v = 1; +} + +class ImplicitExceptionInInitializerUninitializedMustBeDelayed { + + static int a = 10; + static int b = 0; + static int res; + + static { + res = a / b; + } +} + +class PureDependsOnImplicitExceptionUninitializedMustBeDelayed { + + static int a; + + static { + a = ImplicitExceptionInInitializerUninitializedMustBeDelayed.res; + } +} + +class StaticFieldHolderMustBeSafeEarly { + /** + * Other class initializers that modify {@link #a} must not run at image build time so that the + * initial value 111 assigned here can be read at run time. + */ + static int a = 111; + + static void setA(int value) { + a = value; + } +} + +class StaticFieldModifer1MustBeDelayed { + static { + StaticFieldHolderMustBeSafeEarly.a = 222; + } + + static void triggerInitialization() { + } +} + +class StaticFieldModifer2MustBeDelayed { + static { + StaticFieldHolderMustBeSafeEarly.setA(333); + } + + static void triggerInitialization() { + } +} + +class RecursionInInitializerMustBeSafeLate { + static int i = compute(200); + + static int compute(int n) { + if (n <= 1) { + return 1; + } else { + return n + compute(n - 1); + } + } +} + +class UnsafeAccessMustBeSafeLate { + static UnsafeAccessMustBeSafeLate value = compute(); + + int f01; + int f02; + int f03; + int f04; + int f05; + int f06; + int f07; + int f08; + int f09; + int f10; + int f11; + int f12; + int f13; + int f14; + int f15; + int f16; + + static UnsafeAccessMustBeSafeLate compute() { + UnsafeAccessMustBeSafeLate result = new UnsafeAccessMustBeSafeLate(); + /* + * We are writing a random instance field, depending on the header size. But the object is + * big enough so that the write is one of the fields. The unsafe write is converted to a + * proper store field node because the offset is constant, so in the static analysis graph + * there is no unsafe access node. + */ + Unsafe.getUnsafe().putInt(result, 32L, 1234); + return result; + } +} + +enum EnumMustBeSafeEarly { + V1(null), + V2("Hello"), + V3(new Object()); + + final Object value; + + EnumMustBeSafeEarly(Object value) { + this.value = value; + } + + Object getValue() { + /* + * Use an assertion, so that the static final field that stores the assertion status is + * filled in the class initializer. We want to test that using assertions does not impact + * the class initialization analysis. + */ + assert value != null; + return value; + } +} + +class NativeMethodMustBeDelayed { + static int i = compute(); + + static int compute() { + try { + nativeMethod(); + } catch (LinkageError ignored) { + /* Expected since the native method is not implemented. */ + } + return 42; + } + + static native void nativeMethod(); + + static void foo() { + /* + * Even when a class is initialized at run time, the check whether assertions are included + * must be constant folded at image build time. Otherwise we have a performance problem. + */ + assert assertionOnlyCode(); + } + + static boolean assertionOnlyCode() { + AssertionOnlyClassMustBeUnreachable.reference(); + return false; + } +} + +class AssertionOnlyClassMustBeUnreachable { + static void reference() { + } +} + +/** + * Class initializer references a helper class that can be initialized early. Since the early class + * initializer analysis recursviely processes dependent classes, this class is also safe for early + * initialization. + */ +class ReferencesOtherPureClassMustBeSafeEarly { + static { + HelperClassMustBeSafeEarly.foo(); + } + + static void foo() { + } +} + +class HelperClassMustBeSafeEarly { + static void foo() { + } +} + +/** + * Cycle between this class and a helper class. Even though both classes could be initialized early, + * the early analysis bails out because analyzing cycles would be too complicated. + */ +class CycleMustBeSafeLate { + static { + HelperClassMustBeSafeLate.foo(); + } + + static void foo() { + } +} + +class HelperClassMustBeSafeLate { + static { + CycleMustBeSafeLate.foo(); + } + + static void foo() { + } +} + +/** Various reflection lookup methods are safe for execution at image build time. */ +class ReflectionMustBeSafeEarly { + static Class c1; + static Class c2; + static Method m1; + static Field f2; + + static { + try { + Class c1Local = Class.forName("com.oracle.svm.test.clinit.ForNameMustBeSafeEarly", true, ReflectionMustBeSafeEarly.class.getClassLoader()); + c1 = c1Local; + + /** + * Looking up a class that cannot be initialized at build time is allowed, as long as + * `initialize` is `false`. + */ + Class c2Local = Class.forName("com.oracle.svm.test.clinit.ForNameUninitializedMustBeDelayed", false, ReflectionMustBeSafeEarly.class.getClassLoader()); + c2 = c2Local; + + /* + * Calling getDeclaredMethod on the field c1 instead of the variable c1Local would not + * work, because the field load cannot be constant folded by the + * EarlyClassInitializerAnalysis. + */ + m1 = c1Local.getDeclaredMethod("foo", int.class); + f2 = c2Local.getDeclaredField("field"); + + } catch (ReflectiveOperationException ex) { + throw new Error(ex); + } + } +} + +@SuppressWarnings("unused") +class ForNameMustBeSafeEarly { + static void foo(int arg) { + } +} + +class ForNameUninitializedMustBeDelayed { + static { + System.out.println("Delaying ForNameUninitializedMustBeDelayed"); + } + + int field; +} + +class DevirtualizedCallMustBeDelayed { + static { + System.out.println("Delaying DevirtualizedCallMustBeDelayed"); + } + + static final Object value = 42; +} + +class DevirtualizedCallSuperMustBeSafeEarly { + Object foo() { + return -1; + } +} + +class DevirtualizedCallSubMustBeSafeEarly extends DevirtualizedCallSuperMustBeSafeEarly { + @Override + Object foo() { + return DevirtualizedCallMustBeDelayed.value; + } +} + +class DevirtualizedCallUsageMustBeDelayed { + static final Object value = computeValue(); + + private static Object computeValue() { + DevirtualizedCallSuperMustBeSafeEarly provider = createProvider(); + + /* + * The static analysis can prove that DevirtualizedCallSubMustBeDelayed.foo is the only + * callee and de-virtualize this call. So the original target method of the call site and + * the actually invoked method are different - and the analysis that automatically + * initializes classes must properly pick up this dependency. + */ + return provider.foo(); + } + + private static DevirtualizedCallSuperMustBeSafeEarly createProvider() { + return new DevirtualizedCallSubMustBeSafeEarly(); + } +} + +class LargeAllocation1MustBeDelayed { + static final Object value = computeValue(); + + private static Object computeValue() { + return new Object[200_000]; + } +} + +class LargeAllocation2MustBeDelayed { + static final Object value = computeValue(); + + private static Object computeValue() { + return new int[1][200_000]; + } +} + +enum ComplexEnumMustBeSimulated { + V1 { + @Override + int virtualMethod() { + return 41; + } + }, + V2 { + @Override + int virtualMethod() { + return 42; + } + }; + + abstract int virtualMethod(); + + static final Map lookup; + + static { + lookup = new HashMap<>(); + for (var v : values()) { + lookup.put(v.name(), v); + } + } +} + +class StaticFinalFieldFoldingMustBeSafeEarly { + + Object f1; + Object f2; + Object f3; + + StaticFinalFieldFoldingMustBeSafeEarly() { + this.f1 = F1; + this.f2 = F2; + this.f3 = F3; + } + + static final StaticFinalFieldFoldingMustBeSafeEarly before = new StaticFinalFieldFoldingMustBeSafeEarly(); + + /** + * Field value is stored in the class file attribute, so it is available even before this + * assignment. + */ + static final String F1 = "abc"; + /** + * Field is optimized by our {@link IsStaticFinalFieldInitializedNode static final field folding + * feature}. + */ + static final Object F2 = "abc"; + /** Just a regular field. */ + static final Object F3 = new String[]{"abc"}; + + static final StaticFinalFieldFoldingMustBeSafeEarly after = new StaticFinalFieldFoldingMustBeSafeEarly(); +} + +class LambdaMustBeSafeLate { + private static final Predicate IS_AUTOMATIC = s -> s.equals("Hello"); + + static boolean matches(List l) { + return l.stream().anyMatch(IS_AUTOMATIC); + } +} + +@SuppressWarnings("deprecation") +class BoxingMustBeSimulated { + static Integer i1 = 41; + static Integer i2 = new Integer(42); + + static int sum = i1 + i2; + + static final Map, Object> defaultValues = new HashMap<>(); + + static { + defaultValues.put(boolean.class, Boolean.FALSE); + defaultValues.put(byte.class, (byte) 0); + defaultValues.put(short.class, (short) 0); + defaultValues.put(int.class, 0); + defaultValues.put(long.class, 0L); + defaultValues.put(char.class, '\0'); + defaultValues.put(float.class, 0.0F); + defaultValues.put(double.class, 0.0); + } + + public static Object defaultValue(Class clazz) { + return defaultValues.get(clazz); + } + + static Object S1; + static Object O1; + static Object O2; + + static { + short[] shorts = {42, 43, 44, 45, 46, 47, 48}; + S1 = new short[12]; + System.arraycopy(shorts, 1, S1, 2, 5); + System.arraycopy(S1, 3, S1, 5, 5); + + Object[] objects = {"42", "43", "44", "45", "46", "47", "48"}; + O1 = Arrays.copyOf(objects, 3); + O2 = Arrays.copyOfRange(objects, 3, 6, String[].class); + } +} + +class SingleByteFieldMustBeSafeEarly { + static SingleByteFieldMustBeSafeEarly instance1 = new SingleByteFieldMustBeSafeEarly((byte) 42); + static SingleByteFieldMustBeSafeEarly instance2 = new SingleByteFieldMustBeSafeEarly((byte) -42); + + byte b; + + SingleByteFieldMustBeSafeEarly(byte b) { + this.b = b; + } +} + +class SynchronizedMustBeSimulated { + + static Vector vector; + + static { + /* + * Using the normally disallowed "old" synchronized collection classes is the easiest way to + * test what we want to test. + */ + // Checkstyle: stop + vector = new Vector<>(); + // Checkstyle: resume + for (int i = 0; i < 42; i++) { + vector.add(String.valueOf(i)); + } + } +} + +class SynchronizedMustBeDelayed { + static { + synchronizedMethod(); + } + + /** + * This method synchronizes on an object that exists before class initialization is started: the + * class object itself. So we cannot determine at image build time if the class initializer + * would ever finish execution at image run time. Another thread could hold the lock + * indefinitely. + */ + static synchronized int synchronizedMethod() { + return 42; + } +} + +abstract class TestClassInitializationFeature implements Feature { + + private void checkClasses(boolean checkSafeEarly, boolean checkSafeLate) { + System.out.println("=== Checking initialization state of classes: checkSafeEarly=" + checkSafeEarly + ", checkSafeLate=" + checkSafeLate); + + List errors = new ArrayList<>(); + for (Class checkedClass : TestClassInitialization.checkedClasses) { + boolean nameHasSafeEarly = checkedClass.getName().contains("MustBeSafeEarly"); + boolean nameHasSafeLate = checkedClass.getName().contains("MustBeSafeLate"); + boolean nameHasSimulated = checkedClass.getName().contains("MustBeSimulated"); + boolean nameHasDelayed = checkedClass.getName().contains("MustBeDelayed"); + + if ((nameHasSafeEarly ? 1 : 0) + (nameHasSafeLate ? 1 : 0) + (nameHasSimulated ? 1 : 0) + (nameHasDelayed ? 1 : 0) != 1) { + errors.add(checkedClass.getName() + ": Wrongly named class: nameHasSafeEarly=" + nameHasSafeEarly + ", nameHasSafeLate=" + nameHasSafeLate + + ", nameHasSimulated=" + nameHasSimulated + ", nameHasDelayed=" + nameHasDelayed); + } else { + checkClass(checkedClass, checkSafeEarly, checkSafeLate, errors, nameHasSafeEarly, nameHasSafeLate, nameHasSimulated, nameHasDelayed); + } + } + + if (!errors.isEmpty()) { + throw new Error(errors.stream().collect(Collectors.joining(System.lineSeparator()))); + } + } + + abstract void checkClass(Class checkedClass, boolean checkSafeEarly, boolean checkSafeLate, List errors, + boolean nameHasSafeEarly, boolean nameHasSafeLate, boolean nameHasSimulated, boolean nameHasDelayed); + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + /* We need to access the checkedClasses array both at image build time and run time. */ + RuntimeClassInitialization.initializeAtBuildTime(TestClassInitialization.class); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + checkClasses(false, false); + } + + @Override + public void duringAnalysis(DuringAnalysisAccess access) { + checkClasses(true, false); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + if (access.isReachable(AssertionOnlyClassMustBeUnreachable.class)) { + throw new Error("Assertion check was not constant folded for a class that is initialized at run time. " + + "We assume here that the image is built with assertions disabled, which is the case for the gate check."); + } + } + + @Override + public void beforeCompilation(BeforeCompilationAccess access) { + checkClasses(true, true); + } + + @Override + public void afterImageWrite(AfterImageWriteAccess access) { + checkClasses(true, true); + } +} + +/** + * For testing with {@link ClassInitializationOptions#UseNewExperimentalClassInitialization} set to + * false and simulation of class initializer disabled. + */ +class TestClassInitializationFeatureOldPolicyFeature extends TestClassInitializationFeature { + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + super.afterRegistration(access); + + TestClassInitialization.simulationEnabled = false; + } + + @Override + void checkClass(Class checkedClass, boolean checkSafeEarly, boolean checkSafeLate, List errors, + boolean nameHasSafeEarly, boolean nameHasSafeLate, boolean nameHasSimulated, boolean nameHasDelayed) { + + boolean initialized = !Unsafe.getUnsafe().shouldBeInitialized(checkedClass); + + if (nameHasSafeEarly && initialized != checkSafeEarly) { + errors.add(checkedClass.getName() + ": Check for MustBeSafeEarly failed"); + } + if (nameHasSafeLate && initialized != checkSafeLate) { + errors.add(checkedClass.getName() + ": Check for MustBeSafeLate failed"); + } + if (nameHasSimulated && initialized) { + /* + * Class initializer simulation is disabled in this configuration, so these classes must + * be initialized at run time. + */ + errors.add(checkedClass.getName() + ": Check for MustBeSimulated failed"); + } + if (nameHasDelayed && initialized) { + errors.add(checkedClass.getName() + ": Check for MustBeDelayed failed"); + } + } +} + +/** + * For testing with {@link ClassInitializationOptions#UseNewExperimentalClassInitialization} set to + * true and simulation of class initializer enabled. + */ +class TestClassInitializationFeatureNewPolicyFeature extends TestClassInitializationFeature { + @Override + public void afterRegistration(AfterRegistrationAccess access) { + super.afterRegistration(access); + + TestClassInitialization.simulationEnabled = true; + } + + @Override + void checkClass(Class checkedClass, boolean checkSafeEarly, boolean checkSafeLate, List errors, + boolean nameHasSafeEarly, boolean nameHasSafeLate, boolean nameHasSimulated, boolean nameHasDelayed) { + if (!Unsafe.getUnsafe().shouldBeInitialized(checkedClass)) { + errors.add(checkedClass.getName() + ": Class already initialized at image build time"); + } + } +} + +public class TestClassInitialization { + + static boolean simulationEnabled; + + static final Class[] checkedClasses = new Class[]{ + PureMustBeSafeEarly.class, + NonPureMustBeDelayed.class, + PureCallMustBeSafeEarly.class, + InitializesNonPureMustBeDelayed.class, + SystemPropReadMustBeDelayed.class, + SystemPropWriteMustBeDelayed.class, + StartsAThreadMustBeDelayed.class, + CreatesAnExceptionMustBeDelayed.class, + ThrowsAnExceptionUninitializedMustBeDelayed.class, + PureInterfaceMustBeSafeEarly.class, + PureSubclassMustBeDelayed.class, + SuperClassMustBeDelayed.class, + InterfaceNonPureMustBeDelayed.class, + InterfaceNonPureDefaultMustBeDelayed.class, + PureSubclassInheritsDelayedInterfaceMustBeSafeEarly.class, + PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed.class, + ImplicitExceptionInInitializerUninitializedMustBeDelayed.class, + PureDependsOnImplicitExceptionUninitializedMustBeDelayed.class, + StaticFieldHolderMustBeSafeEarly.class, + StaticFieldModifer1MustBeDelayed.class, + StaticFieldModifer2MustBeDelayed.class, + RecursionInInitializerMustBeSafeLate.class, + UnsafeAccessMustBeSafeLate.class, + EnumMustBeSafeEarly.class, + NativeMethodMustBeDelayed.class, + ReferencesOtherPureClassMustBeSafeEarly.class, HelperClassMustBeSafeEarly.class, + CycleMustBeSafeLate.class, HelperClassMustBeSafeLate.class, + ReflectionMustBeSafeEarly.class, ForNameMustBeSafeEarly.class, ForNameUninitializedMustBeDelayed.class, + DevirtualizedCallMustBeDelayed.class, DevirtualizedCallSuperMustBeSafeEarly.class, DevirtualizedCallSubMustBeSafeEarly.class, DevirtualizedCallUsageMustBeDelayed.class, + LargeAllocation1MustBeDelayed.class, LargeAllocation2MustBeDelayed.class, + ComplexEnumMustBeSimulated.class, + StaticFinalFieldFoldingMustBeSafeEarly.class, + LambdaMustBeSafeLate.class, + BoxingMustBeSimulated.class, + SingleByteFieldMustBeSafeEarly.class, + SynchronizedMustBeSimulated.class, SynchronizedMustBeDelayed.class, + }; + + static int pure() { + return transitivelyPure() + 42; + } + + private static int transitivelyPure() { + return 42; + } + + public static void main(String[] args) { + for (var checkedClass : checkedClasses) { + boolean nameHasSimulated = checkedClass.getName().contains("MustBeSimulated"); + boolean nameHasDelayed = checkedClass.getName().contains("MustBeDelayed"); + boolean initialized = !Unsafe.getUnsafe().shouldBeInitialized(checkedClass); + if ((nameHasDelayed || (!simulationEnabled && nameHasSimulated)) == initialized) { + throw new RuntimeException("Class " + checkedClass.getName() + ": nameHasSimulated=" + nameHasSimulated + ", nameHasDelayed=" + nameHasDelayed + ", initialized=" + initialized); + } + } + + assertSame(42, PureMustBeSafeEarly.v); + assertSame(84, PureCallMustBeSafeEarly.v); + assertSame(42, InitializesPureMustBeDelayed.v); + assertSame(1, NonPureMustBeDelayed.v); + assertSame(1, NonPureAccessedFinal.v); + assertSame(1, InitializesNonPureMustBeDelayed.v); + assertSame(1, SystemPropReadMustBeDelayed.v); + assertSame(1, SystemPropWriteMustBeDelayed.v); + assertSame(1, StartsAThreadMustBeDelayed.v); + assertSame(1, PureSubclassMustBeDelayed.v); + assertSame(1, PureSubclassInheritsDelayedInterfaceMustBeSafeEarly.v); + assertSame(1, PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed.v); + assertSame(1, InterfaceNonPureMustBeDelayed.v); + try { + sink(ThrowsAnExceptionUninitializedMustBeDelayed.v); + throw new RuntimeException("should not reach here"); + } catch (ExceptionInInitializerError e) { + assertSame("should fire at runtime", e.getCause().getMessage()); + } + assertSame("should fire at runtime", CreatesAnExceptionMustBeDelayed.e.getMessage()); + try { + sink(ImplicitExceptionInInitializerUninitializedMustBeDelayed.res); + throw new RuntimeException("should not reach here"); + } catch (ExceptionInInitializerError e) { + assertSame(ArithmeticException.class, e.getCause().getClass()); + } + try { + sink(PureDependsOnImplicitExceptionUninitializedMustBeDelayed.a); + throw new RuntimeException("should not reach here"); + } catch (NoClassDefFoundError e) { + /* Expected. */ + } + + assertSame(111, StaticFieldHolderMustBeSafeEarly.a); + StaticFieldModifer1MustBeDelayed.triggerInitialization(); + assertSame(222, StaticFieldHolderMustBeSafeEarly.a); + StaticFieldModifer2MustBeDelayed.triggerInitialization(); + assertSame(333, StaticFieldHolderMustBeSafeEarly.a); + + assertSame(20100, RecursionInInitializerMustBeSafeLate.i); + + UnsafeAccessMustBeSafeLate value = UnsafeAccessMustBeSafeLate.value; + assertSame(1234, value.f01 + value.f02 + value.f03 + value.f04 + value.f05 + value.f06 + value.f07 + value.f08 + + value.f09 + value.f10 + value.f11 + value.f12 + value.f13 + value.f14 + value.f15 + value.f16); + + EnumMustBeSafeEarly[] values = EnumMustBeSafeEarly.values(); + assertSame(null, values[0].getValue()); + assertSame("Hello", values[1].getValue()); + assertSame(Object.class, values[2].getValue().getClass()); + assertSame(EnumMustBeSafeEarly.V1, stringToEnum("v1")); + + assertSame(42, NativeMethodMustBeDelayed.i); + NativeMethodMustBeDelayed.foo(); + ReferencesOtherPureClassMustBeSafeEarly.foo(); + CycleMustBeSafeLate.foo(); + + assertSame(ForNameMustBeSafeEarly.class, ReflectionMustBeSafeEarly.c1); + assertSame(ForNameUninitializedMustBeDelayed.class, ReflectionMustBeSafeEarly.c2); + assertSame("foo", ReflectionMustBeSafeEarly.m1.getName()); + assertSame("field", ReflectionMustBeSafeEarly.f2.getName()); + + assertSame(42, DevirtualizedCallUsageMustBeDelayed.value); + + assertSame(200_000, ((Object[]) LargeAllocation1MustBeDelayed.value).length); + assertSame(1, ((int[][]) LargeAllocation2MustBeDelayed.value).length); + assertSame(200_000, ((int[][]) LargeAllocation2MustBeDelayed.value)[0].length); + + assertSame(ComplexEnumMustBeSimulated.V1, ComplexEnumMustBeSimulated.lookup.get("V1")); + assertSame(42, ComplexEnumMustBeSimulated.lookup.get("V2").virtualMethod()); + + assertSame("abc", StaticFinalFieldFoldingMustBeSafeEarly.before.f1); + assertSame(null, StaticFinalFieldFoldingMustBeSafeEarly.before.f2); + assertSame(null, StaticFinalFieldFoldingMustBeSafeEarly.before.f3); + assertSame("abc", StaticFinalFieldFoldingMustBeSafeEarly.after.f1); + assertSame("abc", StaticFinalFieldFoldingMustBeSafeEarly.after.f2); + assertSame(1, ((Object[]) StaticFinalFieldFoldingMustBeSafeEarly.after.f3).length); + + assertSame(true, LambdaMustBeSafeLate.matches(List.of("1", "2", "3", "Hello", "4"))); + assertSame(false, LambdaMustBeSafeLate.matches(List.of("1", "2", "3", "4"))); + + assertSame(83, BoxingMustBeSimulated.sum); + assertSame(Character.class, BoxingMustBeSimulated.defaultValue(char.class).getClass()); + assertSame(Short.class, BoxingMustBeSimulated.defaultValue(short.class).getClass()); + assertSame(Float.class, BoxingMustBeSimulated.defaultValue(float.class).getClass()); + assertTrue(Arrays.equals((short[]) BoxingMustBeSimulated.S1, new short[]{0, 0, 43, 44, 45, 44, 45, 46, 47, 0, 0, 0})); + assertTrue(Arrays.equals((Object[]) BoxingMustBeSimulated.O1, new Object[]{"42", "43", "44"})); + assertTrue(Arrays.equals((Object[]) BoxingMustBeSimulated.O2, new String[]{"45", "46", "47"})); + + /* + * The unsafe field offset lookup is constant folded at image build time, which also + * registers the field as unsafe accessed. + */ + long bOffset = Unsafe.getUnsafe().objectFieldOffset(SingleByteFieldMustBeSafeEarly.class, "b"); + assertTrue(bOffset % 4 == 0); + /* + * Check that for sub-int values, the padding after the value is not touched by the image + * heap writer. + */ + assertSame(42, readRawByte(SingleByteFieldMustBeSafeEarly.instance1, bOffset + 0)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance1, bOffset + 1)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance1, bOffset + 2)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance1, bOffset + 3)); + assertSame(-42, readRawByte(SingleByteFieldMustBeSafeEarly.instance2, bOffset + 0)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance2, bOffset + 1)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance2, bOffset + 2)); + assertSame(0, readRawByte(SingleByteFieldMustBeSafeEarly.instance2, bOffset + 3)); + + assertSame(42, SynchronizedMustBeSimulated.vector.size()); + assertSame(42, SynchronizedMustBeDelayed.synchronizedMethod()); + + for (var checkedClass : checkedClasses) { + boolean initialized = !Unsafe.getUnsafe().shouldBeInitialized(checkedClass); + boolean expectedUninitialized = checkedClass.getName().contains("Uninitialized"); + if (initialized == expectedUninitialized) { + throw new RuntimeException("Class " + checkedClass.getName() + ": initialized=" + initialized + ", expectedUninitialized=" + expectedUninitialized); + } + } + } + + @NeverInline("prevent constant folding, we read the raw memory after the last field") + static int readRawByte(Object o, long offset) { + return Unsafe.getUnsafe().getByte(o, offset); + } + + private static EnumMustBeSafeEarly stringToEnum(String name) { + if (EnumMustBeSafeEarly.V1.name().equalsIgnoreCase(name)) { + return EnumMustBeSafeEarly.V1; + } else { + return EnumMustBeSafeEarly.V2; + } + } + + private static void assertTrue(boolean condition) { + if (!condition) { + throw new RuntimeException("condition not true"); + } + } + + private static void assertSame(long expected, long actual) { + if (expected != actual) { + throw new RuntimeException("expected " + expected + " but found " + actual); + } + } + + private static void assertSame(Object expected, Object actual) { + if (expected != actual) { + throw new RuntimeException("expected " + expected + " but found " + actual); + } + } + + private static void sink(@SuppressWarnings("unused") Object o) { + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitializationMustBeSafeEarly.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitializationMustBeSafeEarly.java deleted file mode 100644 index f25620659f45..000000000000 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/clinit/TestClassInitializationMustBeSafeEarly.java +++ /dev/null @@ -1,701 +0,0 @@ -/* - * Copyright (c) 2019, 2019, 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.test.clinit; - -// Checkstyle: stop - -import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; - -import com.oracle.svm.util.ModuleSupport; - -import jdk.internal.misc.Unsafe; - -class PureMustBeSafeEarly { - static int v; - static { - v = 1; - v = 42; - } -} - -class InitializesPureMustBeDelayed { - static int v; - static { - v = PureMustBeSafeEarly.v; - } -} - -/* this one should not even show up */ -class NonPureAccessedFinal { - static final int v = 1; - static { - System.out.println("Must not be called at runtime or compile time."); - System.exit(1); - } -} - -class PureCallMustBeSafeEarly { - static int v; - static { - v = TestClassInitializationMustBeSafeEarly.pure(); - } -} - -class NonPureMustBeDelayed { - static int v = 1; - static { - System.out.println("Analysis should not reach here."); - } -} - -class InitializesNonPureMustBeDelayed { - static int v = NonPureMustBeDelayed.v; -} - -class SystemPropReadMustBeDelayed { - static int v = 1; - static { - System.getProperty("test"); - } -} - -class SystemPropWriteMustBeDelayed { - static int v = 1; - static { - System.setProperty("test", ""); - } -} - -class StartsAThreadMustBeDelayed { - static int v = 1; - static { - new Thread().start(); - } -} - -class CreatesAFileMustBeDelayed { - static int v = 1; - static File f = new File("./"); -} - -class CreatesAnExceptionMustBeDelayed { - static Exception e; - static { - e = new Exception("should fire at runtime"); - } -} - -class ThrowsAnExceptionMustBeDelayed { - static int v = 1; - static { - if (PureMustBeSafeEarly.v == 42) { - throw new RuntimeException("should fire at runtime"); - } - } -} - -interface PureInterfaceMustBeSafeEarly { -} - -class PureSubclassMustBeDelayed extends SuperClassMustBeDelayed { - static int v = 1; -} - -class SuperClassMustBeDelayed implements PureInterfaceMustBeSafeEarly { - static { - System.out.println("Delaying this class."); - } -} - -interface InterfaceNonPureMustBeDelayed { - int v = B.v; - - class B { - static int v = 1; - static { - System.out.println("Delaying this class."); - } - } -} - -interface InterfaceNonPureDefaultMustBeDelayed { - int v = B.v; - - class B { - static int v = 1; - static { - System.out.println("Delaying this class."); - } - } - - default int m() { - return v; - } -} - -class PureSubclassInheritsDelayedInterfaceMustBeSafeEarly implements InterfaceNonPureMustBeDelayed { - static int v = 1; -} - -class PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed implements InterfaceNonPureDefaultMustBeDelayed { - static int v = 1; -} - -class ImplicitExceptionInInitializerMustBeDelayed { - - static int a = 10; - static int b = 0; - static int res; - - static { - res = a / b; - } -} - -class PureDependsOnImplicitExceptionMustBeDelayed { - - static int a; - - static { - a = ImplicitExceptionInInitializerMustBeDelayed.res; - } -} - -class StaticFieldHolderMustBeSafeEarly { - /** - * Other class initializers that modify {@link #a} must not run at image build time so that the - * initial value 111 assigned here can be read at run time. - */ - static int a = 111; - - static void setA(int value) { - a = value; - } -} - -class StaticFieldModifer1MustBeDelayed { - static { - StaticFieldHolderMustBeSafeEarly.a = 222; - } - - static void triggerInitialization() { - } -} - -class StaticFieldModifer2MustBeDelayed { - static { - StaticFieldHolderMustBeSafeEarly.setA(333); - } - - static void triggerInitialization() { - } -} - -class RecursionInInitializerMustBeSafeLate { - static int i = compute(200); - - static int compute(int n) { - if (n <= 1) { - return 1; - } else { - return n + compute(n - 1); - } - } -} - -class UnsafeAccessMustBeSafeLate { - static UnsafeAccessMustBeSafeLate value = compute(); - - int f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12, f13, f14, f15, f16; - - static UnsafeAccessMustBeSafeLate compute() { - UnsafeAccessMustBeSafeLate result = new UnsafeAccessMustBeSafeLate(); - /* - * We are writing a random instance field, depending on the header size. But the object is - * big enough so that the write is one of the fields. The unsafe write is converted to a - * proper store field node because the offset is constant, so in the static analysis graph - * there is no unsafe access node. - */ - UnsafeAccess.UNSAFE.putInt(result, 32L, 1234); - return result; - } -} - -enum EnumMustBeSafeEarly { - V1(null), - V2("Hello"), - V3(new Object()); - - final Object value; - - EnumMustBeSafeEarly(Object value) { - this.value = value; - } - - Object getValue() { - /* - * Use an assertion, so that the static final field that stores the assertion status is - * filled in the class initializer. We want to test that using assertions does not impact - * the class initialization analysis. - */ - assert value != null; - return value; - } -} - -class NativeMethodMustBeDelayed { - static int i = compute(); - - static int compute() { - if (i < 0) { - nativeMethod(); - } - return 42; - } - - static native void nativeMethod(); - - static void foo() { - /* - * Even when a class is initialized at run time, the check whether assertions are included - * must be constant folded at image build time. Otherwise we have a performance problem. - */ - assert assertionOnlyCode(); - } - - static boolean assertionOnlyCode() { - AssertionOnlyClassMustBeUnreachable.reference(); - return false; - } -} - -class AssertionOnlyClassMustBeUnreachable { - static void reference() { - } -} - -class UnsafeAccess { - static final Unsafe UNSAFE = initUnsafe(); - - private static Unsafe initUnsafe() { - try { - Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - return (Unsafe) theUnsafe.get(Unsafe.class); - } catch (Exception e) { - throw new RuntimeException("exception while trying to get Unsafe", e); - } - } -} - -/** - * Class initializer references a helper class that can be initialized early. Since the early class - * initializer analysis recursviely processes dependent classes, this class is also safe for early - * initialization. - */ -class ReferencesOtherPureClassMustBeSafeEarly { - static { - HelperClassMustBeSafeEarly.foo(); - } - - static void foo() { - } -} - -class HelperClassMustBeSafeEarly { - static void foo() { - } -} - -/** - * Cycle between this class and a helper class. Even though both classes could be initialized early, - * the early analysis bails out because analyzing cycles would be too complicated. - */ -class CycleMustBeSafeLate { - static { - HelperClassMustBeSafeLate.foo(); - } - - static void foo() { - } -} - -class HelperClassMustBeSafeLate { - static { - CycleMustBeSafeLate.foo(); - } - - static void foo() { - } -} - -/** Various reflection lookup methods are safe for execution at image build time. */ -class ReflectionMustBeSafeEarly { - static Class c1; - static Class c2; - static Method m1; - static Field f2; - - static { - try { - Class c1Local = Class.forName("com.oracle.svm.test.clinit.ForNameMustBeSafeEarly", true, ReflectionMustBeSafeEarly.class.getClassLoader()); - c1 = c1Local; - - /** - * Looking up a class that cannot be initialized at build time is allowed, as long as - * `initialize` is `false`. - */ - Class c2Local = Class.forName("com.oracle.svm.test.clinit.ForNameMustBeDelayed", false, ReflectionMustBeSafeEarly.class.getClassLoader()); - c2 = c2Local; - - /* - * Calling getDeclaredMethod on the field c1 instead of the variable c1Local would not - * work, because the field load cannot be constant folded by the - * EarlyClassInitializerAnalysis. - */ - m1 = c1Local.getDeclaredMethod("foo", int.class); - f2 = c2Local.getDeclaredField("field"); - - } catch (ReflectiveOperationException ex) { - throw new Error(ex); - } - } -} - -@SuppressWarnings("unused") -class ForNameMustBeSafeEarly { - static void foo(int arg) { - } -} - -class ForNameMustBeDelayed { - static { - System.out.println("Delaying " + ForNameMustBeDelayed.class); - } - - int field; -} - -class DevirtualizedCallMustBeDelayed { - static { - System.out.println("Delaying " + DevirtualizedCallMustBeDelayed.class); - } - - static final Object value = 42; -} - -class DevirtualizedCallSuperMustBeSafeEarly { - Object foo() { - return -1; - } -} - -class DevirtualizedCallSubMustBeSafeEarly extends DevirtualizedCallSuperMustBeSafeEarly { - @Override - Object foo() { - return DevirtualizedCallMustBeDelayed.value; - } -} - -class DevirtualizedCallUsageMustBeDelayed { - static final Object value = computeValue(); - - private static Object computeValue() { - DevirtualizedCallSuperMustBeSafeEarly provider = createProvider(); - - /* - * The static analysis can prove that DevirtualizedCallSubMustBeDelayed.foo is the only - * callee and de-virtualize this call. So the original target method of the call site and - * the actually invoked method are different - and the analysis that automatically - * initializes classes must properly pick up this dependency. - */ - return provider.foo(); - } - - private static DevirtualizedCallSuperMustBeSafeEarly createProvider() { - return new DevirtualizedCallSubMustBeSafeEarly(); - } -} - -class LargeAllocation1MustBeDelayed { - static final Object value = computeValue(); - - private static Object computeValue() { - Object[] result = new Object[1_000_000]; - for (int i = 0; i < result.length; i++) { - result[i] = new int[1_000_000]; - } - return result; - } -} - -class LargeAllocation2MustBeDelayed { - static final Object value = computeValue(); - - private static Object computeValue() { - return new int[Integer.MAX_VALUE][Integer.MAX_VALUE]; - } -} - -class TestClassInitializationMustBeSafeEarlyFeature implements Feature { - - static final Class[] checkedClasses = new Class[]{ - PureMustBeSafeEarly.class, - NonPureMustBeDelayed.class, - PureCallMustBeSafeEarly.class, - InitializesNonPureMustBeDelayed.class, - SystemPropReadMustBeDelayed.class, - SystemPropWriteMustBeDelayed.class, - StartsAThreadMustBeDelayed.class, - CreatesAFileMustBeDelayed.class, - CreatesAnExceptionMustBeDelayed.class, - ThrowsAnExceptionMustBeDelayed.class, - PureInterfaceMustBeSafeEarly.class, - PureSubclassMustBeDelayed.class, - SuperClassMustBeDelayed.class, - InterfaceNonPureMustBeDelayed.class, - InterfaceNonPureDefaultMustBeDelayed.class, - PureSubclassInheritsDelayedInterfaceMustBeSafeEarly.class, - PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed.class, - ImplicitExceptionInInitializerMustBeDelayed.class, - PureDependsOnImplicitExceptionMustBeDelayed.class, - StaticFieldHolderMustBeSafeEarly.class, - StaticFieldModifer1MustBeDelayed.class, - StaticFieldModifer2MustBeDelayed.class, - RecursionInInitializerMustBeSafeLate.class, - UnsafeAccessMustBeSafeLate.class, - EnumMustBeSafeEarly.class, - NativeMethodMustBeDelayed.class, - ReferencesOtherPureClassMustBeSafeEarly.class, HelperClassMustBeSafeEarly.class, - CycleMustBeSafeLate.class, HelperClassMustBeSafeLate.class, - ReflectionMustBeSafeEarly.class, ForNameMustBeSafeEarly.class, ForNameMustBeDelayed.class, - DevirtualizedCallMustBeDelayed.class, DevirtualizedCallSuperMustBeSafeEarly.class, DevirtualizedCallSubMustBeSafeEarly.class, DevirtualizedCallUsageMustBeDelayed.class, - LargeAllocation1MustBeDelayed.class, LargeAllocation2MustBeDelayed.class, - }; - - private static void checkClasses(boolean checkSafeEarly, boolean checkSafeLate) { - System.out.println("=== Checking initialization state of classes: checkSafeEarly=" + checkSafeEarly + ", checkSafeLate=" + checkSafeLate); - - List errors = new ArrayList<>(); - for (Class checkedClass : checkedClasses) { - boolean nameHasSafeEarly = checkedClass.getName().contains("MustBeSafeEarly"); - boolean nameHasSafeLate = checkedClass.getName().contains("MustBeSafeLate"); - boolean nameHasDelayed = checkedClass.getName().contains("MustBeDelayed"); - - if ((nameHasSafeEarly ? 1 : 0) + (nameHasSafeLate ? 1 : 0) + (nameHasDelayed ? 1 : 0) != 1) { - errors.add(checkedClass.getName() + ": Wrongly named class (nameHasSafeEarly: " + nameHasSafeEarly + ", nameHasSafeLate: " + nameHasSafeLate + ", nameHasDelayed: " + nameHasDelayed); - } else { - - boolean initialized = !shouldBeInitialized(checkedClass); - - if (nameHasDelayed && initialized) { - errors.add(checkedClass.getName() + ": Check for MustBeDelayed failed"); - } - if (nameHasSafeEarly && initialized != checkSafeEarly) { - errors.add(checkedClass.getName() + ": Check for MustBeSafeEarly failed"); - } - if (nameHasSafeLate && initialized != checkSafeLate) { - errors.add(checkedClass.getName() + ": Check for MustBeSafeLate failed"); - } - } - } - - if (!errors.isEmpty()) { - throw new Error(errors.stream().collect(Collectors.joining(System.lineSeparator()))); - } - } - - @SuppressWarnings("deprecation") - private static boolean shouldBeInitialized(Class c) { - return UnsafeAccess.UNSAFE.shouldBeInitialized(c); - } - - @Override - public void afterRegistration(AfterRegistrationAccess access) { - RuntimeClassInitialization.initializeAtRunTime("com.oracle.svm.test.clinit"); - - ModuleSupport.accessPackagesToClass(ModuleSupport.Access.OPEN, TestClassInitializationMustBeSafeEarly.class, false, - "java.base", "jdk.internal.misc"); - RuntimeClassInitialization.initializeAtBuildTime(UnsafeAccess.class); - } - - @Override - public void beforeAnalysis(BeforeAnalysisAccess access) { - checkClasses(false, false); - } - - @Override - public void duringAnalysis(DuringAnalysisAccess access) { - checkClasses(true, false); - } - - @Override - public void afterAnalysis(AfterAnalysisAccess access) { - if (access.isReachable(AssertionOnlyClassMustBeUnreachable.class)) { - throw new Error("Assertion check was not constant folded for a class that is initialized at run time. " + - "We assume here that the image is built with assertions disabled, which is the case for the gate check."); - } - } - - @Override - public void beforeCompilation(BeforeCompilationAccess access) { - checkClasses(true, true); - } - - @Override - public void afterImageWrite(AfterImageWriteAccess access) { - checkClasses(true, true); - } -} - -/** - * In addition to the initialization checks in - * {@link TestClassInitializationMustBeSafeEarlyFeature}, suffixes MustBeSafe and MustBeDelayed are - * parsed by an external script in the tests after the image is built. Every class that ends with - * `MustBeSafe` should be eagerly initialized and every class that ends with `MustBeDelayed` should - * be initialized at runtime. - */ -public class TestClassInitializationMustBeSafeEarly { - static int pure() { - return transitivelyPure() + 42; - } - - private static int transitivelyPure() { - return 42; - } - - public static void main(String[] args) { - System.out.println(PureMustBeSafeEarly.v); - System.out.println(PureCallMustBeSafeEarly.v); - System.out.println(InitializesPureMustBeDelayed.v); - System.out.println(NonPureMustBeDelayed.v); - System.out.println(NonPureAccessedFinal.v); - System.out.println(InitializesNonPureMustBeDelayed.v); - System.out.println(SystemPropReadMustBeDelayed.v); - System.out.println(SystemPropWriteMustBeDelayed.v); - System.out.println(StartsAThreadMustBeDelayed.v); - System.out.println(CreatesAFileMustBeDelayed.v); - System.out.println(PureSubclassMustBeDelayed.v); - System.out.println(PureSubclassInheritsDelayedInterfaceMustBeSafeEarly.v); - System.out.println(PureSubclassInheritsDelayedDefaultInterfaceMustBeDelayed.v); - System.out.println(InterfaceNonPureMustBeDelayed.v); - try { - System.out.println(ThrowsAnExceptionMustBeDelayed.v); - } catch (Throwable t) { - System.out.println(CreatesAnExceptionMustBeDelayed.e.getMessage()); - } - try { - System.out.println(ImplicitExceptionInInitializerMustBeDelayed.res); - throw new RuntimeException("should not reach here"); - } catch (ExceptionInInitializerError ae) { - if (!(ae.getCause() instanceof ArithmeticException)) { - throw new RuntimeException("should not reach here"); - } - } - try { - System.out.println(PureDependsOnImplicitExceptionMustBeDelayed.a); - throw new RuntimeException("should not reach here"); - } catch (NoClassDefFoundError ae) { - /* This is OK */ - } - - int a = StaticFieldHolderMustBeSafeEarly.a; - if (a != 111) { - throw new RuntimeException("expected 111 but found " + a); - } - - StaticFieldModifer1MustBeDelayed.triggerInitialization(); - a = StaticFieldHolderMustBeSafeEarly.a; - if (a != 222) { - throw new RuntimeException("expected 222 but found " + a); - } - - StaticFieldModifer2MustBeDelayed.triggerInitialization(); - a = StaticFieldHolderMustBeSafeEarly.a; - if (a != 333) { - throw new RuntimeException("expected 333 but found " + a); - } - - System.out.println(RecursionInInitializerMustBeSafeLate.i); - - UnsafeAccessMustBeSafeLate value = UnsafeAccessMustBeSafeLate.value; - System.out.println(value.f01); - System.out.println(value.f02); - System.out.println(value.f03); - System.out.println(value.f04); - System.out.println(value.f05); - System.out.println(value.f06); - System.out.println(value.f07); - System.out.println(value.f08); - System.out.println(value.f09); - System.out.println(value.f10); - System.out.println(value.f11); - System.out.println(value.f12); - System.out.println(value.f13); - System.out.println(value.f14); - System.out.println(value.f15); - System.out.println(value.f16); - - for (EnumMustBeSafeEarly e : EnumMustBeSafeEarly.values()) { - System.out.println(e.getValue()); - } - - System.out.println(NativeMethodMustBeDelayed.i); - NativeMethodMustBeDelayed.foo(); - ReferencesOtherPureClassMustBeSafeEarly.foo(); - CycleMustBeSafeLate.foo(); - - assertSame(ForNameMustBeSafeEarly.class, ReflectionMustBeSafeEarly.c1); - assertSame(ForNameMustBeDelayed.class, ReflectionMustBeSafeEarly.c2); - assertSame("foo", ReflectionMustBeSafeEarly.m1.getName()); - assertSame("field", ReflectionMustBeSafeEarly.f2.getName()); - - System.out.println(DevirtualizedCallUsageMustBeDelayed.value); - - if (System.currentTimeMillis() == 0) { - /* - * Make the class initializers reachable at run time, but do not actually execute them - * because they will allocate a lot of memory before throwing an OutOfMemoryError. - */ - System.out.println(LargeAllocation1MustBeDelayed.value); - System.out.println(LargeAllocation2MustBeDelayed.value); - } - } - - private static void assertSame(Object expected, Object actual) { - if (expected != actual) { - throw new RuntimeException("expected " + expected + " but found " + actual); - } - } -} -// Checkstyle: resume