ZRef — 智慧指標與引用計數
概念:MapleStory 的物件生命週期管理
MapleStory 使用 ZRef<T> 來管理所有重要物件(玩家、封包、UI 元素等)的生命週期,概念類似 C++11 的 std::shared_ptr,但更早實作且針對 x86 單元件進行了優化。
類別體系
ZRefCounted ← 引用計數基底(所有受管理物件必須繼承)
↑
ZRefCountedDummy<T> ← 若 T 不繼承 ZRefCounted,由此包裝
↑
ZRefCountedAccessor<T> ← 空基底(僅保留 vtable 版面對齊,1 byte)
↑
ZRef<T> ← 智慧指標主體(4 bytes on x86: gap0[1] + T* p)
ZRefCounted — 引用計數基底
class ZRefCounted {
public:
union {
volatile long m_nRef; // 引用計數(InterlockedIncrement/Decrement)
ZRefCounted* m_pNext; // 共用記憶體,空閒時作為鏈結串列 next 指標
};
ZRefCounted* m_pPrev; // 雙向鏈結
virtual ~ZRefCounted() = default; // 虛析構(必須!才能正確 delete 子類別)
};
// sizeof(ZRefCounted) == 0x0C(12 bytes)必須有虛析構
delete pBase能正確呼叫子類別析構,依賴virtual ~ZRefCounted()。若仿製 MapleStory 物件時忘記加,ReleaseRaw()呼叫delete pBase時不會觸發子物件的清理。
ZRefCountedDummy — 非繼承型別的包裝器
若你想用 ZRef<int> 或 ZRef<SomeStructNotInheritingZRefCounted>:
template <class T>
class ZRefCountedDummy : public ZRefCounted,
public ZRecyclable<ZRefCountedDummy<T>, 16, T>
{
public:
T t; // 被包裝的實際資料
// new/delete 都走 ZRecyclableAvBuffer(物件回收池)
};
// sizeof(ZRefCountedDummy<int>) == 0x14(20 bytes)
// = sizeof(ZRefCounted)(0xC) + padding(0x4) + sizeof(int)(0x4)ZRef<T> 的 GetBase() 會根據 std::is_base_of<ZRefCounted, T>() 判斷:
- 是:直接把
T*當成ZRefCounted*使用 - 否:往回偏移
sizeof(ZRefCountedDummy<T>) - sizeof(T)= 16 bytes,找到ZRefCountedDummy的頭
ZRef 主體
記憶體版面
ZRef<T>(8 bytes on x86):
┌──────────┬──────────┐
│ gap0[1] │ T* p │
│ (1 byte) │ (4 bytes)│
└──────────┴──────────┘
gap0[1] 是從 ZRefCountedAccessor 繼承的空基底對齊 padding,必須保留才能與 MapleStory 的版面一致。
建構與賦值
// 空指標
ZRef<CPlayer> player; // p = nullptr
// 從 ZRefCounted* 建構(預設 AddRef)
ZRef<CPlayer> player(pSomePlayer); // nRef++
// 不 AddRef(接管已有的引用)
ZRef<CPlayer> player(pSomePlayer, FALSE);
// 從另一個 ZRef 複製
ZRef<CPlayer> player2(&player1); // nRef++
// 分配新物件
ZRef<CPlayer> player;
player.Alloc(); // new T() 或 new ZRefCountedDummy<T>(),nRef = 1存取
CPlayer* raw = player.p; // 直接存取原始指標(public!)
CPlayer* raw2 = (CPlayer*)player; // operator T*()
player->DoSomething(); // operator->()
if (!player) { /* p == nullptr */ }釋放
player = 0; // operator=(int zero) → ReleaseRaw()
// 或讓 ZRef 超出作用域 → ~ZRef() → ReleaseRaw()ReleaseRaw 流程
void ReleaseRaw() {
if (!p) return;
ZRefCounted* pBase = GetBase();
if (InterlockedDecrement(&pBase->m_nRef) <= 0) {
InterlockedIncrement(&pBase->m_nRef); // 防止二次釋放
delete pBase; // 觸發虛析構
}
p = nullptr;
}實際使用範例
// 正確方式:用 ZRef 管理一個受 MapleStory 管理的物件
ZRef<CWvsContext> ctx;
ctx.Alloc();
ctx->SetWorldId(1);
// ctx 超出作用域時自動釋放
// 從 MapleStory 內部取得已存在的物件(不能亂 delete!)
// 此時不應呼叫 Alloc,只應讀取 p
ZRef<CUser>* pLocalUser = reinterpret_cast<ZRef<CUser>*>(
AddyLocations::GetLocalUser() // 讀取已知位址
);
pLocalUser->p->GetHP(); // 使用 MapleStory 管理的物件
// 切勿對 pLocalUser 呼叫 Alloc() 或 delete,會崩潰!常見錯誤
| 錯誤 | 後果 |
|---|---|
用 delete 直接刪除 ZRef::p 指向的物件 | double-free 崩潰 |
把 ZRef<T> 的原始指標傳給 MapleStory,同時 ZRef 超出作用域 | use-after-free |
多執行緒下非原子性操作 m_nRef | 引用計數競爭 |
忘記 gap0[1] padding 導致 struct 偏移錯誤 | 讀到錯誤記憶體位置 |