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 72b2b4041 feat(c++): support polymorphic collection elements
serialization (#2974)
72b2b4041 is described below
commit 72b2b40415a1486acef2807b907a02db6c66c146
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Dec 3 19:11:06 2025 +0800
feat(c++): support polymorphic collection elements serialization (#2974)
## Why?
<!-- Describe the purpose of this PR. -->
## What does this PR do?
support polymorphic collection elements serialization
## Related issues
Closes #2972
#2906
## Does this PR introduce any user-facing change?
<!--
If any user-facing interface changes, please [open an
issue](https://github.com/apache/fory/issues/new/choose) describing the
need to do so and update the document if necessary.
Delete section if not applicable.
-->
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
<!--
When the PR has an impact on performance (if you don't know whether the
PR will have an impact on performance, you can submit the PR first, and
if it will have impact on performance, the code reviewer will explain
it), be sure to attach a benchmark data here.
Delete section if not applicable.
-->
---------
Co-authored-by: Pan Li <[email protected]>
---
cpp/fory/serialization/BUILD | 9 +
cpp/fory/serialization/collection_serializer.h | 901 +++++++++++++--------
.../serialization/collection_serializer_test.cc | 373 +++++++++
3 files changed, 937 insertions(+), 346 deletions(-)
diff --git a/cpp/fory/serialization/BUILD b/cpp/fory/serialization/BUILD
index f08fd441b..417db78c4 100644
--- a/cpp/fory/serialization/BUILD
+++ b/cpp/fory/serialization/BUILD
@@ -87,6 +87,15 @@ cc_test(
],
)
+cc_test(
+ name = "collection_serializer_test",
+ srcs = ["collection_serializer_test.cc"],
+ deps = [
+ ":fory_serialization",
+ "@googletest//:gtest",
+ "@googletest//:gtest_main",
+ ],
+)
cc_binary(
name = "xlang_test_main",
diff --git a/cpp/fory/serialization/collection_serializer.h
b/cpp/fory/serialization/collection_serializer.h
index 4c02d0e94..258d0f508 100644
--- a/cpp/fory/serialization/collection_serializer.h
+++ b/cpp/fory/serialization/collection_serializer.h
@@ -25,12 +25,23 @@
#include <cstdint>
#include <limits>
#include <set>
+#include <typeindex>
#include <unordered_set>
#include <vector>
namespace fory {
namespace serialization {
+// ============================================================================
+// Collection Header Constants
+// ============================================================================
+
+/// Collection header bit flags (per xlang spec section 5.4.4)
+constexpr uint8_t COLL_TRACKING_REF = 0b0001;
+constexpr uint8_t COLL_HAS_NULL = 0b0010;
+constexpr uint8_t COLL_DECL_ELEMENT_TYPE = 0b0100;
+constexpr uint8_t COLL_IS_SAME_TYPE = 0b1000;
+
// ============================================================================
// Collection Header
// ============================================================================
@@ -106,6 +117,386 @@ struct CollectionHeader {
}
};
+// ============================================================================
+// Collection Serialization Helpers
+// ============================================================================
+
+/// Check if we need to write type info for a collection element type.
+/// Matches Rust's need_to_write_type_for_field.
+template <typename T> inline constexpr bool need_type_for_collection_elem() {
+ constexpr TypeId tid = Serializer<T>::type_id;
+ return tid == TypeId::STRUCT || tid == TypeId::COMPATIBLE_STRUCT ||
+ tid == TypeId::NAMED_STRUCT ||
+ tid == TypeId::NAMED_COMPATIBLE_STRUCT || tid == TypeId::EXT ||
+ tid == TypeId::NAMED_EXT;
+}
+
+/// Write collection data for non-polymorphic, non-shared-ref elements.
+/// This is the fast path for common cases like vector<int>, vector<string>.
+template <typename T, typename Container>
+inline Result<void, Error> write_collection_data_fast(const Container &coll,
+ WriteContext &ctx,
+ bool has_generics) {
+ static_assert(!is_polymorphic_v<T>,
+ "Fast path is for non-polymorphic types only");
+ static_assert(!is_shared_ref_v<T>,
+ "Fast path is for non-shared-ref types only");
+
+ // Write length
+ ctx.write_varuint32(static_cast<uint32_t>(coll.size()));
+
+ if (coll.empty()) {
+ return Result<void, Error>();
+ }
+
+ // Check for null elements
+ bool has_null = false;
+ if constexpr (is_nullable_v<T>) {
+ for (const auto &elem : coll) {
+ if (is_null_value(elem)) {
+ has_null = true;
+ break;
+ }
+ }
+ }
+
+ // Build header bitmap
+ uint8_t bitmap = COLL_IS_SAME_TYPE;
+ if (has_null) {
+ bitmap |= COLL_HAS_NULL;
+ }
+
+ // Determine if element type is declared
+ using ElemType = nullable_element_t<T>;
+ bool is_elem_declared =
+ has_generics && !need_type_for_collection_elem<ElemType>();
+ if (is_elem_declared) {
+ bitmap |= COLL_DECL_ELEMENT_TYPE;
+ }
+
+ // Write header
+ ctx.write_uint8(bitmap);
+
+ // Write element type info if not declared
+ if (!is_elem_declared) {
+ FORY_RETURN_NOT_OK(Serializer<ElemType>::write_type_info(ctx));
+ }
+
+ // Write elements
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ if (has_null) {
+ for (const auto &elem : coll) {
+ if (is_null_value(elem)) {
+ ctx.write_int8(NULL_FLAG);
+ } else {
+ ctx.write_int8(NOT_NULL_VALUE_FLAG);
+ if (is_elem_declared) {
+ FORY_RETURN_NOT_OK(
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx));
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<Inner>::write(deref_nullable(elem),
+ ctx, false, false));
+ }
+ }
+ }
+ } else {
+ for (const auto &elem : coll) {
+ if (is_elem_declared) {
+ FORY_RETURN_NOT_OK(
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx));
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<Inner>::write(deref_nullable(elem),
ctx,
+ false, false));
+ }
+ }
+ }
+ } else {
+ for (const auto &elem : coll) {
+ if (is_elem_declared) {
+ if constexpr (is_generic_type_v<T>) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write_data_generic(elem, ctx, true));
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<T>::write_data(elem, ctx));
+ }
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<T>::write(elem, ctx, false, false));
+ }
+ }
+ }
+
+ return Result<void, Error>();
+}
+
+/// Write collection data for polymorphic or shared-ref elements.
+/// This is the slow path that handles runtime type checking.
+template <typename T, typename Container>
+inline Result<void, Error> write_collection_data_slow(const Container &coll,
+ WriteContext &ctx,
+ bool has_generics) {
+ // Write length
+ ctx.write_varuint32(static_cast<uint32_t>(coll.size()));
+
+ if (coll.empty()) {
+ return Result<void, Error>();
+ }
+
+ constexpr bool elem_is_polymorphic = is_polymorphic_v<T>;
+ constexpr bool elem_is_shared_ref = is_shared_ref_v<T>;
+
+ using ElemType = nullable_element_t<T>;
+ bool is_elem_declared =
+ has_generics && !need_type_for_collection_elem<ElemType>();
+
+ // Scan collection to determine header flags
+ bool has_null = false;
+ bool is_same_type = true;
+ std::type_index first_type{typeid(void)};
+ bool first_type_set = false;
+
+ for (const auto &elem : coll) {
+ // Check for nulls
+ if constexpr (is_nullable_v<T>) {
+ if (is_null_value(elem)) {
+ has_null = true;
+ continue;
+ }
+ }
+ // Check runtime types for polymorphic elements
+ if constexpr (elem_is_polymorphic) {
+ if (is_same_type) {
+ auto concrete_id = get_concrete_type_id(elem);
+ if (!first_type_set) {
+ first_type = concrete_id;
+ first_type_set = true;
+ } else if (concrete_id != first_type) {
+ is_same_type = false;
+ }
+ }
+ }
+ }
+
+ // If all polymorphic elements are null, treat as heterogeneous
+ if constexpr (elem_is_polymorphic) {
+ if (is_same_type && !first_type_set) {
+ is_same_type = false;
+ }
+ }
+
+ // Build header bitmap
+ uint8_t bitmap = 0;
+ if (has_null) {
+ bitmap |= COLL_HAS_NULL;
+ }
+ if (is_elem_declared && !elem_is_polymorphic) {
+ bitmap |= COLL_DECL_ELEMENT_TYPE;
+ }
+ if (is_same_type) {
+ bitmap |= COLL_IS_SAME_TYPE;
+ }
+ if constexpr (elem_is_shared_ref) {
+ bitmap |= COLL_TRACKING_REF;
+ }
+
+ // Write header
+ ctx.write_uint8(bitmap);
+
+ // Write element type info if IS_SAME_TYPE && !IS_DECL_ELEMENT_TYPE
+ if (is_same_type && !(bitmap & COLL_DECL_ELEMENT_TYPE)) {
+ if constexpr (elem_is_polymorphic) {
+ // Write concrete type info for polymorphic elements
+ FORY_RETURN_NOT_OK(ctx.write_any_typeinfo(
+ static_cast<uint32_t>(TypeId::UNKNOWN), first_type));
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<ElemType>::write_type_info(ctx));
+ }
+ }
+
+ // Write elements
+ if (is_same_type) {
+ // All elements have same type - type info written once above
+ if (!has_null) {
+ if constexpr (elem_is_shared_ref) {
+ // Write with ref flag, without type
+ for (const auto &elem : coll) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write(elem, ctx, true, false, has_generics));
+ }
+ } else {
+ // Write data directly
+ for (const auto &elem : coll) {
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ FORY_RETURN_NOT_OK(
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx));
+ } else {
+ if constexpr (is_generic_type_v<T>) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write_data_generic(elem, ctx, has_generics));
+ } else {
+ FORY_RETURN_NOT_OK(Serializer<T>::write_data(elem, ctx));
+ }
+ }
+ }
+ }
+ } else {
+ // Has null elements - write with ref flag for null tracking
+ for (const auto &elem : coll) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write(elem, ctx, true, false, has_generics));
+ }
+ }
+ } else {
+ // Heterogeneous types - write type info per element
+ if (!has_null) {
+ if constexpr (elem_is_shared_ref) {
+ for (const auto &elem : coll) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write(elem, ctx, true, true, has_generics));
+ }
+ } else {
+ for (const auto &elem : coll) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write(elem, ctx, false, true, has_generics));
+ }
+ }
+ } else {
+ // Has null elements
+ for (const auto &elem : coll) {
+ FORY_RETURN_NOT_OK(
+ Serializer<T>::write(elem, ctx, true, true, has_generics));
+ }
+ }
+ }
+
+ return Result<void, Error>();
+}
+
+// Helper trait to detect if container has push_back
+template <typename Container, typename T, typename = void>
+struct has_push_back : std::false_type {};
+
+template <typename Container, typename T>
+struct has_push_back<Container, T,
+ std::void_t<decltype(std::declval<Container>().push_back(
+ std::declval<T>()))>> : std::true_type {};
+
+template <typename Container, typename T>
+inline constexpr bool has_push_back_v = has_push_back<Container, T>::value;
+
+// Helper trait to detect if container has reserve
+template <typename Container, typename = void>
+struct has_reserve : std::false_type {};
+
+template <typename Container>
+struct has_reserve<Container,
+ std::void_t<decltype(std::declval<Container>().reserve(0))>>
+ : std::true_type {};
+
+template <typename Container>
+inline constexpr bool has_reserve_v = has_reserve<Container>::value;
+
+// Helper to insert element into container (vector or set)
+template <typename Container, typename T>
+inline void collection_insert(Container &result, T &&elem) {
+ if constexpr (has_push_back_v<Container, T>) {
+ result.push_back(std::forward<T>(elem));
+ } else {
+ result.insert(std::forward<T>(elem));
+ }
+}
+
+/// Read collection data for polymorphic or shared-ref elements.
+template <typename T, typename Container>
+inline Result<Container, Error> read_collection_data_slow(ReadContext &ctx,
+ uint32_t length) {
+ Container result;
+ if constexpr (has_reserve_v<Container>) {
+ result.reserve(length);
+ }
+
+ if (length == 0) {
+ return result;
+ }
+
+ constexpr bool elem_is_polymorphic = is_polymorphic_v<T>;
+ constexpr bool elem_is_shared_ref = is_shared_ref_v<T>;
+
+ Error error;
+ uint8_t bitmap = ctx.read_uint8(&error);
+ if (FORY_PREDICT_FALSE(!error.ok())) {
+ return Unexpected(std::move(error));
+ }
+
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ // Read element type info if IS_SAME_TYPE && !IS_DECL_ELEMENT_TYPE
+ const TypeInfo *elem_type_info = nullptr;
+ if (is_same_type && !is_decl_type) {
+ FORY_TRY(type_info, ctx.read_any_typeinfo());
+ elem_type_info = type_info;
+ }
+
+ // Read elements
+ if (is_same_type) {
+ if (track_ref || elem_is_shared_ref) {
+ for (uint32_t i = 0; i < length; ++i) {
+ if constexpr (elem_is_polymorphic) {
+ FORY_TRY(elem, Serializer<T>::read_with_type_info(ctx, true,
+ *elem_type_info));
+ collection_insert(result, std::move(elem));
+ } else {
+ FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
+ collection_insert(result, std::move(elem));
+ }
+ }
+ } else if (!has_null) {
+ for (uint32_t i = 0; i < length; ++i) {
+ if constexpr (elem_is_polymorphic) {
+ FORY_TRY(elem, Serializer<T>::read_with_type_info(ctx, false,
+ *elem_type_info));
+ collection_insert(result, std::move(elem));
+ } else {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ collection_insert(result, std::move(elem));
+ }
+ }
+ } else {
+ // Has null elements
+ for (uint32_t i = 0; i < length; ++i) {
+ FORY_TRY(has_value, consume_ref_flag(ctx, true));
+ if (!has_value) {
+ if constexpr (has_push_back_v<Container, T>) {
+ result.push_back(T{});
+ }
+ // For sets, skip null elements
+ } else {
+ if constexpr (elem_is_polymorphic) {
+ FORY_TRY(elem, Serializer<T>::read_with_type_info(ctx, false,
+
*elem_type_info));
+ collection_insert(result, std::move(elem));
+ } else {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ collection_insert(result, std::move(elem));
+ }
+ }
+ }
+ }
+ } else {
+ // Heterogeneous types - read type info per element
+ for (uint32_t i = 0; i < length; ++i) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, track_ref, true));
+ collection_insert(result, std::move(elem));
+ }
+ }
+
+ return result;
+}
+
// ============================================================================
// std::vector serializer
// ============================================================================
@@ -276,77 +667,75 @@ struct Serializer<
return std::vector<T, Alloc>();
}
- // Elements header bitmap (CollectionFlags)
- uint8_t bitmap = ctx.read_uint8(&error);
- if (FORY_PREDICT_FALSE(!error.ok())) {
- return Unexpected(std::move(error));
- }
- bool track_ref = (bitmap & 0x1u) != 0;
- bool has_null = (bitmap & 0x2u) != 0;
- bool is_decl_type = (bitmap & 0x4u) != 0;
- bool is_same_type = (bitmap & 0x8u) != 0;
-
- // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
is
- // not. This matches Rust/Java behavior in compatible mode.
- // We read the type info using read_any_typeinfo() and just consume the
- // bytes. Type validation is relaxed for xlang compatibility - we check
- // category matches.
- if (is_same_type && !is_decl_type) {
- FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
- // Type info was consumed; we trust the sender wrote correct element
- // types. We do a relaxed check comparing type categories using
- // type_id_matches.
- using ElemType = nullable_element_t<T>;
- uint32_t expected = static_cast<uint32_t>(Serializer<ElemType>::type_id);
- if (!type_id_matches(elem_type_info->type_id, expected)) {
- return Unexpected(
- Error::type_mismatch(elem_type_info->type_id, expected));
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ return read_collection_data_slow<T, std::vector<T, Alloc>>(ctx, length);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Elements header bitmap (CollectionFlags)
+ uint8_t bitmap = ctx.read_uint8(&error);
+ if (FORY_PREDICT_FALSE(!error.ok())) {
+ return Unexpected(std::move(error));
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
+ // is not. This matches Rust/Java behavior in compatible mode.
+ if (is_same_type && !is_decl_type) {
+ FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
+ using ElemType = nullable_element_t<T>;
+ uint32_t expected =
+ static_cast<uint32_t>(Serializer<ElemType>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ return Unexpected(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ }
}
- }
- std::vector<T, Alloc> result;
- result.reserve(length);
+ std::vector<T, Alloc> result;
+ result.reserve(length);
- // Fast path: no tracking, no nulls, elements have declared type and
- // are homogeneous. Java encodes this via DECL_SAME_TYPE_NOT_HAS_NULL.
- if (!track_ref && !has_null && is_same_type) {
- for (uint32_t i = 0; i < length; ++i) {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.push_back(std::move(elem));
+ // Fast path: no tracking, no nulls, elements have declared type
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < length; ++i) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.push_back(std::move(elem));
+ }
+ return result;
}
- return result;
- }
- // General path: handle HAS_NULL and/or TRACKING_REF.
- for (uint32_t i = 0; i < length; ++i) {
- if (track_ref) {
- // Java uses xwriteRef for elements in this case.
- FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
- result.push_back(std::move(elem));
- } else if (has_null) {
- // Elements encoded with explicit null flag (NULL/NOT_NULL_VALUE).
- FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
- if (!has_value_elem) {
- // Push null/empty value for nullable types
- result.emplace_back();
- } else {
- if constexpr (is_nullable_v<T>) {
- using Inner = nullable_element_t<T>;
- FORY_TRY(inner, Serializer<Inner>::read(ctx, false, false));
- result.emplace_back(std::move(inner));
+ // General path: handle HAS_NULL and/or TRACKING_REF
+ for (uint32_t i = 0; i < length; ++i) {
+ if (track_ref) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
+ result.push_back(std::move(elem));
+ } else if (has_null) {
+ FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
+ if (!has_value_elem) {
+ result.emplace_back();
} else {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.push_back(std::move(elem));
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ FORY_TRY(inner, Serializer<Inner>::read(ctx, false, false));
+ result.emplace_back(std::move(inner));
+ } else {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.push_back(std::move(elem));
+ }
}
+ } else {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.push_back(std::move(elem));
}
- } else {
- // Fallback: behave like fast path.
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.push_back(std::move(elem));
}
- }
- return result;
+ return result;
+ }
}
// Match Rust signature: fory_write(&self, context, write_ref_info,
@@ -378,116 +767,14 @@ struct Serializer<
static inline Result<void, Error>
write_data_generic(const std::vector<T, Alloc> &vec, WriteContext &ctx,
bool has_generics) {
- // Write length
- ctx.write_varuint32(static_cast<uint32_t>(vec.size()));
-
- if (vec.empty()) {
- return Result<void, Error>();
- }
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
- // Build header bitmap
- bool has_null = false;
- if constexpr (is_nullable_v<T>) {
- for (const auto &elem : vec) {
- if (is_null_value(elem)) {
- has_null = true;
- break;
- }
- }
- }
-
- uint8_t bitmap = 0b1000; // IS_SAME_TYPE
- if (has_null) {
- bitmap |= 0b0010; // HAS_NULL
- }
-
- // Per Rust collection.rs: is_elem_declared = has_generics &&
- // !need_to_write_type_for_field(...)
- // When has_generics is true (writing as struct field) AND the element type
- // doesn't need explicit type info, set IS_DECL_ELEMENT_TYPE to indicate
- // element type matches declared type.
- // Types that need type info: STRUCT, COMPATIBLE_STRUCT, NAMED_STRUCT,
- // NAMED_COMPATIBLE_STRUCT, EXT, NAMED_EXT
- bool is_elem_declared = false;
- if (has_generics) {
- // Get the inner type for nullable types (optional, shared_ptr, etc.)
- using ElemType = nullable_element_t<T>;
- constexpr TypeId tid = Serializer<ElemType>::type_id;
- const bool need_type = tid == TypeId::STRUCT ||
- tid == TypeId::COMPATIBLE_STRUCT ||
- tid == TypeId::NAMED_STRUCT ||
- tid == TypeId::NAMED_COMPATIBLE_STRUCT ||
- tid == TypeId::EXT || tid == TypeId::NAMED_EXT;
- is_elem_declared = !need_type;
- }
- if (is_elem_declared) {
- bitmap |= 0b0100; // IS_DECL_ELEMENT_TYPE
- }
-
- // Write header
- ctx.write_uint8(bitmap);
-
- // Write element type info only if !IS_DECL_ELEMENT_TYPE
- if (!is_elem_declared) {
- using ElemType = nullable_element_t<T>;
- FORY_RETURN_NOT_OK(Serializer<ElemType>::write_type_info(ctx));
- }
-
- // Write elements
- if constexpr (is_nullable_v<T>) {
- using Inner = nullable_element_t<T>;
- // Only write null flags when HAS_NULL is set in bitmap
- if (has_null) {
- for (const auto &elem : vec) {
- if (is_null_value(elem)) {
- ctx.write_int8(NULL_FLAG);
- } else {
- ctx.write_int8(NOT_NULL_VALUE_FLAG);
- // When IS_DECL_ELEMENT_TYPE is set, use write_data to skip
ref/type
- // metadata
- if (is_elem_declared) {
- FORY_RETURN_NOT_OK(
- Serializer<Inner>::write_data(deref_nullable(elem), ctx));
- } else {
- FORY_RETURN_NOT_OK(Serializer<Inner>::write(deref_nullable(elem),
- ctx, false, false));
- }
- }
- }
- } else {
- // When has_null=false, all elements are non-null, write directly
- for (const auto &elem : vec) {
- // When IS_DECL_ELEMENT_TYPE is set, use write_data
- if (is_elem_declared) {
- FORY_RETURN_NOT_OK(
- Serializer<Inner>::write_data(deref_nullable(elem), ctx));
- } else {
- FORY_RETURN_NOT_OK(Serializer<Inner>::write(deref_nullable(elem),
- ctx, false, false));
- }
- }
- }
+ if constexpr (is_fast_path) {
+ return write_collection_data_fast<T>(vec, ctx, has_generics);
} else {
- for (const auto &elem : vec) {
- // When IS_DECL_ELEMENT_TYPE is set, write elements without ref/type
- // metadata
- if (is_elem_declared) {
- if constexpr (is_generic_type_v<T>) {
- FORY_RETURN_NOT_OK(
- Serializer<T>::write_data_generic(elem, ctx, true));
- } else {
- FORY_RETURN_NOT_OK(Serializer<T>::write_data(elem, ctx));
- }
- } else {
- // When IS_DECL_ELEMENT_TYPE is not set, element type info is already
- // written in collection header, so don't write it again for each
- // element
- FORY_RETURN_NOT_OK(Serializer<T>::write(elem, ctx, false, false));
- }
- }
+ return write_collection_data_slow<T>(vec, ctx, has_generics);
}
-
- return Result<void, Error>();
}
static inline Result<std::vector<T, Alloc>, Error>
@@ -662,56 +949,14 @@ struct Serializer<std::set<T, Args...>> {
static inline Result<void, Error>
write_data_generic(const std::set<T, Args...> &set, WriteContext &ctx,
bool has_generics) {
- // Write length
- ctx.write_varuint32(static_cast<uint32_t>(set.size()));
-
- // Per xlang spec: header and type_info are omitted when length is 0
- if (set.empty()) {
- return Result<void, Error>();
- }
-
- // Build header bitmap - sets cannot contain nulls
- uint8_t bitmap = 0b1000; // IS_SAME_TYPE
-
- // Per Rust collection.rs: is_elem_declared = has_generics &&
- // !need_to_write_type_for_field(...)
- bool is_elem_declared = false;
- if (has_generics) {
- constexpr TypeId tid = Serializer<T>::type_id;
- const bool need_type = tid == TypeId::STRUCT ||
- tid == TypeId::COMPATIBLE_STRUCT ||
- tid == TypeId::NAMED_STRUCT ||
- tid == TypeId::NAMED_COMPATIBLE_STRUCT ||
- tid == TypeId::EXT || tid == TypeId::NAMED_EXT;
- is_elem_declared = !need_type;
- }
- if (is_elem_declared) {
- bitmap |= 0b0100; // IS_DECL_ELEMENT_TYPE
- }
-
- // Write header
- ctx.write_uint8(bitmap);
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
- // Write element type info only if !IS_DECL_ELEMENT_TYPE
- if (!is_elem_declared) {
- FORY_RETURN_NOT_OK(Serializer<T>::write_type_info(ctx));
- }
-
- // Write elements
- for (const auto &elem : set) {
- if (is_elem_declared) {
- if constexpr (is_generic_type_v<T>) {
- FORY_RETURN_NOT_OK(
- Serializer<T>::write_data_generic(elem, ctx, true));
- } else {
- FORY_RETURN_NOT_OK(Serializer<T>::write_data(elem, ctx));
- }
- } else {
- // Element type info already written in collection header
- FORY_RETURN_NOT_OK(Serializer<T>::write(elem, ctx, false, false));
- }
+ if constexpr (is_fast_path) {
+ return write_collection_data_fast<T>(set, ctx, has_generics);
+ } else {
+ return write_collection_data_slow<T>(set, ctx, has_generics);
}
- return Result<void, Error>();
}
static inline Result<std::set<T, Args...>, Error>
@@ -744,55 +989,58 @@ struct Serializer<std::set<T, Args...>> {
return std::set<T, Args...>();
}
- // Read elements header bitmap (CollectionFlags) in xlang mode
- uint8_t bitmap = ctx.read_uint8(&error);
- if (FORY_PREDICT_FALSE(!error.ok())) {
- return Unexpected(std::move(error));
- }
- bool track_ref = (bitmap & 0x1u) != 0;
- bool has_null = (bitmap & 0x2u) != 0;
- bool is_decl_type = (bitmap & 0x4u) != 0;
- bool is_same_type = (bitmap & 0x8u) != 0;
- // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
- // is not. Uses read_any_typeinfo() for proper handling of all type
- // categories.
- if (is_same_type && !is_decl_type) {
- FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
- uint32_t expected = static_cast<uint32_t>(Serializer<T>::type_id);
- if (!type_id_matches(elem_type_info->type_id, expected)) {
- return Unexpected(
- Error::type_mismatch(elem_type_info->type_id, expected));
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ return read_collection_data_slow<T, std::set<T, Args...>>(ctx, size);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Read elements header bitmap (CollectionFlags) in xlang mode
+ uint8_t bitmap = ctx.read_uint8(&error);
+ if (FORY_PREDICT_FALSE(!error.ok())) {
+ return Unexpected(std::move(error));
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ if (is_same_type && !is_decl_type) {
+ FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
+ uint32_t expected = static_cast<uint32_t>(Serializer<T>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ return Unexpected(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ }
}
- }
- std::set<T, Args...> result;
- // Fast path: no tracking, no nulls, elements have declared type
- if (!track_ref && !has_null && is_same_type) {
- for (uint32_t i = 0; i < size; ++i) {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.insert(std::move(elem));
+ std::set<T, Args...> result;
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < size; ++i) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.insert(std::move(elem));
+ }
+ return result;
}
- return result;
- }
- // General path: handle HAS_NULL and/or TRACKING_REF
- for (uint32_t i = 0; i < size; ++i) {
- if (track_ref) {
- FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
- result.insert(std::move(elem));
- } else if (has_null) {
- FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
- if (has_value_elem) {
+ for (uint32_t i = 0; i < size; ++i) {
+ if (track_ref) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
+ result.insert(std::move(elem));
+ } else if (has_null) {
+ FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
+ if (has_value_elem) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.insert(std::move(elem));
+ }
+ } else {
FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
result.insert(std::move(elem));
}
- // Note: Sets can't contain null, so we skip null elements
- } else {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.insert(std::move(elem));
}
+ return result;
}
- return result;
}
static inline Result<std::set<T, Args...>, Error>
@@ -866,56 +1114,14 @@ struct Serializer<std::unordered_set<T, Args...>> {
static inline Result<void, Error>
write_data_generic(const std::unordered_set<T, Args...> &set,
WriteContext &ctx, bool has_generics) {
- // Write length
- ctx.write_varuint32(static_cast<uint32_t>(set.size()));
-
- // Per xlang spec: header and type_info are omitted when length is 0
- if (set.empty()) {
- return Result<void, Error>();
- }
-
- // Build header bitmap - sets cannot contain nulls
- uint8_t bitmap = 0b1000; // IS_SAME_TYPE
-
- // Per Rust collection.rs: is_elem_declared = has_generics &&
- // !need_to_write_type_for_field(...)
- bool is_elem_declared = false;
- if (has_generics) {
- constexpr TypeId tid = Serializer<T>::type_id;
- const bool need_type = tid == TypeId::STRUCT ||
- tid == TypeId::COMPATIBLE_STRUCT ||
- tid == TypeId::NAMED_STRUCT ||
- tid == TypeId::NAMED_COMPATIBLE_STRUCT ||
- tid == TypeId::EXT || tid == TypeId::NAMED_EXT;
- is_elem_declared = !need_type;
- }
- if (is_elem_declared) {
- bitmap |= 0b0100; // IS_DECL_ELEMENT_TYPE
- }
-
- // Write header
- ctx.write_uint8(bitmap);
-
- // Write element type info only if !IS_DECL_ELEMENT_TYPE
- if (!is_elem_declared) {
- FORY_RETURN_NOT_OK(Serializer<T>::write_type_info(ctx));
- }
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
- // Write elements
- for (const auto &elem : set) {
- if (is_elem_declared) {
- if constexpr (is_generic_type_v<T>) {
- FORY_RETURN_NOT_OK(
- Serializer<T>::write_data_generic(elem, ctx, true));
- } else {
- FORY_RETURN_NOT_OK(Serializer<T>::write_data(elem, ctx));
- }
- } else {
- // Element type info already written in collection header
- FORY_RETURN_NOT_OK(Serializer<T>::write(elem, ctx, false, false));
- }
+ if constexpr (is_fast_path) {
+ return write_collection_data_fast<T>(set, ctx, has_generics);
+ } else {
+ return write_collection_data_slow<T>(set, ctx, has_generics);
}
- return Result<void, Error>();
}
static inline Result<std::unordered_set<T, Args...>, Error>
@@ -949,57 +1155,60 @@ struct Serializer<std::unordered_set<T, Args...>> {
return std::unordered_set<T, Args...>();
}
- // Read elements header bitmap (CollectionFlags) in xlang mode
- uint8_t bitmap = ctx.read_uint8(&error);
- if (FORY_PREDICT_FALSE(!error.ok())) {
- return Unexpected(std::move(error));
- }
- bool track_ref = (bitmap & 0x1u) != 0;
- bool has_null = (bitmap & 0x2u) != 0;
- bool is_decl_type = (bitmap & 0x4u) != 0;
- bool is_same_type = (bitmap & 0x8u) != 0;
-
- // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
- // is not. Uses read_any_typeinfo() for proper handling of all type
- // categories.
- if (is_same_type && !is_decl_type) {
- FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
- uint32_t expected = static_cast<uint32_t>(Serializer<T>::type_id);
- if (!type_id_matches(elem_type_info->type_id, expected)) {
- return Unexpected(
- Error::type_mismatch(elem_type_info->type_id, expected));
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ return read_collection_data_slow<T, std::unordered_set<T, Args...>>(ctx,
+
size);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Read elements header bitmap (CollectionFlags) in xlang mode
+ uint8_t bitmap = ctx.read_uint8(&error);
+ if (FORY_PREDICT_FALSE(!error.ok())) {
+ return Unexpected(std::move(error));
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ if (is_same_type && !is_decl_type) {
+ FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
+ uint32_t expected = static_cast<uint32_t>(Serializer<T>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ return Unexpected(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ }
}
- }
- std::unordered_set<T, Args...> result;
- result.reserve(size);
- // Fast path: no tracking, no nulls, elements have declared type
- if (!track_ref && !has_null && is_same_type) {
- for (uint32_t i = 0; i < size; ++i) {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.insert(std::move(elem));
+ std::unordered_set<T, Args...> result;
+ result.reserve(size);
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < size; ++i) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.insert(std::move(elem));
+ }
+ return result;
}
- return result;
- }
- // General path: handle HAS_NULL and/or TRACKING_REF
- for (uint32_t i = 0; i < size; ++i) {
- if (track_ref) {
- FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
- result.insert(std::move(elem));
- } else if (has_null) {
- FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
- if (has_value_elem) {
+ for (uint32_t i = 0; i < size; ++i) {
+ if (track_ref) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, true, false));
+ result.insert(std::move(elem));
+ } else if (has_null) {
+ FORY_TRY(has_value_elem, consume_ref_flag(ctx, true));
+ if (has_value_elem) {
+ FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
+ result.insert(std::move(elem));
+ }
+ } else {
FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
result.insert(std::move(elem));
}
- // Note: Sets can't contain null, so we skip null elements
- } else {
- FORY_TRY(elem, Serializer<T>::read(ctx, false, false));
- result.insert(std::move(elem));
}
+ return result;
}
- return result;
}
static inline Result<std::unordered_set<T, Args...>, Error>
diff --git a/cpp/fory/serialization/collection_serializer_test.cc
b/cpp/fory/serialization/collection_serializer_test.cc
new file mode 100644
index 000000000..7e70c0758
--- /dev/null
+++ b/cpp/fory/serialization/collection_serializer_test.cc
@@ -0,0 +1,373 @@
+/*
+ * 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.
+ */
+
+#include "fory/serialization/fory.h"
+#include "gtest/gtest.h"
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace fory {
+namespace serialization {
+
+// ============================================================================
+// Polymorphic Base and Derived Types for Testing
+// ============================================================================
+
+struct Animal {
+ virtual ~Animal() = default;
+ virtual std::string speak() const = 0;
+ int32_t age = 0;
+};
+FORY_STRUCT(Animal, age);
+
+struct Dog : Animal {
+ std::string speak() const override { return "Woof"; }
+ std::string name;
+};
+FORY_STRUCT(Dog, age, name);
+
+struct Cat : Animal {
+ std::string speak() const override { return "Meow"; }
+ int32_t lives = 9;
+};
+FORY_STRUCT(Cat, age, lives);
+
+// Holder structs for testing collections as struct fields
+struct VectorPolymorphicHolder {
+ std::vector<std::shared_ptr<Animal>> animals;
+};
+FORY_STRUCT(VectorPolymorphicHolder, animals);
+
+struct VectorHomogeneousHolder {
+ std::vector<std::shared_ptr<Dog>> dogs;
+};
+FORY_STRUCT(VectorHomogeneousHolder, dogs);
+
+namespace {
+
+Fory create_fory() {
+ return Fory::builder().xlang(true).track_ref(true).build();
+}
+
+void register_types(Fory &fory) {
+ fory.register_struct<VectorPolymorphicHolder>(100);
+ fory.register_struct<VectorHomogeneousHolder>(101);
+ fory.register_struct<Dog>("test", "Dog");
+ fory.register_struct<Cat>("test", "Cat");
+}
+
+// ============================================================================
+// Polymorphic Vector Tests
+// ============================================================================
+
+TEST(CollectionSerializerTest, VectorPolymorphicHeterogeneousElements) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ // Create vector with different derived types
+ VectorPolymorphicHolder original;
+ auto dog = std::make_shared<Dog>();
+ dog->age = 3;
+ dog->name = "Buddy";
+ auto cat = std::make_shared<Cat>();
+ cat->age = 5;
+ cat->lives = 9;
+
+ original.animals.push_back(dog);
+ original.animals.push_back(cat);
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorPolymorphicHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.animals.size(), 2u);
+
+ // Verify first element is Dog
+ auto *d = dynamic_cast<Dog *>(deserialized.animals[0].get());
+ ASSERT_NE(d, nullptr) << "First element should be Dog";
+ EXPECT_EQ(d->age, 3);
+ EXPECT_EQ(d->name, "Buddy");
+ EXPECT_EQ(d->speak(), "Woof");
+
+ // Verify second element is Cat
+ auto *c = dynamic_cast<Cat *>(deserialized.animals[1].get());
+ ASSERT_NE(c, nullptr) << "Second element should be Cat";
+ EXPECT_EQ(c->age, 5);
+ EXPECT_EQ(c->lives, 9);
+ EXPECT_EQ(c->speak(), "Meow");
+}
+
+TEST(CollectionSerializerTest, VectorPolymorphicHomogeneousElements) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ // Create vector with same derived type
+ VectorPolymorphicHolder original;
+ auto dog1 = std::make_shared<Dog>();
+ dog1->age = 2;
+ dog1->name = "Max";
+ auto dog2 = std::make_shared<Dog>();
+ dog2->age = 4;
+ dog2->name = "Rex";
+
+ original.animals.push_back(dog1);
+ original.animals.push_back(dog2);
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorPolymorphicHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.animals.size(), 2u);
+
+ // Both should be Dog
+ auto *d1 = dynamic_cast<Dog *>(deserialized.animals[0].get());
+ auto *d2 = dynamic_cast<Dog *>(deserialized.animals[1].get());
+ ASSERT_NE(d1, nullptr);
+ ASSERT_NE(d2, nullptr);
+ EXPECT_EQ(d1->name, "Max");
+ EXPECT_EQ(d2->name, "Rex");
+}
+
+TEST(CollectionSerializerTest, VectorPolymorphicWithNulls) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ VectorPolymorphicHolder original;
+ auto dog = std::make_shared<Dog>();
+ dog->age = 1;
+ dog->name = "Spot";
+
+ original.animals.push_back(dog);
+ original.animals.push_back(nullptr); // null element
+ auto cat = std::make_shared<Cat>();
+ cat->age = 2;
+ cat->lives = 7;
+ original.animals.push_back(cat);
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorPolymorphicHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.animals.size(), 3u);
+ EXPECT_NE(deserialized.animals[0], nullptr);
+ EXPECT_EQ(deserialized.animals[1], nullptr);
+ EXPECT_NE(deserialized.animals[2], nullptr);
+}
+
+TEST(CollectionSerializerTest, VectorNonPolymorphicSharedPtr) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ VectorHomogeneousHolder original;
+ auto dog1 = std::make_shared<Dog>();
+ dog1->age = 5;
+ dog1->name = "Fido";
+ auto dog2 = std::make_shared<Dog>();
+ dog2->age = 3;
+ dog2->name = "Bruno";
+
+ original.dogs.push_back(dog1);
+ original.dogs.push_back(dog2);
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorHomogeneousHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.dogs.size(), 2u);
+ EXPECT_EQ(deserialized.dogs[0]->name, "Fido");
+ EXPECT_EQ(deserialized.dogs[1]->name, "Bruno");
+}
+
+TEST(CollectionSerializerTest, VectorSharedPtrReferenceTracking) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ // Test that shared_ptr reference tracking works
+ VectorHomogeneousHolder original;
+ auto shared_dog = std::make_shared<Dog>();
+ shared_dog->age = 7;
+ shared_dog->name = "Shared";
+
+ original.dogs.push_back(shared_dog);
+ original.dogs.push_back(shared_dog); // Same pointer twice
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorHomogeneousHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.dogs.size(), 2u);
+ // Both should point to the same object due to reference tracking
+ EXPECT_EQ(deserialized.dogs[0], deserialized.dogs[1])
+ << "Reference tracking should preserve shared_ptr aliasing";
+}
+
+TEST(CollectionSerializerTest, VectorEmpty) {
+ auto fory = create_fory();
+ register_types(fory);
+
+ VectorPolymorphicHolder original;
+ // Empty vector
+
+ // Serialize
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ // Deserialize
+ auto deserialize_result = fory.deserialize<VectorPolymorphicHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ EXPECT_TRUE(deserialized.animals.empty());
+}
+
+// ============================================================================
+// Non-Polymorphic Collection Tests (Regression)
+// ============================================================================
+
+struct VectorStringHolder {
+ std::vector<std::string> strings;
+};
+FORY_STRUCT(VectorStringHolder, strings);
+
+struct VectorIntHolder {
+ std::vector<int32_t> numbers;
+};
+FORY_STRUCT(VectorIntHolder, numbers);
+
+TEST(CollectionSerializerTest, VectorStringRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<VectorStringHolder>(200);
+
+ VectorStringHolder original;
+ original.strings = {"hello", "world", "fory"};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<VectorStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.strings.size(), 3u);
+ EXPECT_EQ(deserialized.strings[0], "hello");
+ EXPECT_EQ(deserialized.strings[1], "world");
+ EXPECT_EQ(deserialized.strings[2], "fory");
+}
+
+TEST(CollectionSerializerTest, VectorIntRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<VectorIntHolder>(201);
+
+ VectorIntHolder original;
+ original.numbers = {1, 2, 3, 42, 100};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<VectorIntHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.numbers.size(), 5u);
+ EXPECT_EQ(deserialized.numbers[0], 1);
+ EXPECT_EQ(deserialized.numbers[4], 100);
+}
+
+// ============================================================================
+// Optional/Nullable Element Tests
+// ============================================================================
+
+struct VectorOptionalHolder {
+ std::vector<std::optional<std::string>> values;
+};
+FORY_STRUCT(VectorOptionalHolder, values);
+
+TEST(CollectionSerializerTest, VectorOptionalWithNulls) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<VectorOptionalHolder>(202);
+
+ VectorOptionalHolder original;
+ original.values.push_back("first");
+ original.values.push_back(std::nullopt);
+ original.values.push_back("third");
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<VectorOptionalHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.values.size(), 3u);
+ EXPECT_TRUE(deserialized.values[0].has_value());
+ EXPECT_EQ(*deserialized.values[0], "first");
+ EXPECT_FALSE(deserialized.values[1].has_value());
+ EXPECT_TRUE(deserialized.values[2].has_value());
+ EXPECT_EQ(*deserialized.values[2], "third");
+}
+
+} // namespace
+} // namespace serialization
+} // namespace fory
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]