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 3fddc9c68 feat(compiler): generate getter/setter/has/clear methods for
c++ (#3199)
3fddc9c68 is described below
commit 3fddc9c687050a61db3da0dab535a75cba8f28dd
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jan 25 22:19:19 2026 +0800
feat(compiler): generate getter/setter/has/clear methods for c++ (#3199)
## Why?
Message-typed fields in FDL are effectively optional and need safe
accessors in generated C++ APIs. Aligning defaults and generated APIs
avoids direct member access and keeps cross-language IDL tests
consistent.
## What does this PR do?
- Generate C++ getters/setters/has/clear/mutable for fields, store
message fields as `std::unique_ptr`, and update equality + field config
to use member names.
- Apply FDL defaults so message fields are always optional and assign
tag IDs from field numbers; add validator tests.
- Update IDL integration tests (C++/Go/Rust) for optional message fields
and accessors; treat `unique_ptr` as nullable in C++ type resolver.
- Make Rust derive field ordering deterministic by tag ID/name
tie-breakers.
## Related issues
Closes #3198
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
compiler/fory_compiler/generators/cpp.py | 198 ++++++++++++++++++---
compiler/fory_compiler/ir/validator.py | 66 ++++++-
.../fory_compiler/tests/test_message_defaults.py | 70 ++++++++
cpp/fory/serialization/type_resolver.h | 5 +-
integration_tests/idl_tests/cpp/main.cc | 150 ++++++++--------
.../idl_tests/go/idl_roundtrip_test.go | 4 +-
.../idl_tests/rust/tests/idl_roundtrip.rs | 4 +-
rust/fory-derive/src/object/util.rs | 72 ++++----
8 files changed, 435 insertions(+), 134 deletions(-)
diff --git a/compiler/fory_compiler/generators/cpp.py
b/compiler/fory_compiler/generators/cpp.py
index 49298cb86..a966d2d81 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -18,6 +18,7 @@
"""C++ code generator."""
from typing import Dict, List, Optional, Set, Tuple
+import typing
from fory_compiler.generators.base import BaseGenerator, GeneratedFile
from fory_compiler.ir.ast import (
@@ -120,7 +121,9 @@ class CppGenerator(BaseGenerator):
# Collect includes (including from nested types)
includes.add("<cstdint>")
+ includes.add("<memory>")
includes.add("<string>")
+ includes.add("<utility>")
includes.add('"fory/serialization/fory.h"')
if self.schema_has_unions():
includes.add("<utility>")
@@ -384,6 +387,152 @@ class CppGenerator(BaseGenerator):
qualified_name = self.get_namespaced_type_name(enum.name, parent_stack)
return f"FORY_ENUM({qualified_name}, {value_names});"
+ def resolve_named_type(
+ self, name: str, parent_stack: Optional[List[Message]]
+ ) -> Optional[typing.Union[Message, Enum, Union]]:
+ """Resolve a named type to its schema definition."""
+ parts = name.split(".")
+ if len(parts) > 1:
+ current = self.find_top_level_type(parts[0])
+ for part in parts[1:]:
+ if isinstance(current, Message):
+ current = current.get_nested_type(part)
+ else:
+ return None
+ return current
+ if parent_stack:
+ for msg in reversed(parent_stack):
+ nested = msg.get_nested_type(name)
+ if nested is not None:
+ return nested
+ return self.find_top_level_type(name)
+
+ def find_top_level_type(
+ self, name: str
+ ) -> Optional[typing.Union[Message, Enum, Union]]:
+ """Find a top-level type definition by name."""
+ for enum in self.schema.enums:
+ if enum.name == name:
+ return enum
+ for union in self.schema.unions:
+ if union.name == name:
+ return union
+ for message in self.schema.messages:
+ if message.name == name:
+ return message
+ return None
+
+ def is_message_type(
+ self, field_type: FieldType, parent_stack: Optional[List[Message]]
+ ) -> bool:
+ if not isinstance(field_type, NamedType):
+ return False
+ resolved = self.resolve_named_type(field_type.name, parent_stack)
+ return isinstance(resolved, Message)
+
+ def get_field_member_name(self, field: Field) -> str:
+ return f"{self.to_snake_case(field.name)}_"
+
+ def get_field_storage_type(
+ self, field: Field, parent_stack: Optional[List[Message]]
+ ) -> str:
+ if self.is_message_type(field.field_type, parent_stack):
+ type_name = self.resolve_nested_type_name(
+ field.field_type.name, parent_stack
+ )
+ return f"std::unique_ptr<{type_name}>"
+ return self.generate_type(
+ field.field_type,
+ field.optional,
+ field.ref,
+ field.element_optional,
+ field.element_ref,
+ parent_stack,
+ )
+
+ def get_field_value_type(
+ self, field: Field, parent_stack: Optional[List[Message]]
+ ) -> str:
+ if self.is_message_type(field.field_type, parent_stack):
+ return self.resolve_nested_type_name(field.field_type.name,
parent_stack)
+ return self.generate_type(
+ field.field_type,
+ False,
+ field.ref,
+ field.element_optional,
+ field.element_ref,
+ parent_stack,
+ )
+
+ def get_field_eq_expression(
+ self, field: Field, parent_stack: Optional[List[Message]]
+ ) -> str:
+ member_name = self.get_field_member_name(field)
+ other_member = f"other.{member_name}"
+ if self.is_message_type(field.field_type, parent_stack):
+ return (
+ f"(({member_name} && {other_member}) ? "
+ f"(*{member_name} == *{other_member}) : "
+ f"({member_name} == {other_member}))"
+ )
+ return f"{member_name} == {other_member}"
+
+ def generate_field_accessors(
+ self, field: Field, parent_stack: Optional[List[Message]], indent: str
+ ) -> List[str]:
+ lines: List[str] = []
+ field_name = self.to_snake_case(field.name)
+ member_name = self.get_field_member_name(field)
+ value_type = self.get_field_value_type(field, parent_stack)
+
+ if self.is_message_type(field.field_type, parent_stack):
+ lines.append(f"{indent}bool has_{field_name}() const {{")
+ lines.append(f"{indent} return {member_name} != nullptr;")
+ lines.append(f"{indent}}}")
+ lines.append("")
+ lines.append(f"{indent}const {value_type}& {field_name}() const
{{")
+ lines.append(f"{indent} return *{member_name};")
+ lines.append(f"{indent}}}")
+ lines.append("")
+ lines.append(f"{indent}{value_type}* mutable_{field_name}() {{")
+ lines.append(f"{indent} if (!{member_name}) {{")
+ lines.append(
+ f"{indent} {member_name} =
std::make_unique<{value_type}>();"
+ )
+ lines.append(f"{indent} }}")
+ lines.append(f"{indent} return {member_name}.get();")
+ lines.append(f"{indent}}}")
+ lines.append("")
+ lines.append(f"{indent}void clear_{field_name}() {{")
+ lines.append(f"{indent} {member_name}.reset();")
+ lines.append(f"{indent}}}")
+ return lines
+
+ if field.optional:
+ lines.append(f"{indent}bool has_{field_name}() const {{")
+ lines.append(f"{indent} return {member_name}.has_value();")
+ lines.append(f"{indent}}}")
+ lines.append("")
+
+ lines.append(f"{indent}const {value_type}& {field_name}() const {{")
+ if field.optional:
+ lines.append(f"{indent} return *{member_name};")
+ else:
+ lines.append(f"{indent} return {member_name};")
+ lines.append(f"{indent}}}")
+ lines.append("")
+ lines.append(f"{indent}void set_{field_name}({value_type} value) {{")
+ lines.append(f"{indent} {member_name} = std::move(value);")
+ lines.append(f"{indent}}}")
+
+ if field.optional:
+ lines.append("")
+ lines.append(f"{indent}void clear_{field_name}() {{")
+ lines.append(f"{indent} {member_name}.reset();")
+ lines.append(f"{indent}}}")
+
+ return lines
+
def generate_message_definition(
self,
message: Message,
@@ -394,7 +543,7 @@ class CppGenerator(BaseGenerator):
indent: str,
) -> List[str]:
"""Generate a C++ class definition with nested types."""
- lines = []
+ lines: List[str] = []
class_name = message.name
lineage = parent_stack + [message]
body_indent = f"{indent} "
@@ -402,6 +551,11 @@ class CppGenerator(BaseGenerator):
lines.append(f"{indent}class {class_name} final {{")
lines.append(f"{body_indent}public:")
+ if message.fields:
+ lines.append(
+ f"{body_indent} friend struct
::fory::detail::ForyFieldConfigImpl<{class_name}>;"
+ )
+ lines.append("")
for nested_enum in message.nested_enums:
lines.extend(self.generate_enum_definition(nested_enum,
body_indent))
@@ -432,28 +586,20 @@ class CppGenerator(BaseGenerator):
union_macros.extend(self.generate_union_macros(nested_union,
lineage))
lines.append("")
- for field in message.fields:
- cpp_type = self.generate_type(
- field.field_type,
- field.optional,
- field.ref,
- field.element_optional,
- field.element_ref,
- lineage,
- )
- field_name = self.to_snake_case(field.name)
- lines.append(f"{field_indent}{cpp_type} {field_name};")
-
- lines.append("")
+ if message.fields:
+ for index, field in enumerate(message.fields):
+ lines.extend(self.generate_field_accessors(field, lineage,
body_indent))
+ if index + 1 < len(message.fields):
+ lines.append("")
+ lines.append("")
lines.append(
f"{body_indent}bool operator==(const {class_name}& other) const {{"
)
if message.fields:
- conditions = []
- for field in message.fields:
- field_name = self.to_snake_case(field.name)
- conditions.append(f"{field_name} == other.{field_name}")
+ conditions = [
+ self.get_field_eq_expression(field, lineage) for field in
message.fields
+ ]
lines.append(f"{body_indent} return {' && '.join(conditions)};")
else:
lines.append(f"{body_indent} return true;")
@@ -461,7 +607,17 @@ class CppGenerator(BaseGenerator):
struct_type_name = self.get_qualified_type_name(message.name,
parent_stack)
if message.fields:
- field_names = ", ".join(self.to_snake_case(f.name) for f in
message.fields)
+ lines.append("")
+ lines.append(f"{body_indent}private:")
+ for field in message.fields:
+ field_type = self.get_field_storage_type(field, lineage)
+ member_name = self.get_field_member_name(field)
+ lines.append(f"{field_indent}{field_type} {member_name};")
+ lines.append("")
+ lines.append(f"{body_indent}public:")
+ field_members = ", ".join(
+ self.get_field_member_name(f) for f in message.fields
+ )
field_config_type_name = self.get_field_config_type_and_alias(
message.name, parent_stack
)
@@ -471,7 +627,7 @@ class CppGenerator(BaseGenerator):
)
)
lines.append(
- f"{body_indent}FORY_STRUCT({struct_type_name}, {field_names});"
+ f"{body_indent}FORY_STRUCT({struct_type_name},
{field_members});"
)
else:
lines.append(f"{body_indent}FORY_STRUCT({struct_type_name});")
@@ -1002,7 +1158,7 @@ class CppGenerator(BaseGenerator):
"""Generate FORY_FIELD_CONFIG macro for a message."""
entries = []
for field in message.fields:
- field_name = self.to_snake_case(field.name)
+ field_name = self.get_field_member_name(field)
meta = self.get_field_meta(field)
entries.append(f"({field_name}, {meta})")
joined = ", ".join(entries)
diff --git a/compiler/fory_compiler/ir/validator.py
b/compiler/fory_compiler/ir/validator.py
index d1aaffae3..3bdec8e06 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -18,7 +18,7 @@
"""Schema validation for Fory IDL."""
from dataclasses import dataclass
-from typing import List, Optional
+from typing import List, Optional, Union as TypingUnion
from fory_compiler.ir.ast import (
Schema,
@@ -57,6 +57,7 @@ class SchemaValidator:
self.warnings: List[ValidationIssue] = []
def validate(self) -> bool:
+ self._apply_field_defaults()
self._check_duplicate_type_names()
self._check_duplicate_type_ids()
self._check_messages()
@@ -198,6 +199,69 @@ class SchemaValidator:
for message in self.schema.messages:
validate_message(message)
+ def _apply_field_defaults(self) -> None:
+ def apply_message_fields(
+ message: Message,
+ enclosing_messages: Optional[List[Message]] = None,
+ ) -> None:
+ lineage = (enclosing_messages or []) + [message]
+ for field in message.fields:
+ if self.schema.source_format == "fdl" and field.tag_id is None:
+ field.tag_id = field.number
+ if self._is_message_type(field.field_type, lineage):
+ explicit_optional = field.optional
+ if self.schema.source_format == "fdl" and
explicit_optional:
+ self._error(
+ "Message fields are always optional; remove the
optional modifier",
+ field.location,
+ )
+ field.optional = True
+ for nested_msg in message.nested_messages:
+ apply_message_fields(nested_msg, lineage)
+
+ for message in self.schema.messages:
+ apply_message_fields(message)
+
+ def _is_message_type(
+ self, field_type: FieldType, parent_stack: List[Message]
+ ) -> bool:
+ if not isinstance(field_type, NamedType):
+ return False
+ resolved = self._resolve_named_type(field_type.name, parent_stack)
+ return isinstance(resolved, Message)
+
+ def _resolve_named_type(
+ self, name: str, parent_stack: List[Message]
+ ) -> Optional[TypingUnion[Message, Enum, Union]]:
+ parts = name.split(".")
+ if len(parts) > 1:
+ current = self._find_top_level_type(parts[0])
+ for part in parts[1:]:
+ if isinstance(current, Message):
+ current = current.get_nested_type(part)
+ else:
+ return None
+ return current
+ for msg in reversed(parent_stack):
+ nested = msg.get_nested_type(name)
+ if nested is not None:
+ return nested
+ return self._find_top_level_type(name)
+
+ def _find_top_level_type(
+ self, name: str
+ ) -> Optional[TypingUnion[Message, Enum, Union]]:
+ for enum in self.schema.enums:
+ if enum.name == name:
+ return enum
+ for union in self.schema.unions:
+ if union.name == name:
+ return union
+ for message in self.schema.messages:
+ if message.name == name:
+ return message
+ return None
+
def _check_type_references(self) -> None:
def check_type_ref(
field_type: FieldType,
diff --git a/compiler/fory_compiler/tests/test_message_defaults.py
b/compiler/fory_compiler/tests/test_message_defaults.py
new file mode 100644
index 000000000..e71d8435f
--- /dev/null
+++ b/compiler/fory_compiler/tests/test_message_defaults.py
@@ -0,0 +1,70 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for message field defaults in FDL."""
+
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.ir.validator import SchemaValidator
+
+
+def test_message_fields_are_optional_by_default():
+ source = """
+ message Foo {
+ Bar bar = 1;
+ }
+
+ message Bar {
+ int32 id = 1;
+ }
+ """
+ schema = Parser(Lexer(source).tokenize()).parse()
+ validator = SchemaValidator(schema)
+ assert validator.validate()
+ field = schema.messages[0].fields[0]
+ assert field.optional is True
+
+
+def test_message_fields_disallow_optional_modifier_in_fdl():
+ source = """
+ message Foo {
+ optional Bar bar = 1;
+ }
+
+ message Bar {
+ int32 id = 1;
+ }
+ """
+ schema = Parser(Lexer(source).tokenize()).parse()
+ validator = SchemaValidator(schema)
+ assert not validator.validate()
+ assert any(
+ "Message fields are always optional" in str(err) for err in
validator.errors
+ )
+
+
+def test_fdl_field_numbers_set_tag_ids():
+ source = """
+ message Foo {
+ int32 id = 7;
+ }
+ """
+ schema = Parser(Lexer(source).tokenize()).parse()
+ validator = SchemaValidator(schema)
+ assert validator.validate()
+ field = schema.messages[0].fields[0]
+ assert field.tag_id == field.number
diff --git a/cpp/fory/serialization/type_resolver.h
b/cpp/fory/serialization/type_resolver.h
index 827243d1f..670665607 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -497,9 +497,10 @@ constexpr bool compute_is_nullable() {
} else if constexpr (::fory::detail::has_field_tags_v<T>) {
return ::fory::detail::GetFieldTagEntry<T, Index>::is_nullable;
} else {
- // Default: nullable if std::optional or std::shared_ptr
+ // Default: nullable if std::optional or smart pointers.
return is_optional_v<UnwrappedFieldType> ||
- is_shared_ptr_v<UnwrappedFieldType>;
+ is_shared_ptr_v<UnwrappedFieldType> ||
+ is_unique_ptr_v<UnwrappedFieldType>;
}
}
diff --git a/integration_tests/idl_tests/cpp/main.cc
b/integration_tests/idl_tests/cpp/main.cc
index 026e2f384..9b8740944 100644
--- a/integration_tests/idl_tests/cpp/main.cc
+++ b/integration_tests/idl_tests/cpp/main.cc
@@ -70,33 +70,33 @@ fory::Result<void, fory::Error> RunRoundTrip() {
complex_fbs::RegisterTypes(fory);
addressbook::Person::PhoneNumber mobile;
- mobile.number = "555-0100";
- mobile.phone_type = addressbook::Person::PhoneType::MOBILE;
+ mobile.set_number("555-0100");
+ mobile.set_phone_type(addressbook::Person::PhoneType::MOBILE);
addressbook::Person::PhoneNumber work;
- work.number = "555-0111";
- work.phone_type = addressbook::Person::PhoneType::WORK;
+ work.set_number("555-0111");
+ work.set_phone_type(addressbook::Person::PhoneType::WORK);
addressbook::Person person;
- person.name = "Alice";
- person.id = 123;
- person.email = "[email protected]";
- person.tags = {"friend", "colleague"};
- person.scores = {{"math", 100}, {"science", 98}};
- person.salary = 120000.5;
- person.phones = {mobile, work};
+ person.set_name("Alice");
+ person.set_id(123);
+ person.set_email("[email protected]");
+ person.set_tags({"friend", "colleague"});
+ person.set_scores({{"math", 100}, {"science", 98}});
+ person.set_salary(120000.5);
+ person.set_phones({mobile, work});
addressbook::Dog dog;
- dog.name = "Rex";
- dog.bark_volume = 5;
- person.pet = addressbook::Animal::dog(dog);
+ dog.set_name("Rex");
+ dog.set_bark_volume(5);
+ person.set_pet(addressbook::Animal::dog(dog));
addressbook::Cat cat;
- cat.name = "Mimi";
- cat.lives = 9;
- person.pet = addressbook::Animal::cat(cat);
+ cat.set_name("Mimi");
+ cat.set_lives(9);
+ person.set_pet(addressbook::Animal::cat(cat));
addressbook::AddressBook book;
- book.people = {person};
- book.people_by_name = {{person.name, person}};
+ book.set_people({person});
+ book.set_people_by_name({{person.name(), person}});
FORY_TRY(bytes, fory.serialize(book));
FORY_TRY(roundtrip, fory.deserialize<addressbook::AddressBook>(bytes.data(),
@@ -108,27 +108,27 @@ fory::Result<void, fory::Error> RunRoundTrip() {
}
addressbook::PrimitiveTypes types;
- types.bool_value = true;
- types.int8_value = 12;
- types.int16_value = 1234;
- types.int32_value = -123456;
- types.varint32_value = -12345;
- types.int64_value = -123456789;
- types.varint64_value = -987654321;
- types.tagged_int64_value = 123456789;
- types.uint8_value = 200;
- types.uint16_value = 60000;
- types.uint32_value = 1234567890;
- types.var_uint32_value = 1234567890;
- types.uint64_value = 9876543210ULL;
- types.var_uint64_value = 12345678901ULL;
- types.tagged_uint64_value = 2222222222ULL;
- types.float16_value = 1.5F;
- types.float32_value = 2.5F;
- types.float64_value = 3.5;
- types.contact = addressbook::PrimitiveTypes::Contact::email(
- std::string("[email protected]"));
- types.contact = addressbook::PrimitiveTypes::Contact::phone(12345);
+ types.set_bool_value(true);
+ types.set_int8_value(12);
+ types.set_int16_value(1234);
+ types.set_int32_value(-123456);
+ types.set_varint32_value(-12345);
+ types.set_int64_value(-123456789);
+ types.set_varint64_value(-987654321);
+ types.set_tagged_int64_value(123456789);
+ types.set_uint8_value(200);
+ types.set_uint16_value(60000);
+ types.set_uint32_value(1234567890);
+ types.set_var_uint32_value(1234567890);
+ types.set_uint64_value(9876543210ULL);
+ types.set_var_uint64_value(12345678901ULL);
+ types.set_tagged_uint64_value(2222222222ULL);
+ types.set_float16_value(1.5F);
+ types.set_float32_value(2.5F);
+ types.set_float64_value(3.5);
+ types.set_contact(addressbook::PrimitiveTypes::Contact::email(
+ std::string("[email protected]")));
+ types.set_contact(addressbook::PrimitiveTypes::Contact::phone(12345));
FORY_TRY(primitive_bytes, fory.serialize(types));
FORY_TRY(primitive_roundtrip,
@@ -141,19 +141,19 @@ fory::Result<void, fory::Error> RunRoundTrip() {
}
monster::Vec3 pos;
- pos.x = 1.0F;
- pos.y = 2.0F;
- pos.z = 3.0F;
+ pos.set_x(1.0F);
+ pos.set_y(2.0F);
+ pos.set_z(3.0F);
monster::Monster monster_value;
- monster_value.pos = pos;
- monster_value.mana = 200;
- monster_value.hp = 80;
- monster_value.name = "Orc";
- monster_value.friendly = true;
- monster_value.inventory = {static_cast<uint8_t>(1), static_cast<uint8_t>(2),
- static_cast<uint8_t>(3)};
- monster_value.color = monster::Color::Blue;
+ *monster_value.mutable_pos() = pos;
+ monster_value.set_mana(200);
+ monster_value.set_hp(80);
+ monster_value.set_name("Orc");
+ monster_value.set_friendly(true);
+ monster_value.set_inventory({static_cast<uint8_t>(1),
static_cast<uint8_t>(2),
+ static_cast<uint8_t>(3)});
+ monster_value.set_color(monster::Color::Blue);
FORY_TRY(monster_bytes, fory.serialize(monster_value));
FORY_TRY(monster_roundtrip, fory.deserialize<monster::Monster>(
@@ -164,34 +164,32 @@ fory::Result<void, fory::Error> RunRoundTrip() {
fory::Error::invalid("flatbuffers monster roundtrip mismatch"));
}
- complex_fbs::ScalarPack scalars;
- scalars.b = -8;
- scalars.ub = 200;
- scalars.s = -1234;
- scalars.us = 40000;
- scalars.i = -123456;
- scalars.ui = 123456;
- scalars.l = -123456789;
- scalars.ul = 987654321;
- scalars.f = 1.5F;
- scalars.d = 2.5;
- scalars.ok = true;
-
complex_fbs::Container container;
- container.id = 9876543210ULL;
- container.status = complex_fbs::Status::STARTED;
- container.bytes = {static_cast<int8_t>(1), static_cast<int8_t>(2),
- static_cast<int8_t>(3)};
- container.numbers = {10, 20, 30};
- container.scalars = scalars;
- container.names = {"alpha", "beta"};
- container.flags = {true, false};
+ container.set_id(9876543210ULL);
+ container.set_status(complex_fbs::Status::STARTED);
+ container.set_bytes(
+ {static_cast<int8_t>(1), static_cast<int8_t>(2),
static_cast<int8_t>(3)});
+ container.set_numbers({10, 20, 30});
+ auto *scalars = container.mutable_scalars();
+ scalars->set_b(-8);
+ scalars->set_ub(200);
+ scalars->set_s(-1234);
+ scalars->set_us(40000);
+ scalars->set_i(-123456);
+ scalars->set_ui(123456);
+ scalars->set_l(-123456789);
+ scalars->set_ul(987654321);
+ scalars->set_f(1.5F);
+ scalars->set_d(2.5);
+ scalars->set_ok(true);
+ container.set_names({"alpha", "beta"});
+ container.set_flags({true, false});
complex_fbs::Note note;
- note.text = "alpha";
- container.payload = complex_fbs::Payload::note(note);
+ note.set_text("alpha");
+ container.set_payload(complex_fbs::Payload::note(note));
complex_fbs::Metric metric;
- metric.value = 42.0;
- container.payload = complex_fbs::Payload::metric(metric);
+ metric.set_value(42.0);
+ container.set_payload(complex_fbs::Payload::metric(metric));
FORY_TRY(container_bytes, fory.serialize(container));
FORY_TRY(container_roundtrip,
diff --git a/integration_tests/idl_tests/go/idl_roundtrip_test.go
b/integration_tests/idl_tests/go/idl_roundtrip_test.go
index c588af27e..3e4cd4e7c 100644
--- a/integration_tests/idl_tests/go/idl_roundtrip_test.go
+++ b/integration_tests/idl_tests/go/idl_roundtrip_test.go
@@ -205,7 +205,7 @@ func runFilePrimitiveRoundTrip(t *testing.T, f *fory.Fory,
types PrimitiveTypes)
}
func buildMonster() monster.Monster {
- pos := monster.Vec3{
+ pos := &monster.Vec3{
X: 1.0,
Y: 2.0,
Z: 3.0,
@@ -265,7 +265,7 @@ func runFileMonsterRoundTrip(t *testing.T, f *fory.Fory,
monsterValue monster.Mo
}
func buildContainer() complexfbs.Container {
- scalars := complexfbs.ScalarPack{
+ scalars := &complexfbs.ScalarPack{
B: -8,
Ub: 200,
S: -1234,
diff --git a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
index f0617b080..dfe3572bf 100644
--- a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
+++ b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
@@ -98,7 +98,7 @@ fn build_monster() -> Monster {
z: 3.0,
};
Monster {
- pos,
+ pos: Some(pos),
mana: 200,
hp: 80,
name: "Orc".to_string(),
@@ -132,7 +132,7 @@ fn build_container() -> Container {
status: Status::Started,
bytes: vec![1, 2, 3],
numbers: vec![10, 20, 30],
- scalars,
+ scalars: Some(scalars),
names: vec!["alpha".to_string(), "beta".to_string()],
flags: vec![true, false],
payload,
diff --git a/rust/fory-derive/src/object/util.rs
b/rust/fory-derive/src/object/util.rs
index 866b6cf42..d8182828f 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -1123,7 +1123,8 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups
{
if is_forward_field(&field.ty) {
let raw_ident = get_field_name(field, idx);
let ident = to_snake_case(&raw_ident);
- other_fields.push((ident, "Forward".to_string(), TypeId::UNKNOWN
as u32));
+ // Forward fields don't have explicit IDs; sort by name.
+ other_fields.push((ident.clone(), ident, TypeId::UNKNOWN as u32));
}
}
@@ -1136,8 +1137,13 @@ fn group_fields_by_type(fields: &[&Field]) ->
FieldGroups {
continue;
}
- // Parse field metadata to get encoding attributes
+ // Parse field metadata to get encoding attributes and field ID
let meta = parse_field_meta(field).unwrap_or_default();
+ let sort_key = if meta.uses_tag_id() {
+ meta.effective_id().to_string()
+ } else {
+ ident.clone()
+ };
let ty: String = field
.ty
@@ -1148,27 +1154,28 @@ fn group_fields_by_type(fields: &[&Field]) ->
FieldGroups {
.collect::<String>();
// Closure to group non-option fields, considering encoding attributes
- let mut group_field = |ident: String, ty_str: &str, is_primitive:
bool| {
- let base_type_id = get_type_id_by_name(ty_str);
- // Adjust type ID based on encoding attributes for u32/u64 fields
- let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
-
- // Categorize based on type_id
- if is_primitive {
- primitive_fields.push((ident, ty_str.to_string(), type_id));
- } else if is_internal_type_id(type_id) {
- internal_type_fields.push((ident, ty_str.to_string(),
type_id));
- } else if type_id == TypeId::LIST as u32 {
- list_fields.push((ident, ty_str.to_string(), type_id));
- } else if type_id == TypeId::SET as u32 {
- set_fields.push((ident, ty_str.to_string(), type_id));
- } else if type_id == TypeId::MAP as u32 {
- map_fields.push((ident, ty_str.to_string(), type_id));
- } else {
- // User-defined type
- other_fields.push((ident, ty_str.to_string(), type_id));
- }
- };
+ let mut group_field =
+ |ident: String, sort_key: String, ty_str: &str, is_primitive:
bool| {
+ let base_type_id = get_type_id_by_name(ty_str);
+ // Adjust type ID based on encoding attributes for u32/u64
fields
+ let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
+
+ // Categorize based on type_id
+ if is_primitive {
+ primitive_fields.push((ident, sort_key, type_id));
+ } else if is_internal_type_id(type_id) {
+ internal_type_fields.push((ident, sort_key, type_id));
+ } else if type_id == TypeId::LIST as u32 {
+ list_fields.push((ident, sort_key, type_id));
+ } else if type_id == TypeId::SET as u32 {
+ set_fields.push((ident, sort_key, type_id));
+ } else if type_id == TypeId::MAP as u32 {
+ map_fields.push((ident, sort_key, type_id));
+ } else {
+ // User-defined type
+ other_fields.push((ident, sort_key, type_id));
+ }
+ };
// handle Option<Primitive> specially
if let Some(inner) = extract_option_inner(&ty) {
@@ -1176,14 +1183,14 @@ fn group_fields_by_type(fields: &[&Field]) ->
FieldGroups {
// Get base type ID and adjust for encoding attributes
let base_type_id = get_primitive_type_id(inner);
let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
- nullable_primitive_fields.push((ident, ty.to_string(),
type_id));
+ nullable_primitive_fields.push((ident, sort_key, type_id));
} else {
- group_field(ident, inner, false);
+ group_field(ident, sort_key, inner, false);
}
} else if PRIMITIVE_TYPE_NAMES.contains(&ty.as_str()) {
- group_field(ident, &ty, true);
+ group_field(ident, sort_key, &ty, true);
} else {
- group_field(ident, &ty, false);
+ group_field(ident, sort_key, &ty, false);
}
}
@@ -1197,6 +1204,9 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups
{
.then_with(|| size_b.cmp(&size_a))
// Use descending type_id order to match Java's
COMPARATOR_BY_PRIMITIVE_TYPE_ID
.then_with(|| b.2.cmp(&a.2))
+ // Field identifier (tag ID or name) as tie-breaker
+ .then_with(|| a.1.cmp(&b.1))
+ // Deterministic fallback for duplicate identifiers
.then_with(|| a.0.cmp(&b.0))
}
@@ -1204,11 +1214,13 @@ fn group_fields_by_type(fields: &[&Field]) ->
FieldGroups {
a: &(String, String, u32),
b: &(String, String, u32),
) -> std::cmp::Ordering {
- a.2.cmp(&b.2).then_with(|| a.0.cmp(&b.0))
+ a.2.cmp(&b.2)
+ .then_with(|| a.1.cmp(&b.1))
+ .then_with(|| a.0.cmp(&b.0))
}
fn name_sorter(a: &(String, String, u32), b: &(String, String, u32)) ->
std::cmp::Ordering {
- a.0.cmp(&b.0)
+ a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))
}
primitive_fields.sort_by(numeric_sorter);
@@ -1217,7 +1229,7 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups
{
list_fields.sort_by(name_sorter);
set_fields.sort_by(name_sorter);
map_fields.sort_by(name_sorter);
- other_fields.sort_by(type_id_then_name_sorter);
+ other_fields.sort_by(name_sorter);
(
primitive_fields,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]