This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 655cdd7d6 feat(java): support xlang serialization for GraalVM native
image (#3126)
655cdd7d6 is described below
commit 655cdd7d66ead55eb11294ff479f66ac87e4d341
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Jan 12 14:17:29 2026 +0800
feat(java): support xlang serialization for GraalVM native image (#3126)
## Summary
Closes #3112
This PR adds support for xlang serialization mode with GraalVM native
image builds.
### Changes:
- Add `XtypeResolver.ensureSerializersCompiled()` to ensure serializers
for xlang-registered classes are compiled at GraalVM build time
- Update `Fory.ensureSerializersCompiled()` to also call
`XtypeResolver.ensureSerializersCompiled()` when xlang mode is enabled
- Add `XlangExample` for graalvm_tests to verify xlang mode works with
native-image builds
- Register `XlangExample` for build-time initialization in native-image
properties
### Root cause:
When xlang mode is enabled, classes registered via `Fory.register()` go
through `XtypeResolver`, but `Fory.ensureSerializersCompiled()` only
processed classes in `ClassResolver.classInfoMap`. This meant
xlang-registered classes were not properly compiled at GraalVM build
time, causing NullPointerException during initialization.
## Test plan
- [x] Existing xlang tests pass
- [x] XlangExample added for native-image testing
- [ ] CI should verify the graalvm_tests build succeeds
---
AGENTS.md | 1 +
.../main/java/org/apache/fory/graalvm/Main.java | 1 +
.../java/org/apache/fory/graalvm/XlangExample.java | 56 ++++++++++++++++++++++
.../graalvm_tests/native-image.properties | 1 +
.../src/main/java/org/apache/fory/Fory.java | 2 +-
.../fory/builder/BaseObjectCodecBuilder.java | 13 +++--
.../java/org/apache/fory/builder/CodecUtils.java | 15 ++++--
.../org/apache/fory/resolver/ClassResolver.java | 10 ++--
.../org/apache/fory/resolver/TypeResolver.java | 2 +
.../org/apache/fory/resolver/XtypeResolver.java | 36 ++++++++++++++
.../fory/serializer/DeferedLazySerializer.java | 10 ++++
.../java/org/apache/fory/util/GraalvmSupport.java | 12 +++++
.../fory-core/native-image.properties | 7 +++
13 files changed, 153 insertions(+), 13 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 7ae97b717..58c1666a3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -288,6 +288,7 @@ sbt scalafmt
- All commands must be executed within the `integration_tests` directory.
- For java related integration tests, please install the java libraries first
by `cd ../java && mvn -T16 install -DskipTests`. If no code changes after
installed fory java, you can skip the installation step.
+- For mac, graalvm is installed at
`/Library/Java/JavaVirtualMachines/graalvm-xxx` by default.
```bash
it_dir=$(pwd)
diff --git
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
index 7a2322b29..52631e4ed 100644
---
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
+++
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
@@ -26,6 +26,7 @@ import org.apache.fory.graalvm.record.RecordExample2;
public class Main {
public static void main(String[] args) throws Throwable {
Example.main(args);
+ XlangExample.main(args);
CompatibleExample.main(args);
ScopedCompatibleExample.main(args);
RecordExample.main(args);
diff --git
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/XlangExample.java
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/XlangExample.java
new file mode 100644
index 000000000..cc621e09c
--- /dev/null
+++
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/XlangExample.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.graalvm;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.fory.Fory;
+import org.apache.fory.util.Preconditions;
+
+public class XlangExample {
+ static Fory fory;
+
+ static {
+ fory =
Fory.builder().withName(XlangExample.class.getName()).withXlang(true).build();
+ // register and generate serializer code.
+ fory.register(Foo.class);
+ fory.ensureSerializersCompiled();
+ }
+
+ static void test(Fory fory) {
+
Preconditions.checkArgument("abc".equals(fory.deserialize(fory.serialize("abc"))));
+ Preconditions.checkArgument(
+ List.of(1, 2, 3).equals(fory.deserialize(fory.serialize(List.of(1, 2,
3)))));
+ Map<String, Integer> map = Map.of("k1", 1, "k2", 2);
+
Preconditions.checkArgument(map.equals(fory.deserialize(fory.serialize(map))));
+ Foo foo = new Foo(10, "abc", List.of("str1", "str2"), Map.of("k1", 10L,
"k2", 20L));
+ byte[] bytes = fory.serialize(foo);
+ System.out.println(fory.getXtypeResolver().getSerializer(Foo.class));
+ Object o = fory.deserialize(bytes);
+ System.out.println(foo);
+ System.out.println(o);
+ Preconditions.checkArgument(foo.equals(o));
+ }
+
+ public static void main(String[] args) {
+ test(fory);
+ System.out.println("XlangExample succeed");
+ }
+}
diff --git
a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
index 7337d3ad4..fa2b13ad1 100644
---
a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
+++
b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
@@ -19,6 +19,7 @@
# The unsafe offset get on build time may be different from runtime
Args=-H:+ReportExceptionStackTraces \
--initialize-at-build-time=org.apache.fory.graalvm.Example,\
+ org.apache.fory.graalvm.XlangExample,\
org.apache.fory.graalvm.CompatibleExample,\
org.apache.fory.graalvm.ScopedCompatibleExample,\
org.apache.fory.graalvm.record.RecordExample,\
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java
b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index 7e7c20840..0188055e6 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -1602,7 +1602,7 @@ public final class Fory implements BaseFory {
@Override
public void ensureSerializersCompiled() {
- classResolver.ensureSerializersCompiled();
+ _getTypeResolver().ensureSerializersCompiled();
}
public JITContext getJITContext() {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
index b53e79802..c359d2ea7 100644
---
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
+++
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
@@ -185,7 +185,9 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
super(new CodegenContext(), beanType);
this.fory = fory;
typeResolver = fory._getTypeResolver();
- TypeRef<?> typeResolverType = TypeRef.of(typeResolver.getClass());
+ // Use TypeResolver interface instead of concrete class to support both
xlang and non-xlang
+ // modes
+ TypeRef<?> typeResolverType = TypeRef.of(TypeResolver.class);
this.parentSerializerClass = parentSerializerClass;
if (fory.isCrossLanguage()) {
writeMethodName = "xwrite";
@@ -207,9 +209,7 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
REF_RESOLVER_NAME,
new Cast(refResolverExpr, refResolverTypeRef));
Expression typeResolverExpr =
- cast(
- inlineInvoke(foryRef, "_getTypeResolver",
TypeRef.of(TypeResolver.class)),
- typeResolverType);
+ inlineInvoke(foryRef, "_getTypeResolver",
TypeRef.of(TypeResolver.class));
ctx.addField(ctx.type(typeResolverType), TYPE_RESOLVER_NAME,
typeResolverExpr);
ctx.reserveName(STRING_SERIALIZER_NAME);
stringSerializerRef = fieldRef(STRING_SERIALIZER_NAME,
STRING_SERIALIZER_TYPE_TOKEN);
@@ -234,6 +234,11 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
} else {
nameBuilder.append("Fory");
}
+ if (fory.isCrossLanguage()) {
+ // Generated classes are different when xlang mode is enabled.
+ // So we need to use a different name to generate xwrite/xread methods.
+ nameBuilder.append("Xlang");
+ }
nameBuilder.append("Codec").append(codecSuffix());
Map<String, Integer> subGenerator =
idGenerator.computeIfAbsent(nameBuilder.toString(), k -> new
ConcurrentHashMap<>());
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
index 51b53d6ec..6f58545aa 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
@@ -25,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.fory.Fory;
import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.codegen.CompileUnit;
-import org.apache.fory.collection.Tuple2;
+import org.apache.fory.collection.Tuple3;
import org.apache.fory.meta.ClassDef;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.ClassResolver;
@@ -37,7 +37,8 @@ import org.apache.fory.util.Preconditions;
/** Codec util to create and load jit serializer class. */
@SuppressWarnings("rawtypes")
public class CodecUtils {
- private static ConcurrentHashMap<Tuple2<String, Class<?>>, Class>
graalvmSerializers =
+ // Cache key includes configHash to distinguish between xlang and non-xlang
modes
+ private static ConcurrentHashMap<Tuple3<String, Class<?>, Integer>, Class>
graalvmSerializers =
new ConcurrentHashMap<>();
// TODO(chaokunyang) how to uninstall org.apache.fory.codegen/builder
classes for graalvm build
@@ -49,6 +50,7 @@ public class CodecUtils {
return loadSerializer(
"loadOrGenObjectCodecClass",
cls,
+ fory,
() -> loadOrGenCodecClass(cls, fory, new ObjectCodecBuilder(cls,
fory)));
}
@@ -58,6 +60,7 @@ public class CodecUtils {
return loadSerializer(
"loadOrGenMetaSharedCodecClass",
cls,
+ fory,
() ->
loadOrGenCodecClass(
cls, fory, new MetaSharedCodecBuilder(TypeRef.of(cls), fory,
classDef)));
@@ -78,6 +81,7 @@ public class CodecUtils {
return loadSerializer(
"loadOrGenMetaSharedLayerCodecClass",
cls,
+ fory,
() ->
loadOrGenCodecClass(
cls,
@@ -148,9 +152,10 @@ public class CodecUtils {
}
private static <T> Class<? extends Serializer<T>> loadSerializer(
- String name, Class<?> cls, Callable<Class<? extends Serializer<T>>>
func) {
+ String name, Class<?> cls, Fory fory, Callable<Class<? extends
Serializer<T>>> func) {
+ int configHash = fory.getConfig().getConfigHash();
if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
- Tuple2<String, Class<?>> key = Tuple2.of(name, cls);
+ Tuple3<String, Class<?>, Integer> key = Tuple3.of(name, cls, configHash);
Class serializerClass = graalvmSerializers.get(key);
if (serializerClass != null) {
return serializerClass;
@@ -159,7 +164,7 @@ public class CodecUtils {
try {
Class serializerClass = func.call();
if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
- graalvmSerializers.putIfAbsent(Tuple2.of(name, cls), serializerClass);
+ graalvmSerializers.putIfAbsent(Tuple3.of(name, cls, configHash),
serializerClass);
}
return serializerClass;
} catch (Exception e) {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 4c5f37e95..f5307c0c4 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -1956,6 +1956,7 @@ public class ClassResolver extends TypeResolver {
* <p>Note that this method should be invoked after all registrations and
invoked only once.
* Repeated invocations will have no effect.
*/
+ @Override
public void ensureSerializersCompiled() {
if (extRegistry.ensureSerializersCompiled) {
return;
@@ -1963,9 +1964,12 @@ public class ClassResolver extends TypeResolver {
extRegistry.ensureSerializersCompiled = true;
try {
fory.getJITContext().lock();
- Serializers.newSerializer(fory, LambdaSerializer.STUB_LAMBDA_CLASS,
LambdaSerializer.class);
- Serializers.newSerializer(
- fory, JdkProxySerializer.SUBT_PROXY.getClass(),
JdkProxySerializer.class);
+ // Lambda and JdkProxy serializers use java.lang.Class which is not
supported in xlang mode
+ if (!fory.isCrossLanguage()) {
+ Serializers.newSerializer(fory, LambdaSerializer.STUB_LAMBDA_CLASS,
LambdaSerializer.class);
+ Serializers.newSerializer(
+ fory, JdkProxySerializer.SUBT_PROXY.getClass(),
JdkProxySerializer.class);
+ }
classInfoMap.forEach(
(cls, classInfo) -> {
GraalvmSupport.registerClass(cls,
fory.getConfig().getConfigHash());
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 90600bec1..e1ac536c0 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -496,6 +496,8 @@ public abstract class TypeResolver {
public abstract void initialize();
+ public abstract void ensureSerializersCompiled();
+
public abstract ClassDef getTypeDef(Class<?> cls, boolean resolveParent);
public final boolean isSerializable(Class<?> cls) {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
index eeac4aec1..06fc666c6 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
@@ -265,6 +265,13 @@ public class XtypeResolver extends TypeResolver {
if (serializer == null) {
if (type.isEnum()) {
classInfo.serializer = new EnumSerializer(fory, (Class<Enum>) type);
+ } else if (GraalvmSupport.isGraalBuildtime()) {
+ // For GraalVM build time, directly create the serializer to avoid
+ // issues with DeferedLazySerializer persistence in native image
+ Class<? extends Serializer> c =
+ classResolver.getObjectSerializerClass(
+ type, shareMeta, fory.getConfig().isCodeGenEnabled(), null);
+ classInfo.serializer = Serializers.newSerializer(fory, type, c);
} else {
AtomicBoolean updated = new AtomicBoolean(false);
AtomicReference<Serializer> ref = new AtomicReference(null);
@@ -1028,4 +1035,33 @@ public class XtypeResolver extends TypeResolver {
private boolean isEnum(int internalTypeId) {
return internalTypeId == Types.ENUM || internalTypeId == Types.NAMED_ENUM;
}
+
+ /**
+ * Ensure all serializers for registered classes are compiled at GraalVM
build time. This method
+ * should be called after all classes are registered.
+ */
+ @Override
+ public void ensureSerializersCompiled() {
+ classInfoMap.forEach(
+ (cls, classInfo) -> {
+ GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+ if (classInfo.serializer != null) {
+ // Trigger serializer initialization and resolution for deferred
serializers
+ if (classInfo.serializer instanceof DeferedLazyObjectSerializer) {
+ ((DeferedLazyObjectSerializer)
classInfo.serializer).resolveSerializer();
+ } else {
+ classInfo.serializer.getClass();
+ }
+ }
+ // For enums at GraalVM build time, also handle anonymous enum value
classes
+ if (cls.isEnum() && GraalvmSupport.isGraalBuildtime()) {
+ for (Object enumConstant : cls.getEnumConstants()) {
+ Class<?> enumValueClass = enumConstant.getClass();
+ if (enumValueClass != cls) {
+ getSerializer(enumValueClass);
+ }
+ }
+ }
+ });
+ }
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/DeferedLazySerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/DeferedLazySerializer.java
index 0e580b929..aa75223c6 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/DeferedLazySerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/DeferedLazySerializer.java
@@ -68,6 +68,16 @@ public class DeferedLazySerializer extends Serializer {
return serializer;
}
+ /**
+ * Force resolution of the deferred serializer without writing data. Used
during GraalVM build
+ * time to ensure the actual serializer is compiled.
+ *
+ * @return the resolved serializer
+ */
+ public Serializer resolveSerializer() {
+ return getSerializer();
+ }
+
@Override
public Object copy(Object value) {
return getSerializer().copy(value);
diff --git
a/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java
b/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java
index e46833e33..0f5d66038 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java
+++ b/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java
@@ -163,6 +163,18 @@ public class GraalvmSupport {
return getSerializer().read(buffer);
}
+ @Override
+ public void xwrite(MemoryBuffer buffer, Object value) {
+ // for xlang mode in graalvm native image
+ getSerializer().xwrite(buffer, value);
+ }
+
+ @Override
+ public Object xread(MemoryBuffer buffer) {
+ // for xlang mode in graalvm native image
+ return getSerializer().xread(buffer);
+ }
+
private Serializer getSerializer() {
if (serializer == null) {
try {
diff --git
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
index 89c967274..a669aae69 100644
---
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
+++
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
@@ -285,6 +285,7 @@
Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
org.apache.fory.resolver.TypeResolver$ExtRegistry,\
org.apache.fory.resolver.TypeResolver$GraalvmClassRegistry,\
org.apache.fory.resolver.ClassResolver,\
+ org.apache.fory.resolver.XtypeResolver,\
org.apache.fory.resolver.DisallowedList,\
org.apache.fory.resolver.MetaContext,\
org.apache.fory.resolver.MetaStringBytes,\
@@ -414,6 +415,9 @@
Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
org.apache.fory.serializer.collection.CollectionSerializers$SetFromMapSerializer,\
org.apache.fory.serializer.collection.CollectionSerializers$SortedSetSerializer,\
org.apache.fory.serializer.collection.CollectionSerializers$VectorSerializer,\
+
org.apache.fory.serializer.collection.CollectionSerializers$XlangCollectionDefaultSerializer,\
+
org.apache.fory.serializer.collection.CollectionSerializers$XlangListDefaultSerializer,\
+
org.apache.fory.serializer.collection.CollectionSerializers$XlangSetDefaultSerializer,\
org.apache.fory.serializer.collection.ForyArrayAsListSerializer$ArrayAsList,\
org.apache.fory.serializer.collection.ForyArrayAsListSerializer,\
org.apache.fory.serializer.collection.GuavaCollectionSerializers$1,\
@@ -445,6 +449,7 @@
Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
org.apache.fory.serializer.collection.MapSerializers$LinkedHashMapSerializer,\
org.apache.fory.serializer.collection.MapSerializers$SingletonMapSerializer,\
org.apache.fory.serializer.collection.MapSerializers$SortedMapSerializer,\
+ org.apache.fory.serializer.collection.MapSerializers$XlangMapSerializer,\
org.apache.fory.serializer.collection.SynchronizedSerializers$Offset,\
org.apache.fory.serializer.collection.SynchronizedSerializers$SynchronizedCollectionSerializer,\
org.apache.fory.serializer.collection.SynchronizedSerializers$SynchronizedMapSerializer,\
@@ -458,6 +463,8 @@
Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
org.apache.fory.serializer.FieldGroups.SerializationFieldInfo,\
org.apache.fory.serializer.LazySerializer,\
org.apache.fory.serializer.LazySerializer$LazyObjectSerializer,\
+ org.apache.fory.serializer.DeferedLazySerializer,\
+
org.apache.fory.serializer.DeferedLazySerializer$DeferedLazyObjectSerializer,\
org.apache.fory.serializer.shim.ShimDispatcher,\
org.apache.fory.serializer.shim.ProtobufDispatcher,\
org.apache.fory.serializer.converter.FieldConverter,\
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]