内存块对象的 Lua 封装

这个用法,在 C/C++ 层面是非常好用的,但对于 lua binding 来说,很让人苦恼。因为很难安全的封装。
如果你将 Memory 封装成一个 Lua 的 userdata ,当用户构造出来后却因为种种原因没有使用(可能是发生了 error ,阻断了正常的执行流程),你没有办法消化掉它(因为没有直接释放的 api )。如果用户构造出一个对象,却无法多次使用,也会造成使用上的困扰。
Memory 的构造会多一次内存拷贝,可能也是个浪费。bgfx 提供了用引用来创建 Memory 的方法,但你必须自己保证数据的生命期。这里有两种方案:

  1. 保证引用的数据至少能活过 2 个 frame 。
  2. 提供一个用来释放内存的 callback 。

如果我们用方案 1 ,就需要自己将所有传入 bgfx 的 lua 内存对象暂时引用起来,每隔 2 frame 解除引用;如果用方案 2 , 需要考虑 bgfx 是一个多线程库,而跨线程操作 lua 的 vm 对象需要考虑线程安全问题。
基于这些难点,我一开始做 bgfx 的 binding 时,没有抽象出一个 memory 对象,而是在所有相关 api 处都直接处理输入参数。根据参数是 table / string / userdata 来临时创建出 Memory 对象,传给 bgfx 了事。不把细节暴露出去。
但随着日益开发,我们需要越来越多样的数据构造方法,这使得维护一组 api 变得负担颇大。有些复杂的构造方法(例如传入指针加偏移量等多个信息)参数过多,让相关 api 的参数的复杂性也变得不可接受。所以我还是决定抽象出 lua 层面的 memory 对象,一劳永逸的解决这个问题。
先说结果:

我给 binding 库增加了 bgfx.memory_buffer
这个新 api ,可以用 4 种方法创建出内存块。

  1. 用一个描述数据布局的字符串和一个 table 数组来创建。布局字符串可以描述每个数据段的数据类型(浮点/不同字长的整数),table 数组则是每个字段的数值。
  2. 用一个字符串,以及可选的起始位置及长度来创建一个不可写的内存块。
  3. 用 lightuserdata (指针) 以及可选的长度、数据关联对象来创建。这里的数据关联对象指,让框架帮你引用住这个对象,防止指针引用的内存失效。
  4. 直接指定一个 size ,创建一个可写的内存块。

大部分过去的 api 依旧兼容,但少部分 api ,例如 bgfx.create_vertex_buffer
就必须传入这样一个内存块对象,而不能像过去那样传入 table 。具体使用上的变化可以参考 example 。
我是如何实现这个东西的呢?
首先,这个 lua 内存对象并非对 bgfx::Memory 的直接封装。它在构造出来后,并非 bgfx 的 Memory 对象。所以即使你构造出来不用,也可以被安全的回收。
一旦它被 bgfx 的 api 调用,那么就会用数据引用的形式临时创建出一个 Memory 对象,传递给 bgfx 。并且递增了一个内存引用。当 bgfx 不再使用它后(在两个 frame 之内就会释放)回调函数会递减这个引用。

这个对象的 __gc
方法会检查引用计数。只有是 0 的时候才会安全的释放。如果引用计数不为 0 ,那么会让这个对象多活一小段时间。

怎样做到让一个进入 __gc
方法的对象多活一段时间?我使用的方法是在 __gc
方法内临时创建出一个 userdata ,并把自身挂载在其 uservalue 上。这个临时的 userdata 的 __gc
会再次检查引用计数,如果下次还是未能到 0 ,就继续这个过程。