diff --git a/rust/fory-core/src/serializer/enum_.rs b/rust/fory-core/src/serializer/enum_.rs index 1c615619c6..c0b9e66bb0 100644 --- a/rust/fory-core/src/serializer/enum_.rs +++ b/rust/fory-core/src/serializer/enum_.rs @@ -49,7 +49,16 @@ pub fn write( } #[inline(always)] -pub fn write_type_info(context: &mut WriteContext) -> Result<(), Error> { +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(()); + } 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; @@ -99,9 +108,21 @@ 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())?; +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 (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) + ); + 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/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 3179158608..f68068e8ba 100644 --- a/rust/tests/tests/test_enum.rs +++ b/rust/tests/tests/test_enum.rs @@ -99,3 +99,141 @@ fn named_enum() { assert_eq!(target1, target2); assert_eq!(value1, value2); } + +/// 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_simple_enum_uses_enum_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 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!( + 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) + 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 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 TaggedColor { + #[default] + Red, + Green, + Blue, + Value(i32), + } + + let mut fory = Fory::default().xlang(true); + fory.register::(1002).unwrap(); + + // 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: TaggedColor = fory.deserialize(&bytes).unwrap(); + assert_eq!(deserialized, color); + } +}