Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6186174
fix format for tasks
chaokunyang May 11, 2026
e5fda6f
feat(java): add ForyStruct static serializers
chaokunyang May 11, 2026
8975812
docs(java): document annotation processor usage
chaokunyang May 11, 2026
c0e8642
refactor(java): rename field metadata spec
chaokunyang May 11, 2026
d63c99a
refactor(java): keep ForyField options in Descriptor
chaokunyang May 11, 2026
7f807f4
feat(java): add static compatible meta readers
chaokunyang May 11, 2026
1a7cee0
docs(java): rename static generated serializer guide
chaokunyang May 11, 2026
1bc4cf2
test(java): cover graalvm compatible schema reads
chaokunyang May 11, 2026
6f00ebd
feat(java): add ForyStruct evolution policy
chaokunyang May 11, 2026
0d0b06a
fix(java): keep xlang TypeDef fields root-owned
chaokunyang May 11, 2026
3566f37
fix(java): keep annotation processor JDK 11 compatible
chaokunyang May 11, 2026
497ff8e
fix(java): preserve processor type-use metadata on Java 8
chaokunyang May 11, 2026
95a70a1
fix(java): harden static generated compatible serializers
chaokunyang May 12, 2026
8a234cf
fix(java): cover static compatible read conversions
chaokunyang May 12, 2026
00703ac
fix(java): stabilize static generated xlang serializers
chaokunyang May 12, 2026
7abd025
fix name
chaokunyang May 12, 2026
7325b9c
fix inconsistent field processing
chaokunyang May 12, 2026
0d2a003
fix(java): align static compatible field metadata
chaokunyang May 12, 2026
3ee8866
docs(java): clarify primitive-list field metadata
chaokunyang May 12, 2026
c12da07
add more tests and fix field meta and order
chaokunyang May 12, 2026
064018f
fix more type meta err
chaokunyang May 12, 2026
4f509cc
fix meta error
chaokunyang May 12, 2026
30db8ee
fix(java): split static serializers by protocol mode
chaokunyang May 12, 2026
b150ed6
fix(java): classify compatible primitive array payloads
chaokunyang May 12, 2026
0b85f15
chore(java): satisfy descriptor helper checkstyle
chaokunyang May 12, 2026
28f6e81
fix(java): preserve compatible collection wire shape
chaokunyang May 12, 2026
7ca8bfd
fix(java): preserve compatible list metadata
chaokunyang May 12, 2026
b59f8f7
fix(go): align list element typedef nullability
chaokunyang May 12, 2026
abee3aa
fix(java): skip native primitive list fields from typedef
chaokunyang May 12, 2026
8169724
fix(java): honor native static field nullability
chaokunyang May 12, 2026
17f7dce
fix(java): guard static serializer recursion
chaokunyang May 12, 2026
985fdb0
fix: harden compatible generated serializers
chaokunyang May 12, 2026
7d1a229
fix: harden list-array compatible reads
chaokunyang May 12, 2026
88307a9
Merge remote-tracking branch 'apache/main' into android_annotation_pr…
chaokunyang May 12, 2026
dc0819d
fix(java): harden static compatible schema checks
chaokunyang May 12, 2026
7c3d32c
fix: reject nullable list array compatibility
chaokunyang May 12, 2026
05ee490
fix: align compatible schema bridges
chaokunyang May 13, 2026
79320a2
fix: skip nested list array schema mismatches
chaokunyang May 13, 2026
1281205
test(js): expect nested list array skip
chaokunyang May 13, 2026
4baf41c
test(java): expect static nested list array skip
chaokunyang May 13, 2026
9571aba
ci: avoid caching rust tool binaries
chaokunyang May 13, 2026
165f1ac
docs: fix generated java evolution annotation
chaokunyang May 13, 2026
ee901d9
style: apply python formatting
chaokunyang May 13, 2026
61cd4ed
perf(java): speed up static generated serializers
chaokunyang May 13, 2026
33fb0cc
fix(xlang): allow nullable list schema array reads
chaokunyang May 13, 2026
078c6fc
test(xlang): restore non-java peers to main
chaokunyang May 13, 2026
e330ade
ci: use default rust binary cache behavior
chaokunyang May 13, 2026
8782660
docs(java): clarify Android static serializers
chaokunyang May 13, 2026
2223baf
docs(java): add Android annotation processor setup
chaokunyang May 13, 2026
3c0983a
docs(java): expand Android processor Maven example
chaokunyang May 13, 2026
b2fe661
docs(java): focus static serializer guide on users
chaokunyang May 13, 2026
a4942bd
docs(java): refresh annotation processor readme
chaokunyang May 13, 2026
301c80c
fix(java): clean up static compatible serializer
chaokunyang May 13, 2026
96cf1a7
fix java tests
chaokunyang May 13, 2026
96a09fe
fix(go): read xlang compatible primitive arrays
chaokunyang May 13, 2026
b1c99c1
refactor(java): rename compatible serializers
chaokunyang May 13, 2026
b3f7384
fix(java): allow nullable list payload array read
chaokunyang May 13, 2026
96e359d
fix(java): update GraalVM compatible marker
chaokunyang May 13, 2026
c7da815
test(java): align static list array payload validation
chaokunyang May 13, 2026
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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,51 @@ jobs:
- name: Run Go IDL Tests
run: ./integration_tests/idl_tests/run_go_tests.sh

android_go_xlang:
name: Android Go Xlang Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21"
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: "temurin"
- name: Cache Maven local repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Run Android Go Xlang And IDL Tests
env:
FORY_ANDROID_ENABLED: "1"
FORY_GO_JAVA_CI: "1"
ENABLE_FORY_DEBUG_OUTPUT: "1"
run: |
cd java
mvn -T16 --no-transfer-progress clean install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
cd fory-core
mvn -T16 --no-transfer-progress test -Dtest=org.apache.fory.xlang.GoXlangTest
cd ../..
./integration_tests/idl_tests/run_go_tests.sh
- name: Run Android Go Xlang And IDL Tests With Static Generated Serializers
env:
FORY_ANDROID_ENABLED: "1"
FORY_GO_JAVA_CI: "1"
FORY_STATIC_PROCESSOR_CI: "1"
ENABLE_FORY_DEBUG_OUTPUT: "1"
run: python ./ci/run_ci.py android-go-xlang-static-processor

android_serializer:
name: Android Serializer JVM Round Trip Test
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
- Reject semantic hacks. Do not bypass broken semantics by deleting cases, simplifying callers, adding coercion hooks, or using workaround fallbacks; fix the underlying bug and prove it with focused tests.
- Protect hot paths. Avoid per-call allocations, callback objects, result tuples or records, unnecessary runtime branches, and wrapper-class substitutions in hot codec/runtime paths; prefer conditional imports and allocation-free concrete implementations where they fit the language.
- Keep public APIs minimal. Public APIs must match user ownership and mental model, not internal implementation details; generated flows stay type-owned, while manual serializer registration stays explicit.
- Use semantic naming only. Name things after protocol or domain concepts, not history, runtime origin, or workaround style; avoid vague names such as `Internal`, `java_style_*`, `Runtime`, `Session`, `Plan`, or `Binding` when they do not name the real concept.
- Use semantic naming only. Name things after protocol or domain concepts, not history, runtime origin, or workaround style; avoid vague names such as `Internal`, `java_style_*`, `Runtime`, `Session`, `Plan`, or `Binding` when they do not name the real concept. Never name a class or method with a `Plan` suffix; use the real domain concept instead.
- Keep one implementation path. Do not keep parallel helpers, serializers, harnesses, wrappers, or registration flows for the same concept; extend the existing owner path instead of inventing another one.
- Follow current scope exactly. The latest explicit user instruction overrides earlier plans, and when scope narrows, remove leaked out-of-scope edits immediately.
- Preserve user corrections. When a user corrects code behavior, ownership, invariants, or review feedback in a way that should prevent repeat mistakes, encode the corrected rule where future agents will see it: prefer the nearest source comment for non-obvious code invariants, or the owning docs/spec for user-visible or protocol behavior. If the correction changes API usage, defaults, generated output, tests, or cross-runtime behavior, update the matching docs, examples, or source comments in the same task so future agents do not repeat the violation. Keep the note concise, English-only, and avoid comments that merely restate obvious code.
Expand Down Expand Up @@ -61,7 +61,7 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
- Do not replace existing C, C++, Cython, unsafe, or other low-level optimized paths with simpler high-level implementations just to make a refactor easier.
- If a refactor accidentally changes logic or implementation strategy, revert that part and re-implement the refactor around the existing logic.
- Use English only in code, comments, and documentation.
- After editing any Markdown file, run `prettier --write <file>` on each changed Markdown file before finishing.
- After editing Markdown files outside `tasks/`, run `prettier --write <file>` on each changed Markdown file before finishing. Do not format Markdown under `tasks/`.
- Add comments only when behavior is hard to understand or an algorithm is non-obvious.
- Do not remove existing code comments unless they are stale, misleading, redundant, or no longer necessary after the change.
- Only add tests that verify internal behaviors or fix specific bugs; do not create unnecessary tests unless requested.
Expand Down
5 changes: 5 additions & 0 deletions benchmarks/java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
<path>
<groupId>org.apache.fory</groupId>
<artifactId>fory-annotation-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.apache.fory.benchmark.xlang.generated.FBSSampleList;
import org.apache.fory.config.Int32Encoding;
import org.apache.fory.integration_tests.state.generated.ProtoMessage;
import org.apache.fory.serializer.Serializer;
import org.apache.fory.serializer.StaticGeneratedStructSerializer;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.CompilerControl;
Expand All @@ -48,6 +50,7 @@
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
Expand All @@ -64,6 +67,9 @@ public class XlangBenchmark {

@State(Scope.Thread)
public static class XlangState {
@Param({"true", "false"})
public boolean codegen;

public Fory fory;

public NumericStruct numericStruct;
Expand Down Expand Up @@ -107,6 +113,7 @@ public void setup() {
Fory.builder()
.withXlang(true)
.withCompatible(true)
.withCodegen(codegen)
.withRefTracking(false)
.withClassVersionCheck(false)
.requireClassRegistration(true)
Expand Down Expand Up @@ -152,10 +159,27 @@ public void setup() {
}

private void verifySetup() {
verifyForySerializerMode(NumericStruct.class);
verifyForySerializerMode(Sample.class);
verifyForySerializerMode(MediaContent.class);
fory.deserialize(foryNumericStructBytes);
fromProtoStruct(protobufNumericStructBytes);
fromFlatBufferNumericStruct(flatbufferNumericStructBuffer);
}

private void verifyForySerializerMode(Class<?> type) {
Serializer<?> serializer = fory.getTypeResolver().getSerializer(type);
boolean staticSerializer = serializer instanceof StaticGeneratedStructSerializer;
if (staticSerializer == codegen) {
throw new IllegalStateException(
"Unexpected serializer for "
+ type.getName()
+ " with codegen="
+ codegen
+ ": "
+ serializer.getClass().getName());
}
}
}

private static void registerForyTypes(Fory fory) {
Expand Down
151 changes: 150 additions & 1 deletion ci/run_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
import argparse
import logging
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET

from tasks import cpp, java, javascript, kotlin, rust, python, go, format
from tasks import common, cpp, java, javascript, kotlin, rust, python, go, format
from tasks.common import is_windows

# Configure logging
Expand Down Expand Up @@ -104,6 +107,144 @@ def run_shell_script(command, *args):
sys.exit(subprocess.call(cmd))


def _pom_tag(namespace, name):
return f"{{{namespace}}}{name}" if namespace else name


def _pom_namespace(root):
if root.tag.startswith("{"):
return root.tag[1:].split("}", 1)[0]
return ""


def _find_child(element, namespace, name):
return element.find(_pom_tag(namespace, name))


def _find_or_add_child(element, namespace, name):
child = _find_child(element, namespace, name)
if child is None:
child = ET.SubElement(element, _pom_tag(namespace, name))
return child


def _child_text(element, namespace, name):
child = _find_child(element, namespace, name)
return "" if child is None or child.text is None else child.text.strip()


def _find_or_add_compiler_plugin(root, namespace):
build = _find_or_add_child(root, namespace, "build")
plugins = _find_or_add_child(build, namespace, "plugins")
for plugin in plugins.findall(_pom_tag(namespace, "plugin")):
if _child_text(plugin, namespace, "artifactId") == "maven-compiler-plugin":
return plugin
plugin = ET.SubElement(plugins, _pom_tag(namespace, "plugin"))
group_id = ET.SubElement(plugin, _pom_tag(namespace, "groupId"))
group_id.text = "org.apache.maven.plugins"
artifact_id = ET.SubElement(plugin, _pom_tag(namespace, "artifactId"))
artifact_id.text = "maven-compiler-plugin"
return plugin


def _has_processor_path(annotation_processor_paths, namespace, artifact_id):
for path in annotation_processor_paths.findall(_pom_tag(namespace, "path")):
if _child_text(path, namespace, "artifactId") == artifact_id:
return True
return False


def _add_annotation_processor_path(pom_path):
ET.register_namespace("", "http://maven.apache.org/POM/4.0.0")
ET.register_namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
tree = ET.parse(pom_path)
root = tree.getroot()
namespace = _pom_namespace(root)
compiler_plugin = _find_or_add_compiler_plugin(root, namespace)
configuration = _find_or_add_child(compiler_plugin, namespace, "configuration")
annotation_processor_paths = _find_or_add_child(
configuration, namespace, "annotationProcessorPaths"
)
annotation_processor_paths.set("combine.children", "append")
if not _has_processor_path(
annotation_processor_paths, namespace, "fory-annotation-processor"
):
path = ET.SubElement(annotation_processor_paths, _pom_tag(namespace, "path"))
group_id = ET.SubElement(path, _pom_tag(namespace, "groupId"))
group_id.text = "org.apache.fory"
artifact_id = ET.SubElement(path, _pom_tag(namespace, "artifactId"))
artifact_id.text = "fory-annotation-processor"
version = ET.SubElement(path, _pom_tag(namespace, "version"))
version.text = "${project.version}"
tree.write(pom_path, encoding="UTF-8", xml_declaration=True)


def _copy_pom_with_annotation_processor(pom_path):
pom_dir = os.path.dirname(pom_path)
fd, temp_pom = tempfile.mkstemp(
prefix="pom-static-processor-", suffix=".xml", dir=pom_dir
)
os.close(fd)
shutil.copyfile(pom_path, temp_pom)
_add_annotation_processor_path(temp_pom)
return temp_pom


def _remove_file(path):
if path:
try:
os.remove(path)
except FileNotFoundError:
pass


def _exec_cmd(cmd, cwd):
logging.info(f"running command in {cwd}: {cmd}")
subprocess.check_call(cmd, shell=True, cwd=cwd)


def run_android_go_xlang_static_processor():
"""Run Android-mode Go xlang tests with javac static serializer generation enabled."""
root = common.PROJECT_ROOT_DIR
java_dir = os.path.join(root, "java")
core_dir = os.path.join(java_dir, "fory-core")
core_pom = os.path.join(core_dir, "pom.xml")
idl_java_dir = os.path.join(root, "integration_tests", "idl_tests", "java")
idl_java_pom = os.path.join(idl_java_dir, "pom.xml")
temp_core_pom = None
temp_idl_java_pom = None
try:
temp_core_pom = _copy_pom_with_annotation_processor(core_pom)
temp_idl_java_pom = _copy_pom_with_annotation_processor(idl_java_pom)
os.environ.setdefault("FORY_ANDROID_ENABLED", "1")
os.environ.setdefault("FORY_GO_JAVA_CI", "1")
os.environ.setdefault("FORY_STATIC_PROCESSOR_CI", "1")
os.environ.setdefault("ENABLE_FORY_DEBUG_OUTPUT", "1")
_exec_cmd(
"mvn -T16 --no-transfer-progress clean install -DskipTests "
"-Dmaven.javadoc.skip=true -Dmaven.source.skip=true "
"-pl fory-core,fory-annotation-processor -am",
java_dir,
)
_exec_cmd(
"mvn -T16 --no-transfer-progress "
f"-f {shlex.quote(temp_core_pom)} "
"clean test -Dtest=org.apache.fory.xlang.GoXlangTest",
core_dir,
)
env = os.environ.copy()
env["IDL_JAVA_POM"] = temp_idl_java_pom
logging.info(
f"running command in {root}: ./integration_tests/idl_tests/run_go_tests.sh"
)
subprocess.check_call(
["./integration_tests/idl_tests/run_go_tests.sh"], cwd=root, env=env
)
finally:
_remove_file(temp_core_pom)
_remove_file(temp_idl_java_pom)


def parse_args():
"""Parse command-line arguments and dispatch to the appropriate task module."""
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -225,6 +366,14 @@ def parse_args():
)
go_parser.set_defaults(func=go.run)

android_go_static_parser = subparsers.add_parser(
"android-go-xlang-static-processor",
description="Run Android-mode Go xlang tests with @ForyStruct annotation processing enabled",
help="Run Android Go xlang tests with static generated serializers",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
android_go_static_parser.set_defaults(func=run_android_go_xlang_static_processor)

# Format subparser
format_parser = subparsers.add_parser(
"format",
Expand Down
15 changes: 10 additions & 5 deletions compiler/fory_compiler/generators/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ def generate_union_file(self, union: Union) -> GeneratedFile:

return GeneratedFile(path=path, content="\n".join(lines))

def get_struct_annotation(self, message: Message) -> str:
"""Return the ForyStruct annotation for a generated message."""
if self.get_effective_evolving(message):
return "@ForyStruct"
return "@ForyStruct(evolution = Evolution.DISABLED)"

# Generates a Java class file from a message schema definition.
def generate_message_file(self, message: Message) -> GeneratedFile:
"""Generate a Java class file for a message."""
Expand Down Expand Up @@ -435,8 +441,7 @@ def generate_message_file(self, message: Message) -> GeneratedFile:
comment = self.format_type_id_comment(message, "//")
if comment:
lines.append(comment)
if not self.get_effective_evolving(message):
lines.append("@ForyStruct(evolving = false)")
lines.append(self.get_struct_annotation(message))
lines.append(f"public class {message.name} {{")

# Generate nested enums as static inner classes
Expand Down Expand Up @@ -588,8 +593,9 @@ def collect_message_imports(self, message: Message, imports: Set[str]):
for field in message.fields:
self.collect_field_imports(field, imports)

imports.add("org.apache.fory.annotation.ForyStruct")
if not self.get_effective_evolving(message):
imports.add("org.apache.fory.annotation.ForyStruct")
imports.add("org.apache.fory.annotation.ForyStruct.Evolution")

# Add imports for equals/hashCode
imports.add("java.util.Objects")
Expand Down Expand Up @@ -1041,8 +1047,7 @@ def generate_nested_message(
comment = self.format_type_id_comment(message, " " * indent + "//")
if comment:
lines.append(comment)
if not self.get_effective_evolving(message):
lines.append("@ForyStruct(evolving = false)")
lines.append(self.get_struct_annotation(message))
lines.append(f"public static class {message.name} {{")

# Generate nested enums
Expand Down
28 changes: 28 additions & 0 deletions compiler/fory_compiler/tests/test_generated_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,34 @@ def test_java_unsigned_carriers_and_integer_encoding_annotations():
)


def test_java_evolving_false_generation_uses_struct_evolution_enum():
schema = parse_fdl(
dedent(
"""
package gen;

message DefaultEvolving {
string value = 1;
}

message Stable [evolving=false] {
string name = 1;

message NestedStable [evolving=false] {
int32 id = 1;
}
}
"""
)
)
java_output = render_files(generate_files(schema, JavaGenerator))
assert "import org.apache.fory.annotation.ForyStruct;" in java_output
assert "import org.apache.fory.annotation.ForyStruct.Evolution;" in java_output
assert java_output.count("@ForyStruct(evolution = Evolution.DISABLED)") == 2
assert java_output.count("@ForyStruct") == 3
assert "@ForyStruct(evolving = false)" not in java_output


def test_java_nested_integer_annotations_in_generic_containers():
schema = parse_fdl(
dedent(
Expand Down
Loading
Loading