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 ca2e990cc feat(c++): add SharedWeak<T> for circular reference support
(#3109)
ca2e990cc is described below
commit ca2e990cc4be2d191e22a02871a9e8ead2338540
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jan 4 01:37:04 2026 +0800
feat(c++): add SharedWeak<T> for circular reference support (#3109)
## Why?
C++ serialization needs support for weak pointers to handle circular
references in graph-like structures (e.g., parent-child relationships).
Unlike `std::weak_ptr<T>`, we need a wrapper that allows updating the
internal pointer during deserialization for forward reference
resolution.
## What does this PR do?
Adds `SharedWeak<T>` - a serializable wrapper around `std::weak_ptr<T>`
designed for graph structures with circular references.
### Key features:
1. **`SharedWeak<T>` wrapper class** (`weak_ptr_serializer.h`)
- Stores `std::weak_ptr<T>` inside `std::shared_ptr<Inner>` so all
copies share the same internal storage
- Enables forward reference resolution via callbacks during
deserialization
- Provides `from()`, `upgrade()`, `update()`, `expired()` methods
2. **Forward reference resolution** (`ref_resolver.h`)
- Added `add_update_callback(ref_id, UpdateCallback)` overload to
`RefReader`
- When deserializing a weak pointer that references an object not yet
created, registers a callback to update it later
3. **Serializer implementation** (`weak_ptr_serializer.h`)
- Handles NULL_FLAG, REF_FLAG, and REF_VALUE_FLAG properly
- Requires `track_ref=true` for reference tracking
- Supports forward reference resolution via callbacks
4. **Type traits**
- `is_shared_weak<T>` - detection trait
- `requires_ref_metadata<SharedWeak<T>>` - always true
- `is_nullable<SharedWeak<T>>` - always true
### Example usage:
```cpp
struct Node {
int32_t value;
SharedWeak<Node> parent; // Non-owning back-reference
std::vector<std::shared_ptr<Node>> children; // Owning references
};
FORY_STRUCT(Node, value, parent, children);
auto parent = std::make_shared<Node>();
parent->value = 1;
auto child = std::make_shared<Node>();
child->value = 2;
child->parent = SharedWeak<Node>::from(parent);
parent->children.push_back(child);
// Serialize and deserialize
auto bytes = fory.serialize(parent);
auto result = fory.deserialize<std::shared_ptr<Node>>(bytes);
// Parent reference is preserved
assert(result->children[0]->parent.upgrade() == result);
```
## Related issues
#1017
#2906
Closes #2976
Closes #2977
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- Adds new `SharedWeak<T>` class for C++ users
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A - this is a new feature addition.
---
cpp/fory/serialization/BUILD | 11 +
cpp/fory/serialization/CMakeLists.txt | 6 +
cpp/fory/serialization/fory.h | 1 +
cpp/fory/serialization/ref_resolver.h | 13 +
cpp/fory/serialization/smart_ptr_serializers.h | 73 +++-
cpp/fory/serialization/type_resolver.h | 2 +
cpp/fory/serialization/weak_ptr_serializer.h | 468 +++++++++++++++++++++
cpp/fory/serialization/weak_ptr_serializer_test.cc | 468 +++++++++++++++++++++
cpp/fory/serialization/xlang_test_main.cc | 115 +++++
.../java/org/apache/fory/xlang/CPPXlangTest.java | 8 +-
10 files changed, 1139 insertions(+), 26 deletions(-)
diff --git a/cpp/fory/serialization/BUILD b/cpp/fory/serialization/BUILD
index eff2705e0..f805c3b0c 100644
--- a/cpp/fory/serialization/BUILD
+++ b/cpp/fory/serialization/BUILD
@@ -30,6 +30,7 @@ cc_library(
"type_resolver.h",
"unsigned_serializer.h",
"variant_serializer.h",
+ "weak_ptr_serializer.h",
],
strip_include_prefix = "/cpp",
deps = [
@@ -141,6 +142,16 @@ cc_test(
],
)
+cc_test(
+ name = "weak_ptr_serializer_test",
+ srcs = ["weak_ptr_serializer_test.cc"],
+ deps = [
+ ":fory_serialization",
+ "@googletest//:gtest",
+ "@googletest//:gtest_main",
+ ],
+)
+
cc_binary(
name = "xlang_test_main",
srcs = ["xlang_test_main.cc"],
diff --git a/cpp/fory/serialization/CMakeLists.txt
b/cpp/fory/serialization/CMakeLists.txt
index 84618337f..d657f1f2c 100644
--- a/cpp/fory/serialization/CMakeLists.txt
+++ b/cpp/fory/serialization/CMakeLists.txt
@@ -30,6 +30,7 @@ set(FORY_SERIALIZATION_HEADERS
enum_serializer.h
fory.h
map_serializer.h
+ ref_mode.h
ref_resolver.h
serializer.h
serializer_traits.h
@@ -43,6 +44,7 @@ set(FORY_SERIALIZATION_HEADERS
type_resolver.h
unsigned_serializer.h
variant_serializer.h
+ weak_ptr_serializer.h
)
add_library(fory_serialization ${FORY_SERIALIZATION_SOURCES})
@@ -97,6 +99,10 @@ if(FORY_BUILD_TESTS)
add_executable(fory_serialization_field_test field_serializer_test.cc)
target_link_libraries(fory_serialization_field_test fory_serialization
GTest::gtest)
gtest_discover_tests(fory_serialization_field_test)
+
+ add_executable(fory_serialization_weak_ptr_test
weak_ptr_serializer_test.cc)
+ target_link_libraries(fory_serialization_weak_ptr_test fory_serialization
GTest::gtest GTest::gtest_main)
+ gtest_discover_tests(fory_serialization_weak_ptr_test)
endif()
# xlang test binary
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index cfd288ea3..8af54a1d0 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -31,6 +31,7 @@
#include "fory/serialization/tuple_serializer.h"
#include "fory/serialization/type_resolver.h"
#include "fory/serialization/variant_serializer.h"
+#include "fory/serialization/weak_ptr_serializer.h"
#include "fory/util/buffer.h"
#include "fory/util/error.h"
#include "fory/util/pool.h"
diff --git a/cpp/fory/serialization/ref_resolver.h
b/cpp/fory/serialization/ref_resolver.h
index cde89a116..5e7dfcba8 100644
--- a/cpp/fory/serialization/ref_resolver.h
+++ b/cpp/fory/serialization/ref_resolver.h
@@ -145,6 +145,19 @@ public:
});
}
+ /// Add a callback that will be invoked when references are resolved.
+ /// The callback receives a const reference to the RefReader and can
+ /// look up references by ID.
+ ///
+ /// This overload is useful for SharedWeak and other types that need
+ /// custom callback logic for forward reference resolution.
+ ///
+ /// @param ref_id The reference ID to wait for (for documentation only).
+ /// @param callback The callback to invoke during resolve_callbacks().
+ void add_update_callback(uint32_t /*ref_id*/, UpdateCallback callback) {
+ callbacks_.emplace_back(std::move(callback));
+ }
+
void resolve_callbacks() {
for (const auto &cb : callbacks_) {
cb(*this);
diff --git a/cpp/fory/serialization/smart_ptr_serializers.h
b/cpp/fory/serialization/smart_ptr_serializers.h
index 0ca9cfc26..4e18cd31e 100644
--- a/cpp/fory/serialization/smart_ptr_serializers.h
+++ b/cpp/fory/serialization/smart_ptr_serializers.h
@@ -544,29 +544,52 @@ template <typename T> struct
Serializer<std::shared_ptr<T>> {
"Cannot use monomorphic deserialization for abstract type"));
return nullptr;
} else {
- T value = Serializer<T>::read(ctx, RefMode::None, false);
- if (ctx.has_error()) {
- return nullptr;
- }
- auto result = std::make_shared<T>(std::move(value));
+ // For circular references: pre-allocate and store BEFORE reading
if (is_first_occurrence) {
+ auto result = std::make_shared<T>();
ctx.ref_reader().store_shared_ref_at(reserved_ref_id, result);
+ T value = Serializer<T>::read(ctx, RefMode::None, false);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ *result = std::move(value);
+ return result;
+ } else {
+ T value = Serializer<T>::read(ctx, RefMode::None, false);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ return std::make_shared<T>(std::move(value));
}
- return result;
}
}
} else {
// Non-polymorphic path: T is guaranteed to be a value type (not pointer
// or nullable wrapper) by static_assert, so no inner ref metadata
needed.
- T value = Serializer<T>::read(ctx, RefMode::None, read_type);
- if (ctx.has_error()) {
- return nullptr;
- }
- auto result = std::make_shared<T>(std::move(value));
+ //
+ // For circular references: we need to pre-allocate the shared_ptr and
+ // store it BEFORE reading the struct fields. This allows forward
+ // references (like selfRef pointing back to the parent) to resolve.
if (is_first_occurrence) {
+ // Pre-allocate with default construction and store immediately
+ auto result = std::make_shared<T>();
ctx.ref_reader().store_shared_ref_at(reserved_ref_id, result);
+ // Read struct data - forward refs can now find this object
+ T value = Serializer<T>::read(ctx, RefMode::None, read_type);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ // Move-assign the read value into the pre-allocated object
+ *result = std::move(value);
+ return result;
+ } else {
+ // Not first occurrence, just read and wrap
+ T value = Serializer<T>::read(ctx, RefMode::None, read_type);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ return std::make_shared<T>(std::move(value));
}
- return result;
}
}
@@ -663,16 +686,26 @@ template <typename T> struct
Serializer<std::shared_ptr<T>> {
return result;
} else {
// T is guaranteed to be a value type by static_assert.
- T value =
- Serializer<T>::read_with_type_info(ctx, RefMode::None, type_info);
- if (ctx.has_error()) {
- return nullptr;
- }
- auto result = std::make_shared<T>(std::move(value));
- if (flag == REF_VALUE_FLAG) {
+ // For circular references: pre-allocate and store BEFORE reading
+ const bool is_first_occurrence = flag == REF_VALUE_FLAG;
+ if (is_first_occurrence) {
+ auto result = std::make_shared<T>();
ctx.ref_reader().store_shared_ref_at(reserved_ref_id, result);
+ T value =
+ Serializer<T>::read_with_type_info(ctx, RefMode::None, type_info);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ *result = std::move(value);
+ return result;
+ } else {
+ T value =
+ Serializer<T>::read_with_type_info(ctx, RefMode::None, type_info);
+ if (ctx.has_error()) {
+ return nullptr;
+ }
+ return std::make_shared<T>(std::move(value));
}
- return result;
}
}
diff --git a/cpp/fory/serialization/type_resolver.h
b/cpp/fory/serialization/type_resolver.h
index 8baa42042..896074dd5 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -535,11 +535,13 @@ template <typename T, size_t Index> struct
FieldInfoBuilder {
field_type.nullable = is_nullable;
field_type.ref_tracking = track_ref;
field_type.ref_mode = make_ref_mode(is_nullable, track_ref);
+#ifdef FORY_DEBUG
// DEBUG: Print field info for debugging fingerprint mismatch
std::cerr << "[xlang][debug] FieldInfoBuilder T=" << typeid(T).name()
<< " Index=" << Index << " field=" << field_name << " has_tags="
<< ::fory::detail::has_field_tags_v<T> << " is_nullable="
<< is_nullable << " track_ref=" << track_ref << std::endl;
+#endif
return FieldInfo(std::move(field_name), std::move(field_type));
}
};
diff --git a/cpp/fory/serialization/weak_ptr_serializer.h
b/cpp/fory/serialization/weak_ptr_serializer.h
new file mode 100644
index 000000000..6f44d57ea
--- /dev/null
+++ b/cpp/fory/serialization/weak_ptr_serializer.h
@@ -0,0 +1,468 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "fory/serialization/serializer.h"
+#include "fory/serialization/smart_ptr_serializers.h"
+#include <memory>
+
+namespace fory {
+namespace serialization {
+
+// ============================================================================
+// SharedWeak<T> - A serializable weak pointer wrapper
+// ============================================================================
+
+/// A serializable wrapper around `std::weak_ptr<T>`.
+///
+/// `SharedWeak<T>` is designed for use in graph-like structures where nodes
+/// may need to hold non-owning references to other nodes (e.g., parent
+/// pointers), and you still want them to round-trip through serialization
+/// while preserving reference identity.
+///
+/// Unlike a raw `std::weak_ptr<T>`, cloning `SharedWeak` keeps all clones
+/// pointing to the same internal storage, so updates via deserialization
+/// callbacks affect all copies. This is critical for forward reference
+/// resolution.
+///
+/// ## When to use
+///
+/// Use this wrapper when your graph structure contains parent/child
+/// relationships or other shared edges where a strong pointer would cause a
+/// reference cycle. Storing a weak pointer avoids owning the target strongly,
+/// but serialization will preserve the link by reference ID.
+///
+/// ## Example - Parent/Child Graph
+///
+/// ```cpp
+/// struct Node {
+/// int32_t value;
+/// SharedWeak<Node> parent; // Non-owning back-reference
+/// std::vector<std::shared_ptr<Node>> children; // Owning references
+/// };
+/// FORY_STRUCT(Node, value, parent, children);
+///
+/// auto parent = std::make_shared<Node>();
+/// parent->value = 1;
+///
+/// auto child = std::make_shared<Node>();
+/// child->value = 2;
+/// child->parent = SharedWeak<Node>::from(parent);
+/// parent->children.push_back(child);
+///
+/// // Serialize and deserialize
+/// auto bytes = fory.serialize(parent);
+/// auto result = fory.deserialize<std::shared_ptr<Node>>(bytes);
+///
+/// // Verify parent reference is preserved
+/// assert(result->children[0]->parent.upgrade() == result);
+/// ```
+///
+/// ## Thread Safety
+///
+/// The `update()` method is NOT thread-safe. This is acceptable because
+/// deserialization is single-threaded. Do not share SharedWeak instances
+/// across threads during deserialization.
+///
+/// ## Null handling
+///
+/// If the target `std::shared_ptr<T>` has been dropped or never assigned,
+/// `upgrade()` returns `nullptr` and serialization will write a `NULL_FLAG`
+/// instead of a reference ID.
+template <typename T> class SharedWeak {
+public:
+ /// Default constructor - creates an empty weak pointer.
+ SharedWeak() : inner_(std::make_shared<Inner>()) {}
+
+ /// Create a SharedWeak from a shared_ptr.
+ ///
+ /// @param ptr The shared_ptr to create a weak reference to.
+ /// @return A SharedWeak pointing to the same object.
+ static SharedWeak from(const std::shared_ptr<T> &ptr) {
+ SharedWeak result;
+ result.inner_->weak = ptr;
+ return result;
+ }
+
+ /// Create a SharedWeak from an existing weak_ptr.
+ ///
+ /// @param weak The weak_ptr to wrap.
+ /// @return A SharedWeak wrapping the weak_ptr.
+ static SharedWeak from_weak(std::weak_ptr<T> weak) {
+ SharedWeak result;
+ result.inner_->weak = std::move(weak);
+ return result;
+ }
+
+ /// Try to upgrade to a strong shared_ptr.
+ ///
+ /// @return The shared_ptr if the target is still alive, nullptr otherwise.
+ std::shared_ptr<T> upgrade() const { return inner_->weak.lock(); }
+
+ /// Update the internal weak pointer.
+ ///
+ /// This method is used during deserialization to resolve forward references.
+ /// All clones of this SharedWeak will see the update because they share
+ /// the same internal storage.
+ ///
+ /// @param weak The new weak_ptr value.
+ void update(std::weak_ptr<T> weak) { inner_->weak = std::move(weak); }
+
+ /// Check if the weak pointer is expired.
+ ///
+ /// @return true if the target has been destroyed or was never set.
+ bool expired() const { return inner_->weak.expired(); }
+
+ /// Get the use count of the target object.
+ ///
+ /// @return The number of shared_ptr instances pointing to the target,
+ /// or 0 if the target has been destroyed.
+ long use_count() const { return inner_->weak.use_count(); }
+
+ /// Get the underlying weak_ptr.
+ ///
+ /// @return A copy of the internal weak_ptr.
+ std::weak_ptr<T> get_weak() const { return inner_->weak; }
+
+ /// Check if two SharedWeak instances point to the same target.
+ ///
+ /// @param other The other SharedWeak to compare.
+ /// @return true if both point to the same object (or both are expired).
+ bool owner_equals(const SharedWeak &other) const {
+ return !inner_->weak.owner_before(other.inner_->weak) &&
+ !other.inner_->weak.owner_before(inner_->weak);
+ }
+
+ /// Copy constructor - shares the internal storage.
+ SharedWeak(const SharedWeak &other) = default;
+
+ /// Move constructor.
+ SharedWeak(SharedWeak &&other) noexcept = default;
+
+ /// Copy assignment - shares the internal storage.
+ SharedWeak &operator=(const SharedWeak &other) = default;
+
+ /// Move assignment.
+ SharedWeak &operator=(SharedWeak &&other) noexcept = default;
+
+private:
+ /// Internal storage that is shared across all copies.
+ struct Inner {
+ std::weak_ptr<T> weak;
+ };
+
+ /// Shared pointer to internal storage.
+ /// All copies of SharedWeak share the same Inner instance.
+ std::shared_ptr<Inner> inner_;
+};
+
+// ============================================================================
+// TypeIndex for SharedWeak<T>
+// ============================================================================
+
+template <typename T> struct TypeIndex<SharedWeak<T>> {
+ static constexpr uint64_t value =
+ fnv1a_64_combine(fnv1a_64("fory::SharedWeak"), type_index<T>());
+};
+
+// ============================================================================
+// requires_ref_metadata trait for SharedWeak<T>
+// ============================================================================
+
+template <typename T>
+struct requires_ref_metadata<SharedWeak<T>> : std::true_type {};
+
+// ============================================================================
+// is_nullable trait for SharedWeak<T>
+// ============================================================================
+
+template <typename T> struct is_nullable<SharedWeak<T>> : std::true_type {};
+
+// ============================================================================
+// nullable_element_type for SharedWeak<T>
+// ============================================================================
+
+template <typename T> struct nullable_element_type<SharedWeak<T>> {
+ using type = T;
+};
+
+// ============================================================================
+// is_shared_weak detection trait
+// ============================================================================
+
+template <typename T> struct is_shared_weak : std::false_type {};
+
+template <typename T> struct is_shared_weak<SharedWeak<T>> : std::true_type {};
+
+template <typename T>
+inline constexpr bool is_shared_weak_v = is_shared_weak<T>::value;
+
+// ============================================================================
+// Serializer<SharedWeak<T>>
+// ============================================================================
+
+/// Serializer for SharedWeak<T>.
+///
+/// SharedWeak requires reference tracking to be enabled (track_ref=true).
+/// During serialization:
+/// - If the weak can upgrade to a strong pointer, writes a reference to it.
+/// - If the weak is null/expired, writes NULL_FLAG.
+///
+/// During deserialization:
+/// - NULL_FLAG: returns empty SharedWeak.
+/// - REF_VALUE_FLAG: deserializes the object, stores it, returns weak to it.
+/// - REF_FLAG: looks up existing ref; if not found (forward ref), registers
+/// a callback to update the weak pointer later.
+template <typename T> struct Serializer<SharedWeak<T>> {
+ static_assert(!std::is_pointer_v<T>,
+ "SharedWeak of raw pointer types is not supported");
+
+ /// Use the inner type's type_id (same as Rust's behavior).
+ static constexpr TypeId type_id = Serializer<T>::type_id;
+
+ static inline void write_type_info(WriteContext &ctx) {
+ Serializer<T>::write_type_info(ctx);
+ }
+
+ static inline void read_type_info(ReadContext &ctx) {
+ Serializer<T>::read_type_info(ctx);
+ }
+
+ static inline void write(const SharedWeak<T> &weak, WriteContext &ctx,
+ RefMode ref_mode, bool write_type,
+ bool has_generics = false) {
+ // SharedWeak requires track_ref to be enabled
+ if (FORY_PREDICT_FALSE(!ctx.track_ref())) {
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak requires track_ref to be enabled. Use
"
+ "Fory::builder().track_ref(true).build()"));
+ return;
+ }
+
+ // SharedWeak requires ref metadata - refuse RefMode::None
+ if (FORY_PREDICT_FALSE(ref_mode == RefMode::None)) {
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak requires ref_mode != RefMode::None"));
+ return;
+ }
+
+ // Try to upgrade the weak pointer
+ std::shared_ptr<T> strong = weak.upgrade();
+ if (!strong) {
+ // Weak is expired or empty - write null
+ ctx.write_int8(NULL_FLAG);
+ return;
+ }
+
+ // Try to write as reference to existing object
+ if (ctx.ref_writer().try_write_shared_ref(ctx, strong)) {
+ // Reference was written, we're done
+ return;
+ }
+
+ // First occurrence - write type info and data
+ if (write_type) {
+ Serializer<T>::write_type_info(ctx);
+ }
+ Serializer<T>::write_data_generic(*strong, ctx, has_generics);
+ }
+
+ static inline void write_data(const SharedWeak<T> &weak, WriteContext &ctx) {
+ ctx.set_error(Error::not_allowed(
+ "SharedWeak should be written using write() to handle reference "
+ "tracking properly"));
+ }
+
+ static inline void write_data_generic(const SharedWeak<T> &weak,
+ WriteContext &ctx, bool has_generics) {
+ ctx.set_error(Error::not_allowed(
+ "SharedWeak should be written using write() to handle reference "
+ "tracking properly"));
+ }
+
+ static inline SharedWeak<T> read(ReadContext &ctx, RefMode ref_mode,
+ bool read_type) {
+ // SharedWeak requires track_ref to be enabled
+ if (FORY_PREDICT_FALSE(!ctx.track_ref())) {
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak requires track_ref to be enabled"));
+ return SharedWeak<T>();
+ }
+
+ // Read the reference flag
+ int8_t flag = ctx.read_int8(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ switch (flag) {
+ case NULL_FLAG:
+ // Null weak pointer
+ return SharedWeak<T>();
+
+ case REF_VALUE_FLAG: {
+ // First occurrence - deserialize the object
+ uint32_t reserved_ref_id = ctx.ref_reader().reserve_ref_id();
+
+ // Read type info if needed
+ if (read_type) {
+ Serializer<T>::read_type_info(ctx);
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+ }
+
+ // Read the data
+ T data = Serializer<T>::read_data(ctx);
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ // Create shared_ptr and store it
+ auto strong = std::make_shared<T>(std::move(data));
+ ctx.ref_reader().store_shared_ref_at(reserved_ref_id, strong);
+
+ // Return weak pointer to it
+ return SharedWeak<T>::from(strong);
+ }
+
+ case REF_FLAG: {
+ // Reference to existing object
+ uint32_t ref_id = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ // Try to get the referenced object
+ auto ref_result = ctx.ref_reader().template get_shared_ref<T>(ref_id);
+ if (ref_result.ok()) {
+ // Object already deserialized - return weak pointer to it
+ return SharedWeak<T>::from(ref_result.value());
+ }
+
+ // Forward reference - create empty weak and register callback
+ SharedWeak<T> result;
+ add_weak_update_callback(ctx.ref_reader(), ref_id, result);
+ return result;
+ }
+
+ case NOT_NULL_VALUE_FLAG:
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak cannot hold a NOT_NULL_VALUE ref"));
+ return SharedWeak<T>();
+
+ default:
+ ctx.set_error(Error::invalid_ref("Unexpected reference flag value: " +
+
std::to_string(static_cast<int>(flag))));
+ return SharedWeak<T>();
+ }
+ }
+
+ static inline SharedWeak<T> read_with_type_info(ReadContext &ctx,
+ RefMode ref_mode,
+ const TypeInfo &type_info) {
+ // SharedWeak requires track_ref to be enabled
+ if (FORY_PREDICT_FALSE(!ctx.track_ref())) {
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak requires track_ref to be enabled"));
+ return SharedWeak<T>();
+ }
+
+ // Read the reference flag
+ int8_t flag = ctx.read_int8(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ switch (flag) {
+ case NULL_FLAG:
+ return SharedWeak<T>();
+
+ case REF_VALUE_FLAG: {
+ uint32_t reserved_ref_id = ctx.ref_reader().reserve_ref_id();
+
+ // Read the data using type info
+ T data =
+ Serializer<T>::read_with_type_info(ctx, RefMode::None, type_info);
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ auto strong = std::make_shared<T>(std::move(data));
+ ctx.ref_reader().store_shared_ref_at(reserved_ref_id, strong);
+
+ return SharedWeak<T>::from(strong);
+ }
+
+ case REF_FLAG: {
+ uint32_t ref_id = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return SharedWeak<T>();
+ }
+
+ auto ref_result = ctx.ref_reader().template get_shared_ref<T>(ref_id);
+ if (ref_result.ok()) {
+ return SharedWeak<T>::from(ref_result.value());
+ }
+
+ // Forward reference
+ SharedWeak<T> result;
+ add_weak_update_callback(ctx.ref_reader(), ref_id, result);
+ return result;
+ }
+
+ case NOT_NULL_VALUE_FLAG:
+ ctx.set_error(
+ Error::invalid_ref("SharedWeak cannot hold a NOT_NULL_VALUE ref"));
+ return SharedWeak<T>();
+
+ default:
+ ctx.set_error(Error::invalid_ref("Unexpected reference flag value: " +
+
std::to_string(static_cast<int>(flag))));
+ return SharedWeak<T>();
+ }
+ }
+
+ static inline SharedWeak<T> read_data(ReadContext &ctx) {
+ ctx.set_error(Error::not_allowed(
+ "SharedWeak should be read using read() or read_with_type_info() to "
+ "handle reference tracking properly"));
+ return SharedWeak<T>();
+ }
+
+private:
+ /// Add a callback to update the weak pointer when the strong pointer becomes
+ /// available.
+ static void add_weak_update_callback(RefReader &ref_reader, uint32_t ref_id,
+ SharedWeak<T> &weak) {
+ // Capture a copy of the SharedWeak - it shares internal storage
+ SharedWeak<T> weak_copy = weak;
+ ref_reader.add_update_callback(
+ ref_id, [weak_copy, ref_id](const RefReader &reader) mutable {
+ auto ref_result = reader.template get_shared_ref<T>(ref_id);
+ if (ref_result.ok()) {
+ weak_copy.update(std::weak_ptr<T>(ref_result.value()));
+ }
+ });
+ }
+};
+
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/weak_ptr_serializer_test.cc
b/cpp/fory/serialization/weak_ptr_serializer_test.cc
new file mode 100644
index 000000000..4d6ed7d19
--- /dev/null
+++ b/cpp/fory/serialization/weak_ptr_serializer_test.cc
@@ -0,0 +1,468 @@
+/*
+ * 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 <string>
+#include <vector>
+
+namespace fory {
+namespace serialization {
+namespace {
+
+// ============================================================================
+// Test Helpers
+// ============================================================================
+
+Fory create_serializer(bool track_ref = true) {
+ return Fory::builder().track_ref(track_ref).build();
+}
+
+// ============================================================================
+// Basic SharedWeak Tests
+// ============================================================================
+
+TEST(SharedWeakTest, DefaultConstructor) {
+ SharedWeak<int32_t> weak;
+ EXPECT_TRUE(weak.expired());
+ EXPECT_EQ(weak.upgrade(), nullptr);
+ EXPECT_EQ(weak.use_count(), 0);
+}
+
+TEST(SharedWeakTest, FromSharedPtr) {
+ auto strong = std::make_shared<int32_t>(42);
+ SharedWeak<int32_t> weak = SharedWeak<int32_t>::from(strong);
+
+ EXPECT_FALSE(weak.expired());
+ EXPECT_EQ(weak.use_count(), 1);
+
+ auto upgraded = weak.upgrade();
+ ASSERT_NE(upgraded, nullptr);
+ EXPECT_EQ(*upgraded, 42);
+ EXPECT_EQ(upgraded, strong);
+}
+
+TEST(SharedWeakTest, FromWeakPtr) {
+ auto strong = std::make_shared<int32_t>(123);
+ std::weak_ptr<int32_t> std_weak = strong;
+ SharedWeak<int32_t> weak = SharedWeak<int32_t>::from_weak(std_weak);
+
+ EXPECT_FALSE(weak.expired());
+ auto upgraded = weak.upgrade();
+ ASSERT_NE(upgraded, nullptr);
+ EXPECT_EQ(*upgraded, 123);
+}
+
+TEST(SharedWeakTest, Update) {
+ SharedWeak<int32_t> weak;
+ EXPECT_TRUE(weak.expired());
+
+ auto strong = std::make_shared<int32_t>(999);
+ weak.update(std::weak_ptr<int32_t>(strong));
+
+ EXPECT_FALSE(weak.expired());
+ auto upgraded = weak.upgrade();
+ ASSERT_NE(upgraded, nullptr);
+ EXPECT_EQ(*upgraded, 999);
+}
+
+TEST(SharedWeakTest, ClonesShareInternalStorage) {
+ auto strong = std::make_shared<int32_t>(100);
+ SharedWeak<int32_t> weak1 = SharedWeak<int32_t>::from(strong);
+ SharedWeak<int32_t> weak2 = weak1; // Copy
+
+ // Both should point to the same object
+ EXPECT_EQ(weak1.upgrade(), weak2.upgrade());
+
+ // Now update via weak2 with a new object
+ auto new_strong = std::make_shared<int32_t>(200);
+ weak2.update(std::weak_ptr<int32_t>(new_strong));
+
+ // weak1 should also see the update (they share internal storage)
+ auto upgraded1 = weak1.upgrade();
+ auto upgraded2 = weak2.upgrade();
+ ASSERT_NE(upgraded1, nullptr);
+ ASSERT_NE(upgraded2, nullptr);
+ EXPECT_EQ(*upgraded1, 200);
+ EXPECT_EQ(*upgraded2, 200);
+ EXPECT_EQ(upgraded1, upgraded2);
+}
+
+TEST(SharedWeakTest, ExpiresWhenStrongDestroyed) {
+ SharedWeak<int32_t> weak;
+ {
+ auto strong = std::make_shared<int32_t>(42);
+ weak = SharedWeak<int32_t>::from(strong);
+ EXPECT_FALSE(weak.expired());
+ }
+ // strong is now out of scope
+ EXPECT_TRUE(weak.expired());
+ EXPECT_EQ(weak.upgrade(), nullptr);
+}
+
+TEST(SharedWeakTest, OwnerEquals) {
+ auto strong1 = std::make_shared<int32_t>(1);
+ auto strong2 = std::make_shared<int32_t>(1); // Same value but different ptr
+
+ SharedWeak<int32_t> weak1a = SharedWeak<int32_t>::from(strong1);
+ SharedWeak<int32_t> weak1b = SharedWeak<int32_t>::from(strong1);
+ SharedWeak<int32_t> weak2 = SharedWeak<int32_t>::from(strong2);
+
+ EXPECT_TRUE(weak1a.owner_equals(weak1b));
+ EXPECT_FALSE(weak1a.owner_equals(weak2));
+}
+
+// ============================================================================
+// Serialization Test Structs
+// ============================================================================
+
+struct SimpleStruct {
+ int32_t value;
+};
+FORY_STRUCT(SimpleStruct, value);
+
+struct StructWithWeak {
+ int32_t id;
+ SharedWeak<SimpleStruct> weak_ref;
+};
+FORY_STRUCT(StructWithWeak, id, weak_ref);
+
+struct StructWithBothRefs {
+ int32_t id;
+ std::shared_ptr<SimpleStruct> strong_ref;
+ SharedWeak<SimpleStruct> weak_ref;
+};
+FORY_STRUCT(StructWithBothRefs, id, strong_ref, weak_ref);
+
+struct MultipleWeakRefsWithOwner {
+ std::shared_ptr<SimpleStruct> owner;
+ SharedWeak<SimpleStruct> weak1;
+ SharedWeak<SimpleStruct> weak2;
+ SharedWeak<SimpleStruct> weak3;
+};
+FORY_STRUCT(MultipleWeakRefsWithOwner, owner, weak1, weak2, weak3);
+
+struct NodeWithParent {
+ int32_t value;
+ SharedWeak<NodeWithParent> parent;
+ std::vector<std::shared_ptr<NodeWithParent>> children;
+};
+FORY_STRUCT(NodeWithParent, value, parent, children);
+
+struct MultipleWeakRefs {
+ SharedWeak<SimpleStruct> weak1;
+ SharedWeak<SimpleStruct> weak2;
+ SharedWeak<SimpleStruct> weak3;
+};
+FORY_STRUCT(MultipleWeakRefs, weak1, weak2, weak3);
+
+// ============================================================================
+// Serialization Tests
+// ============================================================================
+
+TEST(WeakPtrSerializerTest, NullWeakRoundTrip) {
+ StructWithWeak original;
+ original.id = 42;
+ original.weak_ref = SharedWeak<SimpleStruct>(); // Empty weak
+
+ auto fory = create_serializer(true);
+ fory.register_struct<SimpleStruct>(100);
+ fory.register_struct<StructWithWeak>(101);
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<StructWithWeak>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ const auto &deserialized = deserialize_result.value();
+ EXPECT_EQ(deserialized.id, 42);
+ EXPECT_TRUE(deserialized.weak_ref.expired());
+ EXPECT_EQ(deserialized.weak_ref.upgrade(), nullptr);
+}
+
+TEST(WeakPtrSerializerTest, ValidWeakRoundTrip) {
+ // Use a structure that has both strong and weak refs to the same object.
+ // The strong ref keeps the object alive after deserialization.
+ auto target = std::make_shared<SimpleStruct>();
+ target->value = 123;
+
+ StructWithBothRefs original;
+ original.id = 1;
+ original.strong_ref = target;
+ original.weak_ref = SharedWeak<SimpleStruct>::from(target);
+
+ auto fory = create_serializer(true);
+ fory.register_struct<SimpleStruct>(100);
+ fory.register_struct<StructWithBothRefs>(101);
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<StructWithBothRefs>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ const auto &deserialized = deserialize_result.value();
+ EXPECT_EQ(deserialized.id, 1);
+
+ // The strong_ref keeps the object alive
+ ASSERT_NE(deserialized.strong_ref, nullptr);
+ EXPECT_EQ(deserialized.strong_ref->value, 123);
+
+ // The weak should upgrade to the same object as strong_ref
+ auto upgraded = deserialized.weak_ref.upgrade();
+ ASSERT_NE(upgraded, nullptr);
+ EXPECT_EQ(upgraded->value, 123);
+ EXPECT_EQ(upgraded, deserialized.strong_ref);
+}
+
+TEST(WeakPtrSerializerTest, MultipleWeakToSameTarget) {
+ // Use a structure with an owner (strong ref) plus multiple weak refs.
+ // The owner keeps the target alive after deserialization.
+ auto target = std::make_shared<SimpleStruct>();
+ target->value = 999;
+
+ MultipleWeakRefsWithOwner original;
+ original.owner = target;
+ original.weak1 = SharedWeak<SimpleStruct>::from(target);
+ original.weak2 = SharedWeak<SimpleStruct>::from(target);
+ original.weak3 = SharedWeak<SimpleStruct>::from(target);
+
+ auto fory = create_serializer(true);
+ fory.register_struct<SimpleStruct>(100);
+ fory.register_struct<MultipleWeakRefsWithOwner>(102);
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<MultipleWeakRefsWithOwner>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ const auto &deserialized = deserialize_result.value();
+
+ // The owner keeps the object alive
+ ASSERT_NE(deserialized.owner, nullptr);
+ EXPECT_EQ(deserialized.owner->value, 999);
+
+ // All three weak pointers should upgrade to the same object as owner
+ auto upgraded1 = deserialized.weak1.upgrade();
+ auto upgraded2 = deserialized.weak2.upgrade();
+ auto upgraded3 = deserialized.weak3.upgrade();
+
+ ASSERT_NE(upgraded1, nullptr);
+ ASSERT_NE(upgraded2, nullptr);
+ ASSERT_NE(upgraded3, nullptr);
+
+ EXPECT_EQ(upgraded1->value, 999);
+ EXPECT_EQ(upgraded1, deserialized.owner);
+ EXPECT_EQ(upgraded1, upgraded2);
+ EXPECT_EQ(upgraded2, upgraded3);
+}
+
+TEST(WeakPtrSerializerTest, ParentChildGraph) {
+ // Create parent
+ auto parent = std::make_shared<NodeWithParent>();
+ parent->value = 1;
+ parent->parent = SharedWeak<NodeWithParent>(); // No parent
+
+ // Create children that point back to parent
+ auto child1 = std::make_shared<NodeWithParent>();
+ child1->value = 2;
+ child1->parent = SharedWeak<NodeWithParent>::from(parent);
+
+ auto child2 = std::make_shared<NodeWithParent>();
+ child2->value = 3;
+ child2->parent = SharedWeak<NodeWithParent>::from(parent);
+
+ parent->children.push_back(child1);
+ parent->children.push_back(child2);
+
+ auto fory = create_serializer(true);
+ fory.register_struct<NodeWithParent>(103);
+
+ auto bytes_result = fory.serialize(parent);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<std::shared_ptr<NodeWithParent>>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_NE(deserialized, nullptr);
+ EXPECT_EQ(deserialized->value, 1);
+ EXPECT_TRUE(deserialized->parent.expired()); // Root has no parent
+
+ // Check children
+ ASSERT_EQ(deserialized->children.size(), 2u);
+
+ auto des_child1 = deserialized->children[0];
+ ASSERT_NE(des_child1, nullptr);
+ EXPECT_EQ(des_child1->value, 2);
+
+ auto des_child2 = deserialized->children[1];
+ ASSERT_NE(des_child2, nullptr);
+ EXPECT_EQ(des_child2->value, 3);
+
+ // Verify parent references point back to the deserialized parent
+ auto parent_from_child1 = des_child1->parent.upgrade();
+ auto parent_from_child2 = des_child2->parent.upgrade();
+
+ ASSERT_NE(parent_from_child1, nullptr);
+ ASSERT_NE(parent_from_child2, nullptr);
+ EXPECT_EQ(parent_from_child1, deserialized);
+ EXPECT_EQ(parent_from_child2, deserialized);
+}
+
+TEST(WeakPtrSerializerTest, ForwardReferenceResolution) {
+ // Create a structure where the weak reference appears before the strong ref
+ // This tests forward reference resolution
+
+ // In this setup, child1's parent weak appears before the actual parent
object
+ // The serialization order depends on struct field order
+
+ auto parent = std::make_shared<NodeWithParent>();
+ parent->value = 100;
+
+ auto child = std::make_shared<NodeWithParent>();
+ child->value = 200;
+ child->parent = SharedWeak<NodeWithParent>::from(parent);
+
+ // Parent's children list includes the child
+ parent->children.push_back(child);
+
+ auto fory = create_serializer(true);
+ fory.register_struct<NodeWithParent>(103);
+
+ auto bytes_result = fory.serialize(parent);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<std::shared_ptr<NodeWithParent>>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+
+ // Verify the circular reference is preserved
+ ASSERT_EQ(deserialized->children.size(), 1u);
+ auto des_child = deserialized->children[0];
+ auto parent_from_child = des_child->parent.upgrade();
+ ASSERT_NE(parent_from_child, nullptr);
+ EXPECT_EQ(parent_from_child, deserialized);
+}
+
+TEST(WeakPtrSerializerTest, DeepNestedGraph) {
+ // Create a deep chain: A -> B -> C with each child having weak to parent
+ auto nodeA = std::make_shared<NodeWithParent>();
+ nodeA->value = 1;
+
+ auto nodeB = std::make_shared<NodeWithParent>();
+ nodeB->value = 2;
+ nodeB->parent = SharedWeak<NodeWithParent>::from(nodeA);
+
+ auto nodeC = std::make_shared<NodeWithParent>();
+ nodeC->value = 3;
+ nodeC->parent = SharedWeak<NodeWithParent>::from(nodeB);
+
+ nodeA->children.push_back(nodeB);
+ nodeB->children.push_back(nodeC);
+
+ auto fory = create_serializer(true);
+ fory.register_struct<NodeWithParent>(103);
+
+ auto bytes_result = fory.serialize(nodeA);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<std::shared_ptr<NodeWithParent>>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto desA = std::move(deserialize_result).value();
+ ASSERT_NE(desA, nullptr);
+ EXPECT_EQ(desA->value, 1);
+ EXPECT_TRUE(desA->parent.expired());
+
+ ASSERT_EQ(desA->children.size(), 1u);
+ auto desB = desA->children[0];
+ EXPECT_EQ(desB->value, 2);
+ EXPECT_EQ(desB->parent.upgrade(), desA);
+
+ ASSERT_EQ(desB->children.size(), 1u);
+ auto desC = desB->children[0];
+ EXPECT_EQ(desC->value, 3);
+ EXPECT_EQ(desC->parent.upgrade(), desB);
+}
+
+// ============================================================================
+// Error Cases
+// ============================================================================
+
+TEST(WeakPtrSerializerTest, RequiresTrackRef) {
+ auto target = std::make_shared<SimpleStruct>();
+ target->value = 42;
+
+ StructWithWeak original;
+ original.id = 1;
+ original.weak_ref = SharedWeak<SimpleStruct>::from(target);
+
+ // Create serializer WITHOUT track_ref
+ auto fory = Fory::builder().track_ref(false).build();
+ fory.register_struct<SimpleStruct>(100);
+ fory.register_struct<StructWithWeak>(101);
+
+ auto bytes_result = fory.serialize(original);
+ EXPECT_FALSE(bytes_result.ok()) << "Should fail when track_ref is disabled";
+ EXPECT_TRUE(bytes_result.error().to_string().find("track_ref") !=
+ std::string::npos);
+}
+
+// ============================================================================
+// Type Traits Tests
+// ============================================================================
+
+TEST(WeakPtrSerializerTest, TypeTraits) {
+ // Test requires_ref_metadata
+ EXPECT_TRUE(requires_ref_metadata_v<SharedWeak<int32_t>>);
+ EXPECT_TRUE(requires_ref_metadata_v<SharedWeak<SimpleStruct>>);
+
+ // Test is_nullable
+ EXPECT_TRUE(is_nullable_v<SharedWeak<int32_t>>);
+ EXPECT_TRUE(is_nullable_v<SharedWeak<SimpleStruct>>);
+
+ // Test is_shared_weak
+ EXPECT_TRUE(is_shared_weak_v<SharedWeak<int32_t>>);
+ EXPECT_TRUE(is_shared_weak_v<SharedWeak<SimpleStruct>>);
+ EXPECT_FALSE(is_shared_weak_v<std::shared_ptr<int32_t>>);
+ EXPECT_FALSE(is_shared_weak_v<std::weak_ptr<int32_t>>);
+ EXPECT_FALSE(is_shared_weak_v<int32_t>);
+}
+
+} // namespace
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/xlang_test_main.cc
b/cpp/fory/serialization/xlang_test_main.cc
index 9cc1bb9d8..d0558f072 100644
--- a/cpp/fory/serialization/xlang_test_main.cc
+++ b/cpp/fory/serialization/xlang_test_main.cc
@@ -552,6 +552,34 @@ FORY_STRUCT(RefOuterCompatible, inner1, inner2);
FORY_FIELD_TAGS(RefOuterCompatible, (inner1, 0, nullable, ref),
(inner2, 1, nullable, ref));
+// ============================================================================
+// Circular Reference Test Types - Self-referencing struct tests
+// ============================================================================
+
+// Struct for circular reference tests.
+// Contains a self-referencing field and a string field.
+// The 'selfRef' field points back to the same object, creating a circular
+// reference. Note: Using 'selfRef' instead of 'self' because 'self' is a
+// reserved keyword in Rust.
+// Matches Java CircularRefStruct with type ID 601 (schema consistent)
+// and 602 (compatible)
+struct CircularRefStruct {
+ std::string name;
+ std::shared_ptr<CircularRefStruct> selfRef;
+
+ bool operator==(const CircularRefStruct &other) const {
+ if (name != other.name)
+ return false;
+ // Compare selfRef by checking if both are null or both point to same
+ // object (for circular refs, we just check if both have values)
+ bool self_eq = (selfRef == nullptr && other.selfRef == nullptr) ||
+ (selfRef != nullptr && other.selfRef != nullptr);
+ return self_eq;
+ }
+};
+FORY_STRUCT(CircularRefStruct, name, selfRef);
+FORY_FIELD_TAGS(CircularRefStruct, (name, 0), (selfRef, 1, nullable, ref));
+
namespace fory {
namespace serialization {
@@ -730,6 +758,8 @@ void RunTestNullableFieldCompatibleNotNull(const
std::string &data_file);
void RunTestNullableFieldCompatibleNull(const std::string &data_file);
void RunTestRefSchemaConsistent(const std::string &data_file);
void RunTestRefCompatible(const std::string &data_file);
+void RunTestCircularRefSchemaConsistent(const std::string &data_file);
+void RunTestCircularRefCompatible(const std::string &data_file);
} // namespace
int main(int argc, char **argv) {
@@ -825,6 +855,10 @@ int main(int argc, char **argv) {
RunTestRefSchemaConsistent(data_file);
} else if (case_name == "test_ref_compatible") {
RunTestRefCompatible(data_file);
+ } else if (case_name == "test_circular_ref_schema_consistent") {
+ RunTestCircularRefSchemaConsistent(data_file);
+ } else if (case_name == "test_circular_ref_compatible") {
+ RunTestCircularRefCompatible(data_file);
} else {
Fail("Unknown test case: " + case_name);
}
@@ -2338,4 +2372,85 @@ void RunTestRefCompatible(const std::string &data_file) {
WriteFile(data_file, out);
}
+// ============================================================================
+// Circular Reference Tests - Self-referencing struct tests
+// ============================================================================
+
+void RunTestCircularRefSchemaConsistent(const std::string &data_file) {
+ auto bytes = ReadFile(data_file);
+ // SCHEMA_CONSISTENT mode: compatible=false, xlang=true,
+ // check_struct_version=true, track_ref=true
+ auto fory = BuildFory(false, true, true, true);
+ EnsureOk(fory.register_struct<CircularRefStruct>(601),
+ "register CircularRefStruct");
+
+ Buffer buffer = MakeBuffer(bytes);
+ auto obj = ReadNext<std::shared_ptr<CircularRefStruct>>(fory, buffer);
+
+ // The object should not be null
+ if (obj == nullptr) {
+ Fail("CircularRefStruct: obj should not be null");
+ }
+
+ // Verify the name field
+ if (obj->name != "circular_test") {
+ Fail("CircularRefStruct: name should be 'circular_test', got " +
obj->name);
+ }
+
+ // selfRef should point to the same object (circular reference)
+ if (obj->selfRef == nullptr) {
+ Fail("CircularRefStruct: selfRef should not be null");
+ }
+
+ // The key test: selfRef should point back to the same object
+ if (obj->selfRef.get() != obj.get()) {
+ Fail("CircularRefStruct: selfRef should point to same object (circular "
+ "reference)");
+ }
+
+ // Re-serialize and write back
+ std::vector<uint8_t> out;
+ AppendSerialized(fory, obj, out);
+ WriteFile(data_file, out);
+}
+
+void RunTestCircularRefCompatible(const std::string &data_file) {
+ auto bytes = ReadFile(data_file);
+ // COMPATIBLE mode: compatible=true, xlang=true, check_struct_version=false,
+ // track_ref=true
+ auto fory = BuildFory(true, true, false, true);
+ EnsureOk(fory.register_struct<CircularRefStruct>(602),
+ "register CircularRefStruct");
+
+ Buffer buffer = MakeBuffer(bytes);
+ auto obj = ReadNext<std::shared_ptr<CircularRefStruct>>(fory, buffer);
+
+ // The object should not be null
+ if (obj == nullptr) {
+ Fail("CircularRefStruct: obj should not be null");
+ }
+
+ // Verify the name field
+ if (obj->name != "compatible_circular") {
+ Fail("CircularRefStruct: name should be 'compatible_circular', got " +
+ obj->name);
+ }
+
+ // selfRef should point to the same object (circular reference)
+ if (obj->selfRef == nullptr) {
+ Fail("CircularRefStruct: selfRef should not be null");
+ }
+
+ // The key test: selfRef should point back to the same object
+ if (obj->selfRef.get() != obj.get()) {
+ Fail("CircularRefStruct: selfRef should point to same object (circular "
+ "reference)");
+ }
+
+ // Re-serialize and write back
+ std::vector<uint8_t> out;
+ AppendSerialized(fory, obj, out);
+ WriteFile(data_file, out);
+}
+
} // namespace
diff --git
a/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
b/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
index 302f2c152..7cc0e9c2e 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
@@ -385,16 +385,12 @@ public class CPPXlangTest extends XlangTestBase {
@Override
@Test(dataProvider = "enableCodegen")
public void testCircularRefSchemaConsistent(boolean enableCodegen) throws
java.io.IOException {
- // Skip: C++ doesn't have circular reference support yet
- throw new SkipException(
- "Skipping testCircularRefSchemaConsistent: C++ circular reference not
implemented");
+ super.testCircularRefSchemaConsistent(enableCodegen);
}
@Override
@Test(dataProvider = "enableCodegen")
public void testCircularRefCompatible(boolean enableCodegen) throws
java.io.IOException {
- // Skip: C++ doesn't have circular reference support yet
- throw new SkipException(
- "Skipping testCircularRefCompatible: C++ circular reference not
implemented");
+ super.testCircularRefCompatible(enableCodegen);
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]