From 70039e30bd100c8712b1c2be4361fb81e932df50 Mon Sep 17 00:00:00 2001 From: userzhy <48518279+userzhy@users.noreply.github.com> Date: Sat, 20 Dec 2025 05:30:29 +0000 Subject: [PATCH 1/2] feat(rust): use UNION type id for tagged enum serialization in xlang mode - Add UNION (38) and NONE (39) type IDs to TypeId enum in types.rs - Modify write_type_info to use UNION type ID in xlang mode - Modify read_type_info to expect UNION type ID in xlang mode - Add tests for UNION type ID in xlang mode Closes #3028 --- rust/fory-core/src/serializer/enum_.rs | 15 ++++- rust/fory-core/src/types.rs | 6 ++ rust/tests/tests/test_enum.rs | 88 ++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/rust/fory-core/src/serializer/enum_.rs b/rust/fory-core/src/serializer/enum_.rs index 1c615619c6..10818d97e8 100644 --- a/rust/fory-core/src/serializer/enum_.rs +++ b/rust/fory-core/src/serializer/enum_.rs @@ -50,6 +50,11 @@ pub fn write( #[inline(always)] pub fn write_type_info(context: &mut WriteContext) -> Result<(), Error> { + // In xlang mode, use UNION type ID for tagged enums + if context.is_xlang() { + context.writer.write_varuint32(TypeId::UNION as u32); + return Ok(()); + } let type_id = T::fory_get_type_id(context.get_type_resolver())?; context.writer.write_varuint32(type_id); let is_named_enum = type_id & 0xff == TypeId::NAMED_ENUM as u32; @@ -100,8 +105,16 @@ pub fn read( #[inline(always)] pub fn read_type_info(context: &mut ReadContext) -> Result<(), Error> { - let local_type_id = T::fory_get_type_id(context.get_type_resolver())?; let remote_type_id = context.reader.read_varuint32()?; + // In xlang mode, expect UNION type ID for tagged enums + if context.is_xlang() { + ensure!( + remote_type_id == TypeId::UNION as u32, + Error::type_mismatch(TypeId::UNION as u32, remote_type_id) + ); + return Ok(()); + } + let local_type_id = T::fory_get_type_id(context.get_type_resolver())?; ensure!( local_type_id == remote_type_id, Error::type_mismatch(local_type_id, remote_type_id) diff --git a/rust/fory-core/src/types.rs b/rust/fory-core/src/types.rs index 6090a493ed..89cd1d8c7c 100644 --- a/rust/fory-core/src/types.rs +++ b/rust/fory-core/src/types.rs @@ -81,6 +81,10 @@ pub enum TypeId { FLOAT16_ARRAY = 35, FLOAT32_ARRAY = 36, FLOAT64_ARRAY = 37, + // Tagged union type (one of several alternatives) + UNION = 38, + // Empty/unit type (no data) + NONE = 39, U8 = 64, U16 = 65, U32 = 66, @@ -136,6 +140,8 @@ pub const INT64_ARRAY: u32 = TypeId::INT64_ARRAY as u32; pub const FLOAT16_ARRAY: u32 = TypeId::FLOAT16_ARRAY as u32; pub const FLOAT32_ARRAY: u32 = TypeId::FLOAT32_ARRAY as u32; pub const FLOAT64_ARRAY: u32 = TypeId::FLOAT64_ARRAY as u32; +pub const UNION: u32 = TypeId::UNION as u32; +pub const NONE: u32 = TypeId::NONE as u32; pub const U8: u32 = TypeId::U8 as u32; pub const U16: u32 = TypeId::U16 as u32; pub const U32: u32 = TypeId::U32 as u32; diff --git a/rust/tests/tests/test_enum.rs b/rust/tests/tests/test_enum.rs index 3179158608..b83c607ad6 100644 --- a/rust/tests/tests/test_enum.rs +++ b/rust/tests/tests/test_enum.rs @@ -99,3 +99,91 @@ fn named_enum() { assert_eq!(target1, target2); assert_eq!(value1, value2); } + +/// Test that tagged enum uses UNION type ID in xlang mode +#[test] +fn xlang_tagged_enum_uses_union_type_id() { + use fory_core::buffer::Reader; + use fory_core::types::TypeId; + + #[derive(ForyObject, Debug, PartialEq, Default)] + enum SimpleEnum { + #[default] + A, + B, + C, + } + + let mut fory = Fory::default().xlang(true); + fory.register::(1000).unwrap(); + + let value = SimpleEnum::B; + let bytes = fory.serialize(&value).unwrap(); + + // Fory header: + // - 2 bytes: magic number (0x62d4) + // - 1 byte: bitmap flags + // - 1 byte: language + // Total header: 4 bytes + + let mut reader = Reader::new(&bytes); + + // Read magic number + let _magic = reader.read_u16().unwrap(); + + // Read bitmap + let _bitmap = reader.read_u8().unwrap(); + + // Read language + let _language = reader.read_u8().unwrap(); + + // Read ref flag (-1 = NotNullValue) + let ref_flag = reader.read_i8().unwrap(); + assert_eq!(ref_flag, -1, "Expected NotNullValue ref flag"); + + // Read type id - should be UNION (38) in xlang mode + let type_id = reader.read_varuint32().unwrap(); + assert_eq!( + type_id, + TypeId::UNION as u32, + "Expected UNION type id ({}) in xlang mode, got {}", + TypeId::UNION as u32, + type_id + ); + + // Read variant index (B = 1) + let variant_index = reader.read_varuint32().unwrap(); + assert_eq!( + variant_index, 1, + "Expected variant index 1 for SimpleEnum::B" + ); + + // Verify roundtrip still works + let deserialized: SimpleEnum = fory.deserialize(&bytes).unwrap(); + assert_eq!(deserialized, value); +} + +/// Test xlang roundtrip with complex tagged enum +#[test] +fn xlang_complex_tagged_enum_roundtrip() { + #[derive(ForyObject, Debug, PartialEq, Default)] + enum Color { + #[default] + Red, + Green, + Blue, + Custom(String), + } + + let mut fory = Fory::default().xlang(true); + fory.register::(1001).unwrap(); + + // Test all variants + let colors = vec![Color::Red, Color::Green, Color::Blue]; + + for color in colors { + let bytes = fory.serialize(&color).unwrap(); + let deserialized: Color = fory.deserialize(&bytes).unwrap(); + assert_eq!(deserialized, color); + } +} From 3ebbb48bdc68bcb9aeb3711a2bc6be43da5c69de Mon Sep 17 00:00:00 2001 From: userzhy <48518279+userzhy@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:13:29 +0000 Subject: [PATCH 2/2] fix(rust): only use UNION type ID for tagged enums in xlang mode Simple enums (all unit variants) should use ENUM type ID. Tagged enums (some variants have data) should use UNION type ID. This fixes the compatibility with Java where simple enums are serialized with ENUM type ID. - Add is_simple_enum() helper to check if all variants are unit types - Pass is_tagged parameter to write_type_info/read_type_info - Update tests to verify correct type ID for both enum types --- rust/fory-core/src/serializer/enum_.rs | 20 ++++-- rust/fory-derive/src/object/derive_enum.rs | 19 +++-- rust/fory-derive/src/object/serializer.rs | 4 +- rust/tests/tests/test_enum.rs | 80 ++++++++++++++++++---- 4 files changed, 96 insertions(+), 27 deletions(-) diff --git a/rust/fory-core/src/serializer/enum_.rs b/rust/fory-core/src/serializer/enum_.rs index 10818d97e8..c0b9e66bb0 100644 --- a/rust/fory-core/src/serializer/enum_.rs +++ b/rust/fory-core/src/serializer/enum_.rs @@ -49,9 +49,13 @@ pub fn write( } #[inline(always)] -pub fn write_type_info(context: &mut WriteContext) -> Result<(), Error> { - // In xlang mode, use UNION type ID for tagged enums - if context.is_xlang() { +pub fn write_type_info( + context: &mut WriteContext, + is_tagged: bool, +) -> Result<(), Error> { + // In xlang mode, use UNION type ID for tagged enums (enums with data variants), + // and regular ENUM type ID for simple enums (all unit variants) + if context.is_xlang() && is_tagged { context.writer.write_varuint32(TypeId::UNION as u32); return Ok(()); } @@ -104,10 +108,14 @@ pub fn read( } #[inline(always)] -pub fn read_type_info(context: &mut ReadContext) -> Result<(), Error> { +pub fn read_type_info( + context: &mut ReadContext, + is_tagged: bool, +) -> Result<(), Error> { let remote_type_id = context.reader.read_varuint32()?; - // In xlang mode, expect UNION type ID for tagged enums - if context.is_xlang() { + // In xlang mode, expect UNION type ID for tagged enums (enums with data variants), + // and regular ENUM type ID for simple enums (all unit variants) + if context.is_xlang() && is_tagged { ensure!( remote_type_id == TypeId::UNION as u32, Error::type_mismatch(TypeId::UNION as u32, remote_type_id) diff --git a/rust/fory-derive/src/object/derive_enum.rs b/rust/fory-derive/src/object/derive_enum.rs index efb67a0e06..e95e512947 100644 --- a/rust/fory-derive/src/object/derive_enum.rs +++ b/rust/fory-derive/src/object/derive_enum.rs @@ -28,6 +28,15 @@ fn temp_var_name(i: usize) -> String { format!("f{}", i) } +/// Check if an enum is a "simple enum" (all variants are unit variants without data) +/// Simple enums use ENUM type ID in xlang mode, while tagged enums use UNION type ID +pub fn is_simple_enum(data_enum: &DataEnum) -> bool { + data_enum + .variants + .iter() + .all(|v| matches!(&v.fields, Fields::Unit)) +} + pub fn gen_actual_type_id() -> TokenStream { quote! { fory_core::serializer::enum_::actual_type_id(type_id, register_by_name, compatible) @@ -386,9 +395,10 @@ pub fn gen_write_data(data_enum: &DataEnum) -> TokenStream { } } -pub fn gen_write_type_info() -> TokenStream { +pub fn gen_write_type_info(data_enum: &DataEnum) -> TokenStream { + let is_tagged = !is_simple_enum(data_enum); quote! { - fory_core::serializer::enum_::write_type_info::(context) + fory_core::serializer::enum_::write_type_info::(context, #is_tagged) } } @@ -755,8 +765,9 @@ pub fn gen_read_data(data_enum: &DataEnum) -> TokenStream { } } -pub fn gen_read_type_info() -> TokenStream { +pub fn gen_read_type_info(data_enum: &DataEnum) -> TokenStream { + let is_tagged = !is_simple_enum(data_enum); quote! { - fory_core::serializer::enum_::read_type_info::(context) + fory_core::serializer::enum_::read_type_info::(context, #is_tagged) } } diff --git a/rust/fory-derive/src/object/serializer.rs b/rust/fory-derive/src/object/serializer.rs index ee5fb72018..cc49494653 100644 --- a/rust/fory-derive/src/object/serializer.rs +++ b/rust/fory-derive/src/object/serializer.rs @@ -118,11 +118,11 @@ pub fn derive_serializer(ast: &syn::DeriveInput, debug_enabled: bool) -> TokenSt syn::Data::Enum(e) => ( derive_enum::gen_write(e), derive_enum::gen_write_data(e), - derive_enum::gen_write_type_info(), + derive_enum::gen_write_type_info(e), derive_enum::gen_read(e), derive_enum::gen_read_with_type_info(e), derive_enum::gen_read_data(e), - derive_enum::gen_read_type_info(), + derive_enum::gen_read_type_info(e), derive_enum::gen_reserved_space(), quote! { fory_core::TypeId::ENUM }, ), diff --git a/rust/tests/tests/test_enum.rs b/rust/tests/tests/test_enum.rs index b83c607ad6..f68068e8ba 100644 --- a/rust/tests/tests/test_enum.rs +++ b/rust/tests/tests/test_enum.rs @@ -100,9 +100,10 @@ fn named_enum() { assert_eq!(value1, value2); } -/// Test that tagged enum uses UNION type ID in xlang mode +/// Test that simple enum (all unit variants) uses ENUM type ID in xlang mode +/// while tagged enum (some variants with data) uses UNION type ID #[test] -fn xlang_tagged_enum_uses_union_type_id() { +fn xlang_simple_enum_uses_enum_type_id() { use fory_core::buffer::Reader; use fory_core::types::TypeId; @@ -141,14 +142,16 @@ fn xlang_tagged_enum_uses_union_type_id() { let ref_flag = reader.read_i8().unwrap(); assert_eq!(ref_flag, -1, "Expected NotNullValue ref flag"); - // Read type id - should be UNION (38) in xlang mode + // Read type id - should be ENUM for simple enum in xlang mode + // The type ID format is: (registered_type_id << 8) + TypeId::ENUM let type_id = reader.read_varuint32().unwrap(); + let base_type_id = type_id & 0xff; assert_eq!( - type_id, - TypeId::UNION as u32, - "Expected UNION type id ({}) in xlang mode, got {}", - TypeId::UNION as u32, - type_id + base_type_id, + TypeId::ENUM as u32, + "Expected ENUM type id ({}) in xlang mode for simple enum, got {}", + TypeId::ENUM as u32, + base_type_id ); // Read variant index (B = 1) @@ -163,27 +166,74 @@ fn xlang_tagged_enum_uses_union_type_id() { assert_eq!(deserialized, value); } -/// Test xlang roundtrip with complex tagged enum +/// Test that tagged enum (has variants with data) uses UNION type ID in xlang mode +#[test] +fn xlang_tagged_enum_uses_union_type_id() { + use fory_core::buffer::Reader; + use fory_core::types::TypeId; + + #[derive(ForyObject, Debug, PartialEq, Default)] + enum TaggedEnum { + #[default] + Empty, + Value(i32), + Name(String), + } + + let mut fory = Fory::default().xlang(true); + fory.register::(1001).unwrap(); + + let value = TaggedEnum::Empty; + let bytes = fory.serialize(&value).unwrap(); + + let mut reader = Reader::new(&bytes); + + // Skip header + let _magic = reader.read_u16().unwrap(); + let _bitmap = reader.read_u8().unwrap(); + let _language = reader.read_u8().unwrap(); + + // Read ref flag + let _ref_flag = reader.read_i8().unwrap(); + + // Read type id - should be UNION (38) in xlang mode for tagged enum + let type_id = reader.read_varuint32().unwrap(); + assert_eq!( + type_id, + TypeId::UNION as u32, + "Expected UNION type id ({}) in xlang mode for tagged enum, got {}", + TypeId::UNION as u32, + type_id + ); + + // Verify roundtrip still works + let deserialized: TaggedEnum = fory.deserialize(&bytes).unwrap(); + assert_eq!(deserialized, value); +} + +/// Test xlang roundtrip with tagged enum (enum with data variants) +/// Note: This test only tests unit variants of a tagged enum. +/// Testing data variants in xlang mode is a separate concern. #[test] fn xlang_complex_tagged_enum_roundtrip() { #[derive(ForyObject, Debug, PartialEq, Default)] - enum Color { + enum TaggedColor { #[default] Red, Green, Blue, - Custom(String), + Value(i32), } let mut fory = Fory::default().xlang(true); - fory.register::(1001).unwrap(); + fory.register::(1002).unwrap(); - // Test all variants - let colors = vec![Color::Red, Color::Green, Color::Blue]; + // Test unit variants - these should work even though the enum is tagged + let colors = vec![TaggedColor::Red, TaggedColor::Green, TaggedColor::Blue]; for color in colors { let bytes = fory.serialize(&color).unwrap(); - let deserialized: Color = fory.deserialize(&bytes).unwrap(); + let deserialized: TaggedColor = fory.deserialize(&bytes).unwrap(); assert_eq!(deserialized, color); } }