Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GR-42113] Fixes for serialization reflection registration. #7230

Merged
merged 2 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,8 @@ public static String digest(String value) {
throw new JVMCIError(ex);
}
}

public static String capturingClass(String className) {
return className.split(LambdaUtils.SERIALIZATION_TEST_LAMBDA_CLASS_SPLIT_PATTERN)[0];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

public final class MissingRegistrationUtils {

public static final String ERROR_EMPHASIS_INDENT = " ";

public static boolean throwMissingRegistrationErrors() {
return SubstrateOptions.ThrowMissingRegistrationErrors.hasBeenSet();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ public final class DynamicHub implements AnnotatedElement, java.lang.reflect.Typ
/** Is the class a proxy class according to {@link java.lang.reflect.Proxy#isProxyClass}? */
private static final int IS_PROXY_CLASS_BIT = 2;

private static final int IS_REGISTERED_FOR_SERIALIZATION = 3;

/**
* The {@link Modifier modifiers} of this class.
*/
Expand Down Expand Up @@ -436,7 +438,8 @@ public void setClassInitializationInfo(ClassInitializationInfo classInitializati

@Platforms(Platform.HOSTED_ONLY.class)
public void setData(int layoutEncoding, int typeID, int monitorOffset, int optionalIdentityHashOffset, short typeCheckStart, short typeCheckRange, short typeCheckSlot,
short[] typeCheckSlots, CFunctionPointer[] vtable, long referenceMapIndex, boolean isInstantiated, boolean canInstantiateAsInstance, boolean isProxyClass) {
short[] typeCheckSlots, CFunctionPointer[] vtable, long referenceMapIndex, boolean isInstantiated, boolean canInstantiateAsInstance, boolean isProxyClass,
boolean isRegisteredForSerialization) {
assert this.vtable == null : "Initialization must be called only once";
assert !(!isInstantiated && canInstantiateAsInstance);
if (LayoutEncoding.isPureInstance(layoutEncoding)) {
Expand All @@ -462,7 +465,8 @@ public void setData(int layoutEncoding, int typeID, int monitorOffset, int optio
this.referenceMapIndex = (int) referenceMapIndex;
this.additionalFlags = NumUtil.safeToUByte(makeFlag(IS_INSTANTIATED_BIT, isInstantiated) |
makeFlag(CAN_INSTANTIATE_AS_INSTANCE_BIT, canInstantiateAsInstance) |
makeFlag(IS_PROXY_CLASS_BIT, isProxyClass));
makeFlag(IS_PROXY_CLASS_BIT, isProxyClass) |
makeFlag(IS_REGISTERED_FOR_SERIALIZATION, isRegisteredForSerialization));
}

@Platforms(Platform.HOSTED_ONLY.class)
Expand Down Expand Up @@ -883,6 +887,10 @@ public boolean isLambdaFormHidden() {
return isFlagSet(flags, IS_LAMBDA_FORM_HIDDEN_BIT);
}

public boolean isRegisteredForSerialization() {
return isFlagSet(additionalFlags, IS_REGISTERED_FOR_SERIALIZATION);
}

@KeepOriginal
private native boolean isLocalClass();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@
package com.oracle.svm.core.jdk;

import java.io.Closeable;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import org.graalvm.compiler.java.LambdaUtils;

import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
Expand All @@ -38,6 +45,7 @@
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.reflect.serialize.MissingSerializationRegistrationUtils;

@TargetClass(java.io.FileDescriptor.class)
final class Target_java_io_FileDescriptor {
Expand All @@ -53,16 +61,47 @@ final class Target_java_io_ObjectStreamClass {
private static boolean hasStaticInitializer(Class<?> cl) {
return DynamicHub.fromClass(cl).getClassInitializationInfo().hasInitializer();
}

@Substitute
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
if (!(all || Serializable.class.isAssignableFrom(cl))) {
return null;
}

if (Serializable.class.isAssignableFrom(cl)) {
if (!DynamicHub.fromClass(cl).isRegisteredForSerialization()) {
boolean isLambda = cl.getTypeName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING);
boolean isProxy = Proxy.isProxyClass(cl);
if (isProxy || isLambda) {
var interfaceList = Arrays.stream(cl.getInterfaces())
.map(Class::getTypeName)
.collect(Collectors.joining(", ", "[", "]"));
if (isProxy) {
MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "proxy type implementing interfaces: " + interfaceList);
} else {
MissingSerializationRegistrationUtils.missingSerializationRegistration(cl,
"lambda declared in: " + LambdaUtils.capturingClass(cl.getTypeName()),
"extending interfaces: " + interfaceList);
}
} else {
MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "type " + cl.getTypeName());
}
}
}

return Target_java_io_ObjectStreamClass_Caches.localDescs0.get(cl);
}

}

@TargetClass(value = java.io.ObjectStreamClass.class, innerClass = "Caches")
final class Target_java_io_ObjectStreamClass_Caches {

@TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "localDescs") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache localDescs0;
@TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "localDescs") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache<ObjectStreamClass> localDescs0;

@TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "reflectors") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache reflectors0;
@TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "reflectors") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache<?> reflectors0;

@TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap<?, ?> localDescs;
@TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap<?, ObjectStreamClass> localDescs;

@TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap<?, ?> reflectors;

Expand All @@ -72,7 +111,9 @@ final class Target_java_io_ObjectStreamClass_Caches {
}

@TargetClass(className = "java.io.ClassCache", onlyWith = JavaIOClassCachePresent.class)
final class Target_java_io_ClassCache {
final class Target_java_io_ClassCache<T> {
@Alias
native T get(Class<?> cl);
}

/** Dummy class to have a class with the file's name. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.core.reflect.serialize;

import java.io.Serial;

/**
* Error thrown when types are not <a href=
* "https://www.graalvm.org/latest/reference-manual/native-image/metadata/#serialization">registered</a>
* for serialization or deserialization.
* <p/>
* The purpose of this exception is to easily discover unregistered elements and to assure that all
* serialization or deserialization operations have expected behavior.
*/
public final class MissingSerializationRegistrationError extends Error {
@Serial private static final long serialVersionUID = 2764341882856270641L;
private final Class<?> culprit;

public MissingSerializationRegistrationError(String message, Class<?> cl) {
super(message);
this.culprit = cl;
}

/**
* Returns the class that was not registered for serialization or deserialization.
*/
public Class<?> getCulprit() {
return culprit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.core.reflect.serialize;

import static com.oracle.svm.core.MissingRegistrationUtils.ERROR_EMPHASIS_INDENT;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.oracle.svm.core.MissingRegistrationUtils;

public final class MissingSerializationRegistrationUtils {

public static void missingSerializationRegistration(Class<?> cl, String... msg) {
report(new MissingSerializationRegistrationError(errorMessage(msg), cl));
}

private static String errorMessage(String... type) {
var typeStr = Arrays.stream(type).collect(Collectors.joining(System.lineSeparator(), ERROR_EMPHASIS_INDENT, ""));
return """
The program tried to serialize or deserialize

%s

without it being registered for serialization. Add this class to the serialization metadata to solve this problem.
See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#serialization for help
""".replaceAll("\n", System.lineSeparator())
.formatted(typeStr);
}

private static void report(MissingSerializationRegistrationError exception) {
StackTraceElement responsibleClass = getResponsibleClass(exception);
MissingRegistrationUtils.report(exception, responsibleClass);
}

/*
* This is a list of all public JDK methods that end up potentially throwing missing
* registration errors. This should be implemented using wrapping substitutions once they are
* available.
*/
private static final Map<String, Set<String>> serializationEntryPoints = Map.of(
ObjectOutputStream.class.getTypeName(), Set.of("writeObject", "writeUnshared"),
ObjectInputStream.class.getTypeName(), Set.of("readObject", "readUnshared"),
ObjectStreamClass.class.getTypeName(), Set.of("lookup"),
"sun.reflect.ReflectionFactory", Set.of("newConstructorForSerialization"),
"jdk.internal.reflect.ReflectionFactory", Set.of("newConstructorForSerialization"));

private static StackTraceElement getResponsibleClass(MissingSerializationRegistrationError t) {
StackTraceElement[] stackTrace = t.getStackTrace();
boolean previous = false;
StackTraceElement lastElem = null;
for (StackTraceElement stackTraceElement : stackTrace) {
if (previous) {
previous = false;
lastElem = stackTraceElement;
}
if (serializationEntryPoints.getOrDefault(stackTraceElement.getClassName(), Set.of())
.contains(stackTraceElement.getMethodName())) {
previous = true;
}
}
return lastElem;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

public interface SerializationRegistry {

boolean isRegisteredForSerialization(Class<?> cl);

Object getSerializationConstructorAccessor(Class<?> serializationTargetClass, Class<?> targetConstructorClass);

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.graalvm.compiler.java.LambdaUtils;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;

import com.oracle.svm.core.util.VMError;

public class SerializationSupport implements SerializationRegistry {

/**
Expand Down Expand Up @@ -125,6 +124,19 @@ public Object addConstructorAccessor(Class<?> declaringClass, Class<?> targetCon
return constructorAccessors.putIfAbsent(key, constructorAccessor);
}

@Platforms(Platform.HOSTED_ONLY.class) private final Set<Class<?>> classes = ConcurrentHashMap.newKeySet();

@Platforms(Platform.HOSTED_ONLY.class)
public void registerSerializationTargetClass(Class<?> serializationTargetClass) {
classes.add(serializationTargetClass);
}

@Override
@Platforms(Platform.HOSTED_ONLY.class)
public boolean isRegisteredForSerialization(Class<?> cl) {
return classes.contains(cl);
}

@Override
public Object getSerializationConstructorAccessor(Class<?> rawDeclaringClass, Class<?> rawTargetConstructorClass) {
Class<?> declaringClass = rawDeclaringClass;
Expand All @@ -140,9 +152,9 @@ public Object getSerializationConstructorAccessor(Class<?> rawDeclaringClass, Cl
return constructorAccessor;
} else {
String targetConstructorClassName = targetConstructorClass.getName();
throw VMError.unsupportedFeature("SerializationConstructorAccessor class not found for declaringClass: " + declaringClass.getName() +
" (targetConstructorClass: " + targetConstructorClassName + "). Usually adding " + declaringClass.getName() +
" to serialization-config.json fixes the problem.");
MissingSerializationRegistrationUtils.missingSerializationRegistration(declaringClass,
"type " + declaringClass.getName() + " with target constructor class: " + targetConstructorClassName);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
import com.oracle.svm.core.meta.MethodPointer;
import com.oracle.svm.core.reflect.SubstrateConstructorAccessor;
import com.oracle.svm.core.reflect.SubstrateMethodAccessor;
import com.oracle.svm.core.reflect.serialize.SerializationRegistry;
import com.oracle.svm.core.util.VMError;
import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl;
import com.oracle.svm.hosted.HostedConfiguration;
Expand Down Expand Up @@ -1071,8 +1072,10 @@ private void buildHubs() {
boolean isProxyClass = ImageSingletons.lookup(DynamicProxyRegistry.class).isProxyClass(type.getJavaClass());

DynamicHub hub = type.getHub();
SerializationRegistry s = ImageSingletons.lookup(SerializationRegistry.class);
hub.setData(layoutHelper, type.getTypeID(), monitorOffset, optionalIdHashOffset, type.getTypeCheckStart(), type.getTypeCheckRange(),
type.getTypeCheckSlot(), type.getTypeCheckSlots(), vtable, referenceMapIndex, type.isInstantiated(), canInstantiateAsInstance, isProxyClass);
type.getTypeCheckSlot(), type.getTypeCheckSlots(), vtable, referenceMapIndex, type.isInstantiated(), canInstantiateAsInstance, isProxyClass,
s.isRegisteredForSerialization(type.getJavaClass()));
}
}

Expand Down
Loading