Skip to content

Conversation

@chaokunyang
Copy link
Collaborator

@chaokunyang chaokunyang commented Dec 25, 2025

Why?

The cross-language (xlang) struct serialization had inconsistent nullable and ref flag handling across different language implementations (Java, Rust, C++, Go, Python). This caused:

  1. Schema hash mismatches: Different languages computed different fingerprints for the same struct schema, causing version compatibility issues
  2. Ref flag inconsistency: Some languages wrote ref flags for non-nullable fields while others didn't
  3. Null handling bugs: Null entries in maps were not properly handled in some implementations

What does this PR do?

Core Protocol Alignment

Unified nullable semantics across all languages:

  • In xlang mode, fields are non-nullable by default (except Optional types and boxed primitives)
  • Ref flags are only written when nullable=true - non-nullable fields skip the ref flag entirely
  • This matches the xlang protocol specification and ensures consistent wire format

Key changes per language:

Language Changes
Java New Fingerprint.java for consistent struct fingerprint computation; updated serializers for xlang nullable defaults
Rust Simplified field_need_write_ref_into() to only check nullable flag
C++ Updated field_is_nullable logic; fixed map null entry handling; aligned struct/ext skip logic
Go Updated codegen encoder/decoder for nullable handling; new pointer utilities
Python Updated typedef encoder/decoder; aligned struct serialization

Code Refactoring (Java)

  • Extracted FieldInfo and FieldTypes from ClassDef.java (reduced from ~900 lines to ~3 lines)
  • Added logging infrastructure (ForyLogger, LogLevel, Logger, Slf4jLogger)
  • Enhanced TypeRef with additional type utilities
  • Updated TypeResolver with improved type handling

Bug Fixes

  • C++ map serializer: Fixed null entry handling - now properly inserts entries with default-constructed keys/values instead of skipping
  • C++ skip logic: Fixed skip_struct and skip_ext to handle different struct type categories correctly (COMPATIBLE_STRUCT, NAMED_COMPATIBLE_STRUCT, NAMED_STRUCT, STRUCT)
  • C++ enum handling: Fixed enum field nullability in xlang mode

CI Improvements

  • Added dedicated python_xlang job for Python cross-language testing
  • Removed unnecessary bazel setup from Java CI jobs (not needed for Java tests)

Test Updates

  • Updated cross-language tests in all languages to use non-nullable defaults
  • Changed std::optional<T> to T in C++ test structs to match xlang semantics
  • Added comprehensive xlang test coverage for Java, Rust, C++, Go, Python

Related issues

#1017
#2982
#2906

Does this PR introduce any user-facing change?

  • Does this PR introduce any public API change?
    • In xlang mode, struct fields are now non-nullable by default. Users who relied on implicit nullability should add explicit @ForyField(nullable = true) or use Optional<T> types.
  • Does this PR introduce any binary protocol compatibility change?
    • Yes, the ref flag behavior has changed for xlang mode. Non-nullable fields no longer write ref flags. This is a breaking change for existing xlang serialized data.

Benchmark

N/A (Protocol alignment, no performance-related changes)

@chaokunyang chaokunyang force-pushed the align_xlang_ref_nullable branch 2 times, most recently from 6b57909 to b772a7f Compare December 26, 2025 08:36
@chaokunyang chaokunyang force-pushed the align_xlang_ref_nullable branch from b772a7f to 36d3243 Compare December 26, 2025 15:56
@chaokunyang chaokunyang force-pushed the align_xlang_ref_nullable branch 12 times, most recently from 877f7af to e0205bd Compare December 29, 2025 15:31
Use lazy_import for numpy and handle the case when numpy is not
available by comparing arrays as lists instead.
…r test

The circular reference changes in xread_ref/xread_no_ref broke the xlang
protocol because they changed how read_ref_ids is populated. This reverts
those changes.

Also skip the circular reference test for XLANG mode since XLANG doesn't
support true circular references - objects must be registered after they
are fully constructed.
Also skip the dict3 circular reference test for XLANG mode since
it also uses circular references (dict3 containing itself).
Add back the ref_tracking check before pushing -1 to read_ref_ids.
Without this check, when ref_tracking is disabled, the code would
fail because NoRefResolver doesn't have read_ref_ids.
This commit fixes cross-language compatibility issues between Java
and Python when handling Object/Any types in xlang mode:

1. Java FieldTypes.java: Treat Object.class as nullable in xlang mode
   to match Python's typing.Any behavior
2. Java TypeResolver.java: Update isFieldNullable() to treat Object.class
   as nullable in xlang mode
3. Java Fingerprint.java: Include boxed types and Object.class in nullable
   check for struct fingerprint computation
4. Python typedef.py: Treat typing.Any as nullable in xlang mode, and
   default ref tracking to false in xlang mode (matching Java's behavior)
5. Python _fory.py and serialization.pyx: Fix circular reference handling
   by not pushing -1 to read_ref_ids when called from xread_ref, since
   try_preserve_ref_id already handles the ref_id tracking
Object (Java) and typing.Any (Python) should NOT be nullable by
default in xlang mode. Only Optional[T] types should be nullable.

This fixes the fingerprint mismatch issue while maintaining the
correct xlang semantics that non-Optional types are non-nullable.
Numpy is already a build dependency in pyproject.toml, so it's always
installed. Remove the lazy import and conditional check for numpy.
The function just returned the nullable parameter, making it
unnecessary. Inline def.nullable directly at the call sites.
@chaokunyang chaokunyang changed the title feat: align xlang struct serialization feat: unifiy xlang struct serialization for nullable meta Dec 30, 2025
@chaokunyang chaokunyang mentioned this pull request Dec 30, 2025
16 tasks
@chaokunyang chaokunyang changed the title feat: unifiy xlang struct serialization for nullable meta feat: consistent nullable meta for xlang struct fields serialization Dec 30, 2025
@chaokunyang chaokunyang force-pushed the align_xlang_ref_nullable branch from deefc6c to 4a7a14f Compare December 30, 2025 04:08
@chaokunyang chaokunyang changed the title feat: consistent nullable meta for xlang struct fields serialization feat(java/python/rust/go/c++): align nullable meta for xlang struct fields serialization Dec 30, 2025
Document xlang field nullable/ref defaults:
- Non-nullable by default in xlang mode
- Only Optional<T> types are nullable
- Ref flags only written for nullable fields
- Language-specific examples and customization
Allow adding new docs without renumbering:
- index: 0
- getting-started: 10
- serialization: 30
- field-nullability: 40
- zero-copy: 50
- row_format: 60
- troubleshooting: 70
- field-nullability.md: focused on null handling only
- reference-tracking.md: new doc for shared/circular refs
Allow field() to be called without the id parameter. When omitted,
defaults to -1 which uses field name encoding instead of tag ID.
This aligns with Java's @ForyField(id default -1).
@chaokunyang chaokunyang merged commit db57e1b into apache:main Dec 30, 2025
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants