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 偏移錯誤讀到錯誤記憶體位置

相關筆記