From c814484fe83110607c59e09a76c082944ab498a7 Mon Sep 17 00:00:00 2001 From: Sacha Coppey Date: Tue, 13 Feb 2024 14:46:15 +0100 Subject: [PATCH] Implement stable name for Proxy type --- substratevm/CHANGELOG.md | 1 + .../svm/hosted/NativeImageGenerator.java | 4 +- .../ProxyRenamingSubstitutionProcessor.java | 141 ++++++++++++++++++ .../reflect/proxy/ProxySubstitutionType.java | 43 ++++++ .../reflect/proxy/StableProxyNameFeature.java | 69 +++++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxyRenamingSubstitutionProcessor.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxySubstitutionType.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/StableProxyNameFeature.java diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index bd0bc3b7569d..ba7c99edda4e 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -7,6 +7,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-49996) Ensure explicitly set image name (e.g., via `-o imagename`) is not accidentally overwritten by `-jar jarfile` option. * (GR-48683) Together with Red Hat, we added partial support for the JFR event `OldObjectSample`. * (GR-47109) Together with Red Hat, we added support for JFR event throttling and the event `ObjectAllocationSample`. +* (GR-52030) Add a stable name for `Proxy` types in Native Image. The name `$Proxy[id]` is replaced by `$Proxy.s[hashCode]` where `hashCode` is computed using the names of the `Proxy` interfaces, the name of the class loader and the name of the module if it is not a dynamic module. ## GraalVM for JDK 22 (Internal Version 24.0.0) * (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics. 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 debcdb7e1784..86cba96c65ec 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 @@ -253,6 +253,7 @@ import com.oracle.svm.hosted.phases.SubstrateClassInitializationPlugin; import com.oracle.svm.hosted.phases.VerifyDeoptLIRFrameStatesPhase; import com.oracle.svm.hosted.phases.VerifyNoGuardsPhase; +import com.oracle.svm.hosted.reflect.proxy.ProxyRenamingSubstitutionProcessor; import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; import com.oracle.svm.hosted.substitute.DeletedFieldsPlugin; @@ -1056,7 +1057,8 @@ public static SubstitutionProcessor createAnalysisSubstitutionProcessor( List additionalSubstitutionProcessors) { List allProcessors = new ArrayList<>(); SubstitutionProcessor cFunctionSubstitutions = new CFunctionSubstitutionProcessor(); - allProcessors.addAll(Arrays.asList(annotationSubstitutions, cFunctionSubstitutions, cEnumProcessor)); + SubstitutionProcessor proxySubstitutionProcessor = new ProxyRenamingSubstitutionProcessor(); + allProcessors.addAll(Arrays.asList(annotationSubstitutions, cFunctionSubstitutions, cEnumProcessor, proxySubstitutionProcessor)); allProcessors.addAll(additionalSubstitutionProcessors); return SubstitutionProcessor.chainUpInOrder(allProcessors.toArray(new SubstitutionProcessor[0])); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxyRenamingSubstitutionProcessor.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxyRenamingSubstitutionProcessor.java new file mode 100644 index 000000000000..5ae2e8ef3358 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxyRenamingSubstitutionProcessor.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.reflect.proxy; + +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.graalvm.nativeimage.hosted.Feature; + +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; +import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor; + +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * A substitution processor that renames Proxy classes using a stable name. The name is created + * using the name of the interfaces composing the Proxy type combined with the name of the class + * loader of the type. + *

+ * Needs to be created before + * {@link org.graalvm.nativeimage.hosted.Feature#duringSetup(Feature.DuringSetupAccess)} as Proxy + * types can be created in the serialization feature. + */ +public class ProxyRenamingSubstitutionProcessor extends SubstitutionProcessor { + public static final String DYNAMIC_MODULE_REGEX = "jdk[.]proxy[0-9]+"; + public static final String NULL_CLASS_LOADER_NAME = "native-image-null-class-loader"; + public static final String NULL_MODULE_NAME = "native-image-null-module"; + public static final String UNNAMED_MODULE_NAME = "native-image-unnamed"; + private static final String STABLE_NAME_TEMPLATE = "/$Proxy.s"; + + private final ConcurrentMap typeSubstitutions = new ConcurrentHashMap<>(); + private final Set uniqueTypeNames = new HashSet<>(); + + public static boolean isProxyType(ResolvedJavaType type) { + Class clazz = OriginalClassProvider.getJavaClass(type); + return Proxy.isProxyClass(clazz); + } + + /** + * The code creating the name of dynamic modules can be found in + * Proxy$ProxyBuilder.getDynamicModule. + */ + private static boolean isModuleDynamic(Module module) { + return module != null && module.getName() != null && module.getName().matches(DYNAMIC_MODULE_REGEX); + } + + @Override + public ResolvedJavaType lookup(ResolvedJavaType type) { + if (!shouldReplace(type)) { + return type; + } + return getSubstitution(type); + } + + private static boolean shouldReplace(ResolvedJavaType type) { + return !(type instanceof ProxySubstitutionType) && isProxyType(type); + } + + private ProxySubstitutionType getSubstitution(ResolvedJavaType original) { + Class clazz = OriginalClassProvider.getJavaClass(original); + return typeSubstitutions.computeIfAbsent(original, key -> new ProxySubstitutionType(key, getUniqueProxyName(clazz))); + } + + public String getUniqueProxyName(Class clazz) { + StringBuilder sb = new StringBuilder(); + + /* + * According to the Proxy documentation: "A proxy class implements exactly the interfaces + * specified at its creation, in the same order. Invoking {@link Class#getInterfaces() + * getInterfaces} on its {@code Class} object will return an array containing the same list + * of interfaces (in the order specified at its creation)." + * + * This means that this order matches the order of the classes in our Proxy configuration + * JSON. The order is important as changing it creates a different Proxy type. + */ + Class[] interfaces = clazz.getInterfaces(); + Arrays.stream(interfaces).forEach(i -> sb.append(i.getName())); + + /* + * Two proxy classes with the same interfaces and two different class loaders with the same + * name or without a name but from the same class will produce the same stable name. + * + * The module of proxy classes without a package private interface contains a unique id that + * depends on the class loader. This id is assigned to the class loaders based on the order + * in which they are used to create a proxy class. In some rare cases, this order can be + * different in two build processes, meaning the module will contain a different id and the + * name will not be stable. + */ + ClassLoader classLoader = clazz.getClassLoader(); + String classLoaderName = classLoader == null ? NULL_CLASS_LOADER_NAME : classLoader.getName(); + sb.append(classLoaderName != null ? classLoaderName : classLoader.getClass().getName()); + + Module module = clazz.getModule(); + if (!isModuleDynamic(module)) { + String moduleName = module == null ? NULL_MODULE_NAME : module.getName(); + sb.append(moduleName != null ? moduleName : UNNAMED_MODULE_NAME); + } + + return findUniqueName(clazz, sb.toString().hashCode()); + } + + private String findUniqueName(Class clazz, int hashCode) { + CharSequence baseName = "L" + clazz.getPackageName().replace('.', '/') + STABLE_NAME_TEMPLATE + Integer.toHexString(hashCode); + String name = baseName + ";"; + synchronized (uniqueTypeNames) { + int suffix = 1; + while (uniqueTypeNames.contains(name)) { + name = baseName + "_" + suffix + ";"; + suffix++; + } + uniqueTypeNames.add(name); + return name; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxySubstitutionType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxySubstitutionType.java new file mode 100644 index 000000000000..69c21f2a53a3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/ProxySubstitutionType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.reflect.proxy; + +import com.oracle.svm.hosted.annotation.CustomSubstitutionType; + +import jdk.vm.ci.meta.ResolvedJavaType; + +public class ProxySubstitutionType extends CustomSubstitutionType { + private final String stableName; + + ProxySubstitutionType(ResolvedJavaType original, String stableName) { + super(original); + this.stableName = stableName; + } + + @Override + public String getName() { + return stableName; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/StableProxyNameFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/StableProxyNameFeature.java new file mode 100644 index 000000000000..b038446ae84e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/proxy/StableProxyNameFeature.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.reflect.proxy; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.hosted.FeatureImpl.AfterAnalysisAccessImpl; + +/** + * @see ProxyRenamingSubstitutionProcessor + */ +@AutomaticallyRegisteredFeature +final class StableProxyNameFeature implements InternalFeature { + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + assert checkProxyNames(((AfterAnalysisAccessImpl) access).getUniverse().getTypes()); + } + + private static boolean checkProxyNames(List types) { + if (!SubstrateUtil.assertionsEnabled()) { + throw new AssertionError("Expensive check: should only run with assertions enabled."); + } + /* There should be no random proxy type names visible to the analysis. */ + if (types.stream().anyMatch(type -> ProxyRenamingSubstitutionProcessor.isProxyType(type) && type.getWrapped().getClass() != ProxySubstitutionType.class)) { + throw new AssertionError("All proxies should be substituted."); + } + + /* Proxy names should be unique. */ + Set proxyNames = new HashSet<>(); + types.stream() + .filter(ProxyRenamingSubstitutionProcessor::isProxyType) + .map(AnalysisType::getName) + .forEach(name -> { + if (proxyNames.contains(name)) { + throw new AssertionError("Duplicate proxy name: " + name); + } + proxyNames.add(name); + }); + return true; + } +}