为 Lua 绑定 C/C++ 对象
如何绑定 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); // 销毁一个被 Lua 引用住的对象,只需要把 *ud 置为 NULL 。 *ud = NULL; } lua_pop(L,2); } else { // 有可能从未调用过 pushobject ,此时注册表中 script 项尚未建立。 lua_pop(L,1); } }
这个函数会解除 C 对象在 Lua 中的引用,后续在 Lua 中对这个对象的访问,都将得到 NULL 指针。
这些代码是在我写这篇 blog 的同时随手写的,并未经过严格测试。它们也有许多改进空间,比如给 C 对象加入类型,对 userdata 做更严格的检查,等等。
Comments
Posted by: shiquan | (9) July 29, 2016 07:09 PM
Posted by: Yang G | (8) February 4, 2013 04:55 PM
Posted by: sc | (7) January 24, 2013 04:24 PM
Posted by: lc | (6) January 23, 2013 02:41 PM
Posted by: sky | (5) January 10, 2013 07:09 PM
Posted by: cykit | (4) January 10, 2013 02:29 PM
Posted by: fastfading | (3) January 10, 2013 12:57 PM
Posted by: fastfading | (2) January 10, 2013 12:56 PM
Posted by: sw | (1) January 10, 2013 12:43 PM