diff --git a/benchmark/benchmark_native_object.cpp b/benchmark/benchmark_native_object.cpp new file mode 100644 index 00000000..aa710f3e --- /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 层父子链上写叶子:用于观察深层对象上的写入开销(简化后应与浅层写入一致)。 +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..c033314b 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -812,6 +813,36 @@ class NativeObject { void Del(std::string_view key); void Clear(); + // ---- 对象级变更版本号(增量存档 / C++ 镜像同步 / UI 缓存失效)---- + // 语义边界:只描述「这个对象自身」,不包含它的子树。 + // * 本对象自身的写入(字段、方法、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 内部容器内容、 + // 外部资源状态(io/socket 等)改变。 + void MarkModified(); + + // 调试自检(编译期开关):仅当库以 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); + // 销毁回调 // 设置 NativeObject 被 Destroy 时(含管理器 Clear / DestroyGroup)的回调, // 用于释放该对象拥有的 C++ 资源(如 socket)。回调内不应再操作 Lua 状态。 @@ -832,11 +863,26 @@ class NativeObject { private: struct Impl; Impl *impl_; + // 对象级版本戳保存在 NativeObject 本身(不随 Impl 释放), + // 因此 Destroy 之后仍能查到最后一次版本,保持单调性。 + uint64_t version_ = 0; explicit NativeObject(std::string type_name); ~NativeObject(); + // 版本号内部维护:TouchVersion 只提升本对象自身(O(1),无任何父子边簿记)。 + void TouchVersion(); +#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/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..74c4dfb1 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -9,13 +9,58 @@ #include "var/var_multi.h" #include +#include #include #include #include #include +#include namespace fakelua { +namespace { + +// ---- 对象级版本戳基础设施 ---- +// 版本号使用全局单调时间戳(不是每对象的计数器)。语义是「对象自身」粒度: +// 只反映本对象自己的写入,不包含子树;需要子树粒度时由调用方自行聚合。 +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); + if (old == kMaxVersionStamp) { + // fetch_add 已把计数器回绕成 0,这里写回 MAX,之后每次调用都返回 MAX。 + g_version_seq.store(kMaxVersionStamp, std::memory_order_relaxed); + return kMaxVersionStamp; + } + 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) { + h ^= static_cast(p[i]); + h *= 1099511628211ULL; + } +} + +inline void FnvHashU64(uint64_t &h, uint64_t v) { + FnvHashBytes(h, &v, sizeof(v)); +} +#endif + +}// namespace + // CVar ↔ NativeField 转换 CVar NativeFieldToCVar(const NativeField &field, State *s) { @@ -488,14 +533,19 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { 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; + obj->TouchVersion(); + return; } + obj->TouchVersion(); return; } // 快照已存在:增量维护(更新已有键 / 追加 / swap-remove),避免每次 O(n) 全量重建。 if (v.type_ == static_cast(VarType::Nil)) { obj->impl_->kv.erase(skey); + obj->TouchVersion(); const int idx = FindSpecKeyIndex(tbl, key); if (idx >= 0 && tbl->spec_count > 0) { const uint32_t uidx = static_cast(idx); @@ -509,7 +559,9 @@ void NativeSpecSet(VarTable *tbl, CVar k, CVar v, bool *finish) { return; } - obj->impl_->kv[skey] = CVarToNativeField(v); + auto new_field = CVarToNativeField(v); + 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,6 +580,11 @@ 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 defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + if (g_version_debug.load(std::memory_order_relaxed)) RefreshDebugHash(); +#endif } NativeObject::~NativeObject() { @@ -544,6 +601,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 +619,7 @@ void NativeObject::Destroy(NativeObject *obj) { obj->impl_->finalizer = nullptr; fn(obj); } + // 不维护父子边索引:销毁前调用方必须已从所有父对象字段中移除该对象。 delete obj->impl_; obj->impl_ = nullptr; } @@ -626,18 +685,27 @@ 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; + } + impl_->kv.erase(it); + TouchVersion(); } void NativeObject::Clear() { if (!impl_) return; 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 +716,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,7 +726,11 @@ 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()) { + impl_->kv.erase(it); + } + TouchVersion(); } void NativeObject::SetInt(std::string_view key, int64_t val) { @@ -665,6 +738,8 @@ void NativeObject::SetInt(std::string_view key, int64_t val) { auto &f = impl_->kv[std::string(key)]; f.kind = NativeField::Kind::Int; f.i = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetFloat(std::string_view key, double val) { @@ -672,6 +747,8 @@ void NativeObject::SetFloat(std::string_view key, double val) { auto &f = impl_->kv[std::string(key)]; f.kind = NativeField::Kind::Float; f.f = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetBool(std::string_view key, bool val) { @@ -679,6 +756,8 @@ void NativeObject::SetBool(std::string_view key, bool val) { auto &f = impl_->kv[std::string(key)]; f.kind = NativeField::Kind::Bool; f.b = val; + f.obj = nullptr; + TouchVersion(); } void NativeObject::SetString(std::string_view key, std::string_view val) { @@ -687,6 +766,8 @@ void NativeObject::SetString(std::string_view key, std::string_view val) { 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) { @@ -694,11 +775,13 @@ void NativeObject::SetObject(std::string_view key, NativeObject *obj) { auto &f = impl_->kv[std::string(key)]; 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); + TouchVersion(); } // Get 系列 @@ -778,6 +861,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 +871,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 +988,134 @@ 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) { +#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() { + if (!impl_) return; + TouchVersion(); +} + +void NativeObject::TouchVersion() { + if (!impl_) return; + // 只提升本对象自己的版本号:不向父对象传播,也不影响子对象。 + // 不做任何父子边簿记,写入代价是 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); + 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; +} +#endif + +bool NativeObject::SelfCheck(const NativeObject *obj, bool recursive) { +#if defined(FAKELUA_NATIVE_OBJECT_DEBUG_HASH) + if (obj && obj->impl_ && g_version_debug.load(std::memory_order_relaxed)) { + // 内容哈希比对:若某次写入绕过了对象 API(因此没有 TouchVersion), + // 记录下来的哈希会与真实内容不一致。 + 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; + } + } +#else + (void) obj; +#endif + // 不维护父子边,也不做子树递归:版本号是「对象自身」粒度。 + // recursive 参数为兼容保留,当前忽略。 + (void) recursive; + return true; +} + }// namespace fakelua diff --git a/src/native/object/native_object.h b/src/native/object/native_object.h index c192d574..b253facf 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,18 @@ struct NativeObject::Impl { std::unordered_map methods; // 可选销毁回调:NativeObject 被 Destroy 时调用(无论是否经 close 方法),用于释放关联资源 std::function finalizer; + + // ---- 对象级版本号的内部簿记(不进入存档,不改变既有数据格式)---- + // 版本号只是「本对象可能变过」的信号,不做精确引用账本: + // 不记录子对象引用计数/父对象集合,父对象结构变化时一律直接 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/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..45434e16 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -780,3 +780,348 @@ 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("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 读取路径(GetAsCVar)由 Lua 侧只读用例间接覆盖(见 lua_paths_and_container)。 + 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_writes_do_not_bump_ancestors) { + 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(leaf->IsChangedSince(vl)); + EXPECT_FALSE(a->IsChangedSince(va)); + EXPECT_FALSE(root->IsChangedSince(vr)); + EXPECT_FALSE(b->IsChangedSince(vb)); + + // 中间层写入同样不向上蔓延。 + va = a->GetVersion(); + 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_FALSE(p1->IsChangedSince(v1)); + EXPECT_FALSE(p2->IsChangedSince(v2)); + + // 直接子对象的挂载/卸载属于父对象自身的结构变化:只 bump 该父对象, + // 不 bump 它的父对象(祖先链上不蔓延)。 + auto *gp = mgr.Create(gid, "gp", 7); + gp->SetObject("p1", p1); + uint64_t vgp = gp->GetVersion(); + v1 = p1->GetVersion(); + p1->SetObject("d", leaf); + EXPECT_TRUE(p1->IsChangedSince(v1)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); + vgp = gp->GetVersion(); + v1 = p1->GetVersion(); + p1->SetNil("d"); + EXPECT_TRUE(p1->IsChangedSince(v1)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, child_create_and_clear_bump_parent) { + 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));// 子对象创建/挂载 + + // 约束:销毁子对象前必须先把它从父对象字段中移除;移除本身会 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_FALSE(child->Alive()); + EXPECT_EQ(child->GetVersion(), vc);// 自身最后一次版本保留,单调不回退 + // 不维护父子边索引:销毁子对象不会再通知/bump 父对象。 + EXPECT_FALSE(parent->IsChangedSince(vp)); + + vp = parent->GetVersion(); + parent->Clear(); + EXPECT_TRUE(parent->IsChangedSince(vp));// 重置/批量清空 + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +TEST(test_native_version, monotonic_versions) { + 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 饱和边界无法通过公开 API 触达(需要 2^64 次写入),不再做白盒验证; + // 饱和实现见 NextVersionStamp:达到最大值后写回 MAX,不回绕。 + + mgr.DestroyGroup(gid); + FakeluaDeleteState(s); +} + +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。对象模型允许成环(SetObject 不做环校验), + // 因此下游不能依赖「对象图无环」这个前提。 + a->SetObject("b", b); + b->SetObject("a", a); + + // SelfCheck 只校验本对象(不做子树递归,也不再维护父子边), + // 成环对象图上必须直接返回、不崩不挂起。 + EXPECT_TRUE(NativeObject::SelfCheck(a, true)) + << "SelfCheck 在环状对象图(A->B->A)上未能正常结束或误报"; + EXPECT_TRUE(NativeObject::SelfCheck(b, true)) + << "SelfCheck 在环状对象图(B->A->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); +} + +TEST(test_native_version, destroy_requires_detach_first) { + 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); + + // 约定:销毁被引用对象前,先从所有父对象字段中移除它。 + 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(); + vp = parent->GetVersion(); + const uint64_t vc = child->GetVersion(); + mgr.DestroySingle("child", 3); + 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); + EXPECT_TRUE(parent->IsChangedSince(vp2)); + EXPECT_FALSE(gp->IsChangedSince(vgp)); + + mgr.DestroyGroup(gid); + 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(); + 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); + } +}