diff --git a/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java b/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java index 4e0332b7a7..54753c654d 100644 --- a/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java @@ -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 command) throws IOException { @@ -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. + * + *

This test verifies that: + *

+ */ + private void testReferenceAlignment(Language language, List 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 (reference type, with RefFlag) + fory.serialize(buffer, Arrays.asList("a", "b")); + + // 5. Map (reference type, with RefFlag) + Map 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, 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 value mismatch"); + Assert.assertEquals(fory.deserialize(buffer2), "hello", "String value mismatch"); + Assert.assertEquals( + fory.deserialize(buffer2), Arrays.asList("a", "b"), "Vec value mismatch"); + + @SuppressWarnings("unchecked") + Map deserializedMap = (Map) 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!"); + } } diff --git a/rust/fory-core/src/serializer/core.rs b/rust/fory-core/src/serializer/core.rs index a8888ada8e..cc7311a395 100644 --- a/rust/fory-core/src/serializer/core.rs +++ b/rust/fory-core/src/serializer/core.rs @@ -1217,30 +1217,62 @@ pub trait Serializer: 'static { std::mem::size_of::() } - /// **[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` | `Integer` | `false` (Option handles null) | + /// | `String` | `String` | `true` | + /// | `Vec` | `List` | `true` | + /// | `HashMap` | `Map` | `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::()` 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; } diff --git a/rust/tests/tests/test_cross_language.rs b/rust/tests/tests/test_cross_language.rs index d3957f8449..ab4246c202 100644 --- a/rust/tests/tests/test_cross_language.rs +++ b/rust/tests/tests/test_cross_language.rs @@ -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 = fory.deserialize_from(&mut reader).unwrap(); + assert_eq!(v2, Some(100), "Option 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 (reference type, with RefFlag) + let v4: Vec = fory.deserialize_from(&mut reader).unwrap(); + assert_eq!( + v4, + vec!["a".to_string(), "b".to_string()], + "Vec value mismatch" + ); + + // 5. Map (reference type, with RefFlag) + let v5: HashMap = 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(); +}