Skip to content
Open
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
77 changes: 77 additions & 0 deletions java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public void testRust() throws Exception {
testStructVersionCheck(Language.RUST, command);
command.set(RUST_TESTCASE_INDEX, "test_consistent_named");
testConsistentNamed(Language.RUST, command);
command.set(RUST_TESTCASE_INDEX, "test_reference_alignment");
testReferenceAlignment(Language.RUST, command);
}

private void testBuffer(Language language, List<String> command) throws IOException {
Expand Down Expand Up @@ -941,4 +943,79 @@ private void assertStringEquals(Object actual, Object expected, boolean useToStr
Assert.assertEquals(actual, expected);
}
}

/**
* Test reference type alignment between Java and Rust in xlang mode.
*
* <p>This test verifies that:
* <ul>
* <li>Primitive types (int, double, boolean) don't write RefFlag
* <li>Reference types (String, List, Map) write RefFlag in xlang mode
* <li>Rust-serialized data can be correctly deserialized in Java
* <li>Java-serialized data can be correctly deserialized in Rust
* </ul>
*/
private void testReferenceAlignment(Language language, List<String> command)
throws IOException {
Fory fory = Fory.builder()
.withLanguage(Language.XLANG)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.build();

// Serialize test data from Java
MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(256);

// 1. Primitive int (no RefFlag)
fory.serialize(buffer, 42);

// 2. Boxed Integer (with RefFlag for null handling)
fory.serialize(buffer, Integer.valueOf(100));

// 3. String (reference type, with RefFlag)
fory.serialize(buffer, "hello");

// 4. List<String> (reference type, with RefFlag)
fory.serialize(buffer, Arrays.asList("a", "b"));

// 5. Map<String, Integer> (reference type, with RefFlag)
Map<String, Integer> map = new HashMap<>();
map.put("key1", 10);
map.put("key2", 20);
fory.serialize(buffer, map);

// 6. Double (primitive, no RefFlag)
fory.serialize(buffer, 3.14);

// 7. Boolean (primitive, no RefFlag)
fory.serialize(buffer, true);

byte[] bytes = buffer.getBytes(0, buffer.writerIndex());
LOG.info("Java serialized {} bytes for reference alignment test", bytes.length);

// Send to Rust for verification
Path dataFile = Files.createTempFile("test_reference_alignment", "data");
Pair<Map<String, String>, File> env_workdir = setFilePath(language, command, dataFile, bytes);
Assert.assertTrue(
executeCommand(command, 30, env_workdir.getLeft(), env_workdir.getRight()),
"Rust test failed");

// Read back Rust's serialization and verify
MemoryBuffer buffer2 = MemoryUtils.wrap(Files.readAllBytes(dataFile));

Assert.assertEquals(fory.deserialize(buffer2), 42, "i32 value mismatch");
Assert.assertEquals(fory.deserialize(buffer2), Integer.valueOf(100), "Option<i32> value mismatch");
Assert.assertEquals(fory.deserialize(buffer2), "hello", "String value mismatch");
Assert.assertEquals(
fory.deserialize(buffer2), Arrays.asList("a", "b"), "Vec<String> value mismatch");

@SuppressWarnings("unchecked")
Map<String, Integer> deserializedMap = (Map<String, Integer>) fory.deserialize(buffer2);
Assert.assertEquals(deserializedMap.get("key1"), Integer.valueOf(10), "HashMap key1 mismatch");
Assert.assertEquals(deserializedMap.get("key2"), Integer.valueOf(20), "HashMap key2 mismatch");

Assert.assertEquals(fory.deserialize(buffer2), 3.14, "f64 value mismatch");
Assert.assertEquals(fory.deserialize(buffer2), true, "bool value mismatch");

LOG.info("Reference alignment test passed!");
}
}
54 changes: 43 additions & 11 deletions rust/fory-core/src/serializer/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,30 +1217,62 @@ pub trait Serializer: 'static {
std::mem::size_of::<Self>()
}

/// **[USER IMPLEMENTATION REQUIRED]** Downcast to `&dyn Any` for dynamic type checking.
/// Indicate whether this type should be treated as a reference type in cross-language (xlang) serialization.
///
/// This method enables runtime type checking and downcasting, required for
/// Fory's type system integration.
/// In cross-language scenarios, type systems differ between languages. For example:
/// - In Java: `String`, `List`, `Map` are reference types (need RefFlag)
/// - In Rust: `String`, `Vec`, `HashMap` are value types (by default, no RefFlag)
///
/// This method bridges the gap by allowing Rust types to declare their cross-language reference semantics.
///
/// # Returns
///
/// A reference to this instance as `&dyn Any`.
/// - `true` if this type should be treated as a reference type in xlang mode
/// (will write RefFlag during serialization)
/// - `false` if this type should be treated as a value type in xlang mode
/// (will not write RefFlag, default)
///
/// # Implementation Pattern
/// # Type Mapping Guidelines
///
/// Always implement this by returning `self`:
/// | Rust Type | Java Type | Should Return |
/// |-----------|-----------|---------------|
/// | `i32`, `f64`, `bool` | `int`, `double`, `boolean` | `false` |
/// | `Option<i32>` | `Integer` | `false` (Option handles null) |
/// | `String` | `String` | `true` |
/// | `Vec<T>` | `List<T>` | `true` |
/// | `HashMap<K,V>` | `Map<K,V>` | `true` |
/// | User struct | Java object | `true` |
///
/// # Default Implementation
///
/// Returns `false` for all types. Override for types that correspond to reference types in other languages.
///
/// # Examples
///
/// ```rust,ignore
/// fn as_any(&self) -> &dyn Any {
/// self
/// // String should be treated as reference type in Java
/// impl Serializer for String {
/// fn fory_is_xlang_ref_type() -> bool {
/// true
/// }
/// }
///
/// // i32 is primitive in Java (int)
/// impl Serializer for i32 {
/// fn fory_is_xlang_ref_type() -> bool {
/// false // default
/// }
/// }
/// ```
///
/// # Implementation Notes
///
/// - Required for all types implementing `Serializer`
/// - Enables `downcast_ref::<T>()` on serialized values
/// - Used by Fory's polymorphic deserialization
/// - Only affects behavior when `context.is_xlang()` is true
/// - Used in [`fory_write`] to determine RefFlag behavior
/// - Fory implements this for all built-in types
/// - User types should override this for proper xlang interop
///
/// [`fory_write`]: Serializer::fory_write
fn as_any(&self) -> &dyn Any;
}

Expand Down
71 changes: 71 additions & 0 deletions rust/tests/tests/test_cross_language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,3 +734,74 @@ fn test_struct_version_check() {
assert_eq!(new_local_obj, local_obj);
fs::write(&data_file_path, new_bytes).unwrap();
}

/// Test reference type alignment between Java and Rust in xlang mode.
///
/// This test verifies that:
/// 1. Primitive types (i32, f64, bool) don't write RefFlag
/// 2. Reference types (String, Vec, HashMap) write RefFlag in xlang mode
/// 3. Java-serialized data can be correctly deserialized in Rust
/// 4. Rust-serialized data can be correctly deserialized in Java
#[test]
#[ignore]
#[allow(clippy::approx_constant)]
fn test_reference_alignment() {
let data_file_path = get_data_file();

// Read data serialized by Java
let bytes = fs::read(&data_file_path).unwrap();
let fory = Fory::default().xlang(true).compatible(true);
let mut reader = Reader::new(bytes.as_slice());

// Deserialize values in the same order as Java wrote them
// 1. Primitive int (no RefFlag)
let v1: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(v1, 42, "i32 value mismatch");

// 2. Boxed Integer (with RefFlag for null handling)
let v2: Option<i32> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(v2, Some(100), "Option<i32> value mismatch");

// 3. String (reference type, with RefFlag)
let v3: String = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(v3, "hello", "String value mismatch");

// 4. List<String> (reference type, with RefFlag)
let v4: Vec<String> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(
v4,
vec!["a".to_string(), "b".to_string()],
"Vec<String> value mismatch"
);

// 5. Map<String, Integer> (reference type, with RefFlag)
let v5: HashMap<String, i32> = fory.deserialize_from(&mut reader).unwrap();
let mut expected_map = HashMap::new();
expected_map.insert("key1".to_string(), 10);
expected_map.insert("key2".to_string(), 20);
assert_eq!(v5, expected_map, "HashMap value mismatch");

// 6. Double (primitive, no RefFlag)
let v6: f64 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(v6, 3.14, "f64 value mismatch");

// 7. Boolean (primitive, no RefFlag)
let v7: bool = fory.deserialize_from(&mut reader).unwrap();
assert!(v7, "bool value mismatch");

// Now serialize data back to Java
let mut buf = Vec::new();

// Serialize in the same order
fory.serialize_to(&42i32, &mut buf).unwrap();
fory.serialize_to(&Some(100i32), &mut buf).unwrap();
fory.serialize_to(&"hello".to_string(), &mut buf).unwrap();
fory.serialize_to(&vec!["a".to_string(), "b".to_string()], &mut buf)
.unwrap();
fory.serialize_to(&expected_map, &mut buf).unwrap();
fory.serialize_to(&3.14f64, &mut buf).unwrap();
fory.serialize_to(&true, &mut buf).unwrap();

// Write back to file for Java to verify
fs::write(&data_file_path, buf).unwrap();
}
Loading