« 房租分配问题 | 返回首页 | Pixel light 中的场景管理 »

为 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

评论里面三楼说不安全的可能,我觉得三楼说的不对,因为gc是lua先调用,然后再告诉c++的,而不是三楼说的那样C++先释放lua还未gc的情况。

你好,最近本人也是写了一个这样的系统,主要目的就是解决这种生命期的问题,我的做法是lua层对c++对象层之间加一个Proxy,lua的操作都通过Proxy传递到c++部分,因为在导出c++类到lua上有一个类型记录,所有这个传递把类型恢复了。而c++对象对userdata的引用我用的的luaL_ref返回的值(基于:ref的访问要比用实际的key访问快),这里想请教下大牛,如果说luaL_ref的表是一个weak表,luaL_ref机制会不会被打乱了?

to lc(6):你说得这个情况我碰到了好多次。

其实从理论上来说, 上面的代码并不是安全的, 因为lua的垃圾收集不是实时的, 所以, 有一种情况, 你没有考虑到, 那就是c对象已经被释放了, 后来重新分配到了一个相同的地址, 但lua的这期间还未执行垃圾收集,因些弱表中的对象的地址还是之前的旧对象的地址, 这种情况从理论上来说是存在的,因些你写的上面的代码并不是百分百安全

一直来看,从未留过言,虽然大过你写的博文都看不太懂,但是一直都在努力,希望有一天能与你一起共事吧!

我一直在做Android,在JAVA和C++层也是遇到这种生命期管理的问题。我采用的方案是给C++对象增加引用计数的方式来处理。一个C++对象绑定到JAVA对象时就增加一个引用,取消绑定时较少一个引用。

luabind就很好用了啊
这个轮子的优势在于?

用luabind 不都可以完美解决么。
这个轮子相对luabind的优势是?

我的lbind库(github.com/starwing/lbind)目前开发完成的runtime部分带了这个功能(等会儿我把目前工作的版本更新上去)。另外还有其他的一些功能,比如说绕过较慢的pushXstring,采用二次哈希的方法在C层面使用字符串的哈希值来加速索引操作等等。Lua的代码生成部分遇到了问题还没完成。不过这个runtime可以拿来手写C/C++绑定了,能大幅增加开发效率。

Post a comment

非这个主题相关的留言请到:留言本