« skynet 1.4.0 | 返回首页 | 粒子系统的设计 »

Lua binding 的一些方法

这几天在给 RmlUi 这个库写 Lua binding 。

这个库原本有一个官方的 lua binding ,但是新特性 Data Model 却没有实现。作者坦承自己对 Lua 不是特别熟悉,这个新特性也在开发中,暂时没想好该怎么做,所以只完成了 C++ 接口,Lua 接口留待这方面跟懂的人来做。

我觉得这个新特性有点意思,打算帮助这个项目实现 Lua 接口。在实现的过程中,发现原版的 Lua binding 做的过于复杂,且那些复杂度完全没有必要。所以打算自己全部重新实现一套。

原版的 binding 存在的问题非常有代表性,在很多提供了 Lua binding 的 C++ 项目中都出现过。那就是实现了一套非常复杂的对象生命期管理。这是因为,作者试图可以从 C++ 方面和 Lua 方面同时全面控制同一个对象,包括对象的构造和销毁。每个对象都有一个 C++ 版本和一个 Lua 版本,它们相互关联在一起,一边销毁后,都需要妥善通知另一方可以正确处理。

整套机制使用了 lua object 的 gc 方法以及 C++ 对象的引用计数,且整个业务逻辑都在 C++ 里实现,变得异常繁杂。

我的建议是:像这种本来就是 script 驱动的东西(Lua 在 RmlUi 中的地位类似于 javascript 于 web 页面),只应该由 Lua 管理对象的生命,C++ 的对象引用只是它的附属品。也就是说,即使你想在 C++ 里创建会销毁一个对象,也必须通过 Lua 接口的间接调用,而不应该提供任何 C++ API 直接控制。

第二,所有 C++ 对象都被映射为 Lua 中的 Userdata ,并为其提供 metamethod ,让这些 userdata 尽可能地表现和 C++ 对象一致。这用了大量的 C++ 代码去实现这些机制。本质上,这些都是胶水代码,没有实现任何新的特性,只是用来给转译 C++ 功能到 Lua 搭桥而已。这其中还包括了将 C++ 里的多种数据类型以及函数类型用模板映射到 Lua 中,以及反向映射。每个在 Lua 中的操作,都不断地通过 metamethod 映射到 C++ 中做对应操作。这些不仅臃肿而且低效。

我的建议是:只映射必要的 C style API 到 Lua 。例如 Object::Method 如果是必要的,那么只需要提供一个对应的 Lua API ObjectMethod 转发这个操作。所有 C++ 中的 API 都平坦的导入 Lua ,Object(this) 以 lightuserdata 传入,不做任何特殊处理。Object 的构造和销毁,也用 ObjectCreate 和 ObjectRlease 两个 API 的形式提供。

对于数据(参数和返回值)传递,做有限的转换支持即可。

最后,我们在 Lua 中将这些 raw api 组合起来,模拟出对象系统,让 Lua API 看起来和 C++ API 类似,对用户更加友好。即,胶水层放在 Lua 中,而不是在 C++ 里用 lua api 硬写(binding 代码生成的意义也不大)。动态语言天生适合做这个。


有一个常见的难题:如果一块数据同时需要从 Lua 和 C++ 直接访问,该如何实现。这通常出现在 binding 中存在回调代码时。如果数据在 Lua 中,而 C++ 侧开始做回调绑定时,只有一个指针或 handle 来代替对象做绑定,那么在 C++ 侧如何取到 Lua 中对应的对象去通知它处理回调?

比如,RmlUi 的 DataModel 对象,其 C++ Api 是这样的:

你可以将一个内存地址 bind 到一个 DataModel 中的变量上。具体可以看官方的例子:https://github.com/mikke89/RmlUi/blob/master/Samples/basic/databinding/src/main.cpp

例中,将 my_data.title 的内存地址绑定到了 basics 这个 DataModel 的 title 这个变量上。在 C++ 中改变这块内存, rml 中对应引用的地方就会随之改变。

显然,Lua 中的变量并不能直接为内存地址,所以要在 Lua 中实现这个特性就要绕一下。好在,RmlUi 的 Api 要求的是提供一个 DataVariable 对象,这个对象由 VariableDefinition 和一个 void * 构成。我们可以自定义 VariableDefinition 的 Get 和 Set 方法去执行解释这个 void * 。

大概有两种方法:

  1. 为 DataModel 生成一个自定义的内存结构,以 userdata 的形式放在 Lua 中。变量还是映射到内存地址上,使用标准的 VariableDefinition 。如果 Lua 中需要读写这个数据结构,再用自定义的 metamethod 去解析它。

  2. 为 DataModel 生成一个 lua table ,直接用 Lua 的原生形式保存数据。但在自定义的 VariableDefinition 去访问这个 lua table 。

我选择第二个方法,因为这样生命期管理要简单的多。也更方便在 Lua 侧调试。如果选用第一种方法,不仅生命管理更复杂,还需要对自定义内存结构编写动态解析的过程。这是因为,数据结构是动态的,无法在编译时写成 C struct 。

第二个方案的复杂处在于,如何实现自定义的 VariableDefinition 。它从 C++ 侧调用,却需要访问指定的 Lua 虚拟机中的数据。Lua 不像 Python ,可以用一个 pyobject 指针直接映射 python 虚拟机中的对象,我们需要找到一个合适的方法。

这是个经典问题。经典方法是使用 LuaL_ref , 在 C++ 中保存一个 handle 做引用,每次想从 C++ 中访问 Lua 对应数据时,都用 handle 查一次表,找到对象的 Lua 对象。

今天,我想介绍另一种方法。它更简单高效,代价是多一点点内存开销。

我们可以用一个 lua thread 而不是 lua table 来保存 C++ 中的数据。因为,thread 即 lua_State ,是唯一种可以从指针访问的 Lua 对象。

我为 DataModel 创建一个 Lua 的 userdata ,这个 userdata 里不直接保存数据(userdata 用来触发 metamethods),而用 lua_newthread 创建一个独立的 thread 来保存用户数据。然后,将这个 thread 放在 userdata 的 uservalue 中,保护其生命期;同时将 thread 的指针保存在 userdata 中供 C++ 侧直接读写。

也就是说,当你在 C++ 侧想访问这块数据,那么用一开始绑定的指针(这里保存在 VariableDefinition 对象中)就可以找到对应的 lua_State * ,直接可以读写里面的数据;而在 Lua 侧,metatable 中的 __index__newindex 可以简单使用标准 lua api 读写数据。

具体代码可以参考 我为 RmlUi 提交的 PR

Comments

学习了

学习了赞一个

导出到 Lua 的 Create Release 不要直接给最终用户用。它是供 Lua 中的胶水层使用的。

Object 的构造和销毁,也用 ObjectCreate 和 ObjectRlease 两个 API 的形式提供。

这样存在的一个问题是在lua这边,有机会因为用了太低层的API导致出一些问题。比如调用create忘记release导致内存泄漏
有没有办法可以保证不出这种错误

为开源大牛鼓掌

风哥最棒!

@lizhi
云风所指的“C++里”应该是在与 lua 相关的 C++ 代码中,交由 Lua 管理的对象就用 Lua 方式创建,而不是指整个框架与 Lua 高度耦合。

@lizhi
"...作者试图可以从 C++ 方面和 Lua 方面同时全面控制同一个对象,包括对象的构造和销毁。..."
既然上层也有全面的构建和销毁控制权,云风的建议确实再正确不过了,操作必须统一起来,一个对象的创建和销毁都由上下层都可各自直接插手的话,是不好的设计,这就好比一份配置表,统一由一个人来改和多个人都有权限直接改,哪个合适?

```也就是说,即使你想在 C++ 里创建会销毁一个对象,也必须通过 Lua 接口的间接调用,而不应该提供任何 C++ API 直接控制。```
我个人拙见,觉得这样不妥。因为主体是c++,lua,js,c#,java都可以作为绑定接口。肯定是lua之类的脚本去调用c++ api,而不是c++去调用lua。那如果出一套js接口怎么搞?c++再去写一份调用js?我觉得主次问题。谁是主人,谁是客人。

拙见,不见得正确,请指正。

Post a comment

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