5

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

 5 months ago
source link: https://blog.codingnow.com/2023/11/lua_c_memory.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 清理内存。

云风 提交于 November 24, 2023 04:08 PM | 固定链接


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK