This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new b4f090842 refactor(rust): unify tuple struct and named struct
protocol, and make schema evolution happy (#3092)
b4f090842 is described below
commit b4f090842ef61f2422cb413972e3af520445c8b9
Author: Damon Zhao <[email protected]>
AuthorDate: Fri Dec 26 10:40:33 2025 +0800
refactor(rust): unify tuple struct and named struct protocol, and make
schema evolution happy (#3092)
### Why?
This PR fixes a schema evolution issue with tuple structs.
Previously, tuple struct fields were sorted by type (same as named
structs), which caused schema evolution to break when adding fields of
different types.
For example, evolving `struct Point(f64, u8)` to `struct Point(f64, u8,
f64)` would cause fields to be incorrectly matched during
deserialization because the new `f64` field would be sorted before `u8`.
### What does this PR do?
1. Introduce SortedField struct: A helper struct that preserves the
original field index alongside the field reference. This allows us to
correctly track field positions regardless of serialization order.
2. Preserve tuple struct field order: For tuple structs, fields are no
longer sorted by type. Instead, they maintain their original definition
order ("0", "1", "2", ...). This ensures that field names consistently
map to their positions, enabling proper schema evolution.
3. Unify protocol for tuple and named structs: Both tuple structs and
named structs now use the same underlying protocol (field name based
matching), but with different field name strategies:
* Named structs: use field identifiers as names (sorted by type for
optimal layout)
* Tuple structs: use positional indices as names (unsorted to preserve
schema evolution)
4. Add schema evolution tests: Comprehensive tests for tuple struct
schema evolution, including:
* Adding fields at the end
* Removing fields from the end
* Adding fields with different types (`i64`, `u8`, `f64`)
### Related issues
Does this PR introduce any user-facing change?
[ ] Does this PR introduce any public API change?
[x] Does this PR introduce any binary protocol compatibility change?
**Note**: Yes, but since tuple struct support is just supported, I think
no one(except me) is using this feature now : )
### Benchmark
---
rust/fory-derive/src/fory_row.rs | 7 +-
rust/fory-derive/src/object/derive_enum.rs | 49 ++++-----
rust/fory-derive/src/object/misc.rs | 158 ++++++++++++++---------------
rust/fory-derive/src/object/read.rs | 153 +++++++++++++---------------
rust/fory-derive/src/object/serializer.rs | 78 ++++++--------
rust/fory-derive/src/object/util.rs | 20 ++++
rust/fory-derive/src/object/write.rs | 13 +--
rust/fory-derive/src/util.rs | 117 +++++++++++++++++----
rust/tests/tests/test_tuple_struct.rs | 132 ++++++++++++++++++++++++
9 files changed, 469 insertions(+), 258 deletions(-)
diff --git a/rust/fory-derive/src/fory_row.rs b/rust/fory-derive/src/fory_row.rs
index d044564a4..02173e98d 100644
--- a/rust/fory-derive/src/fory_row.rs
+++ b/rust/fory-derive/src/fory_row.rs
@@ -15,18 +15,19 @@
// specific language governing permissions and limitations
// under the License.
-use crate::util::sorted_fields;
+use crate::util::{extract_fields, source_fields};
use proc_macro::TokenStream;
use quote::quote;
pub fn derive_row(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
- let fields = match &ast.data {
- syn::Data::Struct(s) => sorted_fields(&s.fields),
+ let source_fields = match &ast.data {
+ syn::Data::Struct(s) => source_fields(&s.fields),
_ => {
panic!("only struct be supported")
}
};
+ let fields = extract_fields(&source_fields);
let write_exprs = fields.iter().enumerate().map(|(index, field)| {
let ty = &field.ty;
diff --git a/rust/fory-derive/src/object/derive_enum.rs
b/rust/fory-derive/src/object/derive_enum.rs
index 6261c5f9e..93f9dbef3 100644
--- a/rust/fory-derive/src/object/derive_enum.rs
+++ b/rust/fory-derive/src/object/derive_enum.rs
@@ -20,7 +20,7 @@ use crate::object::misc;
use crate::object::read::gen_read_field;
use crate::object::util::{get_filtered_fields_iter, get_sorted_field_names};
use crate::object::write::gen_write_field;
-use crate::util::sorted_fields;
+use crate::util::{extract_fields, source_fields};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{DataEnum, Fields};
@@ -119,8 +119,9 @@ pub(crate) fn
gen_named_variant_meta_type_impl_with_enum_name(
fields: &syn::FieldsNamed,
) -> TokenStream {
let fields_clone = syn::Fields::Named(fields.clone());
- let sorted_fields_slice = sorted_fields(&fields_clone);
- let filtered_fields: Vec<_> =
get_filtered_fields_iter(&sorted_fields_slice).collect();
+ let source_fields = source_fields(&fields_clone);
+ let fields_slice = extract_fields(&source_fields);
+ let filtered_fields: Vec<_> =
get_filtered_fields_iter(&fields_slice).collect();
let sorted_field_names_vec = get_sorted_field_names(&filtered_fields);
// Generate individual field name literals
@@ -131,7 +132,7 @@ pub(crate) fn
gen_named_variant_meta_type_impl_with_enum_name(
})
.collect();
- let fields_info_ts = misc::gen_field_fields_info(&sorted_fields_slice);
+ let fields_info_ts = misc::gen_field_fields_info(&source_fields);
// Include enum name to make meta type unique
let meta_type_ident = Ident::new(
@@ -239,20 +240,20 @@ fn rust_variant_branches(data_enum: &DataEnum,
default_variant_value: u32) -> Ve
}
}
Fields::Named(fields_named) => {
- use crate::util::sorted_fields;
+ use crate::util::source_fields;
let fields_clone =
syn::Fields::Named(fields_named.clone());
- let sorted_fields = sorted_fields(&fields_clone);
+ let source_fields = source_fields(&fields_clone);
- let field_idents: Vec<_> = sorted_fields
+ let field_idents: Vec<_> = source_fields
.iter()
- .map(|f| f.ident.as_ref().unwrap())
+ .map(|sf| sf.field.ident.as_ref().unwrap())
.collect();
- let write_fields: Vec<_> = sorted_fields
+ let write_fields: Vec<_> = source_fields
.iter()
.zip(field_idents.iter())
- .map(|(f, ident)| gen_write_field(f, ident, false))
+ .map(|(sf, ident)| gen_write_field(sf.field, ident,
false))
.collect();
quote! {
@@ -322,16 +323,16 @@ fn rust_compatible_variant_write_branches(
proc_macro2::Span::call_site()
);
let fields_clone =
syn::Fields::Named(fields_named.clone());
- let sorted_fields = sorted_fields(&fields_clone);
- let field_idents: Vec<_> = sorted_fields
+ let source_fields = source_fields(&fields_clone);
+ let field_idents: Vec<_> = source_fields
.iter()
- .map(|f| f.ident.as_ref().unwrap())
+ .map(|sf| sf.field.ident.as_ref().unwrap())
.collect();
- let write_fields: Vec<_> = sorted_fields
+ let write_fields: Vec<_> = source_fields
.iter()
.zip(field_idents.iter())
- .map(|(f, ident)| gen_write_field(f, ident, false))
+ .map(|(sf, ident)| gen_write_field(sf.field, ident,
false))
.collect();
quote! {
@@ -505,19 +506,19 @@ fn rust_variant_read_branches(
}
Fields::Named(fields_named) => {
let fields_clone =
syn::Fields::Named(fields_named.clone());
- let sorted_fields = sorted_fields(&fields_clone);
+ let source_fields = source_fields(&fields_clone);
- let field_idents: Vec<_> = sorted_fields
+ let field_idents: Vec<_> = source_fields
.iter()
- .map(|f| f.ident.as_ref().unwrap())
+ .map(|sf| sf.field.ident.as_ref().unwrap())
.collect();
- let read_fields: Vec<_> = sorted_fields
+ let read_fields: Vec<_> = source_fields
.iter()
.zip(field_idents.iter())
- .map(|(f, ident)| {
+ .map(|(sf, ident)| {
let field_name = ident.to_string();
- gen_read_field(f, ident, &field_name)
+ gen_read_field(sf.field, ident, &field_name)
})
.collect();
@@ -629,16 +630,16 @@ fn rust_compatible_variant_read_branches(
}
}
Fields::Named(fields_named) => {
- use crate::util::sorted_fields;
+ use crate::util::source_fields;
// Sort fields to match the meta type generation
let fields_clone =
syn::Fields::Named(fields_named.clone());
- let sorted_fields_slice = sorted_fields(&fields_clone);
+ let source_fields = source_fields(&fields_clone);
// Generate compatible read logic using
gen_read_compatible_with_construction
let compatible_read_body =
crate::object::read::gen_read_compatible_with_construction(
- &sorted_fields_slice,
+ &source_fields,
Some(ident),
);
diff --git a/rust/fory-derive/src/object/misc.rs
b/rust/fory-derive/src/object/misc.rs
index ae815d009..bc1f48123 100644
--- a/rust/fory-derive/src/object/misc.rs
+++ b/rust/fory-derive/src/object/misc.rs
@@ -22,9 +22,10 @@ use syn::Field;
use super::field_meta::{classify_field_type, parse_field_meta};
use super::util::{
- classify_trait_object_field, generic_tree_to_tokens,
get_filtered_fields_iter,
+ classify_trait_object_field, generic_tree_to_tokens,
get_filtered_source_fields_iter,
get_sort_fields_ts, parse_generic_tree, StructField,
};
+use crate::util::SourceField;
// Global type ID counter that auto-grows from 0 at macro processing time
static TYPE_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
@@ -72,95 +73,94 @@ pub fn gen_get_sorted_field_names(fields: &[&Field]) ->
TokenStream {
}
}
-pub fn gen_field_fields_info(fields: &[&Field]) -> TokenStream {
- let field_infos = get_filtered_fields_iter(fields)
- .enumerate()
- .map(|(idx, field)| {
- let ty = &field.ty;
- let name = super::util::get_field_name(field, idx);
+pub fn gen_field_fields_info(source_fields: &[SourceField<'_>]) -> TokenStream
{
+ let field_infos = get_filtered_source_fields_iter(source_fields).map(|sf| {
+ let field = sf.field;
+ let ty = &field.ty;
+ let name = &sf.field_name;
- // Parse field metadata for nullable/ref tracking and field ID
- let meta = parse_field_meta(field).unwrap_or_default();
- let type_class = classify_field_type(ty);
- let nullable = meta.effective_nullable(type_class);
- let ref_tracking = meta.effective_ref_tracking(type_class);
- // Only use explicit field ID when user sets #[fory(id = N)]
- // Otherwise use -1 to indicate field name encoding should be used
- let field_id = if meta.uses_tag_id() {
- meta.effective_id() as i16
- } else {
- -1i16 // Use field name encoding when no explicit ID
- };
+ // Parse field metadata for nullable/ref tracking and field ID
+ let meta = parse_field_meta(field).unwrap_or_default();
+ let type_class = classify_field_type(ty);
+ let nullable = meta.effective_nullable(type_class);
+ let ref_tracking = meta.effective_ref_tracking(type_class);
+ // Only use explicit field ID when user sets #[fory(id = N)]
+ // Otherwise use -1 to indicate field name encoding should be used
+ let field_id = if meta.uses_tag_id() {
+ meta.effective_id() as i16
+ } else {
+ -1i16 // Use field name encoding when no explicit ID
+ };
- match classify_trait_object_field(ty) {
- StructField::None => {
- let generic_tree = parse_generic_tree(ty);
- let generic_token = generic_tree_to_tokens(&generic_tree);
- quote! {
- fory_core::meta::FieldInfo::new_with_id(
- #field_id,
- #name,
- {
- let mut ft = #generic_token;
- ft.nullable = #nullable;
- ft.ref_tracking = #ref_tracking;
- ft
- }
- )
- }
+ match classify_trait_object_field(ty) {
+ StructField::None => {
+ let generic_tree = parse_generic_tree(ty);
+ let generic_token = generic_tree_to_tokens(&generic_tree);
+ quote! {
+ fory_core::meta::FieldInfo::new_with_id(
+ #field_id,
+ #name,
+ {
+ let mut ft = #generic_token;
+ ft.nullable = #nullable;
+ ft.ref_tracking = #ref_tracking;
+ ft
+ }
+ )
+ }
+ }
+ StructField::VecBox(_) | StructField::VecRc(_) |
StructField::VecArc(_) => {
+ quote! {
+ fory_core::meta::FieldInfo::new_with_id(#field_id, #name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::LIST as u32,
+ nullable: #nullable,
+ ref_tracking: #ref_tracking,
+ generics: vec![fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::UNKNOWN as u32,
+ nullable: false,
+ ref_tracking: false,
+ generics: Vec::new()
+ }]
+ })
}
- StructField::VecBox(_) | StructField::VecRc(_) |
StructField::VecArc(_) => {
- quote! {
- fory_core::meta::FieldInfo::new_with_id(#field_id,
#name, fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::LIST as u32,
- nullable: #nullable,
- ref_tracking: #ref_tracking,
- generics: vec![fory_core::meta::FieldType {
+ }
+ StructField::HashMapBox(key_ty, _)
+ | StructField::HashMapRc(key_ty, _)
+ | StructField::HashMapArc(key_ty, _) => {
+ let key_generic_tree = parse_generic_tree(key_ty.as_ref());
+ let key_generic_token =
generic_tree_to_tokens(&key_generic_tree);
+ quote! {
+ fory_core::meta::FieldInfo::new_with_id(#field_id, #name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::MAP as u32,
+ nullable: #nullable,
+ ref_tracking: #ref_tracking,
+ generics: vec![
+ #key_generic_token,
+ fory_core::meta::FieldType {
type_id: fory_core::types::TypeId::UNKNOWN as
u32,
nullable: false,
ref_tracking: false,
generics: Vec::new()
- }]
- })
- }
- }
- StructField::HashMapBox(key_ty, _)
- | StructField::HashMapRc(key_ty, _)
- | StructField::HashMapArc(key_ty, _) => {
- let key_generic_tree = parse_generic_tree(key_ty.as_ref());
- let key_generic_token =
generic_tree_to_tokens(&key_generic_tree);
- quote! {
- fory_core::meta::FieldInfo::new_with_id(#field_id,
#name, fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::MAP as u32,
- nullable: #nullable,
- ref_tracking: #ref_tracking,
- generics: vec![
- #key_generic_token,
- fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::UNKNOWN
as u32,
- nullable: false,
- ref_tracking: false,
- generics: Vec::new()
- }
- ]
- })
- }
+ }
+ ]
+ })
}
- _ => {
- quote! {
- fory_core::meta::FieldInfo::new_with_id(#field_id,
#name, fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::UNKNOWN as u32,
- nullable: #nullable,
- ref_tracking: #ref_tracking,
- generics: Vec::new()
- })
- }
+ }
+ _ => {
+ quote! {
+ fory_core::meta::FieldInfo::new_with_id(#field_id, #name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::UNKNOWN as u32,
+ nullable: #nullable,
+ ref_tracking: #ref_tracking,
+ generics: Vec::new()
+ })
}
}
- });
+ }
+ });
- // Get sorted field names for sorting
- let static_field_names = get_sort_fields_ts(fields);
+ let fields: Vec<&Field> = source_fields.iter().map(|sf|
sf.field).collect();
+ let static_field_names = get_sort_fields_ts(&fields);
quote! {
let mut field_infos: Vec<fory_core::meta::FieldInfo> =
vec![#(#field_infos),*];
diff --git a/rust/fory-derive/src/object/read.rs
b/rust/fory-derive/src/object/read.rs
index 260531cba..07b15223e 100644
--- a/rust/fory-derive/src/object/read.rs
+++ b/rust/fory-derive/src/object/read.rs
@@ -25,6 +25,7 @@ use super::util::{
is_debug_enabled, is_direct_primitive_numeric_type, is_primitive_type,
is_skip_field,
should_skip_type_info_for_field, skip_ref_flag, StructField,
};
+use crate::util::SourceField;
/// Create a private variable name for a field during deserialization.
/// For named fields: `_field_name`
@@ -41,17 +42,15 @@ fn need_declared_by_option(field: &Field) -> bool {
type_name == "Option" || !is_primitive_type(type_name.as_str())
}
-pub(crate) fn declare_var(fields: &[&Field]) -> Vec<TokenStream> {
- fields
+pub(crate) fn declare_var(source_fields: &[SourceField<'_>]) ->
Vec<TokenStream> {
+ source_fields
.iter()
- .enumerate()
- .map(|(idx, field)| {
+ .map(|sf| {
+ let field = sf.field;
let ty = &field.ty;
- let var_name = create_private_field_name(field, idx);
+ let var_name = create_private_field_name(field, sf.original_index);
match classify_trait_object_field(ty) {
- StructField::BoxDyn
- | StructField::RcDyn(_)
- | StructField::ArcDyn(_) => {
+ StructField::BoxDyn | StructField::RcDyn(_) |
StructField::ArcDyn(_) => {
quote! {
let mut #var_name: #ty = <#ty as
fory_core::serializer::ForyDefault>::fory_default();
}
@@ -76,15 +75,18 @@ pub(crate) fn declare_var(fields: &[&Field]) ->
Vec<TokenStream> {
.collect()
}
-pub(crate) fn assign_value(fields: &[&Field]) -> Vec<TokenStream> {
- let is_tuple = super::util::is_tuple_struct(fields);
+pub(crate) fn assign_value(source_fields: &[SourceField<'_>]) ->
Vec<TokenStream> {
+ let is_tuple = source_fields
+ .first()
+ .map(|sf| sf.is_tuple_struct)
+ .unwrap_or(false);
- fields
+ // Generate field value expressions with original index for sorting
+ let mut indexed: Vec<_> = source_fields
.iter()
- .enumerate()
- .map(|(idx, field)| {
- let var_name = create_private_field_name(field, idx);
- let value_expr = match classify_trait_object_field(&field.ty) {
+ .map(|sf| {
+ let var_name = create_private_field_name(sf.field,
sf.original_index);
+ let value_expr = match classify_trait_object_field(&sf.field.ty) {
StructField::BoxDyn | StructField::RcDyn(_) |
StructField::ArcDyn(_) => {
quote! { #var_name }
}
@@ -92,25 +94,24 @@ pub(crate) fn assign_value(fields: &[&Field]) ->
Vec<TokenStream> {
quote! { #var_name.unwrap() }
}
_ => {
- if need_declared_by_option(field) {
- let ty = &field.ty;
+ if need_declared_by_option(sf.field) {
+ let ty = &sf.field.ty;
quote! { #var_name.unwrap_or_else(|| <#ty as
fory_core::ForyDefault>::fory_default()) }
} else {
quote! { #var_name }
}
}
};
-
- if is_tuple {
- // For tuple structs, just return the value
- value_expr
- } else {
- // For named structs, include the field name
- let name = &field.ident;
- quote! { #name: #value_expr }
- }
+ (sf.original_index, sf.field_init(value_expr))
})
- .collect()
+ .collect();
+
+ // For tuple structs, sort by original index to construct Self(field0,
field1, ...) correctly
+ if is_tuple {
+ indexed.sort_by_key(|(idx, _)| *idx);
+ }
+
+ indexed.into_iter().map(|(_, ts)| ts).collect()
}
pub fn gen_read_field(field: &Field, private_ident: &Ident, field_name: &str)
-> TokenStream {
@@ -268,14 +269,12 @@ pub fn gen_read_type_info() -> TokenStream {
}
}
-fn get_fields_loop_ts(fields: &[&Field]) -> TokenStream {
- let read_fields_ts: Vec<_> = fields
+fn get_source_fields_loop_ts(source_fields: &[SourceField<'_>]) -> TokenStream
{
+ let read_fields_ts: Vec<_> = source_fields
.iter()
- .enumerate()
- .map(|(idx, field)| {
- let private_ident = create_private_field_name(field, idx);
- let field_name = super::util::get_field_name(field, idx);
- gen_read_field(field, &private_ident, &field_name)
+ .map(|sf| {
+ let private_ident = create_private_field_name(sf.field,
sf.original_index);
+ gen_read_field(sf.field, &private_ident, &sf.field_name)
})
.collect();
quote! {
@@ -283,38 +282,39 @@ fn get_fields_loop_ts(fields: &[&Field]) -> TokenStream {
}
}
-pub fn gen_read_data(fields: &[&Field]) -> TokenStream {
- let version_hash = compute_struct_version_hash(fields);
- let sorted_read = if fields.is_empty() {
+pub fn gen_read_data(source_fields: &[SourceField<'_>]) -> TokenStream {
+ let fields: Vec<&Field> = source_fields.iter().map(|sf|
sf.field).collect();
+ let version_hash = compute_struct_version_hash(&fields);
+ let read_fields = if source_fields.is_empty() {
quote! {}
} else {
- let loop_ts = get_fields_loop_ts(fields);
+ let loop_ts = get_source_fields_loop_ts(source_fields);
quote! {
#loop_ts
}
};
- let is_tuple = super::util::is_tuple_struct(fields);
- let field_idents: Vec<_> = fields
+ let is_tuple = source_fields
+ .first()
+ .map(|sf| sf.is_tuple_struct)
+ .unwrap_or(false);
+
+ // Generate field initializations, sorted by original index for tuple
structs
+ let mut indexed: Vec<_> = source_fields
.iter()
- .enumerate()
- .map(|(idx, field)| {
- let private_ident = create_private_field_name(field, idx);
- if is_tuple {
- // For tuple structs, just use the variable
- quote! { #private_ident }
- } else {
- // For named structs, include the field name
- let original_ident = &field.ident;
- quote! { #original_ident: #private_ident }
- }
+ .map(|sf| {
+ let private_ident = create_private_field_name(sf.field,
sf.original_index);
+ let value = quote! { #private_ident };
+ (sf.original_index, sf.field_init(value))
})
.collect();
- let self_construction = if is_tuple {
- quote! { Ok(Self( #(#field_idents),* )) }
- } else {
- quote! { Ok(Self { #(#field_idents),* }) }
- };
+
+ if is_tuple {
+ indexed.sort_by_key(|(idx, _)| *idx);
+ }
+
+ let field_inits: Vec<_> = indexed.into_iter().map(|(_, ts)| ts).collect();
+ let self_construction = crate::util::ok_self_construction(is_tuple,
&field_inits);
quote! {
// Read and check version hash when class version checking is enabled
@@ -323,7 +323,7 @@ pub fn gen_read_data(fields: &[&Field]) -> TokenStream {
let type_name = std::any::type_name::<Self>();
fory_core::meta::TypeMeta::check_struct_version(read_version,
#version_hash, type_name)?;
}
- #sorted_read
+ #read_fields
#self_construction
}
}
@@ -593,28 +593,27 @@ pub fn gen_read_with_type_info() -> TokenStream {
}
}
-pub fn gen_read_compatible(fields: &[&Field]) -> TokenStream {
- gen_read_compatible_with_construction(fields, None)
+pub fn gen_read_compatible(source_fields: &[SourceField<'_>]) -> TokenStream {
+ gen_read_compatible_with_construction(source_fields, None)
}
pub(crate) fn gen_read_compatible_with_construction(
- fields: &[&Field],
+ source_fields: &[SourceField<'_>],
variant_ident: Option<&Ident>,
) -> TokenStream {
- let declare_ts: Vec<TokenStream> = declare_var(fields);
- let assign_ts: Vec<TokenStream> = assign_value(fields);
+ let declare_ts: Vec<TokenStream> = declare_var(source_fields);
+ let assign_ts: Vec<TokenStream> = assign_value(source_fields);
- let match_arms: Vec<TokenStream> = fields
+ let match_arms: Vec<TokenStream> = source_fields
.iter()
.enumerate()
- .map(|(i, field)| {
- let var_name = create_private_field_name(field, i);
- let field_name = super::util::get_field_name(field, i);
+ .map(|(sorted_idx, sf)| {
+ let var_name = create_private_field_name(sf.field,
sf.original_index);
// Use sorted index for matching. The assign_field_ids function
assigns
// the sorted index to matched fields after lookup (by ID or name).
// Fields that don't match or have type mismatches get field_id =
-1.
- let field_id = i as i16;
- let body = gen_read_compatible_match_arm_body(field, &var_name,
&field_name);
+ let field_id = sorted_idx as i16;
+ let body = gen_read_compatible_match_arm_body(sf.field, &var_name,
&sf.field_name);
quote! {
#field_id => {
#body
@@ -663,7 +662,11 @@ pub(crate) fn gen_read_compatible_with_construction(
};
// Generate construction based on whether this is a struct or enum variant
- let is_tuple = super::util::is_tuple_struct(fields);
+ let is_tuple = source_fields
+ .first()
+ .map(|sf| sf.is_tuple_struct)
+ .unwrap_or(false);
+
let construction = if let Some(variant) = variant_ident {
// Enum variants use named syntax (struct variants) or tuple syntax
(tuple variants)
quote! {
@@ -671,18 +674,8 @@ pub(crate) fn gen_read_compatible_with_construction(
#(#assign_ts),*
})
}
- } else if is_tuple {
- // Tuple structs use parentheses
- quote! {
- Ok(Self( #(#assign_ts),* ))
- }
} else {
- // Named structs use braces
- quote! {
- Ok(Self {
- #(#assign_ts),*
- })
- }
+ crate::util::ok_self_construction(is_tuple, &assign_ts)
};
quote! {
diff --git a/rust/fory-derive/src/object/serializer.rs
b/rust/fory-derive/src/object/serializer.rs
index e221a0a4d..b8a9cc6ec 100644
--- a/rust/fory-derive/src/object/serializer.rs
+++ b/rust/fory-derive/src/object/serializer.rs
@@ -16,7 +16,7 @@
// under the License.
use crate::object::{derive_enum, misc, read, write};
-use crate::util::sorted_fields;
+use crate::util::{extract_fields, source_fields};
use crate::ForyAttrs;
use proc_macro::TokenStream;
use quote::quote;
@@ -73,13 +73,14 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs:
ForyAttrs) -> TokenStrea
enum_variant_meta_types,
) = match &ast.data {
syn::Data::Struct(s) => {
- let fields = sorted_fields(&s.fields);
+ let source_fields = source_fields(&s.fields);
+ let fields = extract_fields(&source_fields);
(
misc::gen_actual_type_id(),
misc::gen_get_sorted_field_names(&fields),
- misc::gen_field_fields_info(&fields),
+ misc::gen_field_fields_info(&source_fields),
quote! { Ok(Vec::new()) }, // No variants for structs
- read::gen_read_compatible(&fields),
+ read::gen_read_compatible(&source_fields),
vec![], // No variant meta types for structs
)
}
@@ -116,14 +117,15 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs:
ForyAttrs) -> TokenStrea
static_type_id_ts,
) = match &ast.data {
syn::Data::Struct(s) => {
- let fields = sorted_fields(&s.fields);
+ let source_fields = source_fields(&s.fields);
+ let fields = extract_fields(&source_fields);
(
write::gen_write(),
- write::gen_write_data(&fields),
+ write::gen_write_data(&source_fields),
write::gen_write_type_info(),
read::gen_read(name),
read::gen_read_with_type_info(),
- read::gen_read_data(&fields),
+ read::gen_read_data(&source_fields),
read::gen_read_type_info(),
write::gen_reserved_space(&fields),
quote! { fory_core::TypeId::STRUCT },
@@ -269,78 +271,60 @@ fn generate_default_impl(
match &ast.data {
Data::Struct(s) => {
- let fields = sorted_fields(&s.fields);
- let is_tuple_struct = super::util::is_tuple_struct(&fields);
+ let source_fields = source_fields(&s.fields);
+ let is_tuple_struct = source_fields
+ .first()
+ .map(|sf| sf.is_tuple_struct)
+ .unwrap_or(false);
use super::util::{
classify_trait_object_field, create_wrapper_types_arc,
create_wrapper_types_rc,
StructField,
};
- let field_inits: Vec<_> = fields
+ // Generate field initializations with original index for sorting
+ let mut indexed: Vec<_> = source_fields
.iter()
- .map(|field| {
- let ident = &field.ident;
- let ty = &field.ty;
-
- match classify_trait_object_field(ty) {
+ .map(|sf| {
+ let ty = &sf.field.ty;
+ let value = match classify_trait_object_field(ty) {
StructField::RcDyn(trait_name) => {
let types = create_wrapper_types_rc(&trait_name);
let wrapper_ty = types.wrapper_ty;
let trait_ident = types.trait_ident;
- let value = quote! {
+ quote! {
{
let wrapper = #wrapper_ty::default();
std::rc::Rc::<dyn
#trait_ident>::from(wrapper)
}
- };
- if is_tuple_struct {
- value
- } else {
- quote! { #ident: #value }
}
}
StructField::ArcDyn(trait_name) => {
let types = create_wrapper_types_arc(&trait_name);
let wrapper_ty = types.wrapper_ty;
let trait_ident = types.trait_ident;
- let value = quote! {
+ quote! {
{
let wrapper = #wrapper_ty::default();
std::sync::Arc::<dyn
#trait_ident>::from(wrapper)
}
- };
- if is_tuple_struct {
- value
- } else {
- quote! { #ident: #value }
- }
- }
- StructField::Forward => {
- let value = quote! { <#ty as
fory_core::ForyDefault>::fory_default() };
- if is_tuple_struct {
- value
- } else {
- quote! { #ident: #value }
}
}
_ => {
- let value = quote! { <#ty as
fory_core::ForyDefault>::fory_default() };
- if is_tuple_struct {
- value
- } else {
- quote! { #ident: #value }
- }
+ quote! { <#ty as
fory_core::ForyDefault>::fory_default() }
}
- }
+ };
+ (sf.original_index, sf.field_init(value))
})
.collect();
- let self_construction = if is_tuple_struct {
- quote! { Self( #(#field_inits),* ) }
- } else {
- quote! { Self { #(#field_inits),* } }
- };
+ // For tuple structs, sort by original index
+ if is_tuple_struct {
+ indexed.sort_by_key(|(idx, _)| *idx);
+ }
+
+ let field_inits: Vec<_> = indexed.into_iter().map(|(_, ts)|
ts).collect();
+ let self_construction =
crate::util::self_construction(is_tuple_struct, &field_inits);
if should_generate_default {
// User requested Default generation via
#[fory(generate_default)]
diff --git a/rust/fory-derive/src/object/util.rs
b/rust/fory-derive/src/object/util.rs
index 9f77f25ff..08bdd2281 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -1019,6 +1019,19 @@ fn group_fields_by_type(fields: &[&Field]) ->
FieldGroups {
}
pub(crate) fn get_sorted_field_names(fields: &[&Field]) -> Vec<String> {
+ // For tuple structs, preserve the original field order.
+ // Tuple struct field names are "0", "1", "2", etc., which are positional.
+ // Sorting would break schema evolution when adding fields in the middle
+ // (e.g., (f64, u8) -> (f64, u8, f64) would change sorted order).
+ if is_tuple_struct(fields) {
+ return fields
+ .iter()
+ .enumerate()
+ .map(|(idx, field)| get_field_name(field, idx))
+ .collect();
+ }
+
+ // For named structs, sort by type for optimal memory layout
let (
primitive_fields,
nullable_primitive_fields,
@@ -1045,6 +1058,13 @@ pub(crate) fn get_filtered_fields_iter<'a>(
) -> impl Iterator<Item = &'a Field> {
fields.iter().filter(|field| !is_skip_field(field)).copied()
}
+
+pub(super) fn get_filtered_source_fields_iter<'a>(
+ source_fields: &'a [crate::util::SourceField<'a>],
+) -> impl Iterator<Item = &'a crate::util::SourceField<'a>> {
+ source_fields.iter().filter(|sf| !is_skip_field(sf.field))
+}
+
pub(super) fn get_sort_fields_ts(fields: &[&Field]) -> TokenStream {
let filterd_fields: Vec<&Field> =
get_filtered_fields_iter(fields).collect();
let sorted_names = get_sorted_field_names(&filterd_fields);
diff --git a/rust/fory-derive/src/object/write.rs
b/rust/fory-derive/src/object/write.rs
index d7ba72a4a..5aa89c0e4 100644
--- a/rust/fory-derive/src/object/write.rs
+++ b/rust/fory-derive/src/object/write.rs
@@ -18,10 +18,11 @@
use super::util::{
classify_trait_object_field, compute_struct_version_hash,
create_wrapper_types_arc,
create_wrapper_types_rc, extract_type_name, get_field_accessor,
get_field_name,
- get_filtered_fields_iter, get_primitive_writer_method, get_struct_name,
+ get_filtered_source_fields_iter, get_primitive_writer_method,
get_struct_name,
get_type_id_by_type_ast, is_debug_enabled,
is_direct_primitive_numeric_type,
should_skip_type_info_for_field, skip_ref_flag, StructField,
};
+use crate::util::SourceField;
use fory_core::types::TypeId;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
@@ -305,13 +306,13 @@ fn gen_write_field_impl(
}
}
-pub fn gen_write_data(fields: &[&Field]) -> TokenStream {
- let write_fields_ts: Vec<_> = get_filtered_fields_iter(fields)
- .enumerate()
- .map(|(idx, field)| gen_write_field_with_index(field, idx, true))
+pub fn gen_write_data(source_fields: &[SourceField<'_>]) -> TokenStream {
+ let fields: Vec<&Field> = source_fields.iter().map(|sf|
sf.field).collect();
+ let write_fields_ts: Vec<_> =
get_filtered_source_fields_iter(source_fields)
+ .map(|sf| gen_write_field_with_index(sf.field, sf.original_index,
true))
.collect();
- let version_hash = compute_struct_version_hash(fields);
+ let version_hash = compute_struct_version_hash(&fields);
quote! {
// Write version hash when class version checking is enabled
if context.is_check_struct_version() {
diff --git a/rust/fory-derive/src/util.rs b/rust/fory-derive/src/util.rs
index ffd84d880..2490823cf 100644
--- a/rust/fory-derive/src/util.rs
+++ b/rust/fory-derive/src/util.rs
@@ -15,38 +15,117 @@
// specific language governing permissions and limitations
// under the License.
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
use syn::{Field, Fields, GenericArgument, PathArguments, Type, TypePath,
TypeTraitObject};
-pub fn sorted_fields(fields: &Fields) -> Vec<&Field> {
- let fields = fields.iter().collect::<Vec<&Field>>();
- get_sorted_fields(&fields)
+/// Source field with its original index and computed field name preserved.
+///
+/// For tuple structs, `original_index` is the field's position in the original
+/// struct definition (0, 1, 2, ...), and `field_name` is the index as a
string.
+/// For named structs, `field_name` is the field identifier.
+#[derive(Clone)]
+pub struct SourceField<'a> {
+ pub original_index: usize,
+ pub field: &'a Field,
+ pub field_name: String,
+ pub is_tuple_struct: bool,
}
-pub fn get_sorted_fields<'a>(fields: &[&'a Field]) -> Vec<&'a Field> {
- use crate::object::util::{get_sorted_field_names, is_tuple_struct};
+impl<'a> SourceField<'a> {
+ /// Generate field initialization syntax for struct construction.
+ /// - tuple struct: just the value
+ /// - named struct: `field_name: value`
+ pub fn field_init(&self, value: TokenStream) -> TokenStream {
+ if self.is_tuple_struct {
+ value
+ } else {
+ let ident = format_ident!("{}", self.field_name);
+ quote! { #ident: #value }
+ }
+ }
+}
- // For tuple structs, we must preserve the original field order
- // because fields are accessed by index (self.0, self.1, etc.)
- // Sorting would cause type mismatches during
serialization/deserialization.
- if is_tuple_struct(fields) {
- return fields.to_vec();
+/// Generate Self construction syntax.
+/// - tuple struct: `Self(field0, field1, ...)`
+/// - named struct: `Self { field0, field1, ... }`
+pub fn self_construction(is_tuple_struct: bool, field_inits: &[TokenStream])
-> TokenStream {
+ if is_tuple_struct {
+ quote! { Self( #(#field_inits),* ) }
+ } else {
+ quote! { Self { #(#field_inits),* } }
}
+}
- // For named structs, sort fields by type for optimal memory layout
+/// Generate Ok(Self(...)) construction syntax for Result return.
+pub fn ok_self_construction(is_tuple_struct: bool, field_inits:
&[TokenStream]) -> TokenStream {
+ if is_tuple_struct {
+ quote! { Ok(Self( #(#field_inits),* )) }
+ } else {
+ quote! { Ok(Self { #(#field_inits),* }) }
+ }
+}
+
+/// Returns source fields with their original indices preserved.
+///
+/// For named structs, fields are sorted by type for optimal serialization.
+/// For tuple structs, fields keep their original order.
+/// The original index is preserved so that:
+/// - For named structs: field names can be used directly
+/// - For tuple structs: the original index (0, 1, 2, ...) is used as field
name
+pub fn source_fields(fields: &Fields) -> Vec<SourceField<'_>> {
+ let fields: Vec<&Field> = fields.iter().collect();
+ get_source_fields(&fields)
+}
+
+/// Returns source fields with their original indices and field names
preserved.
+pub fn get_source_fields<'a>(fields: &[&'a Field]) -> Vec<SourceField<'a>> {
+ use crate::object::util::get_sorted_field_names;
+
+ let is_tuple = !fields.is_empty() && fields[0].ident.is_none();
let sorted_names = get_sorted_field_names(fields);
- let mut sorted_fields = Vec::with_capacity(fields.len());
+ let mut result = Vec::with_capacity(fields.len());
for name in &sorted_names {
- // For named structs, field.ident is Some
- if let Some(field) = fields
- .iter()
- .find(|f| f.ident.as_ref().map(|ident| ident ==
name).unwrap_or(false))
- {
- sorted_fields.push(*field);
+ if is_tuple {
+ // For tuple structs, field name is the original index as string
+ if let Ok(idx) = name.parse::<usize>() {
+ if idx < fields.len() {
+ result.push(SourceField {
+ original_index: idx,
+ field: fields[idx],
+ field_name: name.clone(),
+ is_tuple_struct: true,
+ });
+ }
+ }
+ } else {
+ // For named structs, match by field identifier
+ for (idx, field) in fields.iter().enumerate() {
+ if field
+ .ident
+ .as_ref()
+ .map(|ident| ident == name)
+ .unwrap_or(false)
+ {
+ result.push(SourceField {
+ original_index: idx,
+ field,
+ field_name: name.clone(),
+ is_tuple_struct: false,
+ });
+ break;
+ }
+ }
}
}
- sorted_fields
+ result
+}
+
+/// Extract just the fields from source fields.
+pub fn extract_fields<'a>(source_fields: &[SourceField<'a>]) -> Vec<&'a Field>
{
+ source_fields.iter().map(|sf| sf.field).collect()
}
/// Check if a type is `Box<dyn Trait>` and return the trait type and trait
name if it is
diff --git a/rust/tests/tests/test_tuple_struct.rs
b/rust/tests/tests/test_tuple_struct.rs
index 8c6512130..44bd60846 100644
--- a/rust/tests/tests/test_tuple_struct.rs
+++ b/rust/tests/tests/test_tuple_struct.rs
@@ -298,3 +298,135 @@ fn test_large_tuple_struct() {
let result: LargeTupleStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(result, data);
}
+
+// Schema Evolution Tests for Tuple Structs
+
+// Simulate remote version with fewer fields
+mod remote_v1 {
+ use fory_derive::ForyObject;
+
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct Point(pub f64, pub f64);
+}
+
+// Simulate local version with more fields (added field 2)
+mod local_v2 {
+ use fory_derive::ForyObject;
+
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct Point(pub f64, pub f64, pub f64);
+}
+
+// Simulate remote version with more fields
+mod remote_v3 {
+ use fory_derive::ForyObject;
+
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct Point(pub f64, pub f64, pub f64, pub f64);
+}
+
+/// Test schema evolution: remote has fewer fields than local.
+#[test]
+fn test_tuple_struct_schema_evolution_add_field() {
+ let mut fory_writer = Fory::default().compatible(true);
+ fory_writer.register::<remote_v1::Point>(100).unwrap();
+
+ let remote_data = remote_v1::Point(1.0, 2.0);
+ let bytes = fory_writer.serialize(&remote_data).unwrap();
+
+ let mut fory_reader = Fory::default().compatible(true);
+ fory_reader.register::<local_v2::Point>(100).unwrap();
+
+ let local_data: local_v2::Point = fory_reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(local_data.0, 1.0);
+ assert_eq!(local_data.1, 2.0);
+ assert_eq!(local_data.2, 0.0);
+}
+
+/// Test schema evolution: remote has more fields than local.
+#[test]
+fn test_tuple_struct_schema_evolution_remove_field() {
+ let mut fory_writer = Fory::default().compatible(true);
+ fory_writer.register::<remote_v3::Point>(100).unwrap();
+
+ let remote_data = remote_v3::Point(1.0, 2.0, 3.0, 4.0);
+ let bytes = fory_writer.serialize(&remote_data).unwrap();
+
+ let mut fory_reader = Fory::default().compatible(true);
+ fory_reader.register::<remote_v1::Point>(100).unwrap();
+
+ let local_data: remote_v1::Point =
fory_reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(local_data.0, 1.0);
+ assert_eq!(local_data.1, 2.0);
+}
+
+// Test with mixed types to verify sorting doesn't break schema evolution
+mod remote_mixed_v1 {
+ use fory_derive::ForyObject;
+
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct MixedPoint(pub f64, pub f64);
+}
+
+mod local_mixed_v2 {
+ use fory_derive::ForyObject;
+
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct MixedPoint(pub f64, pub f64, pub i64);
+}
+
+mod local_mixed_v3 {
+ use fory_derive::ForyObject;
+
+ // Adding u8 (which has smaller size than f64)
+ #[derive(ForyObject, Debug, PartialEq, Clone)]
+ pub struct MixedPoint(pub f64, pub f64, pub u8);
+}
+
+/// Test that adding i64 (compress=true) doesn't break schema evolution
+#[test]
+fn test_tuple_struct_schema_evolution_add_i64() {
+ let mut fory_writer = Fory::default().compatible(true);
+ fory_writer
+ .register::<remote_mixed_v1::MixedPoint>(100)
+ .unwrap();
+
+ let remote_data = remote_mixed_v1::MixedPoint(1.0, 2.0);
+ let bytes = fory_writer.serialize(&remote_data).unwrap();
+
+ let mut fory_reader = Fory::default().compatible(true);
+ fory_reader
+ .register::<local_mixed_v2::MixedPoint>(100)
+ .unwrap();
+
+ let local_data: local_mixed_v2::MixedPoint =
fory_reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(local_data.0, 1.0);
+ assert_eq!(local_data.1, 2.0);
+ assert_eq!(local_data.2, 0);
+}
+
+/// Test that adding u8 (smaller size) doesn't break schema evolution
+#[test]
+fn test_tuple_struct_schema_evolution_add_u8() {
+ let mut fory_writer = Fory::default().compatible(true);
+ fory_writer
+ .register::<remote_mixed_v1::MixedPoint>(100)
+ .unwrap();
+
+ let remote_data = remote_mixed_v1::MixedPoint(1.0, 2.0);
+ let bytes = fory_writer.serialize(&remote_data).unwrap();
+
+ let mut fory_reader = Fory::default().compatible(true);
+ fory_reader
+ .register::<local_mixed_v3::MixedPoint>(100)
+ .unwrap();
+
+ let local_data: local_mixed_v3::MixedPoint =
fory_reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(local_data.0, 1.0);
+ assert_eq!(local_data.1, 2.0);
+ assert_eq!(local_data.2, 0);
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]