diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java index c4a78e77c2ea..83aeb042797c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java @@ -41,6 +41,7 @@ import org.graalvm.nativeimage.impl.ConfigurationCondition; import com.oracle.svm.configure.ClassNameSupport; +import com.oracle.svm.configure.config.ConfigurationType; import com.oracle.svm.core.configure.ConditionalRuntimeValue; import com.oracle.svm.core.configure.RuntimeConditionSet; import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; @@ -49,6 +50,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; @@ -341,17 +343,22 @@ private static Class forName(String className, ClassLoader classLoader, boole } private Object forName0(String className, ClassLoader classLoader) { - var conditional = knownClasses.get(className); - Object result = conditional == null ? null : conditional.getValue(); if (className.endsWith("[]")) { /* Querying array classes with their "TypeName[]" name always throws */ - result = NEGATIVE_QUERY; + return new ClassNotFoundException(className); } + var conditional = knownClasses.get(className); + Object result = conditional == null ? null : conditional.getValue(); if (result == null) { result = PredefinedClassesSupport.getLoadedForNameOrNull(className, classLoader); } if (result == null && !ClassNameSupport.isValidReflectionName(className)) { - result = NEGATIVE_QUERY; + /* Invalid class names always throw, no need for reflection data */ + return new ClassNotFoundException(className); + } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + // NB: the early returns above ensure we do not trace calls with bad type args. + MetadataTracer.singleton().traceReflectionType(className); } return result == NEGATIVE_QUERY ? new ClassNotFoundException(className) : result; } @@ -445,7 +452,16 @@ public static boolean canUnsafeInstantiateAsInstance(DynamicHub hub) { break; } } - return conditionSet != null && conditionSet.satisfied(); + if (conditionSet != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType type = MetadataTracer.singleton().traceReflectionType(clazz.getName()); + if (type != null) { + type.setUnsafeAllocated(); + } + } + return conditionSet.satisfied(); + } + return false; } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index f0061b6c3e67..d8bad63d0146 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.hub; +import static com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberAccessibility; +import static com.oracle.svm.configure.config.ConfigurationMemberInfo.ConfigurationMemberDeclaration; import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; import static com.oracle.svm.core.annotate.TargetElement.CONSTRUCTOR_NAME; @@ -84,19 +86,21 @@ import java.util.function.BiFunction; import java.util.function.IntFunction; -import com.oracle.svm.core.TrackDynamicAccessEnabled; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.word.WordBase; +import com.oracle.svm.configure.config.ConfigurationType; +import com.oracle.svm.configure.config.SignatureUtil; import com.oracle.svm.core.BuildPhaseProvider.AfterHostedUniverse; import com.oracle.svm.core.BuildPhaseProvider.CompileQueueFinished; import com.oracle.svm.core.NeverInline; import com.oracle.svm.core.NeverInlineTrivial; import com.oracle.svm.core.RuntimeAssertionsSupport; import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.TrackDynamicAccessEnabled; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Delete; @@ -121,6 +125,7 @@ import com.oracle.svm.core.jdk.ProtectionDomainSupport; import com.oracle.svm.core.jdk.Resources; import com.oracle.svm.core.meta.SharedType; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.reflect.RuntimeMetadataDecoder; import com.oracle.svm.core.reflect.RuntimeMetadataDecoder.ConstructorDescriptor; @@ -704,6 +709,9 @@ private ReflectionMetadata reflectionMetadata() { } private void checkClassFlag(int mask, String methodName) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceClassFlagQuery(mask); + } if (throwMissingRegistrationErrors() && !(isClassFlagSet(mask) && getConditions().satisfied())) { MissingReflectionRegistrationUtils.forBulkQuery(DynamicHub.toClass(this), methodName); } @@ -726,6 +734,30 @@ private static boolean isClassFlagSet(int mask, ReflectionMetadata reflectionMet return reflectionMetadata != null && (reflectionMetadata.classFlags & mask) != 0; } + private void traceClassFlagQuery(int mask) { + ConfigurationType type = MetadataTracer.singleton().traceReflectionType(getName()); + if (type == null) { + return; + } + // TODO (GR-64765): We over-approximate member accessibility here because we don't trace + // accesses. Once we trace accesses, it will suffice to register the class for reflection. + switch (mask) { + case ALL_FIELDS_FLAG -> type.setAllPublicFields(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_FIELDS_FLAG -> type.setAllDeclaredFields(ConfigurationMemberAccessibility.ACCESSED); + case ALL_METHODS_FLAG -> type.setAllPublicMethods(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_METHODS_FLAG -> type.setAllDeclaredMethods(ConfigurationMemberAccessibility.ACCESSED); + case ALL_CONSTRUCTORS_FLAG -> type.setAllPublicConstructors(ConfigurationMemberAccessibility.ACCESSED); + case ALL_DECLARED_CONSTRUCTORS_FLAG -> type.setAllDeclaredConstructors(ConfigurationMemberAccessibility.ACCESSED); + case ALL_CLASSES_FLAG -> type.setAllPublicClasses(); + case ALL_DECLARED_CLASSES_FLAG -> type.setAllDeclaredClasses(); + case ALL_RECORD_COMPONENTS_FLAG -> type.setAllRecordComponents(); + case ALL_PERMITTED_SUBCLASSES_FLAG -> type.setAllPermittedSubclasses(); + case ALL_NEST_MEMBERS_FLAG -> type.setAllNestMembers(); + case ALL_SIGNERS_FLAG -> type.setAllSigners(); + default -> throw VMError.shouldNotReachHere("unknown class flag " + mask); + } + } + /** Executed at runtime. */ private static Object initEnumConstantsAtRuntime(Method values) { try { @@ -1282,6 +1314,11 @@ public Field getField(String fieldName) throws NoSuchFieldException, SecurityExc private void checkField(String fieldName, Field field, boolean publicOnly) throws NoSuchFieldException { boolean throwMissingErrors = throwMissingRegistrationErrors(); Class clazz = DynamicHub.toClass(this); + + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceFieldLookup(fieldName, field, publicOnly); + } + if (field == null) { if (throwMissingErrors && !allElementsRegistered(publicOnly, ALL_DECLARED_FIELDS_FLAG, ALL_FIELDS_FLAG)) { MissingReflectionRegistrationUtils.forField(clazz, fieldName); @@ -1305,6 +1342,25 @@ private void checkField(String fieldName, Field field, boolean publicOnly) throw } } + private void traceFieldLookup(String fieldName, Field field, boolean publicOnly) { + ConfigurationMemberDeclaration declaration = publicOnly ? ConfigurationMemberDeclaration.PRESENT : ConfigurationMemberDeclaration.DECLARED; + if (field != null) { + // register declaring type and field + ConfigurationType declaringType = MetadataTracer.singleton().traceReflectionType(field.getDeclaringClass().getName()); + if (declaringType != null) { + declaringType.addField(fieldName, declaration, false); + } + // register receiver type + MetadataTracer.singleton().traceReflectionType(getName()); + } else { + // register receiver type and negative field query + ConfigurationType receiverType = MetadataTracer.singleton().traceReflectionType(getName()); + if (receiverType != null) { + receiverType.addField(fieldName, declaration, false); + } + } + } + @Substitute private Method getMethod(String methodName, Class... parameterTypes) throws NoSuchMethodException { Objects.requireNonNull(methodName); @@ -1340,6 +1396,11 @@ private void checkConstructor(Class[] parameterTypes, Constructor construc private boolean checkExecutableExists(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) { boolean throwMissingErrors = throwMissingRegistrationErrors(); Class clazz = DynamicHub.toClass(this); + + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + traceMethodLookup(methodName, parameterTypes, method, publicOnly); + } + if (method == null) { boolean isConstructor = methodName.equals(CONSTRUCTOR_NAME); int allDeclaredFlag = isConstructor ? ALL_DECLARED_CONSTRUCTORS_FLAG : ALL_DECLARED_METHODS_FLAG; @@ -1366,6 +1427,38 @@ private boolean checkExecutableExists(String methodName, Class[] parameterTyp } } + private void traceMethodLookup(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) { + ConfigurationMemberDeclaration declaration = publicOnly ? ConfigurationMemberDeclaration.PRESENT : ConfigurationMemberDeclaration.DECLARED; + if (method != null) { + // register declaring type and method + ConfigurationType declaringType = MetadataTracer.singleton().traceReflectionType(method.getDeclaringClass().getName()); + if (declaringType != null) { + declaringType.addMethod(methodName, toInternalSignature(parameterTypes), declaration); + } + // register receiver type + MetadataTracer.singleton().traceReflectionType(getName()); + } else { + // register receiver type and negative method query + ConfigurationType receiverType = MetadataTracer.singleton().traceReflectionType(getName()); + if (receiverType != null) { + receiverType.addMethod(methodName, toInternalSignature(parameterTypes), declaration, ConfigurationMemberAccessibility.QUERIED); + } + } + } + + private static String toInternalSignature(Class[] classes) { + List names; + if (classes == null) { + names = List.of(); + } else { + names = new ArrayList<>(classes.length); + for (Class aClass : classes) { + names.add(aClass.getName()); + } + } + return SignatureUtil.toInternalSignature(names); + } + private boolean allElementsRegistered(boolean publicOnly, int allDeclaredElementsFlag, int allPublicElementsFlag) { return isClassFlagSet(allDeclaredElementsFlag) || (publicOnly && isClassFlagSet(allPublicElementsFlag)); } @@ -1882,6 +1975,8 @@ public DynamicHub arrayType() { } if (companion.arrayHub == null) { MissingReflectionRegistrationUtils.forClass(getTypeName() + "[]"); + } else if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceReflectionType(companion.arrayHub.getTypeName()); } return companion.arrayHub; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java index 71a52c7ccff0..f9956c1f5ce6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java @@ -35,8 +35,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; -import jdk.graal.compiler.java.LambdaUtils; - import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; @@ -45,8 +43,11 @@ import com.oracle.svm.core.annotate.TargetElement; import com.oracle.svm.core.fieldvaluetransformer.NewInstanceFieldValueTransformer; import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.serialize.MissingSerializationRegistrationUtils; +import jdk.graal.compiler.java.LambdaUtils; + @TargetClass(java.io.FileDescriptor.class) final class Target_java_io_FileDescriptor { @@ -68,8 +69,8 @@ static ObjectStreamClass lookup(Class cl, boolean all) { return null; } - if (Serializable.class.isAssignableFrom(cl)) { - if (!cl.isArray() && !DynamicHub.fromClass(cl).isRegisteredForSerialization()) { + if (Serializable.class.isAssignableFrom(cl) && !cl.isArray()) { + if (!DynamicHub.fromClass(cl).isRegisteredForSerialization()) { boolean isLambda = cl.getTypeName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING); boolean isProxy = Proxy.isProxyClass(cl); if (isProxy || isLambda) { @@ -87,6 +88,9 @@ static ObjectStreamClass lookup(Class cl, boolean all) { MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "type " + cl.getTypeName()); } } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceSerializationType(cl.getName()); + } } return Target_java_io_ObjectStreamClass_Caches.localDescs0.get(cl); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java index 7e5c2db054a0..652f1bcc738a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangReflectSubstitutions.java @@ -37,6 +37,7 @@ import com.oracle.svm.core.config.ConfigurationValues; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.hub.LayoutEncoding; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.snippets.KnownIntrinsics; @@ -386,6 +387,9 @@ private static void set(Object a, int index, Object value) { @Substitute private static Object newArray(Class componentType, int length) throws NegativeArraySizeException { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceReflectionType(componentType.arrayType().getName()); + } return KnownIntrinsics.unvalidatedNewArray(componentType, length); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index c5c0d46d36b1..10eb5db9b94c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -73,6 +73,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.GlobUtils; @@ -401,6 +402,9 @@ public static ResourceStorageEntryBase getAtRuntime(Module module, String resour return null; } } + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceResource(resourceName, moduleName); + } if (!entry.getConditions().satisfied()) { return missingMetadata(resourceName, throwOnMissing); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java index 36d8bb4e6e21..470264065b6c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java @@ -57,6 +57,7 @@ import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.configure.RuntimeConditionSet; import com.oracle.svm.core.jdk.Resources; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.ReflectionUtil; @@ -293,6 +294,12 @@ public boolean isRegisteredBundleLookup(String baseName, Locale locale, Object c /* Those cases will throw a NullPointerException before any lookup */ return true; } - return registeredBundles.containsKey(baseName) && registeredBundles.get(baseName).satisfied(); + if (registeredBundles.containsKey(baseName)) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceResourceBundle(baseName); + } + return registeredBundles.get(baseName).satisfied(); + } + return false; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java index 2bb1fe5a58a7..3daeaeb64bab 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java @@ -98,10 +98,19 @@ public boolean isClassInitializer() { return WRAPPED_CSTRING_EQUIVALENCE.equals(name, INITIALIZER_NAME); } + /** + * Returns the method name as a String. Can be used if the descriptor is known to be a String + * (i.e., it does not come from a JNI call); otherwise, use {@link #getNameConvertToString()}. + */ public String getName() { return (String) name; } + /** + * Returns the method signature as a String. Can be used if the descriptor is known to be a + * String (i.e., it does not come from a JNI call); otherwise, use + * {@link #getSignatureConvertToString()}. + */ public String getSignature() { return (String) signature; } @@ -113,6 +122,13 @@ public String getNameConvertToString() { return name.toString(); } + /** + * Performs a potentially costly conversion to string, only for slow paths. + */ + public String getSignatureConvertToString() { + return signature.toString(); + } + public String getSignatureWithoutReturnType() { String signatureString = signature.toString(); int parametersEnd = signatureString.lastIndexOf(')'); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java index 2021f81dfbfa..574e2a7ed995 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java @@ -42,6 +42,8 @@ import org.graalvm.word.Pointer; import com.oracle.svm.configure.ClassNameSupport; +import com.oracle.svm.configure.config.ConfigurationMemberInfo; +import com.oracle.svm.configure.config.ConfigurationType; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.Heap; @@ -53,6 +55,7 @@ import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.Utf8.WrappedAsciiCString; @@ -181,6 +184,9 @@ public static Class getClassObjectByName(CharSequence name) { JNIAccessibleClass clazz = dictionary.classesByName.get(name); if (clazz == null && !ClassNameSupport.isValidJNIName(name.toString())) { clazz = NEGATIVE_CLASS_LOOKUP; + } else if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + // trace if class exists (positive query) or name is valid (negative query) + MetadataTracer.singleton().traceJNIType(ClassNameSupport.jniNameToTypeName(name.toString())); } clazz = checkClass(clazz, name.toString()); if (clazz != null) { @@ -274,6 +280,12 @@ private static JNIAccessibleMethod getDeclaredMethod(Class classObject, JNIAc foundClass = true; JNIAccessibleMethod method = clazz.getMethod(descriptor); if (method != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType clazzType = MetadataTracer.singleton().traceJNIType(classObject.getName()); + if (clazzType != null) { + clazzType.addMethod(descriptor.getNameConvertToString(), descriptor.getSignatureConvertToString(), ConfigurationMemberInfo.ConfigurationMemberDeclaration.DECLARED); + } + } return method; } } @@ -329,6 +341,12 @@ private static JNIAccessibleField getDeclaredField(Class classObject, CharSeq foundClass = true; JNIAccessibleField field = clazz.getField(name); if (field != null && (field.isStatic() == isStatic || field.isNegative())) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + ConfigurationType clazzType = MetadataTracer.singleton().traceJNIType(classObject.getName()); + if (clazzType != null) { + clazzType.addField(name.toString(), ConfigurationMemberInfo.ConfigurationMemberDeclaration.DECLARED, false); + } + } return field; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java index 60a138fe0966..a7e8e770b9f6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/metadata/MetadataTracer.java @@ -28,12 +28,23 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; +import com.oracle.svm.configure.ConfigurationTypeDescriptor; +import com.oracle.svm.configure.NamedConfigurationTypeDescriptor; +import com.oracle.svm.configure.ProxyConfigurationTypeDescriptor; +import com.oracle.svm.configure.UnresolvedConfigurationCondition; +import com.oracle.svm.configure.config.ConfigurationFileCollection; import com.oracle.svm.configure.config.ConfigurationSet; +import com.oracle.svm.configure.config.ConfigurationType; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; @@ -46,59 +57,169 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionStability; /** * Implements reachability metadata tracing during native image execution. Enabling * {@link Options#MetadataTracingSupport} at build time will generate code to trace all accesses of - * reachability metadata. When {@link Options#RecordMetadata} is specified at run time, the image - * will trace and emit metadata to the specified path. + * reachability metadata, and then the run-time option {@link Options#RecordMetadata} enables + * tracing. */ public final class MetadataTracer { public static class Options { - @Option(help = "Enables the run-time code to trace reachability metadata accesses in the produced native image by using -XX:RecordMetadata=.")// + @Option(help = "Generate an image that supports reachability metadata access tracing. " + + "When tracing is supported, use the -XX:RecordMetadata option to enable tracing at run time.")// public static final HostedOptionKey MetadataTracingSupport = new HostedOptionKey<>(false); - @Option(help = "The path of the directory to write traced metadata to. Metadata tracing is enabled only when this option is provided.")// - public static final RuntimeOptionKey RecordMetadata = new RuntimeOptionKey<>(""); - } + static final String RECORD_METADATA_HELP = """ + Enables metadata tracing at run time. This option is only supported if -H:+MetadataTracingSupport is set when building the image. + The value of this option is a comma-separated list of arguments specified as key-value pairs. The following arguments are supported: + + - path= (required): Specifies the directory to write traced metadata to. + - merge= (optional): Specifies whether to merge or overwrite metadata with existing files at the output path (default: true). - private ConfigurationSet config; + Example usage: + -H:RecordMetadata=path=trace_output_directory + -H:RecordMetadata=path=trace_output_directory,merge=false + """; - private Path recordMetadataPath; + @Option(help = RECORD_METADATA_HELP, stability = OptionStability.EXPERIMENTAL)// + public static final RuntimeOptionKey RecordMetadata = new RuntimeOptionKey<>(null); + } + + private RecordOptions options; + private volatile ConfigurationSet config; @Fold public static MetadataTracer singleton() { return ImageSingletons.lookup(MetadataTracer.class); } - private static void initialize() { - assert Options.MetadataTracingSupport.getValue(); - MetadataTracer singleton = MetadataTracer.singleton(); - String recordMetadataValue = Options.RecordMetadata.getValue(); - if (recordMetadataValue.isEmpty()) { - throw new IllegalArgumentException("Empty path provided for " + Options.RecordMetadata.getName() + "."); + /** + * Returns whether tracing is enabled at run time (using {@code -XX:RecordMetadata}). + */ + public boolean enabled() { + VMError.guarantee(Options.MetadataTracingSupport.getValue()); + return options != null; + } + + /** + * Marks the given type as reachable from reflection. + * + * @return the corresponding {@link ConfigurationType} or {@code null} if tracing is not active + * (e.g., during shutdown). + */ + public ConfigurationType traceReflectionType(String className) { + return traceReflectionTypeImpl(new NamedConfigurationTypeDescriptor(className)); + } + + /** + * Marks the given proxy type as reachable from reflection. + */ + public void traceProxyType(List interfaceNames) { + traceReflectionTypeImpl(new ProxyConfigurationTypeDescriptor(interfaceNames)); + } + + private ConfigurationType traceReflectionTypeImpl(ConfigurationTypeDescriptor typeDescriptor) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + return configurationSet.getReflectionConfiguration().getOrCreateType(UnresolvedConfigurationCondition.alwaysTrue(), typeDescriptor); + } + return null; + } + + /** + * Marks the given type as reachable from JNI. + * + * @return the corresponding {@link ConfigurationType} or {@code null} if tracing is not active + * (e.g., during shutdown). + */ + public ConfigurationType traceJNIType(String className) { + assert enabled(); + ConfigurationType result = traceReflectionType(className); + if (result != null) { + result.setJniAccessible(); + } + return result; + } + + /** + * Marks the given resource within the given (optional) module as reachable. + */ + public void traceResource(String resourceName, String moduleName) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + configurationSet.getResourceConfiguration().addGlobPattern(UnresolvedConfigurationCondition.alwaysTrue(), resourceName, moduleName); + } + } + + /** + * Marks the given resource bundle within the given locale as reachable. + */ + public void traceResourceBundle(String baseName) { + assert enabled(); + ConfigurationSet configurationSet = config; + if (configurationSet != null) { + configurationSet.getResourceConfiguration().addBundle(UnresolvedConfigurationCondition.alwaysTrue(), baseName, List.of()); } - Path recordMetadataPath = Paths.get(recordMetadataValue); + } + + /** + * Marks the given type as serializable. + */ + public void traceSerializationType(String className) { + assert enabled(); + ConfigurationType result = traceReflectionType(className); + if (result != null) { + result.setSerializable(); + } + } + + private static void initialize(String recordMetadataValue) { + assert Options.MetadataTracingSupport.getValue(); + + RecordOptions parsedOptions = RecordOptions.parse(recordMetadataValue); try { - Files.createDirectories(recordMetadataPath); + Files.createDirectories(parsedOptions.path()); } catch (IOException ex) { - throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + recordMetadataPath + ")", ex); + throw new IllegalArgumentException("Exception occurred creating the output directory for tracing (" + parsedOptions.path() + ")", ex); } - singleton.recordMetadataPath = recordMetadataPath; - singleton.config = new ConfigurationSet(); + + MetadataTracer singleton = MetadataTracer.singleton(); + singleton.options = parsedOptions; + singleton.config = initializeConfigurationSet(parsedOptions); + } + + private static ConfigurationSet initializeConfigurationSet(RecordOptions options) { + if (options.merge() && Files.exists(options.path())) { + ConfigurationFileCollection mergeConfigs = new ConfigurationFileCollection(); + mergeConfigs.addDirectory(options.path()); + try { + return mergeConfigs.loadConfigurationSet(ioexception -> ioexception, null, null); + } catch (Exception ex) { + // suppress and fall back on empty configuration set. + Log.log().string("An exception occurred when loading merge metadata from path " + options.path() + ". ") + .string("Any existing metadata may be overwritten.").newline() + .string("Exception: ").exception(ex).newline(); + } + } + return new ConfigurationSet(); } private static void shutdown() { assert Options.MetadataTracingSupport.getValue(); MetadataTracer singleton = MetadataTracer.singleton(); ConfigurationSet config = singleton.config; + singleton.config = null; // clear config so that shutdown events are not traced. if (config != null) { try { - config.writeConfiguration(configFile -> singleton.recordMetadataPath.resolve(configFile.getFileName())); + config.writeConfiguration(configFile -> singleton.options.path().resolve(configFile.getFileName())); } catch (IOException ex) { Log log = Log.log(); - log.string("Failed to write out reachability metadata to directory ").string(singleton.recordMetadataPath.toString()); + log.string("Failed to write out reachability metadata to directory ").string(singleton.options.path().toString()); log.string(":").string(ex.getMessage()); log.newline(); } @@ -112,7 +233,7 @@ static RuntimeSupport.Hook initializeMetadataTracingHook() { } VMError.guarantee(Options.MetadataTracingSupport.getValue()); if (Options.RecordMetadata.hasBeenSet()) { - initialize(); + initialize(Options.RecordMetadata.getValue()); } }; } @@ -142,12 +263,91 @@ static RuntimeSupport.Hook checkImproperOptionUsageHook() { throw new IllegalArgumentException( "The option " + Options.RecordMetadata.getName() + " can only be used if metadata tracing is enabled at build time (using " + hostedOptionCommandArgument + ")."); - } }; } } +record RecordOptions(Path path, boolean merge) { + + private static final int ARGUMENT_PARTS = 2; + + static RecordOptions parse(String recordMetadataValue) { + if (recordMetadataValue.isEmpty()) { + throw printHelp("Option " + MetadataTracer.Options.RecordMetadata.getName() + " cannot be empty."); + } else if (recordMetadataValue.equals("help")) { + throw printHelp("Option " + MetadataTracer.Options.RecordMetadata.getName() + " value is 'help'. Printing a description and aborting."); + } + + Map parsedArguments = new HashMap<>(); + Set allArguments = new LinkedHashSet<>(List.of("path", "merge")); + for (String argument : recordMetadataValue.split(",")) { + String[] parts = SubstrateUtil.split(argument, "=", ARGUMENT_PARTS); + if (parts.length != ARGUMENT_PARTS) { + throw badArgumentError(argument, "Argument should be a key-value pair separated by '='"); + } else if (!allArguments.contains(parts[0])) { + throw badArgumentError(argument, "Argument key should be one of " + allArguments); + } else if (parsedArguments.containsKey(parts[0])) { + throw badArgumentError(argument, "Argument '" + parts[0] + "' was already specified with value '" + parsedArguments.get(parts[0]) + "'"); + } else if (parts[1].isEmpty()) { + throw badArgumentError(argument, "Value cannot be empty"); + } + parsedArguments.put(parts[0], parts[1]); + } + + String path = requiredArgument(parsedArguments, "path", IDENTITY_PARSER); + boolean merge = optionalArgument(parsedArguments, "merge", true, BOOLEAN_PARSER); + return new RecordOptions(Paths.get(path), merge); + } + + private static IllegalArgumentException printHelp(String errorMessage) { + throw new IllegalArgumentException(""" + %s + + %s description: + + %s + """.formatted(errorMessage, MetadataTracer.Options.RecordMetadata.getName(), MetadataTracer.Options.RECORD_METADATA_HELP)); + } + + private static IllegalArgumentException parseError(String message) { + return new IllegalArgumentException(message + ". For more information (including usage examples), pass 'help' as an argument to " + MetadataTracer.Options.RecordMetadata.getName() + "."); + } + + private static IllegalArgumentException badArgumentError(String argument, String message) { + throw parseError("Bad argument provided for " + MetadataTracer.Options.RecordMetadata.getName() + ": '" + argument + "'. " + message); + } + + private static IllegalArgumentException badArgumentValueError(String argumentKey, String argumentValue, String message) { + throw badArgumentError(argumentKey + "=" + argumentValue, message); + } + + private interface ArgumentParser { + T parse(String argumentKey, String argumentValue); + } + + private static final ArgumentParser IDENTITY_PARSER = ((argumentKey, argumentValue) -> argumentValue); + private static final ArgumentParser BOOLEAN_PARSER = ((argumentKey, argumentValue) -> switch (argumentValue) { + case "true" -> true; + case "false" -> false; + default -> throw badArgumentValueError(argumentKey, argumentValue, "Value must be a literal 'true' or 'false'"); + }); + + private static T requiredArgument(Map arguments, String key, ArgumentParser parser) { + if (arguments.containsKey(key)) { + return parser.parse(key, arguments.get(key)); + } + throw parseError(MetadataTracer.Options.RecordMetadata.getName() + " missing required argument '" + key + "'"); + } + + private static T optionalArgument(Map options, String key, T defaultValue, ArgumentParser parser) { + if (options.containsKey(key)) { + return parser.parse(key, options.get(key)); + } + return defaultValue; + } +} + @AutomaticallyRegisteredFeature class MetadataTracerFeature implements InternalFeature { @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java index e03f04449681..40883789eba9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/proxy/DynamicProxySupport.java @@ -26,8 +26,10 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.regex.Pattern; import org.graalvm.collections.EconomicMap; @@ -46,6 +48,7 @@ import com.oracle.svm.core.layeredimagesingleton.DuplicableImageSingleton; import com.oracle.svm.core.layeredimagesingleton.ImageSingletonWriter; import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonBuilderFlags; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; @@ -188,6 +191,14 @@ private static ClassLoader getCommonClassLoaderOrFail(ClassLoader loader, Class< @Override public Class getProxyClass(ClassLoader loader, Class... interfaces) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + List interfaceNames = new ArrayList<>(interfaces.length); + for (Class iface : interfaces) { + interfaceNames.add(iface.getName()); + } + MetadataTracer.singleton().traceProxyType(interfaceNames); + } + ProxyCacheKey key = new ProxyCacheKey(interfaces); ConditionalRuntimeValue clazzOrError = proxyCache.get(key); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java index 866d3be261a0..8ca407aa2cd4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java @@ -48,6 +48,7 @@ import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.layeredimagesingleton.MultiLayeredImageSingleton; import com.oracle.svm.core.layeredimagesingleton.UnsavedSingleton; +import com.oracle.svm.core.metadata.MetadataTracer; import com.oracle.svm.core.reflect.SubstrateConstructorAccessor; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; @@ -269,6 +270,9 @@ public static Object getSerializationConstructorAccessor(Class serializationT for (var singleton : layeredSingletons()) { Object constructorAccessor = singleton.getSerializationConstructorAccessor0(declaringClass, targetConstructorClass); if (constructorAccessor != null) { + if (MetadataTracer.Options.MetadataTracingSupport.getValue() && MetadataTracer.singleton().enabled()) { + MetadataTracer.singleton().traceSerializationType(declaringClass.getName()); + } return constructorAccessor; } }