如何綁定 C/C++ 對象到 Lua 裡?通常是創建一個 userdata ,存放 C/C++ 對象指針,然後給 userdata 添加元表,用 index 元方法映射 C/C++ 中的對象方法。 也有另一個手段,直接用 lightuserdata 保存 C/C++ 對象指針放到 Lua 中,在 Lua 中創建一個 table 附加元表來來包裝這個指針,效果是類似的。區別在於對象生命期的管理方式有所不同。就這個問題,幾年前我寫過一篇 blog。 綁定 C/C++ 對象到 Lua 裡的設計難點往往在這個正確的生命期管理上。因為 C/C++ 沒有 GC 系統,依賴手工管理資源;而 Lua 則是利用 GC 做自動回收。這兩者的差異容易導致在 Lua 中的對象對應的 C/C++ 對象已經銷毀而 Lua 層不自知,或 Lua 層中已無對象之引用,而 C/C++ 層中卻未能及時回收資源而造成內存洩露。 理清這個問題,首先你要確定,你打算以 Lua 為主干來維護對象的生命期,還是以 C/C++ 層為主干 Lua 部分只是做一些對這些對象的行為控制。 我個人主張圍繞 Lua 來開發,C/C++ 只是寫一些性能相關的庫供 Lua 調用,即框架層在 Lua 中。這樣,C/C++ 層只提供對象的創建和銷毀函數,不要用 C 指針做對象的相互引用。Lua 中對象被回收時,銷毀對應的 C 對象即可。 但是,也有相當多的項目做不到這點。Lua 是在後期引入的,之前 C/C++ 框架層中已做好了相當之復雜的對象管理。或者構架師不希望把腳本層過多的侵入引擎的設計。 那麼,下面給出另一個方案。 我們將包裝進 Lua 的 C 對象稱為 script object ,那麼只需要提供三個函數即可。 int script_pushobject(lua_State *L, void * object) { void **ud; if (luaL_newmetatable(L, "script")) { // 在注冊表中創建一個表存放所有的 object 指針到 userdata 的關系。 // 這個表應該是一個 weak table ,當 Lua 中不再存在對 C 對象的引用會刪除對應的記錄。 lua_newtable(L); lua_pushliteral(L, "kv"); lua_setfield(L, -2, "__mode"); lua_setmetatable(L, -2); } lua_rawgetp(L,-1,object); if (lua_type(L,-1)==LUA_TUSERDATA) { ud = (void **)lua_touserdata(L,-1); if (*ud == object) { lua_replace(L, -2); return 0; } // C 對象指針被釋放後,有可能地址被重用。 // 這個時候,可能取到曾經保存起來的 userdata ,裡面的指針必然為空。 assert(*ud == NULL); } ud = (void **)lua_newuserdata(L, sizeof(void*)); *ud = object; lua_pushvalue(L, -1); lua_rawsetp(L, -4, object); lua_replace(L, -3); lua_pop(L,1); return 1; } 這個函數把一個 C 對象指針置入對應的 userdata ,如果是第一次 push 則創建出新的 userdata ,否則復用曾經創建過的。 void * script_toobject(lua_State *L, int index) { void **ud = (void **)lua_touserdata(L,index); if (ud == NULL) return NULL; // 如果 object 已在 C 代碼中銷毀,*ud 為 NULL 。 return *ud; } 這個函數把 index 處的 userdata 轉換為一個 C 對象。如果對象已經銷毀,則返回 NULL 指針。 在給這個對象綁定 C 方法時,應注意在 toobject 調用後,全部對指針做檢查,空指針應該被正確處理。 void script_deleteobject(lua_State *L, void *object) { luaL_getmetatable(L, "script"); if (lua_istable(L,-1)) { lua_rawgetp(L, -1, object); if (lua_type(L,-1) == LUA_TUSERDATA) { void **ud = (void **)lua_touserdata(L,-1); // 這個 assert 防止 deleteobject 被重復調用。 assert(*ud == object); www.2cto.com // 銷毀一個被 Lua 引用住的對象,只需要把 *ud 置為 NULL 。 *ud = NULL; } lua_pop(L,2); } else { // 有可能從未調用過 pushobject ,此時注冊表中 script 項尚未建立。 lua_pop(L,1); } } 這個函數會解除 C 對象在 Lua 中的引用,後續在 Lua 中對這個對象的訪問,都將得到 NULL 指針。 這些代碼是在我寫這篇 blog 的同時隨手寫的,並未經過嚴格測試。它們也有許多改進空間,比如給 C 對象加入類型,對 userdata 做更嚴格的檢查,等等。