為 Lua 綁定 C/C++ 對象如何綁定 C/C++ 對象到 Lua 里?通常是創(chuàng)建一個(gè) userdata ,存放 C/C++ 對象指針,然后給 userdata 添加元表,用 index 元方法映射 C/C++ 中的對象方法。 也有另一個(gè)手段,直接用 lightuserdata 保存 C/C++ 對象指針放到 Lua 中,在 Lua 中創(chuàng)建一個(gè) table 附加元表來來包裝這個(gè)指針,效果是類似的。區(qū)別在于對象生命期的管理方式有所不同。就這個(gè)問題,幾年前我寫過一篇 blog 。 綁定 C/C++ 對象到 Lua 里的設(shè)計(jì)難點(diǎn)往往在這個(gè)正確的生命期管理上。因?yàn)?C/C++ 沒有 GC 系統(tǒng),依賴手工管理資源;而 Lua 則是利用 GC 做自動(dòng)回收。這兩者的差異容易導(dǎo)致在 Lua 中的對象對應(yīng)的 C/C++ 對象已經(jīng)銷毀而 Lua 層不自知,或 Lua 層中已無對象之引用,而 C/C++ 層中卻未能及時(shí)回收資源而造成內(nèi)存泄露。 理清這個(gè)問題,首先你要確定,你打算以 Lua 為主干來維護(hù)對象的生命期,還是以 C/C++ 層為主干 Lua 部分只是做一些對這些對象的行為控制。 我個(gè)人主張圍繞 Lua 來開發(fā),C/C++ 只是寫一些性能相關(guān)的庫供 Lua 調(diào)用,即框架層在 Lua 中。這樣,C/C++ 層只提供對象的創(chuàng)建和銷毀函數(shù),不要用 C 指針做對象的相互引用。Lua 中對象被回收時(shí),銷毀對應(yīng)的 C 對象即可。 但是,也有相當(dāng)多的項(xiàng)目做不到這點(diǎn)。Lua 是在后期引入的,之前 C/C++ 框架層中已做好了相當(dāng)之復(fù)雜的對象管理?;蛘邩?gòu)架師不希望把腳本層過多的侵入引擎的設(shè)計(jì)。 那么,下面給出另一個(gè)方案。 我們將包裝進(jìn) Lua 的 C 對象稱為 script object ,那么只需要提供三個(gè)函數(shù)即可。 int script_pushobject(lua_State *L, void * object) { void **ud; if (luaL_newmetatable(L, "script")) { // 在注冊表中創(chuàng)建一個(gè)表存放所有的 object 指針到 userdata 的關(guān)系。 // 這個(gè)表應(yīng)該是一個(gè) weak table ,當(dāng) Lua 中不再存在對 C 對象的引用會(huì)刪除對應(yīng)的記錄。 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 對象指針被釋放后,有可能地址被重用。 // 這個(gè)時(shí)候,可能取到曾經(jīng)保存起來的 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; } 這個(gè)函數(shù)把一個(gè) C 對象指針置入對應(yīng)的 userdata ,如果是第一次 push 則創(chuàng)建出新的 userdata ,否則復(fù)用曾經(jīng)創(chuàng)建過的。 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; } 這個(gè)函數(shù)把 index 處的 userdata 轉(zhuǎn)換為一個(gè) C 對象。如果對象已經(jīng)銷毀,則返回 NULL 指針。 在給這個(gè)對象綁定 C 方法時(shí),應(yīng)注意在 toobject 調(diào)用后,全部對指針做檢查,空指針應(yīng)該被正確處理。 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); // 這個(gè) assert 防止 deleteobject 被重復(fù)調(diào)用。 assert(*ud == object); // 銷毀一個(gè)被 Lua 引用住的對象,只需要把 *ud 置為 NULL 。 *ud = NULL; } lua_pop(L,2); } else { // 有可能從未調(diào)用過 pushobject ,此時(shí)注冊表中 script 項(xiàng)尚未建立。 lua_pop(L,1); } } 這個(gè)函數(shù)會(huì)解除 C 對象在 Lua 中的引用,后續(xù)在 Lua 中對這個(gè)對象的訪問,都將得到 NULL 指針。 這些代碼是在我寫這篇 blog 的同時(shí)隨手寫的,并未經(jīng)過嚴(yán)格測試。它們也有許多改進(jìn)空間,比如給 C 對象加入類型,對 userdata 做更嚴(yán)格的檢查,等等。 |
|