« 贴图管理模块及 UI 上的 3D 模型 | 返回首页 | 游戏数据包的补丁和更新 »

Lua 的 C 模块之间如何传递内存块

Lua 的数据类型非常有限,用 C 编写的 Lua 模块也没有统一的生态。在不同模块间传递内存块就是件很头疼的事情。

简单通用的方法就是用 Lua 内建的 string 类型表示内存块。比如 Lua 原生的 IO 库就是这么干的。读取文件接口返回的就是字符串。但这样做有额外的内存复制开销。如果你用 Lua 编写一个处理文件的程序,即使你的处理函数也是 C 编写的模块,也会复制大量的临时字符串。

我们的游戏引擎是基于 Lua 开发的,在文件 IO 上就封装了自己的库,就是为了减少这个不必要的字符串复制开销。比如读一个贴图、模型、材质等文件,最后把它们生成成渲染层用的 handle ,数据并不需要停留在 Lua 虚拟机里。但是,文件 IO 和资源组装(比如贴图构造)的部分是两个不同的 C 模块,这就需要有效的内存交换协议。

我们又不想让所有的 C 模块统一依赖同一个自定义的 userdata 类型。例如 bgfx 的 Lua binding 就是一个通用模块,不一定只在我们这个游戏引擎中使用。引入一个特定的 userdata 感觉不太好。

所以,我倾向于协定一个数据交互的协议,而不是共同依赖同一个库实现的特定用户类型。

首先,用 string 交换内存块肯定是最通用的协议,它的问题是低效,有无谓的内存拷贝,多余的对象需要通过 gc 清理。

我们很早就给几乎所有的 C 库增加了 raw userdata 的支持:即把不带 metatable 的 userdata 视为普通的 string 。userdata 和 string 在 Lua 的内部实现中也非常类似,均可以表达一个带长度的内存块,区别在于 userdata 的数据是可变的,string 的数据是不变的。

我在很多自己编写的 C 库中增加了第三种协议,用一个 lightuserdata + integer 表示一个内存地址和长度。比如 skynet 的 C 库就支持这种协议。这个协议的问题有两个,其一参数变成了两个,和单个 string 或 userdata 不一样,处理起来非常麻烦;其二,无法管理 lightuserdata 的生命期。

为了解决生命期管理问题,在实现 bgfx lua binding 时,我又增强第三种协议:在内存地址和长度之后,允许再增加一个叫 lifetime 的 object 。如果需要管理生命期,Lua 侧就把这个对象引用住,不再使用那个地址后,就解开引用。当这个 lifetime object 是 string 时,我们就可以用前面的 lightuserdata 指定字符串内的子串,而不需要真正构建一个新的字串对象了;这个lifetime object 也可以是带 gc 元方法的 table 或 userdata ,负责最后释放内存指针。

今天,我们又重新审视了这个问题。动机是这样的:

过去,我们在每个线程(独立虚拟机)中分别做 IO 。这样,我们自己实现的 IO 库可以使用上面的第二种协议返回一个 userdata ,传递给其它模块使用。最近,我们想把 IO 全部挪到唯一的 IO 线程做,它读取数据后,再传递给请求方。这样,就涉及虚拟机间的数据传递。

在上面第二方案中,raw userdata 必须在同一虚拟机内创建再使用,无法接收外部传来的数据。而换成第三方案(在我们现在的游戏引擎中并未使用过)又没有很好的解决第一个问题:多于一个参数和单个 string / userdata 不同,会让协议实施起来很麻烦。

考虑再三后,我觉得可以引入第四个方案:用单个 lua object 承担内存地址、长度、生命期管理三项数据。

简单说,我们需要一个 tuple ,把三元组打包在一起。Lua 中可以用来表示 tuple 的有三种东西,table (array) ,userdata + uservalue ,function closure 。因为 raw userdata 已经放在第二方案中使用,我不想和它冲突,那么可选的就是 table 和 function 了。我觉得 function 最为合适。

当传递一个 function 时,我们用 lua_call(L, 3, 0); 调用它,就可以拿到一个三元组。前两个就是内存地址(lightuserdata)和长度(integer);第三个是可选项,用来管理这个地址的生命期。进一步,当这个生命期对象是另一个 function 时,我们还可以直接在使用完内存后调用一下这个关闭函数,解除内存的引用;或者(当它不是 function)和前面第三方案一样,依赖这个对象的 gc 清理内存。

Comments

超出Lua语言本身的控制内存和状态机堆栈问题,使用CAPI(Lua语言源码有40%的代码在描述CApi)和控制元表。

理解的很深刻,一个lua应用的边界情况,也正在把lua的轻量加重。

Post a comment

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