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]

Reply via email to