From 3d4196c292b01a1dff2abd77e125c1ec4679ed93 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 6 Nov 2026 20:20:24 +0800 Subject: [PATCH 1/6] feat(native): add object-level change version for O(1) subtree dirty checks * feat(native): monotonic per-object change stamp with eager upward propagation Every NativeObject now owns a monotonically increasing version_ taken from a global saturated counter. Any write (Set*, Del/Clear, SetNil, SetId/GroupId, finalizer/method registration, MarkModified, container mutation, child link) bumps it and propagates the same stamp to all ancestors, so a parent's version is always >= any descendant's. Reads never touch it. Design choice: eager propagation of a global timestamp + per-object max, instead of lazy recursive comparison (which is not O(1)) or a parent-side sum of child versions (not monotonic under child removal/sharing). Cost: query is O(1), write is O(number of ancestors); ~8B version, 8B propagation epoch per object plus lazily allocated parent/child edge sets. Track child edges (children ref counts + parents set) so detaching, re-parenting and object destruction can bump former ancestors and avoid dangling pointers. Destroy() detaches first, then frees Impl; version_ lives outside Impl and keeps increasing after Destroy. Public API (include/fakelua.h): uint64_t GetVersion() const; // current subtree stamp bool IsChangedSince(uint64_t v) const; // O(1) dirty test void MarkModified(); // external state linked to an object static void SetVersionDebugEnabled(bool); // FNV-1a content hash self-check static bool SelfCheck(const NativeObject*, bool recursive = true); Callers remember the last seen version per object and compare later; no payload or serialization change, versions are never persisted. * fix(native): guard propagation walk against roots without parent sets Only push parents that actually own a parent edge table; a root reached during the upward walk previously dereferenced a null unique_ptr in the lazy edge bookkeeping. * test(native): cover writes, reads, nesting, saturation, cycles and self-check Add 8 GTest cases plus Lua fixtures covering every write entry point, read-only access paths (direct, snapshot, pairs, containers), nested propagation and sibling isolation, child create/destroy/clear, monotonicity and uint64 saturation, debug hash detection of a deliberately unmarked raw write, cycle guarding, and Lua/container paths. * perf(native): add NativeObject micro-benchmarks for cross-revision comparison SetInt/GetInt/SetString/CreateDestroy/SetObject/DeepLeafWrite use only APIs that exist both before and after this change so numbers stay comparable. --- benchmark/benchmark_native_object.cpp | 102 ++++++ include/fakelua.h | 47 +++ src/native/container/native_container.cpp | 55 +++- src/native/object/native_object.cpp | 378 +++++++++++++++++++++- src/native/object/native_object.h | 18 ++ test/lua/native/test_native_get_set.lua | 38 +++ test/test_native.cpp | 296 +++++++++++++++++ 7 files changed, 918 insertions(+), 16 deletions(-) create mode 100644 benchmark/benchmark_native_object.cpp diff --git a/benchmark/benchmark_native_object.cpp b/benchmark/benchmark_native_object.cpp new file mode 100644 index 00000000..e3b5733c --- /dev/null +++ b/benchmark/benchmark_native_object.cpp @@ -0,0 +1,102 @@ +#include "benchmark/benchmark.h" +#include "fakelua.h" + +#include + +using namespace fakelua; + +// NativeObject 热路径基准:字段读写、对象创建、父子绑定与子树版本传播。 +// 这些用例只使用长期存在的公开 API,因此同一份基准可分别跑在加版本号前后的构建上。 + +static void BM_NativeObjectSetInt(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "bench", 1); + for (auto _: state) { + obj->SetInt("hp", 42); + benchmark::DoNotOptimize(obj); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +static void BM_NativeObjectGetInt(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "bench", 1); + obj->SetInt("hp", 42); + for (auto _: state) { + benchmark::DoNotOptimize(obj->GetInt("hp")); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +static void BM_NativeObjectSetString(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "bench", 1); + const std::string value = "benchmark-value"; + for (auto _: state) { + obj->SetString("name", value); + benchmark::DoNotOptimize(obj); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +static void BM_NativeObjectCreateDestroy(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + int64_t id = 0; + for (auto _: state) { + auto *obj = mgr.Create(gid, "bench", ++id); + benchmark::DoNotOptimize(obj); + mgr.DestroySingle("bench", id); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +static void BM_NativeObjectSetObject(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *parent = mgr.Create(gid, "bench_parent", 1); + auto *child = mgr.Create(gid, "bench_child", 2); + for (auto _: state) { + parent->SetObject("child", child); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +// 8 层父子链上写叶子:用于观察子树脏版本传播的 O(depth) 开销。 +static void BM_NativeObjectDeepLeafWrite(benchmark::State &state) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + NativeObject *cur = mgr.Create(gid, "bench_node", 1); + for (int i = 0; i < 8; ++i) { + auto *child = mgr.Create(gid, "bench_node", i + 2); + cur->SetObject("child", child); + cur = child; + } + for (auto _: state) { + cur->SetInt("leaf", 1); + benchmark::DoNotOptimize(cur); + } + mgr.Clear(); + FakeluaDeleteState(s); +} + +BENCHMARK(BM_NativeObjectSetInt); +BENCHMARK(BM_NativeObjectGetInt); +BENCHMARK(BM_NativeObjectSetString); +BENCHMARK(BM_NativeObjectCreateDestroy); +BENCHMARK(BM_NativeObjectSetObject); +BENCHMARK(BM_NativeObjectDeepLeafWrite); diff --git a/include/fakelua.h b/include/fakelua.h index 4f67c46c..a7196522 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -772,6 +773,19 @@ namespace io { void RegisterIoLibraryApi(State *s); } +// 仅用于单元测试的白盒访问器,不属于稳定公开 API。生产代码不得使用。 +// 定义见 src/native/object/native_object.cpp。 +struct NativeObjectVersionTestAccess { + // 绕过版本号/父子边维护直接写字段,用于验证 SelfCheck 能发现漏标写入。 + static void RawSetInt(NativeObject *obj, std::string_view key, int64_t val); + // 调整全局版本戳,用于测试饱和边界。 + static void SetStamp(uint64_t stamp); + [[nodiscard]] static uint64_t GetStamp(); + // 转发到 NativeObject 的私有 CVar 写入/读取路径。 + static void SetFromCVar(NativeObject *obj, std::string_view key, CVar v); + [[nodiscard]] static CVar GetAsCVar(NativeObject *obj, std::string_view key, State *s); +}; + class NativeObject { public: // 禁止拷贝 @@ -812,6 +826,26 @@ class NativeObject { void Del(std::string_view key); void Clear(); + // ---- 对象级变更版本号(增量存档 / C++ 镜像同步 / UI 缓存失效)---- + // 语义:每次写入(字段、方法、子对象关系、Clear)都会让本对象及其所有祖先的 + // 版本号单调变大;读取不改变版本号。返回的是全局单调时间戳,不是计数器, + // 只应与同一对象自己上一次记录的版本号比较,不应跨对象比较大小。 + // 调用方:记下 v = obj->GetVersion();之后用 obj->IsChangedSince(v) 做 O(1) 判断。 + // 子树的任何写入都会向上传播,因此对根对象取版本即可覆盖整棵子树。 + // 版本号不写入存档、不改变既有数据格式;对象销毁后仍保留最后一次版本号。 + [[nodiscard]] uint64_t GetVersion() const; + [[nodiscard]] bool IsChangedSince(uint64_t version) const; + + // 显式标记本对象(及其祖先)已变更。用于通过字段写入无法表达的副作用修改, + // 例如 native/container 内部容器内容、外部资源状态等。 + void MarkModified(); + + // 调试自检(默认关闭):开启后每次写入都会维护内容哈希;SelfCheck 会重新计算 + // 字段/方法内容哈希并与记录值比对,并递归校验「父版本 >= 子版本」, + // 用于交叉验证是否有写入点漏标版本。只在调试/测试中使用,开销较大。 + static void SetVersionDebugEnabled(bool enabled); + [[nodiscard]] static bool SelfCheck(const NativeObject *obj, bool recursive = true); + // 销毁回调 // 设置 NativeObject 被 Destroy 时(含管理器 Clear / DestroyGroup)的回调, // 用于释放该对象拥有的 C++ 资源(如 socket)。回调内不应再操作 Lua 状态。 @@ -832,10 +866,22 @@ class NativeObject { private: struct Impl; Impl *impl_; + // 对象级版本戳保存在 NativeObject 本身(不随 Impl 释放), + // 因此 Destroy 之后仍能查到最后一次版本,保持单调性。 + uint64_t version_ = 0; explicit NativeObject(std::string type_name); ~NativeObject(); + // 版本号内部维护 + void TouchVersion(); + void LinkChild(NativeObject *child); + void UnlinkChild(NativeObject *child); + // 销毁/析构时断开父子边:从父对象移除自己并 bump 父对象,从子对象移除自己但不 bump。 + void DetachAll(); + void RefreshDebugHash(); + [[nodiscard]] static uint64_t ComputeContentHash(const NativeObject *obj); + // 内部申请与释放(只能由 NativeObjectManager 管理调配) static NativeObject *Create(int64_t group_id, std::string type_name, int64_t id = 0); static void Destroy(NativeObject *obj); @@ -845,6 +891,7 @@ class NativeObject { [[nodiscard]] CVar GetAsCVar(std::string_view key, State *s) const; friend class NativeObjectManager; + friend struct NativeObjectVersionTestAccess; friend CVar NativeSpecGet(VarTable *tbl, CVar k, bool *finish); friend void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish); friend void RefreshSpecKeys(VarTable *tbl, const NativeObject *obj, State *s); diff --git a/src/native/container/native_container.cpp b/src/native/container/native_container.cpp index 10cb1bab..f084e400 100644 --- a/src/native/container/native_container.cpp +++ b/src/native/container/native_container.cpp @@ -249,6 +249,8 @@ CVar ClearOf(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { c->set.clear(); break; } + // 容器内容存放在 NativeObject 之外的 C++ 结构中,字段写入无法感知,需要显式标记版本变更。 + self->MarkModified(); return inter::NativeToFakeluaNil(s); } @@ -257,12 +259,14 @@ CVar ClearOf(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { CVar DequePushBack(NativeObject *self, State *s, CVar *args, int n) { if (n < 1) ThrowBadArgument(1, "deque:push_back", "value expected"); Require(self)->deque.push_back(PersistValue(inter::GetNativeArg(s, args, n, 0), "deque:push_back")); + self->MarkModified(); return inter::NativeToFakeluaNil(s); } CVar DequePushFront(NativeObject *self, State *s, CVar *args, int n) { if (n < 1) ThrowBadArgument(1, "deque:push_front", "value expected"); Require(self)->deque.push_front(PersistValue(inter::GetNativeArg(s, args, n, 0), "deque:push_front")); + self->MarkModified(); return inter::NativeToFakeluaNil(s); } @@ -271,6 +275,7 @@ CVar DequePopBack(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { if (d.empty()) return inter::NativeToFakeluaNil(s); NativeField v = std::move(d.back()); d.pop_back(); + self->MarkModified(); return FieldToLua(v, s); } @@ -279,6 +284,7 @@ CVar DequePopFront(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { if (d.empty()) return inter::NativeToFakeluaNil(s); NativeField v = std::move(d.front()); d.pop_front(); + self->MarkModified(); return FieldToLua(v, s); } @@ -310,6 +316,7 @@ CVar DequeSet(NativeObject *self, State *s, CVar *args, int n) { ThrowBadArgument(1, "deque:set", "index out of range"); } d[static_cast(i - 1)] = PersistValue(inter::GetNativeArg(s, args, n, 1), "deque:set"); + self->MarkModified(); return inter::NativeToFakeluaNil(s); } @@ -329,6 +336,7 @@ CVar MapSet(NativeObject *self, State *s, CVar *args, int n) { auto &m = Require(self)->map; ContainerKey k = ContainerKey::FromCVar(inter::GetNativeArg(s, args, n, 0)); m[std::move(k)] = PersistValue(inter::GetNativeArg(s, args, n, 1), "map:set"); + self->MarkModified(); return inter::NativeToFakeluaNil(s); } @@ -350,6 +358,7 @@ CVar MapErase(NativeObject *self, State *s, CVar *args, int n) { if (n < 1) ThrowBadArgument(1, "map:erase", "key expected"); auto &m = Require(self)->map; bool ok = m.erase(ContainerKey::FromCVar(inter::GetNativeArg(s, args, n, 0))) > 0; + self->MarkModified(); return inter::NativeToFakeluaBool(s, ok); } @@ -378,6 +387,7 @@ CVar SetInsert(NativeObject *self, State *s, CVar *args, int n) { if (n < 1) ThrowBadArgument(1, "set:insert", "value expected"); auto &st = Require(self)->set; auto r = st.insert(ContainerKey::FromCVar(inter::GetNativeArg(s, args, n, 0))); + self->MarkModified(); return inter::NativeToFakeluaBool(s, r.second); } @@ -391,6 +401,7 @@ CVar SetErase(NativeObject *self, State *s, CVar *args, int n) { if (n < 1) ThrowBadArgument(1, "set:erase", "value expected"); auto &st = Require(self)->set; bool ok = st.erase(ContainerKey::FromCVar(inter::GetNativeArg(s, args, n, 0))) > 0; + self->MarkModified(); return inter::NativeToFakeluaBool(s, ok); } @@ -506,10 +517,14 @@ boost::container::list &ListOf(NativeObject *self) { } CVar VectorPushBack(NativeObject *self, State *s, CVar *args, int n) { - return SeqPushBack(VecOf(self), s, args, n, "vector:push_back"); + auto r = SeqPushBack(VecOf(self), s, args, n, "vector:push_back"); + self->MarkModified(); + return r; } CVar VectorPopBack(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { - return SeqPopBack(VecOf(self), s); + auto r = SeqPopBack(VecOf(self), s); + self->MarkModified(); + return r; } CVar VectorFront(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqFront(VecOf(self), s); @@ -521,17 +536,23 @@ CVar VectorAt(NativeObject *self, State *s, CVar *args, int n) { return SeqAt(VecOf(self), s, args, n, "vector:at"); } CVar VectorSet(NativeObject *self, State *s, CVar *args, int n) { - return SeqSet(VecOf(self), s, args, n, "vector:set"); + auto r = SeqSet(VecOf(self), s, args, n, "vector:set"); + self->MarkModified(); + return r; } CVar VectorToTable(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqToTable(VecOf(self), s); } CVar SmallPushBack(NativeObject *self, State *s, CVar *args, int n) { - return SeqPushBack(SmallOf(self), s, args, n, "small_vector:push_back"); + auto r = SeqPushBack(SmallOf(self), s, args, n, "small_vector:push_back"); + self->MarkModified(); + return r; } CVar SmallPopBack(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { - return SeqPopBack(SmallOf(self), s); + auto r = SeqPopBack(SmallOf(self), s); + self->MarkModified(); + return r; } CVar SmallFront(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqFront(SmallOf(self), s); @@ -543,23 +564,33 @@ CVar SmallAt(NativeObject *self, State *s, CVar *args, int n) { return SeqAt(SmallOf(self), s, args, n, "small_vector:at"); } CVar SmallSet(NativeObject *self, State *s, CVar *args, int n) { - return SeqSet(SmallOf(self), s, args, n, "small_vector:set"); + auto r = SeqSet(SmallOf(self), s, args, n, "small_vector:set"); + self->MarkModified(); + return r; } CVar SmallToTable(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqToTable(SmallOf(self), s); } CVar ListPushBack(NativeObject *self, State *s, CVar *args, int n) { - return SeqPushBack(ListOf(self), s, args, n, "list:push_back"); + auto r = SeqPushBack(ListOf(self), s, args, n, "list:push_back"); + self->MarkModified(); + return r; } CVar ListPushFront(NativeObject *self, State *s, CVar *args, int n) { - return SeqPushFront(ListOf(self), s, args, n, "list:push_front"); + auto r = SeqPushFront(ListOf(self), s, args, n, "list:push_front"); + self->MarkModified(); + return r; } CVar ListPopBack(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { - return SeqPopBack(ListOf(self), s); + auto r = SeqPopBack(ListOf(self), s); + self->MarkModified(); + return r; } CVar ListPopFront(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { - return SeqPopFront(ListOf(self), s); + auto r = SeqPopFront(ListOf(self), s); + self->MarkModified(); + return r; } CVar ListFront(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqFront(ListOf(self), s); @@ -571,7 +602,9 @@ CVar ListAt(NativeObject *self, State *s, CVar *args, int n) { return SeqAt(ListOf(self), s, args, n, "list:at"); } CVar ListSet(NativeObject *self, State *s, CVar *args, int n) { - return SeqSet(ListOf(self), s, args, n, "list:set"); + auto r = SeqSet(ListOf(self), s, args, n, "list:set"); + self->MarkModified(); + return r; } CVar ListToTable(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { return SeqToTable(ListOf(self), s); diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 1f1175b1..2bfada74 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -9,13 +9,55 @@ #include "var/var_multi.h" #include +#include #include #include #include #include +#include namespace fakelua { +namespace { + +// ---- 对象级版本戳基础设施 ---- +// 版本号使用全局单调时间戳(不是每对象的计数器),这样父对象只需保存 +// max(自身戳, 所有子对象戳) 就能在 O(1) 内回答「整棵子树自某版本后是否变过」。 +constexpr uint64_t kMaxVersionStamp = std::numeric_limits::max(); + +// 全局时间戳:每次写入分配一个更大的值。饱和而不是回绕,保证单调性永不被破坏。 +std::atomic g_version_seq{0}; +// 传播去重/防环用的 epoch。每个对象记录最后一次访问它的 epoch,命中即停止。 +std::atomic g_prop_epoch{0}; +// 调试自检开关(默认关闭)。 +std::atomic g_version_debug{false}; +// 调试 epoch:每次切换调试开关都递增,使旧对象上记录的哈希失效。 +std::atomic g_debug_epoch{0}; + +uint64_t NextVersionStamp() { + const uint64_t old = g_version_seq.fetch_add(1, std::memory_order_relaxed); + if (old == kMaxVersionStamp) { + // fetch_add 已把计数器回绕成 0,这里写回 MAX,之后每次调用都返回 MAX。 + g_version_seq.store(kMaxVersionStamp, std::memory_order_relaxed); + return kMaxVersionStamp; + } + return old + 1; +} + +inline void FnvHashBytes(uint64_t &h, const void *data, size_t n) { + const unsigned char *p = static_cast(data); + for (size_t i = 0; i < n; ++i) { + h ^= static_cast(p[i]); + h *= 1099511628211ULL; + } +} + +inline void FnvHashU64(uint64_t &h, uint64_t v) { + FnvHashBytes(h, &v, sizeof(v)); +} + +}// namespace + // CVar ↔ NativeField 转换 CVar NativeFieldToCVar(const NativeField &field, State *s) { @@ -483,19 +525,35 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { const std::string skey(key); State *s = spec->state; + // 记录被覆盖前的字段(可能是嵌套对象),写入后维护父子边并 bump 版本号。 + const auto old_it = obj->impl_->kv.find(skey); + NativeObject *prev_obj = nullptr; + if (old_it != obj->impl_->kv.end() && old_it->second.kind == NativeField::Kind::Object) { + prev_obj = old_it->second.obj; + } + // 快照尚未建立:只写 kv,保持惰性;首次 pairs/next/GET_TABLE_ENTRY 再 RefreshSpecKeys。 if (!spec->snapshot_ready) { if (v.type_ == static_cast(VarType::Nil)) { obj->impl_->kv.erase(skey); } else { - obj->impl_->kv[skey] = CVarToNativeField(v); + auto new_field = CVarToNativeField(v); + obj->impl_->kv[skey] = new_field; + if (prev_obj && prev_obj != new_field.obj) obj->UnlinkChild(prev_obj); + if (new_field.obj && prev_obj != new_field.obj) obj->LinkChild(new_field.obj); + obj->TouchVersion(); + return; } + if (prev_obj) obj->UnlinkChild(prev_obj); + obj->TouchVersion(); return; } // 快照已存在:增量维护(更新已有键 / 追加 / swap-remove),避免每次 O(n) 全量重建。 if (v.type_ == static_cast(VarType::Nil)) { obj->impl_->kv.erase(skey); + if (prev_obj) obj->UnlinkChild(prev_obj); + obj->TouchVersion(); const int idx = FindSpecKeyIndex(tbl, key); if (idx >= 0 && tbl->spec_count > 0) { const uint32_t uidx = static_cast(idx); @@ -509,7 +567,11 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { return; } - obj->impl_->kv[skey] = CVarToNativeField(v); + auto new_field = CVarToNativeField(v); + if (prev_obj && prev_obj != new_field.obj) obj->UnlinkChild(prev_obj); + if (new_field.obj && prev_obj != new_field.obj) obj->LinkChild(new_field.obj); + obj->impl_->kv[skey] = new_field; + obj->TouchVersion(); const CVar new_val = NativeFieldToCVar(obj->impl_->kv[skey], s); const int idx = FindSpecKeyIndex(tbl, key); @@ -528,11 +590,16 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { // NativeObject 公开实现 NativeObject::NativeObject(std::string type_name) : impl_(new Impl{std::move(type_name)}) { + // 创建本身也是一次变化:分配一个全新的时间戳,保证新对象的版本号大于任何既有记录。 + version_ = NextVersionStamp(); + if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); } NativeObject::~NativeObject() { // 触发销毁回调,让拥有者释放关联的 C++ 资源(在 impl_ 仍有效时调用)。 if (impl_ && impl_->finalizer) impl_->finalizer(this); + // 未走 NativeObject::Destroy 直接析构时,也要断开父子边,避免幸存对象持有悬空指针。 + DetachAll(); delete impl_; impl_ = nullptr; } @@ -544,6 +611,7 @@ bool NativeObject::Alive() const { void NativeObject::SetFinalizer(const std::function &fn) { if (!impl_) return; impl_->finalizer = fn; + TouchVersion(); } NativeObject *NativeObject::Create(int64_t group_id, std::string type_name, int64_t id) { @@ -561,6 +629,8 @@ void NativeObject::Destroy(NativeObject *obj) { obj->impl_->finalizer = nullptr; fn(obj); } + // 子对象被删除属于父对象子树的变化,需要让父对象(及其祖先)的版本号前进。 + obj->DetachAll(); delete obj->impl_; obj->impl_ = nullptr; } @@ -626,18 +696,31 @@ bool NativeObject::Has(std::string_view key) const { void NativeObject::Del(std::string_view key) { if (!impl_) return; - impl_->kv.erase(std::string(key)); + auto it = impl_->kv.find(std::string(key)); + if (it == impl_->kv.end()) { + // 删除不存在的 key 仍然算一次写,保持「任何写都 bump」的简单语义。 + TouchVersion(); + return; + } + if (it->second.kind == NativeField::Kind::Object) UnlinkChild(it->second.obj); + impl_->kv.erase(it); + TouchVersion(); } void NativeObject::Clear() { if (!impl_) return; + for (const auto &[_, field]: impl_->kv) { + if (field.kind == NativeField::Kind::Object) UnlinkChild(field.obj); + } impl_->kv.clear(); impl_->methods.clear(); + TouchVersion(); } void NativeObject::RegisterMethod(std::string_view name, NativeMethod method) { if (!impl_) return; impl_->methods[std::string(name)] = std::move(method); + TouchVersion(); } bool NativeObject::HasMethod(std::string_view name) const { @@ -648,6 +731,7 @@ bool NativeObject::HasMethod(std::string_view name) const { void NativeObject::UnregisterMethod(std::string_view name) { if (!impl_) return; impl_->methods.erase(std::string(name)); + TouchVersion(); } size_t NativeObject::Size() const { @@ -657,48 +741,78 @@ size_t NativeObject::Size() const { // Set 系列 void NativeObject::SetNil(std::string_view key) { if (!impl_) return; - impl_->kv.erase(std::string(key)); + auto it = impl_->kv.find(std::string(key)); + if (it != impl_->kv.end()) { + if (it->second.kind == NativeField::Kind::Object) UnlinkChild(it->second.obj); + impl_->kv.erase(it); + } + TouchVersion(); } void NativeObject::SetInt(std::string_view key, int64_t val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; + if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Int; f.i = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetFloat(std::string_view key, double val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; + if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Float; f.f = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetBool(std::string_view key, bool val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; + if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Bool; f.b = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetString(std::string_view key, std::string_view val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; + if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::String; f.s = std::string(val); f.vs_dirty = true; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetObject(std::string_view key, NativeObject *obj) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; + NativeObject *prev = f.kind == NativeField::Kind::Object ? f.obj : nullptr; + if (prev && prev != obj) UnlinkChild(prev); + if (obj) { + if (prev != obj) LinkChild(obj); + } f.kind = NativeField::Kind::Object; f.obj = obj; + TouchVersion(); } void NativeObject::SetFromCVar(std::string_view key, CVar v) { if (!impl_) return; - impl_->kv[std::string(key)] = CVarToNativeField(v); + auto nf = CVarToNativeField(v); + auto it = impl_->kv.find(std::string(key)); + NativeObject *prev = nullptr; + if (it != impl_->kv.end() && it->second.kind == NativeField::Kind::Object) prev = it->second.obj; + if (prev && prev != nf.obj) UnlinkChild(prev); + if (nf.obj && prev != nf.obj) LinkChild(nf.obj); + impl_->kv[std::string(key)] = nf; + TouchVersion(); } // Get 系列 @@ -778,6 +892,7 @@ int64_t NativeObject::GetId() const { void NativeObject::SetId(int64_t id) { if (!impl_) return; impl_->id = id; + TouchVersion(); } int64_t NativeObject::GetGroupId() const { @@ -787,6 +902,7 @@ int64_t NativeObject::GetGroupId() const { void NativeObject::SetGroupId(int64_t group_id) { if (!impl_) return; impl_->group_id = group_id; + TouchVersion(); } // Iterate(只读快照) @@ -903,4 +1019,256 @@ void RegisterNativeObjectApi(State *s) { io::RegisterIoLibraryApi(s); } +// ---- 对象级变更版本号实现 ---- + +uint64_t NativeObject::GetVersion() const { + // Impl 释放后仍保留最后一次版本号,避免销毁后版本“归零”破坏单调性。 + return version_; +} + +bool NativeObject::IsChangedSince(uint64_t version) const { + return version_ > version; +} + +void NativeObject::SetVersionDebugEnabled(bool enabled) { + g_version_debug.store(enabled, std::memory_order_relaxed); + // 失效旧 epoch 的哈希:关闭期间发生的写入不会被误判为“漏标”。 + g_debug_epoch.fetch_add(1, std::memory_order_relaxed); +} + +void NativeObject::MarkModified() { + if (!impl_) return; + TouchVersion(); +} + +void NativeObject::LinkChild(NativeObject *child) { + if (!impl_ || !child || !child->impl_) return; + if (!impl_->children) { + impl_->children = std::make_unique>(); + } else { + auto it = impl_->children->find(child); + if (it != impl_->children->end()) { + ++it->second; + return; + } + } + impl_->children->emplace(child, 1u); + if (!child->impl_->parents) { + child->impl_->parents = std::make_unique>(); + } + child->impl_->parents->insert(this); +} + +void NativeObject::UnlinkChild(NativeObject *child) { + if (!impl_ || !child || !impl_->children) return; + auto it = impl_->children->find(child); + if (it == impl_->children->end()) return; + if (it->second > 1) { + --it->second; + return; + } + impl_->children->erase(it); + if (impl_->children->empty()) impl_->children.reset(); + if (child->impl_ && child->impl_->parents) { + child->impl_->parents->erase(this); + if (child->impl_->parents->empty()) child->impl_->parents.reset(); + } +} + +void NativeObject::DetachAll() { + if (!impl_) return; + // 1) 从所有父对象的直接子表里移除自己;父对象的子树发生变化,需要 bump。 + // 复制一份 parents,避免传播过程中任何潜在的结构变动影响遍历。 + boost::container::small_vector parent_list; + if (impl_->parents) { + parent_list.assign(impl_->parents->begin(), impl_->parents->end()); + impl_->parents->clear(); + } + for (NativeObject *p: parent_list) { + if (!p || !p->impl_ || p == this || !p->impl_->children) continue; + p->impl_->children->erase(this); + if (p->impl_->children->empty()) p->impl_->children.reset(); + p->TouchVersion(); + } + // 2) 从所有子对象的父集合里移除自己;子对象自身内容未变,不需要 bump。 + if (impl_->children) { + for (const auto &[child, _]: *impl_->children) { + if (child && child->impl_ && child != this && child->impl_->parents) { + child->impl_->parents->erase(this); + if (child->impl_->parents->empty()) child->impl_->parents.reset(); + } + } + impl_->children->clear(); + } + impl_->children.reset(); + impl_->parents.reset(); +} + +void NativeObject::TouchVersion() { + if (!impl_) return; + // 全局单调时间戳:比任何已分配的版本号都大(除非已饱和)。 + const uint64_t stamp = NextVersionStamp(); + if (version_ < stamp) version_ = stamp; + if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); + + // 沿父子边把时间戳向上传播给所有祖先。父对象保存 max(自身, 子对象) 即可 + // 在 O(1) 内回答子树是否变化;写入代价为 O(祖先数量)。 + // 用 epoch 标记访问过的节点,O(1) 去重并防止用户构造的环导致死循环。 + if (!impl_->parents || impl_->parents->empty()) return;// 热点:无父对象不进入传播循环 + const uint64_t epoch = g_prop_epoch.fetch_add(1, std::memory_order_relaxed) + 1; + impl_->prop_epoch = epoch; + boost::container::small_vector stack; + stack.push_back(this); + while (!stack.empty()) { + NativeObject *node = stack.back(); + stack.pop_back(); + for (NativeObject *parent: *node->impl_->parents) { + if (!parent || !parent->impl_) continue; + if (parent->version_ < stamp) parent->version_ = stamp; + if (parent->impl_->prop_epoch == epoch) continue; + parent->impl_->prop_epoch = epoch; + // 祖先链到头(该父对象自己没有父对象):无需入栈,出栈时也不会空解引用。 + if (!parent->impl_->parents) continue; + stack.push_back(parent); + } + } +} + +void NativeObject::RefreshDebugHash() { + if (!impl_) return; + impl_->debug_hash = ComputeContentHash(this); + impl_->debug_hash_epoch = g_debug_epoch.load(std::memory_order_relaxed); +} + +uint64_t NativeObject::ComputeContentHash(const NativeObject *obj) { + if (!obj || !obj->impl_) return 0; + uint64_t h = 1469598103934665603ULL; + + // 字段:按 key 排序保证哈希与 unordered_map 的遍历顺序无关。 + std::vector *> fields; + fields.reserve(obj->impl_->kv.size()); + for (const auto &item: obj->impl_->kv) fields.push_back(&item); + std::sort(fields.begin(), fields.end(), [](const auto *a, const auto *b) { + return a->first < b->first; + }); + FnvHashU64(h, fields.size()); + for (const auto *item: fields) { + const auto &key = item->first; + const auto &f = item->second; + FnvHashU64(h, key.size()); + FnvHashBytes(h, key.data(), key.size()); + FnvHashU64(h, static_cast(f.kind)); + switch (f.kind) { + case NativeField::Kind::Nil: + break; + case NativeField::Kind::Int: + FnvHashU64(h, static_cast(f.i)); + break; + case NativeField::Kind::Float: { + // 用位模式哈希,避免 NaN/负值转整数的未定义行为。 + uint64_t bits = 0; + std::memcpy(&bits, &f.f, sizeof(bits)); + FnvHashU64(h, bits); + break; + } + case NativeField::Kind::Bool: + FnvHashU64(h, static_cast(f.b)); + break; + case NativeField::Kind::String: + FnvHashU64(h, f.s.size()); + FnvHashBytes(h, f.s.data(), f.s.size()); + break; + case NativeField::Kind::Object: + FnvHashU64(h, static_cast(reinterpret_cast(f.obj))); + break; + case NativeField::Kind::Table: + FnvHashU64(h, static_cast(reinterpret_cast(f.t))); + break; + } + } + + // 方法:只对名字排序哈希(std::function 无稳定内容表示)。 + std::vector method_names; + method_names.reserve(obj->impl_->methods.size()); + for (const auto &item: obj->impl_->methods) method_names.push_back(&item.first); + std::sort(method_names.begin(), method_names.end(), + [](const std::string *a, const std::string *b) { return *a < *b; }); + FnvHashU64(h, method_names.size()); + for (const auto *name: method_names) { + FnvHashU64(h, name->size()); + FnvHashBytes(h, name->data(), name->size()); + } + + FnvHashU64(h, static_cast(obj->impl_->id)); + FnvHashU64(h, static_cast(obj->impl_->group_id)); + return h; +} + +bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { + if (!obj || !obj->impl_) return true; + + const bool debug = g_version_debug.load(std::memory_order_relaxed); + const uint64_t debug_epoch = g_debug_epoch.load(std::memory_order_relaxed); + const auto hash_is_current = [debug_epoch](const NativeObject::Impl *impl) { + return impl->debug_hash_epoch != 0 && impl->debug_hash_epoch == debug_epoch; + }; + if (debug) { + // 内容哈希比对:若某次写入绕过了对象 API(因此没有 TouchVersion), + // 记录下来的哈希会与真实内容不一致。 + if (hash_is_current(obj->impl_) && obj->impl_->debug_hash != ComputeContentHash(obj)) { + return false; + } + } + if (!recursive) return true; + + // 递归校验所有嵌套对象,并检查传播不变式:父版本 >= 任意子版本。 + // 用 visited 防止用户构造的环导致无限递归。 + std::unordered_set visited; + boost::container::small_vector stack; + stack.push_back(obj); + visited.insert(obj); + while (!stack.empty()) { + const NativeObject *node = stack.back(); + stack.pop_back(); + if (!node->impl_->children) continue; + for (const auto &[child, _]: *node->impl_->children) { + if (!child || !child->impl_) return false; + if (debug) { + if (hash_is_current(child->impl_) && child->impl_->debug_hash != ComputeContentHash(child)) { + return false; + } + } + if (node->version_ < child->version_) return false; + if (visited.insert(child).second) stack.push_back(child); + } + } + return true; +} + +// ---- 单元测试白盒访问器(生产代码禁止使用)---- + +void NativeObjectVersionTestAccess::RawSetInt(NativeObject *obj, std::string_view key, int64_t val) { + if (!obj || !obj->impl_) return; + // 故意绕过 TouchVersion / 父子边维护,用于验证 SelfCheck 的检测能力。 + auto &f = obj->impl_->kv[std::string(key)]; + f.kind = NativeField::Kind::Int; + f.i = val; +} + +void NativeObjectVersionTestAccess::SetStamp(uint64_t stamp) { + g_version_seq.store(stamp, std::memory_order_relaxed); +} + +uint64_t NativeObjectVersionTestAccess::GetStamp() { + return g_version_seq.load(std::memory_order_relaxed); +} + +void NativeObjectVersionTestAccess::SetFromCVar(NativeObject *obj, std::string_view key, CVar v) { + if (obj) obj->SetFromCVar(key, v); +} + +CVar NativeObjectVersionTestAccess::GetAsCVar(NativeObject *obj, std::string_view key, State *s) { + return obj ? obj->GetAsCVar(key, s) : CVar{}; +} + }// namespace fakelua diff --git a/src/native/object/native_object.h b/src/native/object/native_object.h index c192d574..9a42c396 100644 --- a/src/native/object/native_object.h +++ b/src/native/object/native_object.h @@ -10,11 +10,14 @@ #include "var/var_type.h" #include +#include #include #include +#include #include #include #include +#include #include namespace fakelua { @@ -76,6 +79,21 @@ struct NativeObject::Impl { std::unordered_map methods; // 可选销毁回调:NativeObject 被 Destroy 时调用(无论是否经 close 方法),用于释放关联资源 std::function finalizer; + + // ---- 对象级版本号的内部簿记(不进入存档,不改变既有数据格式)---- + // 直接子对象及引用计数:同一个子对象可被同一父对象的多个字段引用, + // 只有引用计数归零才真正断开父子边。 + // 懒分配:绝大多数对象没有任何嵌套关系,空容器不占堆内存。 + std::unique_ptr> children; + // 直接父对象集合:写入后沿这些边把新版本戳传播给所有祖先。 + std::unique_ptr> parents; + // 版本传播的去重/防环标记;每次传播使用一个自增 epoch,O(1) 判断是否已访问。 + uint64_t prop_epoch = 0; + // 调试自检模式下的内容哈希(关闭调试时不维护)。 + uint64_t debug_hash = 0; + // 该哈希所属的调试 epoch。每次开关调试开关都会换 epoch, + // 避免把关闭期间发生的写入和旧哈希错误地比较(误报漏标)。 + uint64_t debug_hash_epoch = 0; }; // spec_get / spec_set 静态实现 diff --git a/test/lua/native/test_native_get_set.lua b/test/lua/native/test_native_get_set.lua index ec7dc0cd..241cc084 100644 --- a/test/lua/native/test_native_get_set.lua +++ b/test/lua/native/test_native_get_set.lua @@ -3,3 +3,41 @@ function test_get_set() p.hp = p.hp + 50 return p.hp end + +-- 版本号测试:只读路径(字段读取 + pairs 快照)不应改变对象版本号 +function test_version_read_only() + local p = get_player() + local total = p.hp + for k, v in pairs(p) do + total = total + 0 + end + return total +end + +-- 版本号测试:快照已建立后的增量写入(NativeSpecSet 的 snapshot_ready 分支) +function test_version_write_after_pairs() + local p = get_player() + for _ in pairs(p) do end + p.hp = p.hp + 1 + return p.hp +end + +-- 版本号测试:容器内部写入(MarkModified)需要能传播到父对象 +function test_version_container_write() + local root = get_player() + local c = container.deque() + root.bag_container = c + c:push_back(1) + return 1 +end + +-- 版本号测试:容器只读方法不应改变版本号 +function test_version_container_read() + local root = get_player() + local c = root.bag_container + if c == nil then return 0 end + if c:size() ~= 1 then return -1 end + if c:empty() then return -2 end + local t = c:to_table() + return t[1] +end diff --git a/test/test_native.cpp b/test/test_native.cpp index c45d5b4a..3f392ac6 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -780,3 +780,299 @@ end SetDebugLogLevel(s, LogLevel::Warn); FakeluaDeleteState(s); } + +// ==================== 对象级变更版本号 ==================== + +TEST(test_native_version, all_write_paths_bump) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "vobj", 1); + auto *child = mgr.Create(gid, "vchild", 2); + + const auto expect_bump = [&](const char *what, const std::function &fn) { + const uint64_t before = obj->GetVersion(); + fn(); + EXPECT_GT(obj->GetVersion(), before) << "write path not counted: " << what; + }; + + expect_bump("SetInt", [&] { obj->SetInt("i", 1); }); + expect_bump("SetFloat", [&] { obj->SetFloat("f", 1.5); }); + expect_bump("SetBool", [&] { obj->SetBool("b", true); }); + expect_bump("SetString", [&] { obj->SetString("s", "x"); }); + expect_bump("SetObject", [&] { obj->SetObject("c", child); }); + expect_bump("SetFromCVar", [&] { NativeObjectVersionTestAccess::SetFromCVar(obj, "cv", inter::NativeToFakeluaLonglong(s, 7)); }); + expect_bump("SetNil(existing)", [&] { obj->SetNil("i"); }); + expect_bump("Del(existing)", [&] { obj->Del("f"); }); + expect_bump("Del(missing)", [&] { obj->Del("missing"); }); + expect_bump("RegisterMethod", [&] { obj->RegisterMethod("m", NativeMethod{}); }); + expect_bump("UnregisterMethod", [&] { obj->UnregisterMethod("m"); }); + expect_bump("SetId", [&] { obj->SetId(11); }); + expect_bump("SetGroupId", [&] { obj->SetGroupId(gid); }); + expect_bump("SetFinalizer", [&] { obj->SetFinalizer([](NativeObject *) {}); }); + expect_bump("MarkModified", [&] { obj->MarkModified(); }); + expect_bump("Clear", [&] { obj->Clear(); }); + + // 版本号不是可见字段,不改变既有数据格式 + EXPECT_FALSE(obj->Has("version")); + EXPECT_FALSE(obj->Has("__version__")); + + // 本用例改过 id,manager 的 (type,id) 索引与对象当前 id 不一致; + // 用 Clear() 统一销毁,避免 DestroyGroup 按新 id 擦除索引造成重复释放。 + mgr.Clear(); + FakeluaDeleteState(s); +} + +TEST(test_native_version, read_paths_do_not_bump) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "vobj", 1); + auto *child = mgr.Create(gid, "vchild", 2); + child->SetInt("gold", 5); + obj->SetInt("hp", 10); + obj->SetFloat("speed", 2.5); + obj->SetBool("alive", true); + obj->SetString("name", "hero"); + obj->SetObject("child", child); + obj->RegisterMethod("m", NativeMethod{}); + + const uint64_t v = obj->GetVersion(); + const uint64_t vc = child->GetVersion(); + + EXPECT_EQ(obj->GetInt("hp"), 10); + EXPECT_DOUBLE_EQ(obj->GetFloat("speed"), 2.5); + EXPECT_TRUE(obj->GetBool("alive")); + EXPECT_EQ(obj->GetString("name"), "hero"); + EXPECT_EQ(obj->GetObject("child"), child); + EXPECT_EQ(obj->GetInt("missing", 42), 42); + EXPECT_TRUE(obj->Has("hp")); + EXPECT_EQ(obj->Size(), 5); + EXPECT_TRUE(obj->HasMethod("m")); + EXPECT_EQ(obj->GetId(), 1); + EXPECT_EQ(obj->GetGroupId(), gid); + EXPECT_NE(obj->GetTypeName(), ""); + int fields = 0; + obj->ForEach([&](std::string_view, NativeObject::FieldKind) { ++fields; }); + EXPECT_EQ(fields, 5); + + CVar as_cvar = NativeObjectVersionTestAccess::GetAsCVar(obj, "name", s); + EXPECT_EQ(inter::FakeluaToNative(s, as_cvar), "hero"); + CVar wrapped = obj->Wrap(s); + EXPECT_EQ(NativeObject::Unwrap(wrapped), obj); + + EXPECT_EQ(obj->GetVersion(), v); + EXPECT_EQ(child->GetVersion(), vc); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, nested_propagation_and_isolation) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *root = mgr.Create(gid, "root", 1); + auto *a = mgr.Create(gid, "a", 2); + auto *b = mgr.Create(gid, "b", 3); + auto *leaf = mgr.Create(gid, "leaf", 4); + root->SetObject("a", a); + root->SetObject("b", b); + a->SetObject("leaf", leaf); + + uint64_t vr = root->GetVersion(), va = a->GetVersion(), vb = b->GetVersion(); + const uint64_t vl = leaf->GetVersion(); + leaf->SetInt("x", 1); + EXPECT_TRUE(root->IsChangedSince(vr)); + EXPECT_TRUE(a->IsChangedSince(va)); + EXPECT_TRUE(leaf->IsChangedSince(vl)); + EXPECT_FALSE(b->IsChangedSince(vb)); + + va = a->GetVersion(); + vb = b->GetVersion(); + b->SetInt("y", 2); + EXPECT_TRUE(root->IsChangedSince(vb)); + EXPECT_TRUE(b->IsChangedSince(vb)); + EXPECT_FALSE(a->IsChangedSince(va)); + + // 共享子对象:被多个父对象引用时都能感知 + auto *p1 = mgr.Create(gid, "p1", 5); + auto *p2 = mgr.Create(gid, "p2", 6); + p1->SetObject("c", leaf); + p2->SetObject("c", leaf); + uint64_t v1 = p1->GetVersion(), v2 = p2->GetVersion(); + leaf->SetInt("x", 2); + EXPECT_TRUE(p1->IsChangedSince(v1)); + EXPECT_TRUE(p2->IsChangedSince(v2)); + + // 同一父对象的多字段引用:任一字段仍在引用时,写入仍能传播 + p1->SetObject("d", leaf); + p1->SetNil("c"); + v1 = p1->GetVersion(); + leaf->SetInt("x", 3); + EXPECT_TRUE(p1->IsChangedSince(v1)); + p1->SetNil("d"); + v1 = p1->GetVersion(); + leaf->SetInt("x", 4); + EXPECT_FALSE(p1->IsChangedSince(v1)); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, child_create_destroy_and_clear) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *parent = mgr.Create(gid, "parent", 1); + auto *child = mgr.Create(gid, "child", 2); + + uint64_t vp = parent->GetVersion(); + parent->SetObject("child", child); + EXPECT_TRUE(parent->IsChangedSince(vp));// 子对象创建/挂载 + + vp = parent->GetVersion(); + const uint64_t vc = child->GetVersion(); + mgr.DestroySingle("child", 2); + EXPECT_TRUE(parent->IsChangedSince(vp));// 子对象删除 + EXPECT_FALSE(child->Alive()); + EXPECT_EQ(child->GetVersion(), vc);// 自身最后一次版本保留,单调不回退 + + auto *c2 = mgr.Create(gid, "child2", 3); + parent->SetObject("c2", c2); + vp = parent->GetVersion(); + parent->Clear(); + EXPECT_TRUE(parent->IsChangedSince(vp));// 重置/批量清空 + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, monotonic_and_saturation) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *obj = mgr.Create(gid, "vobj", 1); + + uint64_t prev = obj->GetVersion(); + for (int i = 0; i < 1000; ++i) { + obj->SetInt("x", i); + EXPECT_GT(obj->GetVersion(), prev); + prev = obj->GetVersion(); + } + + // 饱和:计数器达到 UINT64_MAX 后不再回绕,版本号保持最大值(不再可区分新旧)。 + const uint64_t saved = NativeObjectVersionTestAccess::GetStamp(); + NativeObjectVersionTestAccess::SetStamp(UINT64_MAX); + obj->SetInt("x", 1); + const uint64_t sat1 = obj->GetVersion(); + obj->SetInt("x", 2); + const uint64_t sat2 = obj->GetVersion(); + NativeObjectVersionTestAccess::SetStamp(saved); + EXPECT_EQ(sat1, UINT64_MAX); + EXPECT_EQ(sat2, UINT64_MAX); + EXPECT_FALSE(obj->IsChangedSince(UINT64_MAX)); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, debug_self_check_detects_missed_bump) { + NativeObject::SetVersionDebugEnabled(true); + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *root = mgr.Create(gid, "root", 1); + auto *child = mgr.Create(gid, "child", 2); + root->SetObject("child", child); + child->SetInt("a", 1); + child->RegisterMethod("m", NativeMethod{}); + + EXPECT_TRUE(NativeObject::SelfCheck(root, true)); + EXPECT_TRUE(NativeObject::SelfCheck(child, false)); + + // 绕过对象 API 的直接写入(模拟漏标写入点):版本号不变,但内容哈希不一致。 + const uint64_t vc = child->GetVersion(); + NativeObjectVersionTestAccess::RawSetInt(child, "bypass", 7); + EXPECT_EQ(child->GetVersion(), vc); + EXPECT_FALSE(NativeObject::SelfCheck(child, false)); + EXPECT_FALSE(NativeObject::SelfCheck(root, true)); + + // 正常写入会刷新哈希,自检恢复通过。 + child->SetInt("a", 2); + EXPECT_TRUE(NativeObject::SelfCheck(child, false)); + EXPECT_TRUE(NativeObject::SelfCheck(root, true)); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); + NativeObject::SetVersionDebugEnabled(false); +} + +TEST(test_native_version, cycle_is_guard) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *a = mgr.Create(gid, "a", 1); + auto *b = mgr.Create(gid, "b", 2); + a->SetObject("b", b); + b->SetObject("a", a); + + const uint64_t vb = b->GetVersion(); + a->SetInt("x", 1);// 不应因环而死循环 + EXPECT_TRUE(b->IsChangedSince(vb)); + EXPECT_TRUE(NativeObject::SelfCheck(a, true)); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, lua_paths_and_container) { + for (auto jit_type: AllJitTypes()) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + static NativeObject *global_player = nullptr; + global_player = mgr.Create(gid, "player", 100); + global_player->SetInt("hp", 100); + RegisterNativeFunction(s, "get_player", 0, false, + [](State *state, CVar *, int) -> CVar { + return inter::NativeToFakelua(state, global_player); + }); + + CompileConfig config; + CompileFile(s, "./native/test_native_get_set.lua", config); + + // 只读(字段读取 + pairs 快照)不涨版本 + const uint64_t v0 = global_player->GetVersion(); + CVar ret; + Call(s, jit_type, "test_version_read_only", ret); + EXPECT_EQ(global_player->GetVersion(), v0); + inter::Reset(s); + + // 快照已建立后的增量写入(NativeSpecSet snapshot_ready 分支) + const uint64_t v1 = global_player->GetVersion(); + Call(s, jit_type, "test_version_write_after_pairs", ret); + EXPECT_TRUE(global_player->IsChangedSince(v1)); + EXPECT_EQ(global_player->GetInt("hp"), 101); + inter::Reset(s); + + // 容器内部内容写入通过 MarkModified 传播到父对象 + const uint64_t v2 = global_player->GetVersion(); + Call(s, jit_type, "test_version_container_write", ret); + EXPECT_TRUE(global_player->IsChangedSince(v2)); + auto *container = global_player->GetObject("bag_container"); + ASSERT_NE(container, nullptr); + const uint64_t vc = container->GetVersion(); + EXPECT_GT(vc, 0); + inter::Reset(s); + + // 容器只读方法不涨版本 + Call(s, jit_type, "test_version_container_read", ret); + EXPECT_EQ(inter::FakeluaToNative(s, ret), 1); + EXPECT_EQ(container->GetVersion(), vc); + + mgr.DestroyGroup(gid); + global_player = nullptr; + FakeluaDeleteState(s); + } +} From 8bf39bda6e4f6c1eb6f9173c97575f0966267352 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 6 Nov 2026 20:32:27 +0800 Subject: [PATCH 2/6] refactor(native): keep change version object-local, drop ancestor propagation * refactor(native): remove upward version propagation TouchVersion now only raises the object's own stamp (O(1), independent of tree depth); LinkChild/UnlinkChild no longer push stamps to ancestors and the propagation epoch bookkeeping is gone. Direct child attach/detach still bumps the owning object because that is its own structural change, and child deletion bumps only the direct parent (DetachAll). Semantics is now object-local: a write inside a child never surfaces on its parent or ancestors. Callers that need subtree granularity traverse the graph themselves and aggregate the per-object versions they read (traversal is pointer-only and cheap; field serialization is the expensive part). * test(native): assert child writes do not bump ancestors and SelfCheck is cycle-safe Replace the propagation test with nested_writes_do_not_bump_ancestors, add destroy_bumps_direct_parent_only, and add self_check_is_cycle_safe (A<->B) proving the visited-set walk terminates on cyclic graphs and still detects a write that bypassed the object API. Object graphs may contain cycles: LinkChild does not reject them, so SelfCheck must stay cycle-safe. * docs(native): document object-local scope and cycle safety on the public API --- benchmark/benchmark_native_object.cpp | 4 +- include/fakelua.h | 23 ++++--- src/native/object/native_object.cpp | 47 ++++--------- src/native/object/native_object.h | 5 +- test/test_native.cpp | 98 ++++++++++++++++++++------- 5 files changed, 107 insertions(+), 70 deletions(-) diff --git a/benchmark/benchmark_native_object.cpp b/benchmark/benchmark_native_object.cpp index e3b5733c..aa710f3e 100644 --- a/benchmark/benchmark_native_object.cpp +++ b/benchmark/benchmark_native_object.cpp @@ -5,7 +5,7 @@ using namespace fakelua; -// NativeObject 热路径基准:字段读写、对象创建、父子绑定与子树版本传播。 +// NativeObject 热路径基准:字段读写、对象创建、父子绑定与深树叶子写入。 // 这些用例只使用长期存在的公开 API,因此同一份基准可分别跑在加版本号前后的构建上。 static void BM_NativeObjectSetInt(benchmark::State &state) { @@ -75,7 +75,7 @@ static void BM_NativeObjectSetObject(benchmark::State &state) { FakeluaDeleteState(s); } -// 8 层父子链上写叶子:用于观察子树脏版本传播的 O(depth) 开销。 +// 8 层父子链上写叶子:用于观察深层对象上的写入开销(简化后应与浅层写入一致)。 static void BM_NativeObjectDeepLeafWrite(benchmark::State &state) { auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); diff --git a/include/fakelua.h b/include/fakelua.h index a7196522..94af361b 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -827,21 +827,28 @@ class NativeObject { void Clear(); // ---- 对象级变更版本号(增量存档 / C++ 镜像同步 / UI 缓存失效)---- - // 语义:每次写入(字段、方法、子对象关系、Clear)都会让本对象及其所有祖先的 - // 版本号单调变大;读取不改变版本号。返回的是全局单调时间戳,不是计数器, - // 只应与同一对象自己上一次记录的版本号比较,不应跨对象比较大小。 + // 语义边界:只描述「这个对象自身」,不包含它的子树。 + // * 本对象自身的写入(字段、方法、id/group、finalizer、Clear、直接子对象的挂载/卸载) + // 会让本对象的版本号单调变大; + // * 子对象内部的写入只改变子对象自己的版本号,不会改变本对象; + // * 所有读操作都不改变版本号。 + // 返回的是全局单调时间戳,不是计数器;只应与同一对象自己上一次记录的版本号比较, + // 不应跨对象比较大小。 // 调用方:记下 v = obj->GetVersion();之后用 obj->IsChangedSince(v) 做 O(1) 判断。 - // 子树的任何写入都会向上传播,因此对根对象取版本即可覆盖整棵子树。 + // 需要「整棵子树」粒度时,调用方自行遍历对象图、记录读到的每个对象的版本集合, + // 再在上层聚合判断(遍历只读指针,成本远低于字段序列化)。 // 版本号不写入存档、不改变既有数据格式;对象销毁后仍保留最后一次版本号。 [[nodiscard]] uint64_t GetVersion() const; [[nodiscard]] bool IsChangedSince(uint64_t version) const; - // 显式标记本对象(及其祖先)已变更。用于通过字段写入无法表达的副作用修改, - // 例如 native/container 内部容器内容、外部资源状态等。 + // 显式标记本对象(仅本对象,不含父对象/子对象)已变更。 + // 用于通过字段写入无法表达的副作用修改,例如 native/container 内部容器内容、 + // 外部资源状态(io/socket 等)改变。 void MarkModified(); // 调试自检(默认关闭):开启后每次写入都会维护内容哈希;SelfCheck 会重新计算 - // 字段/方法内容哈希并与记录值比对,并递归校验「父版本 >= 子版本」, + // 字段/方法内容哈希并与记录值比对,并递归校验所有嵌套对象的内容哈希 + // (visited 集合去重,对象图成环或共享子对象都是安全的), // 用于交叉验证是否有写入点漏标版本。只在调试/测试中使用,开销较大。 static void SetVersionDebugEnabled(bool enabled); [[nodiscard]] static bool SelfCheck(const NativeObject *obj, bool recursive = true); @@ -873,7 +880,7 @@ class NativeObject { explicit NativeObject(std::string type_name); ~NativeObject(); - // 版本号内部维护 + // 版本号内部维护:TouchVersion 只提升本对象自身(O(1),不做祖先传播)。 void TouchVersion(); void LinkChild(NativeObject *child); void UnlinkChild(NativeObject *child); diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 2bfada74..83cafc6a 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -21,14 +21,12 @@ namespace fakelua { namespace { // ---- 对象级版本戳基础设施 ---- -// 版本号使用全局单调时间戳(不是每对象的计数器),这样父对象只需保存 -// max(自身戳, 所有子对象戳) 就能在 O(1) 内回答「整棵子树自某版本后是否变过」。 +// 版本号使用全局单调时间戳(不是每对象的计数器)。语义是「对象自身」粒度: +// 只反映本对象自己的写入,不包含子树;需要子树粒度时由调用方自行聚合。 constexpr uint64_t kMaxVersionStamp = std::numeric_limits::max(); // 全局时间戳:每次写入分配一个更大的值。饱和而不是回绕,保证单调性永不被破坏。 std::atomic g_version_seq{0}; -// 传播去重/防环用的 epoch。每个对象记录最后一次访问它的 epoch,命中即停止。 -std::atomic g_prop_epoch{0}; // 调试自检开关(默认关闭)。 std::atomic g_version_debug{false}; // 调试 epoch:每次切换调试开关都递增,使旧对象上记录的哈希失效。 @@ -629,7 +627,7 @@ void NativeObject::Destroy(NativeObject *obj) { obj->impl_->finalizer = nullptr; fn(obj); } - // 子对象被删除属于父对象子树的变化,需要让父对象(及其祖先)的版本号前进。 + // 子对象被删除属于父对象自身的结构变化,DetachAll 会让直接父对象的版本号前进。 obj->DetachAll(); delete obj->impl_; obj->impl_ = nullptr; @@ -1077,8 +1075,9 @@ void NativeObject::UnlinkChild(NativeObject *child) { void NativeObject::DetachAll() { if (!impl_) return; - // 1) 从所有父对象的直接子表里移除自己;父对象的子树发生变化,需要 bump。 - // 复制一份 parents,避免传播过程中任何潜在的结构变动影响遍历。 + // 1) 从所有父对象的直接子表里移除自己:子对象的直接删除属于父对象自身的结构变化, + // 因此 bump 直接父对象(只 bump 直接父对象,不再向上蔓延)。 + // 复制一份 parents,避免遍历过程中任何潜在的结构变动影响迭代。 boost::container::small_vector parent_list; if (impl_->parents) { parent_list.assign(impl_->parents->begin(), impl_->parents->end()); @@ -1106,32 +1105,11 @@ void NativeObject::DetachAll() { void NativeObject::TouchVersion() { if (!impl_) return; - // 全局单调时间戳:比任何已分配的版本号都大(除非已饱和)。 + // 只提升本对象自己的版本号:不向父对象传播,也不影响子对象。 + // 因此写入代价是 O(1),与树的深度无关。 const uint64_t stamp = NextVersionStamp(); if (version_ < stamp) version_ = stamp; if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); - - // 沿父子边把时间戳向上传播给所有祖先。父对象保存 max(自身, 子对象) 即可 - // 在 O(1) 内回答子树是否变化;写入代价为 O(祖先数量)。 - // 用 epoch 标记访问过的节点,O(1) 去重并防止用户构造的环导致死循环。 - if (!impl_->parents || impl_->parents->empty()) return;// 热点:无父对象不进入传播循环 - const uint64_t epoch = g_prop_epoch.fetch_add(1, std::memory_order_relaxed) + 1; - impl_->prop_epoch = epoch; - boost::container::small_vector stack; - stack.push_back(this); - while (!stack.empty()) { - NativeObject *node = stack.back(); - stack.pop_back(); - for (NativeObject *parent: *node->impl_->parents) { - if (!parent || !parent->impl_) continue; - if (parent->version_ < stamp) parent->version_ = stamp; - if (parent->impl_->prop_epoch == epoch) continue; - parent->impl_->prop_epoch = epoch; - // 祖先链到头(该父对象自己没有父对象):无需入栈,出栈时也不会空解引用。 - if (!parent->impl_->parents) continue; - stack.push_back(parent); - } - } } void NativeObject::RefreshDebugHash() { @@ -1221,8 +1199,10 @@ bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { } if (!recursive) return true; - // 递归校验所有嵌套对象,并检查传播不变式:父版本 >= 任意子版本。 - // 用 visited 防止用户构造的环导致无限递归。 + // 递归校验所有嵌套对象的内容哈希。 + // 环安全:用 visited 集合去重,对象图成环(A→B→A)或共享子对象时不会重复入栈、 + // 也不会无限递归。代价是 O(N) 的额外内存,只在本调试开关打开时发生; + // 不使用深度上限,因此不会因为截断深度而产生误报/漏报。 std::unordered_set visited; boost::container::small_vector stack; stack.push_back(obj); @@ -1232,13 +1212,12 @@ bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { stack.pop_back(); if (!node->impl_->children) continue; for (const auto &[child, _]: *node->impl_->children) { - if (!child || !child->impl_) return false; + if (!child || !child->impl_) continue; if (debug) { if (hash_is_current(child->impl_) && child->impl_->debug_hash != ComputeContentHash(child)) { return false; } } - if (node->version_ < child->version_) return false; if (visited.insert(child).second) stack.push_back(child); } } diff --git a/src/native/object/native_object.h b/src/native/object/native_object.h index 9a42c396..5e85d972 100644 --- a/src/native/object/native_object.h +++ b/src/native/object/native_object.h @@ -85,10 +85,9 @@ struct NativeObject::Impl { // 只有引用计数归零才真正断开父子边。 // 懒分配:绝大多数对象没有任何嵌套关系,空容器不占堆内存。 std::unique_ptr> children; - // 直接父对象集合:写入后沿这些边把新版本戳传播给所有祖先。 + // 直接父对象集合:子对象销毁/解绑时据此 bump 直接父对象的自身版本号。 + // 不做任何祖先传播;父集合只在需要「子对象删除通知父对象」时才用到。 std::unique_ptr> parents; - // 版本传播的去重/防环标记;每次传播使用一个自增 epoch,O(1) 判断是否已访问。 - uint64_t prop_epoch = 0; // 调试自检模式下的内容哈希(关闭调试时不维护)。 uint64_t debug_hash = 0; // 该哈希所属的调试 epoch。每次开关调试开关都会换 epoch, diff --git a/test/test_native.cpp b/test/test_native.cpp index 3f392ac6..e95048af 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -868,7 +868,7 @@ TEST(test_native_version, read_paths_do_not_bump) { FakeluaDeleteState(s); } -TEST(test_native_version, nested_propagation_and_isolation) { +TEST(test_native_version, nested_writes_do_not_bump_ancestors) { auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); int64_t gid = mgr.CreateGroup(); @@ -880,41 +880,46 @@ TEST(test_native_version, nested_propagation_and_isolation) { root->SetObject("b", b); a->SetObject("leaf", leaf); + // 子对象内部的写入只改变子对象自己的计数,父对象与祖先都不变。 uint64_t vr = root->GetVersion(), va = a->GetVersion(), vb = b->GetVersion(); const uint64_t vl = leaf->GetVersion(); leaf->SetInt("x", 1); - EXPECT_TRUE(root->IsChangedSince(vr)); - EXPECT_TRUE(a->IsChangedSince(va)); EXPECT_TRUE(leaf->IsChangedSince(vl)); + EXPECT_FALSE(a->IsChangedSince(va)); + EXPECT_FALSE(root->IsChangedSince(vr)); EXPECT_FALSE(b->IsChangedSince(vb)); + // 中间层写入同样不向上蔓延。 va = a->GetVersion(); - vb = b->GetVersion(); - b->SetInt("y", 2); - EXPECT_TRUE(root->IsChangedSince(vb)); - EXPECT_TRUE(b->IsChangedSince(vb)); - EXPECT_FALSE(a->IsChangedSince(va)); + vr = root->GetVersion(); + a->SetInt("y", 2); + EXPECT_TRUE(a->IsChangedSince(va)); + EXPECT_FALSE(root->IsChangedSince(vr)); - // 共享子对象:被多个父对象引用时都能感知 + // 共享子对象:多个父对象引用同一个子对象,子对象写入不影响任一父对象。 auto *p1 = mgr.Create(gid, "p1", 5); auto *p2 = mgr.Create(gid, "p2", 6); p1->SetObject("c", leaf); p2->SetObject("c", leaf); uint64_t v1 = p1->GetVersion(), v2 = p2->GetVersion(); leaf->SetInt("x", 2); - EXPECT_TRUE(p1->IsChangedSince(v1)); - EXPECT_TRUE(p2->IsChangedSince(v2)); + EXPECT_FALSE(p1->IsChangedSince(v1)); + EXPECT_FALSE(p2->IsChangedSince(v2)); - // 同一父对象的多字段引用:任一字段仍在引用时,写入仍能传播 - p1->SetObject("d", leaf); - p1->SetNil("c"); + // 直接子对象的挂载/卸载属于父对象自身的结构变化:只 bump 该父对象, + // 不 bump 它的父对象(祖先链上不蔓延)。 + auto *gp = mgr.Create(gid, "gp", 7); + gp->SetObject("p1", p1); + uint64_t vgp = gp->GetVersion(); v1 = p1->GetVersion(); - leaf->SetInt("x", 3); + p1->SetObject("d", leaf); EXPECT_TRUE(p1->IsChangedSince(v1)); - p1->SetNil("d"); + EXPECT_FALSE(gp->IsChangedSince(vgp)); + vgp = gp->GetVersion(); v1 = p1->GetVersion(); - leaf->SetInt("x", 4); - EXPECT_FALSE(p1->IsChangedSince(v1)); + p1->SetNil("d"); + EXPECT_TRUE(p1->IsChangedSince(v1)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); mgr.DestroyGroup(gid); FakeluaDeleteState(s); @@ -1008,19 +1013,66 @@ TEST(test_native_version, debug_self_check_detects_missed_bump) { NativeObject::SetVersionDebugEnabled(false); } -TEST(test_native_version, cycle_is_guard) { +TEST(test_native_version, self_check_is_cycle_safe) { + NativeObject::SetVersionDebugEnabled(true); auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); int64_t gid = mgr.CreateGroup(); auto *a = mgr.Create(gid, "a", 1); auto *b = mgr.Create(gid, "b", 2); + // 故意成环:A 挂 B,B 又指回 A。对象模型允许成环(LinkChild 不做环校验), + // 因此下游不能依赖「对象图无环」这个前提。 a->SetObject("b", b); b->SetObject("a", a); + // 环状对象图下自检必须正常终止,且内容一致时返回 true。 + EXPECT_TRUE(NativeObject::SelfCheck(a, true)) + << "SelfCheck 在环状对象图(A->B->A)上未能正常结束或误报"; + EXPECT_TRUE(NativeObject::SelfCheck(b, true)) + << "SelfCheck 在环状对象图(B->A->B)上未能正常结束或误报"; + + // 环上的漏标写入仍然能被发现(证明遍历确实走到了环的另一端)。 const uint64_t vb = b->GetVersion(); - a->SetInt("x", 1);// 不应因环而死循环 - EXPECT_TRUE(b->IsChangedSince(vb)); - EXPECT_TRUE(NativeObject::SelfCheck(a, true)); + NativeObjectVersionTestAccess::RawSetInt(b, "bypass", 7); + EXPECT_EQ(b->GetVersion(), vb); + EXPECT_FALSE(NativeObject::SelfCheck(a, true)) + << "SelfCheck 未能通过环遍历发现 b 上的漏标写入"; + + // 环中任一对象写入都只改自己的计数,不会波及对方(无传播、无递归风险)。 + const uint64_t va = a->GetVersion(); + b->SetInt("ok", 1); + EXPECT_EQ(a->GetVersion(), va); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); + NativeObject::SetVersionDebugEnabled(false); +} + +TEST(test_native_version, destroy_bumps_direct_parent_only) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *gp = mgr.Create(gid, "gp", 1); + auto *parent = mgr.Create(gid, "parent", 2); + auto *child = mgr.Create(gid, "child", 3); + gp->SetObject("parent", parent); + parent->SetObject("child", child); + + const uint64_t vgp = gp->GetVersion(); + const uint64_t vp = parent->GetVersion(); + const uint64_t vc = child->GetVersion(); + mgr.DestroySingle("child", 3); + EXPECT_TRUE(parent->IsChangedSince(vp)) << "子对象删除应 bump 直接父对象"; + EXPECT_FALSE(gp->IsChangedSince(vgp)) << "子对象删除不应 bump 祖父对象"; + EXPECT_FALSE(child->Alive()); + EXPECT_EQ(child->GetVersion(), vc) << "销毁后版本号应保留,不回退"; + + // 直接子表维护本身也算父对象的结构变化。 + auto *c2 = mgr.Create(gid, "child2", 4); + const uint64_t vp2 = parent->GetVersion(); + parent->SetObject("child2", c2); + EXPECT_TRUE(parent->IsChangedSince(vp2)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); mgr.DestroyGroup(gid); FakeluaDeleteState(s); @@ -1056,7 +1108,7 @@ TEST(test_native_version, lua_paths_and_container) { EXPECT_EQ(global_player->GetInt("hp"), 101); inter::Reset(s); - // 容器内部内容写入通过 MarkModified 传播到父对象 + // 容器内部内容写入通过 MarkModified 标记到容器对象自身 const uint64_t v2 = global_player->GetVersion(); Call(s, jit_type, "test_version_container_write", ret); EXPECT_TRUE(global_player->IsChangedSince(v2)); From b64d22d61d9907856e676955e9ce947dc5962acd Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 6 Nov 2026 20:57:22 +0800 Subject: [PATCH 3/6] refactor(native): move version test accessors out of the public header * refactor(native): keep only a forward + friend declaration for test access NativeObjectVersionTestAccess was fully declared in include/fakelua.h, the installed public header. Move its declarations to the internal src/native/object/native_object_test_access.h (included only by the unit tests and native_object.cpp) and leave a single forward declaration plus the friend line in the public header, so installed headers expose no test-only surface. Public API is unchanged: GetVersion/IsChangedSince/MarkModified and the debug SetVersionDebugEnabled/SelfCheck stay in fakelua.h because embedders need them for incremental sync and for diagnosing missed markings in their own builds; they cost nothing while debug is off. * test(native): include the internal test-access header --- include/fakelua.h | 16 +++--------- src/native/object/native_object.cpp | 1 + src/native/object/native_object_test_access.h | 25 +++++++++++++++++++ test/test_native.cpp | 1 + 4 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/native/object/native_object_test_access.h diff --git a/include/fakelua.h b/include/fakelua.h index 94af361b..316f4cbe 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -773,18 +773,10 @@ namespace io { void RegisterIoLibraryApi(State *s); } -// 仅用于单元测试的白盒访问器,不属于稳定公开 API。生产代码不得使用。 -// 定义见 src/native/object/native_object.cpp。 -struct NativeObjectVersionTestAccess { - // 绕过版本号/父子边维护直接写字段,用于验证 SelfCheck 能发现漏标写入。 - static void RawSetInt(NativeObject *obj, std::string_view key, int64_t val); - // 调整全局版本戳,用于测试饱和边界。 - static void SetStamp(uint64_t stamp); - [[nodiscard]] static uint64_t GetStamp(); - // 转发到 NativeObject 的私有 CVar 写入/读取路径。 - static void SetFromCVar(NativeObject *obj, std::string_view key, CVar v); - [[nodiscard]] static CVar GetAsCVar(NativeObject *obj, std::string_view key, State *s); -}; +// 仅供单元测试的白盒访问器,不属于公开 API,生产代码不得使用。 +// 方法声明在 src/native/object/native_object_test_access.h(不随安装头发布), +// 这里只保留前向声明 + friend 声明,避免把测试辅助接口暴露给下游。 +struct NativeObjectVersionTestAccess; class NativeObject { public: diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 83cafc6a..586f725a 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -1,4 +1,5 @@ #include "native/object/native_object.h" +#include "native/object/native_object_test_access.h" #include "jit/jit_error_boundary.h" #include "jit/vm.h" #include "native/basic/native_basic.h" diff --git a/src/native/object/native_object_test_access.h b/src/native/object/native_object_test_access.h new file mode 100644 index 00000000..4644c179 --- /dev/null +++ b/src/native/object/native_object_test_access.h @@ -0,0 +1,25 @@ +#pragma once + +// 仅供单元测试使用的白盒访问器,不属于公开 API,也不会随安装头发布。 +// 公开头 include/fakelua.h 里只保留该类型的前向声明与 friend 声明, +// 方法声明在本文件,定义在 src/native/object/native_object.cpp。 + +#include "fakelua.h" + +#include +#include + +namespace fakelua { + +struct NativeObjectVersionTestAccess { + // 绕过版本号维护直接写字段,用于验证 SelfCheck 能发现漏标写入。 + static void RawSetInt(NativeObject *obj, std::string_view key, int64_t val); + // 调整全局版本戳,用于测试饱和边界。 + static void SetStamp(uint64_t stamp); + [[nodiscard]] static uint64_t GetStamp(); + // 转发到 NativeObject 的私有 CVar 写入/读取路径。 + static void SetFromCVar(NativeObject *obj, std::string_view key, CVar v); + [[nodiscard]] static CVar GetAsCVar(NativeObject *obj, std::string_view key, State *s); +}; + +}// namespace fakelua diff --git a/test/test_native.cpp b/test/test_native.cpp index e95048af..05e48ffc 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -1,4 +1,5 @@ #include "fakelua.h" +#include "native/object/native_object_test_access.h" #include "state/state.h" #include "var/var_table.h" #include "test_jit.h" From 8940b5c9a61a458839e768c13aea6256a6f81288 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 6 Nov 2026 23:30:54 +0800 Subject: [PATCH 4/6] test(native): drop test-only accessors, cover version API via public surface * test(native): remove the white-box accessor header and rewrite tests Delete src/native/object/native_object_test_access.h together with the forward and friend declarations that made it reachable from the installed header include/fakelua.h. The public header now carries no test-only traces. Tests now use public API only: - drop the SetFromCVar bump case (same path is reached by Lua-side writes); - drop direct GetAsCVar reading (covered by the Lua read-only case); - drop uint64 saturation (unreachable via public API, would need 2^64 writes); - drop the "bypass the API, then detect with SelfCheck" negative case, since without a backdoor a missed marking cannot be simulated; - keep write paths, read-only paths, nesting isolation, direct child create/destroy, destroy-marks-only-direct-parent and cycle-safe SelfCheck. --- include/fakelua.h | 6 --- src/native/object/native_object.cpp | 27 ------------- test/test_native.cpp | 61 ++++------------------------- 3 files changed, 7 insertions(+), 87 deletions(-) diff --git a/include/fakelua.h b/include/fakelua.h index 316f4cbe..b3ac7743 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -773,11 +773,6 @@ namespace io { void RegisterIoLibraryApi(State *s); } -// 仅供单元测试的白盒访问器,不属于公开 API,生产代码不得使用。 -// 方法声明在 src/native/object/native_object_test_access.h(不随安装头发布), -// 这里只保留前向声明 + friend 声明,避免把测试辅助接口暴露给下游。 -struct NativeObjectVersionTestAccess; - class NativeObject { public: // 禁止拷贝 @@ -890,7 +885,6 @@ class NativeObject { [[nodiscard]] CVar GetAsCVar(std::string_view key, State *s) const; friend class NativeObjectManager; - friend struct NativeObjectVersionTestAccess; friend CVar NativeSpecGet(VarTable *tbl, CVar k, bool *finish); friend void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish); friend void RefreshSpecKeys(VarTable *tbl, const NativeObject *obj, State *s); diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 586f725a..fc76c748 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -1,5 +1,4 @@ #include "native/object/native_object.h" -#include "native/object/native_object_test_access.h" #include "jit/jit_error_boundary.h" #include "jit/vm.h" #include "native/basic/native_basic.h" @@ -1225,30 +1224,4 @@ bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { return true; } -// ---- 单元测试白盒访问器(生产代码禁止使用)---- - -void NativeObjectVersionTestAccess::RawSetInt(NativeObject *obj, std::string_view key, int64_t val) { - if (!obj || !obj->impl_) return; - // 故意绕过 TouchVersion / 父子边维护,用于验证 SelfCheck 的检测能力。 - auto &f = obj->impl_->kv[std::string(key)]; - f.kind = NativeField::Kind::Int; - f.i = val; -} - -void NativeObjectVersionTestAccess::SetStamp(uint64_t stamp) { - g_version_seq.store(stamp, std::memory_order_relaxed); -} - -uint64_t NativeObjectVersionTestAccess::GetStamp() { - return g_version_seq.load(std::memory_order_relaxed); -} - -void NativeObjectVersionTestAccess::SetFromCVar(NativeObject *obj, std::string_view key, CVar v) { - if (obj) obj->SetFromCVar(key, v); -} - -CVar NativeObjectVersionTestAccess::GetAsCVar(NativeObject *obj, std::string_view key, State *s) { - return obj ? obj->GetAsCVar(key, s) : CVar{}; -} - }// namespace fakelua diff --git a/test/test_native.cpp b/test/test_native.cpp index 05e48ffc..71a5b0cf 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -1,5 +1,4 @@ #include "fakelua.h" -#include "native/object/native_object_test_access.h" #include "state/state.h" #include "var/var_table.h" #include "test_jit.h" @@ -802,7 +801,6 @@ TEST(test_native_version, all_write_paths_bump) { expect_bump("SetBool", [&] { obj->SetBool("b", true); }); expect_bump("SetString", [&] { obj->SetString("s", "x"); }); expect_bump("SetObject", [&] { obj->SetObject("c", child); }); - expect_bump("SetFromCVar", [&] { NativeObjectVersionTestAccess::SetFromCVar(obj, "cv", inter::NativeToFakeluaLonglong(s, 7)); }); expect_bump("SetNil(existing)", [&] { obj->SetNil("i"); }); expect_bump("Del(existing)", [&] { obj->Del("f"); }); expect_bump("Del(missing)", [&] { obj->Del("missing"); }); @@ -857,8 +855,7 @@ TEST(test_native_version, read_paths_do_not_bump) { obj->ForEach([&](std::string_view, NativeObject::FieldKind) { ++fields; }); EXPECT_EQ(fields, 5); - CVar as_cvar = NativeObjectVersionTestAccess::GetAsCVar(obj, "name", s); - EXPECT_EQ(inter::FakeluaToNative(s, as_cvar), "hero"); + // CVar 读取路径(GetAsCVar)由 Lua 侧只读用例间接覆盖(见 lua_paths_and_container)。 CVar wrapped = obj->Wrap(s); EXPECT_EQ(NativeObject::Unwrap(wrapped), obj); @@ -954,7 +951,7 @@ TEST(test_native_version, child_create_destroy_and_clear) { FakeluaDeleteState(s); } -TEST(test_native_version, monotonic_and_saturation) { +TEST(test_native_version, monotonic_versions) { auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); int64_t gid = mgr.CreateGroup(); @@ -967,53 +964,13 @@ TEST(test_native_version, monotonic_and_saturation) { prev = obj->GetVersion(); } - // 饱和:计数器达到 UINT64_MAX 后不再回绕,版本号保持最大值(不再可区分新旧)。 - const uint64_t saved = NativeObjectVersionTestAccess::GetStamp(); - NativeObjectVersionTestAccess::SetStamp(UINT64_MAX); - obj->SetInt("x", 1); - const uint64_t sat1 = obj->GetVersion(); - obj->SetInt("x", 2); - const uint64_t sat2 = obj->GetVersion(); - NativeObjectVersionTestAccess::SetStamp(saved); - EXPECT_EQ(sat1, UINT64_MAX); - EXPECT_EQ(sat2, UINT64_MAX); - EXPECT_FALSE(obj->IsChangedSince(UINT64_MAX)); + // 注:uint64 饱和边界无法通过公开 API 触达(需要 2^64 次写入),不再做白盒验证; + // 饱和实现见 NextVersionStamp:达到最大值后写回 MAX,不回绕。 mgr.DestroyGroup(gid); FakeluaDeleteState(s); } -TEST(test_native_version, debug_self_check_detects_missed_bump) { - NativeObject::SetVersionDebugEnabled(true); - auto *s = FakeluaNewState(); - auto &mgr = GetNativeObjectManager(s); - int64_t gid = mgr.CreateGroup(); - auto *root = mgr.Create(gid, "root", 1); - auto *child = mgr.Create(gid, "child", 2); - root->SetObject("child", child); - child->SetInt("a", 1); - child->RegisterMethod("m", NativeMethod{}); - - EXPECT_TRUE(NativeObject::SelfCheck(root, true)); - EXPECT_TRUE(NativeObject::SelfCheck(child, false)); - - // 绕过对象 API 的直接写入(模拟漏标写入点):版本号不变,但内容哈希不一致。 - const uint64_t vc = child->GetVersion(); - NativeObjectVersionTestAccess::RawSetInt(child, "bypass", 7); - EXPECT_EQ(child->GetVersion(), vc); - EXPECT_FALSE(NativeObject::SelfCheck(child, false)); - EXPECT_FALSE(NativeObject::SelfCheck(root, true)); - - // 正常写入会刷新哈希,自检恢复通过。 - child->SetInt("a", 2); - EXPECT_TRUE(NativeObject::SelfCheck(child, false)); - EXPECT_TRUE(NativeObject::SelfCheck(root, true)); - - mgr.DestroyGroup(gid); - FakeluaDeleteState(s); - NativeObject::SetVersionDebugEnabled(false); -} - TEST(test_native_version, self_check_is_cycle_safe) { NativeObject::SetVersionDebugEnabled(true); auto *s = FakeluaNewState(); @@ -1032,18 +989,14 @@ TEST(test_native_version, self_check_is_cycle_safe) { EXPECT_TRUE(NativeObject::SelfCheck(b, true)) << "SelfCheck 在环状对象图(B->A->B)上未能正常结束或误报"; - // 环上的漏标写入仍然能被发现(证明遍历确实走到了环的另一端)。 - const uint64_t vb = b->GetVersion(); - NativeObjectVersionTestAccess::RawSetInt(b, "bypass", 7); - EXPECT_EQ(b->GetVersion(), vb); - EXPECT_FALSE(NativeObject::SelfCheck(a, true)) - << "SelfCheck 未能通过环遍历发现 b 上的漏标写入"; - // 环中任一对象写入都只改自己的计数,不会波及对方(无传播、无递归风险)。 const uint64_t va = a->GetVersion(); b->SetInt("ok", 1); EXPECT_EQ(a->GetVersion(), va); + // 正常写入后自检仍然通过(内容哈希在写入钩子内刷新)。 + EXPECT_TRUE(NativeObject::SelfCheck(a, true)); + mgr.DestroyGroup(gid); FakeluaDeleteState(s); NativeObject::SetVersionDebugEnabled(false); From aba58cbdb5a0cda6269711d8776d03849e731012 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 6 Nov 2026 23:31:07 +0800 Subject: [PATCH 5/6] test(native): delete the unused white-box accessor header Follow-up to the previous commit: remove the (now unread) header file itself so the tree contains no test-only sources. --- src/native/object/native_object_test_access.h | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/native/object/native_object_test_access.h diff --git a/src/native/object/native_object_test_access.h b/src/native/object/native_object_test_access.h deleted file mode 100644 index 4644c179..00000000 --- a/src/native/object/native_object_test_access.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -// 仅供单元测试使用的白盒访问器,不属于公开 API,也不会随安装头发布。 -// 公开头 include/fakelua.h 里只保留该类型的前向声明与 friend 声明, -// 方法声明在本文件,定义在 src/native/object/native_object.cpp。 - -#include "fakelua.h" - -#include -#include - -namespace fakelua { - -struct NativeObjectVersionTestAccess { - // 绕过版本号维护直接写字段,用于验证 SelfCheck 能发现漏标写入。 - static void RawSetInt(NativeObject *obj, std::string_view key, int64_t val); - // 调整全局版本戳,用于测试饱和边界。 - static void SetStamp(uint64_t stamp); - [[nodiscard]] static uint64_t GetStamp(); - // 转发到 NativeObject 的私有 CVar 写入/读取路径。 - static void SetFromCVar(NativeObject *obj, std::string_view key, CVar v); - [[nodiscard]] static CVar GetAsCVar(NativeObject *obj, std::string_view key, State *s); -}; - -}// namespace fakelua From 6fb97d6db30867072d3711f4362bc62a8853b283 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sat, 7 Nov 2026 00:01:24 +0800 Subject: [PATCH 6/6] refactor(native): drop child/parent bookkeeping, gate debug hash at compile time * refactor(native): keep only version_ plus the per-write hook A change version is a "may have changed" signal, not a reference ledger. Delete the children refcount map and the parents set: SetObject/Del/SetNil/ Clear/SetFromCVar now only bump the owning object, and Destroy no longer notifies any parent. This removes two per-object unique_ptr fields and one heap allocation per link. Contract (documented in the public header): detach an object (Del/SetNil/ Clear or overwrite) from every parent field before Destroy. Group destruction is unaffected since all objects in the group die together. * perf(native): compile-time gate for the content-hash self-check New CMake option FAKELUA_NATIVE_OBJECT_DEBUG_HASH (default OFF) enables debug_hash/debug_hash_epoch and their epoch maintenance. Default builds carry no debug fields: SetVersionDebugEnabled is a no-op and SelfCheck returns true. SelfCheck no longer walks children because no child links are tracked. * test(native): pin the detach-first contract and shared-child behaviour Replace destroy-bump expectations, add same_child_in_multiple_fields and destroy_requires_detach_first, and keep the cycle-safe SelfCheck smoke test. --- include/fakelua.h | 24 ++-- src/CMakeLists.txt | 7 ++ src/native/object/native_object.cpp | 168 +++++----------------------- src/native/object/native_object.h | 14 +-- test/test_native.cpp | 65 +++++++++-- 5 files changed, 113 insertions(+), 165 deletions(-) diff --git a/include/fakelua.h b/include/fakelua.h index b3ac7743..c033314b 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -833,10 +833,13 @@ class NativeObject { // 外部资源状态(io/socket 等)改变。 void MarkModified(); - // 调试自检(默认关闭):开启后每次写入都会维护内容哈希;SelfCheck 会重新计算 - // 字段/方法内容哈希并与记录值比对,并递归校验所有嵌套对象的内容哈希 - // (visited 集合去重,对象图成环或共享子对象都是安全的), - // 用于交叉验证是否有写入点漏标版本。只在调试/测试中使用,开销较大。 + // 调试自检(编译期开关):仅当库以 FAKELUA_NATIVE_OBJECT_DEBUG_HASH 编译时, + // 每次写入才维护内容哈希;SelfCheck 重新计算本对象字段/方法的内容哈希并与记录值 + // 比对,用于交叉验证是否有写入点漏标版本。 + // 默认构建不包含哈希字段与维护逻辑:SetVersionDebugEnabled 为空操作, + // SelfCheck 恒返回 true(不做任何检查)。 + // 不做子树递归:版本号是对象自身粒度,且不再维护父子边, + // recursive 参数为兼容保留、当前被忽略。 static void SetVersionDebugEnabled(bool enabled); [[nodiscard]] static bool SelfCheck(const NativeObject *obj, bool recursive = true); @@ -867,16 +870,19 @@ class NativeObject { explicit NativeObject(std::string type_name); ~NativeObject(); - // 版本号内部维护:TouchVersion 只提升本对象自身(O(1),不做祖先传播)。 + // 版本号内部维护:TouchVersion 只提升本对象自身(O(1),无任何父子边簿记)。 void TouchVersion(); - void LinkChild(NativeObject *child); - void UnlinkChild(NativeObject *child); - // 销毁/析构时断开父子边:从父对象移除自己并 bump 父对象,从子对象移除自己但不 bump。 - void DetachAll(); +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + // 调试哈希维护(仅调试构建存在)。 void RefreshDebugHash(); [[nodiscard]] static uint64_t ComputeContentHash(const NativeObject *obj); +#endif // 内部申请与释放(只能由 NativeObjectManager 管理调配) + // 约束:销毁前必须先从所有父对象的字段中移除该对象(Del/SetNil/Clear 或覆盖字段)。 + // 实现不维护父子边索引,无法自动通知或清空父对象字段;销毁仍被引用/嵌套的对象 + // 属于调用方错误(父对象字段会保持悬空指针)。整组销毁(DestroyGroup/Clear)不受此限, + // 因为组内对象同时销毁。 static NativeObject *Create(int64_t group_id, std::string type_name, int64_t id = 0); static void Destroy(NativeObject *obj); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a5275577..dc7d5783 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -121,6 +121,13 @@ if (USE_COV) target_compile_definitions(fakelua PRIVATE "FAKELUA_JIT_COV_DIR=\"${CMAKE_BINARY_DIR}/jit_cov\"") endif () +# NativeObject 版本号调试自检:开启后每个对象额外维护内容哈希(仅调试/排障用,默认关闭, +# 默认构建不占这些字段)。配合 NativeObject::SetVersionDebugEnabled / SelfCheck 使用。 +option(FAKELUA_NATIVE_OBJECT_DEBUG_HASH "Enable NativeObject version content-hash self-check" OFF) +if (FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + target_compile_definitions(fakelua PRIVATE FAKELUA_NATIVE_OBJECT_DEBUG_HASH=1) +endif () + find_package(Threads REQUIRED) if (WIN32) diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index fc76c748..74c4dfb1 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -27,10 +27,13 @@ constexpr uint64_t kMaxVersionStamp = std::numeric_limits::max(); // 全局时间戳:每次写入分配一个更大的值。饱和而不是回绕,保证单调性永不被破坏。 std::atomic g_version_seq{0}; -// 调试自检开关(默认关闭)。 + +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) +// 调试自检开关(默认关闭,仅调试构建存在)。 std::atomic g_version_debug{false}; // 调试 epoch:每次切换调试开关都递增,使旧对象上记录的哈希失效。 std::atomic g_debug_epoch{0}; +#endif uint64_t NextVersionStamp() { const uint64_t old = g_version_seq.fetch_add(1, std::memory_order_relaxed); @@ -42,6 +45,7 @@ uint64_t NextVersionStamp() { return old + 1; } +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) inline void FnvHashBytes(uint64_t &h, const void *data, size_t n) { const unsigned char *p = static_cast(data); for (size_t i = 0; i < n; ++i) { @@ -53,6 +57,7 @@ inline void FnvHashBytes(uint64_t &h, const void *data, size_t n) { inline void FnvHashU64(uint64_t &h, uint64_t v) { FnvHashBytes(h, &v, sizeof(v)); } +#endif }// namespace @@ -523,13 +528,6 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { const std::string skey(key); State *s = spec->state; - // 记录被覆盖前的字段(可能是嵌套对象),写入后维护父子边并 bump 版本号。 - const auto old_it = obj->impl_->kv.find(skey); - NativeObject *prev_obj = nullptr; - if (old_it != obj->impl_->kv.end() && old_it->second.kind == NativeField::Kind::Object) { - prev_obj = old_it->second.obj; - } - // 快照尚未建立:只写 kv,保持惰性;首次 pairs/next/GET_TABLE_ENTRY 再 RefreshSpecKeys。 if (!spec->snapshot_ready) { if (v.type_ == static_cast(VarType::Nil)) { @@ -537,12 +535,9 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { } else { auto new_field = CVarToNativeField(v); obj->impl_->kv[skey] = new_field; - if (prev_obj && prev_obj != new_field.obj) obj->UnlinkChild(prev_obj); - if (new_field.obj && prev_obj != new_field.obj) obj->LinkChild(new_field.obj); obj->TouchVersion(); return; } - if (prev_obj) obj->UnlinkChild(prev_obj); obj->TouchVersion(); return; } @@ -550,7 +545,6 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { // 快照已存在:增量维护(更新已有键 / 追加 / swap-remove),避免每次 O(n) 全量重建。 if (v.type_ == static_cast(VarType::Nil)) { obj->impl_->kv.erase(skey); - if (prev_obj) obj->UnlinkChild(prev_obj); obj->TouchVersion(); const int idx = FindSpecKeyIndex(tbl, key); if (idx >= 0 && tbl->spec_count > 0) { @@ -566,8 +560,6 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { } auto new_field = CVarToNativeField(v); - if (prev_obj && prev_obj != new_field.obj) obj->UnlinkChild(prev_obj); - if (new_field.obj && prev_obj != new_field.obj) obj->LinkChild(new_field.obj); obj->impl_->kv[skey] = new_field; obj->TouchVersion(); const CVar new_val = NativeFieldToCVar(obj->impl_->kv[skey], s); @@ -590,14 +582,14 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { NativeObject::NativeObject(std::string type_name) : impl_(new Impl{std::move(type_name)}) { // 创建本身也是一次变化:分配一个全新的时间戳,保证新对象的版本号大于任何既有记录。 version_ = NextVersionStamp(); +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); +#endif } NativeObject::~NativeObject() { // 触发销毁回调,让拥有者释放关联的 C++ 资源(在 impl_ 仍有效时调用)。 if (impl_ && impl_->finalizer) impl_->finalizer(this); - // 未走 NativeObject::Destroy 直接析构时,也要断开父子边,避免幸存对象持有悬空指针。 - DetachAll(); delete impl_; impl_ = nullptr; } @@ -627,8 +619,7 @@ void NativeObject::Destroy(NativeObject *obj) { obj->impl_->finalizer = nullptr; fn(obj); } - // 子对象被删除属于父对象自身的结构变化,DetachAll 会让直接父对象的版本号前进。 - obj->DetachAll(); + // 不维护父子边索引:销毁前调用方必须已从所有父对象字段中移除该对象。 delete obj->impl_; obj->impl_ = nullptr; } @@ -700,16 +691,12 @@ void NativeObject::Del(std::string_view key) { TouchVersion(); return; } - if (it->second.kind == NativeField::Kind::Object) UnlinkChild(it->second.obj); impl_->kv.erase(it); TouchVersion(); } void NativeObject::Clear() { if (!impl_) return; - for (const auto &[_, field]: impl_->kv) { - if (field.kind == NativeField::Kind::Object) UnlinkChild(field.obj); - } impl_->kv.clear(); impl_->methods.clear(); TouchVersion(); @@ -741,7 +728,6 @@ void NativeObject::SetNil(std::string_view key) { if (!impl_) return; auto it = impl_->kv.find(std::string(key)); if (it != impl_->kv.end()) { - if (it->second.kind == NativeField::Kind::Object) UnlinkChild(it->second.obj); impl_->kv.erase(it); } TouchVersion(); @@ -750,7 +736,6 @@ void NativeObject::SetNil(std::string_view key) { void NativeObject::SetInt(std::string_view key, int64_t val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; - if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Int; f.i = val; f.obj = nullptr; @@ -760,7 +745,6 @@ void NativeObject::SetInt(std::string_view key, int64_t val) { void NativeObject::SetFloat(std::string_view key, double val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; - if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Float; f.f = val; f.obj = nullptr; @@ -770,7 +754,6 @@ void NativeObject::SetFloat(std::string_view key, double val) { void NativeObject::SetBool(std::string_view key, bool val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; - if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::Bool; f.b = val; f.obj = nullptr; @@ -780,7 +763,6 @@ void NativeObject::SetBool(std::string_view key, bool val) { void NativeObject::SetString(std::string_view key, std::string_view val) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; - if (f.kind == NativeField::Kind::Object) UnlinkChild(f.obj); f.kind = NativeField::Kind::String; f.s = std::string(val); f.vs_dirty = true; @@ -791,11 +773,6 @@ void NativeObject::SetString(std::string_view key, std::string_view val) { void NativeObject::SetObject(std::string_view key, NativeObject *obj) { if (!impl_) return; auto &f = impl_->kv[std::string(key)]; - NativeObject *prev = f.kind == NativeField::Kind::Object ? f.obj : nullptr; - if (prev && prev != obj) UnlinkChild(prev); - if (obj) { - if (prev != obj) LinkChild(obj); - } f.kind = NativeField::Kind::Object; f.obj = obj; TouchVersion(); @@ -803,13 +780,7 @@ void NativeObject::SetObject(std::string_view key, NativeObject *obj) { void NativeObject::SetFromCVar(std::string_view key, CVar v) { if (!impl_) return; - auto nf = CVarToNativeField(v); - auto it = impl_->kv.find(std::string(key)); - NativeObject *prev = nullptr; - if (it != impl_->kv.end() && it->second.kind == NativeField::Kind::Object) prev = it->second.obj; - if (prev && prev != nf.obj) UnlinkChild(prev); - if (nf.obj && prev != nf.obj) LinkChild(nf.obj); - impl_->kv[std::string(key)] = nf; + impl_->kv[std::string(key)] = CVarToNativeField(v); TouchVersion(); } @@ -1029,9 +1000,14 @@ bool NativeObject::IsChangedSince(uint64_t version) const { } void NativeObject::SetVersionDebugEnabled(bool enabled) { +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) g_version_debug.store(enabled, std::memory_order_relaxed); // 失效旧 epoch 的哈希:关闭期间发生的写入不会被误判为“漏标”。 g_debug_epoch.fetch_add(1, std::memory_order_relaxed); +#else + // 默认构建不含哈希字段,开关为空操作。 + (void) enabled; +#endif } void NativeObject::MarkModified() { @@ -1039,79 +1015,18 @@ void NativeObject::MarkModified() { TouchVersion(); } -void NativeObject::LinkChild(NativeObject *child) { - if (!impl_ || !child || !child->impl_) return; - if (!impl_->children) { - impl_->children = std::make_unique>(); - } else { - auto it = impl_->children->find(child); - if (it != impl_->children->end()) { - ++it->second; - return; - } - } - impl_->children->emplace(child, 1u); - if (!child->impl_->parents) { - child->impl_->parents = std::make_unique>(); - } - child->impl_->parents->insert(this); -} - -void NativeObject::UnlinkChild(NativeObject *child) { - if (!impl_ || !child || !impl_->children) return; - auto it = impl_->children->find(child); - if (it == impl_->children->end()) return; - if (it->second > 1) { - --it->second; - return; - } - impl_->children->erase(it); - if (impl_->children->empty()) impl_->children.reset(); - if (child->impl_ && child->impl_->parents) { - child->impl_->parents->erase(this); - if (child->impl_->parents->empty()) child->impl_->parents.reset(); - } -} - -void NativeObject::DetachAll() { - if (!impl_) return; - // 1) 从所有父对象的直接子表里移除自己:子对象的直接删除属于父对象自身的结构变化, - // 因此 bump 直接父对象(只 bump 直接父对象,不再向上蔓延)。 - // 复制一份 parents,避免遍历过程中任何潜在的结构变动影响迭代。 - boost::container::small_vector parent_list; - if (impl_->parents) { - parent_list.assign(impl_->parents->begin(), impl_->parents->end()); - impl_->parents->clear(); - } - for (NativeObject *p: parent_list) { - if (!p || !p->impl_ || p == this || !p->impl_->children) continue; - p->impl_->children->erase(this); - if (p->impl_->children->empty()) p->impl_->children.reset(); - p->TouchVersion(); - } - // 2) 从所有子对象的父集合里移除自己;子对象自身内容未变,不需要 bump。 - if (impl_->children) { - for (const auto &[child, _]: *impl_->children) { - if (child && child->impl_ && child != this && child->impl_->parents) { - child->impl_->parents->erase(this); - if (child->impl_->parents->empty()) child->impl_->parents.reset(); - } - } - impl_->children->clear(); - } - impl_->children.reset(); - impl_->parents.reset(); -} - void NativeObject::TouchVersion() { if (!impl_) return; // 只提升本对象自己的版本号:不向父对象传播,也不影响子对象。 - // 因此写入代价是 O(1),与树的深度无关。 + // 不做任何父子边簿记,写入代价是 O(1),与树的深度和子对象数量无关。 const uint64_t stamp = NextVersionStamp(); if (version_ < stamp) version_ = stamp; +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); +#endif } +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) void NativeObject::RefreshDebugHash() { if (!impl_) return; impl_->debug_hash = ComputeContentHash(this); @@ -1181,46 +1096,25 @@ uint64_t NativeObject::ComputeContentHash(const NativeObject *obj) { FnvHashU64(h, static_cast(obj->impl_->group_id)); return h; } +#endif bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { - if (!obj || !obj->impl_) return true; - - const bool debug = g_version_debug.load(std::memory_order_relaxed); - const uint64_t debug_epoch = g_debug_epoch.load(std::memory_order_relaxed); - const auto hash_is_current = [debug_epoch](const NativeObject::Impl *impl) { - return impl->debug_hash_epoch != 0 && impl->debug_hash_epoch == debug_epoch; - }; - if (debug) { +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + if (obj && obj->impl_ && g_version_debug.load(std::memory_order_relaxed)) { // 内容哈希比对:若某次写入绕过了对象 API(因此没有 TouchVersion), // 记录下来的哈希会与真实内容不一致。 - if (hash_is_current(obj->impl_) && obj->impl_->debug_hash != ComputeContentHash(obj)) { + const uint64_t debug_epoch = g_debug_epoch.load(std::memory_order_relaxed); + if (obj->impl_->debug_hash_epoch != 0 && obj->impl_->debug_hash_epoch == debug_epoch && + obj->impl_->debug_hash != ComputeContentHash(obj)) { return false; } } - if (!recursive) return true; - - // 递归校验所有嵌套对象的内容哈希。 - // 环安全:用 visited 集合去重,对象图成环(A→B→A)或共享子对象时不会重复入栈、 - // 也不会无限递归。代价是 O(N) 的额外内存,只在本调试开关打开时发生; - // 不使用深度上限,因此不会因为截断深度而产生误报/漏报。 - std::unordered_set visited; - boost::container::small_vector stack; - stack.push_back(obj); - visited.insert(obj); - while (!stack.empty()) { - const NativeObject *node = stack.back(); - stack.pop_back(); - if (!node->impl_->children) continue; - for (const auto &[child, _]: *node->impl_->children) { - if (!child || !child->impl_) continue; - if (debug) { - if (hash_is_current(child->impl_) && child->impl_->debug_hash != ComputeContentHash(child)) { - return false; - } - } - if (visited.insert(child).second) stack.push_back(child); - } - } +#else + (void) obj; +#endif + // 不维护父子边,也不做子树递归:版本号是「对象自身」粒度。 + // recursive 参数为兼容保留,当前忽略。 + (void) recursive; return true; } diff --git a/src/native/object/native_object.h b/src/native/object/native_object.h index 5e85d972..b253facf 100644 --- a/src/native/object/native_object.h +++ b/src/native/object/native_object.h @@ -81,18 +81,16 @@ struct NativeObject::Impl { std::function finalizer; // ---- 对象级版本号的内部簿记(不进入存档,不改变既有数据格式)---- - // 直接子对象及引用计数:同一个子对象可被同一父对象的多个字段引用, - // 只有引用计数归零才真正断开父子边。 - // 懒分配:绝大多数对象没有任何嵌套关系,空容器不占堆内存。 - std::unique_ptr> children; - // 直接父对象集合:子对象销毁/解绑时据此 bump 直接父对象的自身版本号。 - // 不做任何祖先传播;父集合只在需要「子对象删除通知父对象」时才用到。 - std::unique_ptr> parents; - // 调试自检模式下的内容哈希(关闭调试时不维护)。 + // 版本号只是「本对象可能变过」的信号,不做精确引用账本: + // 不记录子对象引用计数/父对象集合,父对象结构变化时一律直接 bump 自己即可。 + // 对象在销毁前必须先从所有父对象的字段中移除(Del/SetNil/Clear),见公开头注释。 +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + // 调试自检模式下的内容哈希(仅在定义该宏的调试构建中存在,默认构建不占字段)。 uint64_t debug_hash = 0; // 该哈希所属的调试 epoch。每次开关调试开关都会换 epoch, // 避免把关闭期间发生的写入和旧哈希错误地比较(误报漏标)。 uint64_t debug_hash_epoch = 0; +#endif }; // spec_get / spec_set 静态实现 diff --git a/test/test_native.cpp b/test/test_native.cpp index 71a5b0cf..45434e16 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -923,7 +923,7 @@ TEST(test_native_version, nested_writes_do_not_bump_ancestors) { FakeluaDeleteState(s); } -TEST(test_native_version, child_create_destroy_and_clear) { +TEST(test_native_version, child_create_and_clear_bump_parent) { auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); int64_t gid = mgr.CreateGroup(); @@ -934,15 +934,20 @@ TEST(test_native_version, child_create_destroy_and_clear) { parent->SetObject("child", child); EXPECT_TRUE(parent->IsChangedSince(vp));// 子对象创建/挂载 + // 约束:销毁子对象前必须先把它从父对象字段中移除;移除本身会 bump 父对象。 + vp = parent->GetVersion(); + parent->Del("child"); + EXPECT_TRUE(parent->IsChangedSince(vp)); + EXPECT_EQ(parent->GetObject("child"), nullptr); + vp = parent->GetVersion(); const uint64_t vc = child->GetVersion(); mgr.DestroySingle("child", 2); - EXPECT_TRUE(parent->IsChangedSince(vp));// 子对象删除 EXPECT_FALSE(child->Alive()); EXPECT_EQ(child->GetVersion(), vc);// 自身最后一次版本保留,单调不回退 + // 不维护父子边索引:销毁子对象不会再通知/bump 父对象。 + EXPECT_FALSE(parent->IsChangedSince(vp)); - auto *c2 = mgr.Create(gid, "child2", 3); - parent->SetObject("c2", c2); vp = parent->GetVersion(); parent->Clear(); EXPECT_TRUE(parent->IsChangedSince(vp));// 重置/批量清空 @@ -978,12 +983,13 @@ TEST(test_native_version, self_check_is_cycle_safe) { int64_t gid = mgr.CreateGroup(); auto *a = mgr.Create(gid, "a", 1); auto *b = mgr.Create(gid, "b", 2); - // 故意成环:A 挂 B,B 又指回 A。对象模型允许成环(LinkChild 不做环校验), + // 故意成环:A 挂 B,B 又指回 A。对象模型允许成环(SetObject 不做环校验), // 因此下游不能依赖「对象图无环」这个前提。 a->SetObject("b", b); b->SetObject("a", a); - // 环状对象图下自检必须正常终止,且内容一致时返回 true。 + // SelfCheck 只校验本对象(不做子树递归,也不再维护父子边), + // 成环对象图上必须直接返回、不崩不挂起。 EXPECT_TRUE(NativeObject::SelfCheck(a, true)) << "SelfCheck 在环状对象图(A->B->A)上未能正常结束或误报"; EXPECT_TRUE(NativeObject::SelfCheck(b, true)) @@ -1002,7 +1008,7 @@ TEST(test_native_version, self_check_is_cycle_safe) { NativeObject::SetVersionDebugEnabled(false); } -TEST(test_native_version, destroy_bumps_direct_parent_only) { +TEST(test_native_version, destroy_requires_detach_first) { auto *s = FakeluaNewState(); auto &mgr = GetNativeObjectManager(s); int64_t gid = mgr.CreateGroup(); @@ -1012,16 +1018,23 @@ TEST(test_native_version, destroy_bumps_direct_parent_only) { gp->SetObject("parent", parent); parent->SetObject("child", child); + // 约定:销毁被引用对象前,先从所有父对象字段中移除它。 + uint64_t vp = parent->GetVersion(); + parent->Del("child"); + EXPECT_TRUE(parent->IsChangedSince(vp)) << "解绑子对象应 bump 父对象自身"; + EXPECT_EQ(parent->GetObject("child"), nullptr); + const uint64_t vgp = gp->GetVersion(); - const uint64_t vp = parent->GetVersion(); + vp = parent->GetVersion(); const uint64_t vc = child->GetVersion(); mgr.DestroySingle("child", 3); - EXPECT_TRUE(parent->IsChangedSince(vp)) << "子对象删除应 bump 直接父对象"; - EXPECT_FALSE(gp->IsChangedSince(vgp)) << "子对象删除不应 bump 祖父对象"; EXPECT_FALSE(child->Alive()); EXPECT_EQ(child->GetVersion(), vc) << "销毁后版本号应保留,不回退"; + // 不维护父子边索引:销毁不再通知父对象,也不再向上蔓延。 + EXPECT_FALSE(parent->IsChangedSince(vp)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); - // 直接子表维护本身也算父对象的结构变化。 + // 直接子对象挂载仍是父对象自身的结构变化。 auto *c2 = mgr.Create(gid, "child2", 4); const uint64_t vp2 = parent->GetVersion(); parent->SetObject("child2", c2); @@ -1032,6 +1045,36 @@ TEST(test_native_version, destroy_bumps_direct_parent_only) { FakeluaDeleteState(s); } +// 事实前提:同一个子对象可被同一父对象的多个字段引用(对象模型不禁止共享)。 +TEST(test_native_version, same_child_in_multiple_fields) { + auto *s = FakeluaNewState(); + auto &mgr = GetNativeObjectManager(s); + int64_t gid = mgr.CreateGroup(); + auto *parent = mgr.Create(gid, "parent", 1); + auto *child = mgr.Create(gid, "child", 2); + + parent->SetObject("a", child); + parent->SetObject("b", child); + EXPECT_EQ(parent->GetObject("a"), child); + EXPECT_EQ(parent->GetObject("b"), child); + + uint64_t vp = parent->GetVersion(); + parent->SetNil("a"); + EXPECT_TRUE(parent->IsChangedSince(vp)); + // 另一个字段仍然引用该子对象:不会因为解绑一处而被销毁或悬空。 + EXPECT_EQ(parent->GetObject("b"), child); + EXPECT_TRUE(child->Alive()); + + vp = parent->GetVersion(); + parent->Del("b"); + EXPECT_TRUE(parent->IsChangedSince(vp)); + mgr.DestroySingle("child", 2); + EXPECT_FALSE(child->Alive()); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + TEST(test_native_version, lua_paths_and_container) { for (auto jit_type: AllJitTypes()) { auto *s = FakeluaNewState();