之前介绍过了如何通过 C 语言来编写新函数来扩展 Lua,这篇文章将介绍如何用 C 来编写新的类型来扩展 Lua。同时还会介绍除内存以外的其他资源。
用户数据
如下实现了一个布尔数组,将每个布尔值存储在一个比特中,所以相比于 Lua 的表实现,其内存使用量要小得多。
1 |
|
为了在 Lua 中表示一个 C 语言结构体,Lua 提供了一个称为 用户数据
(userdata)的基本类型。用户数据为 Lua 提供了可以用来存储任何数据的原始内存区域,没有预定义操作。lua_newuserdata
分配一块指定大小的内存,然后将用户数据压栈,并返回该块内存的地址。
如下还定义了一个获取数组大小的函数以及一些初始化库的额外代码:
1 | static int getsize(lua_State *L) { |
元表
当前实现有漏洞,例如 setarray
并没有确认用户传入的参数的确是一个 BitArray*
指针,如果用户传入其他类型的指针,可能会造成内存越界访问。要区别不用类型的用户数据,一种常用的方法是每一种类型创建唯一的元表。每次创建用户数据时,用相应的元表进行标记,每当获取用户时,检查其是否有正确的元表。由于 Lua 代码不能改变用户数据的元表,因此不能绕过这些检查。
在 Lua 中,惯例是将所有新的 C 语言类型注册到注册表中,用类型名作为索引,以元表作为值。由于注册表中还有其他索引,所以需要谨慎选择类型名称以避免冲突。
辅助库提供了一些帮助函数:
luaL_newmetatable
创建一张新表(被称为元表),然后将其压入栈顶,并将该表与注册表中的指定名称关联起来luaL_getmetatable
从注册表中获取与 tname 关联的元表luaL_checkudata
会检查栈中指定位置上的对象是否是与指定名称的元表匹配的用户数据
对上述代码进行修改:
1 | int luaopen_array(lua_State *L) { |
1 | static int newarray(lua_State *L) { |
setarray
、getarray
、getsize
则必须检查第一个参数是否为有效的数组:
1 |
面对对象访问
接下来将使这种新类型对象转换为一个对象,使得能够以普通的面向对象语法来操作该实例:
1 | a = array.new(1000) |
a:size()
等价于 a.size(a)
,因此我们需要让表达式 a.size()
返回函数 getsize()
。这里的关键机制在于元方法 __index
。对于表而言,Lua 会在找不到指定键时调用该元方法,而对于用户数据而言,由于它根本不提供 keys,所以每次访问时都会调用该元方法。
由于 array
的实例自己就是对象,因此 getsize
、setsize
等方法就不再需要放到表 array 中。我们的库只需要导出一个用于创建新数组的函数 new 就行了,其他操作都成为对象的方法。C 代码允许我们直接这样注册。
1 | static const struct luaL_Reg arraylib_f[] = { |
这里使用了 luaL_setfuncs
将列表 arraylib_m
中的函数复制到栈顶的元表中,然后调用 luaL_newlib
创建一张新表,并在该表中注册来自列表中 arraylib_f
的函数。
我们可以继续在 arraylib_m 中继续添加其他元方法,例如 __tostring
,这样 array 对象调用 print
时就可以打印其内容了。
数组访问
另一种更好的面向对象的表示方法是,使用普通的数组符号来访问数组,例如 a[i]
替代 a:get(i)
。一种快速解决方案就是直接在 Lua 中定义这些元方法,例如:
1 | local metaarray = getmetatable(array.new(1)) |
如果还要更加完美,可以在 C 语言代码中注册这些元方法。因此,需要需要重新修改初始化函数:
1 | static const struct luaL_Reg arraylib_f[] = { |
轻量级用户数据
目前使用的用户数据称为 完全用户数据
,Lua 还提供了另一种用户数据,称为 轻量级用户数据
。轻量级用户数据是一个代表 C 语言的指针,即它是一个 void* 值,由于轻量级用户数据是一个值而不是一个对象,所以无需创建它。要将轻量级用户数据让入栈中,可以调用 lua_pushlightuserdata
。
轻量级用户数据只是一个值,它没有元表,也不受垃圾收集器的管理。它真正的用途是相等性判,因此可以使用轻量级用户数据在 Lua 语言中查找 C 语言对象。
轻量级用户数据的一种典型用法,即在注册表中被用作键。此时轻量级用户数据的相等性是非常重要的。每次使用 lua_pushlightuserdata
压入相同的地址,都可以从注册表中得到相同的元素。
Lua 的另外一种典型场景是把 Lua 语言对象当做 C 语言对象的代理。例如输入/输出库使用 Lua 中的用户数据来表示 C 语言中的流:
- 当操作是从 Lua 到 C 时,从 Lua 对象到 C 对应的映射很简单,每个 Lua 对象保存指向其相应 C 语言流的指针即可
- 当操作是从 C 到 Lua 时,由于 C 语言流是由标准库定义的,我们无法在 C 语言流中存储任何对象。一种解决方案是,可以保存一张表(这张表很可能是弱应用的),其中键是
流地址的轻量级用户数据
,而值则是 Lua 中表示流的对象,从而在该表中使用 C 地址来检索对应的 Lua 对象
管理资源
除了内存之外,对象可能还需要使用其他资源。此时当一个对象被当成垃圾回收后,其他资源也需要被释放。此时,当一个对象被当成垃圾收集后,其他资源也需要被释放。Lua 以 __gc
元方法的形式提供了析构器,为了完整地演示在 C 语言中对该元方法和 API 的使用,将会开发两个示例。
目录迭代器
如下实现了函数 dir.open
,它是一个工厂函数,Lua 调用该函数创建迭代器:它打开一个 DIR 结构体并将该结构体作为 upvalue 创建一个迭代函数的闭包:
1 | #include <dirent.h> |
函数 dir_gc
就是元方法 __gc
,该元方法用于关闭目录。一旦设置了元表,元方法 __gc
就一定会调用。因此在设置元表前,所以 l_dir
在设置元表前,需要使用 NULL 预先初始化用户数据,以确保用户数据具有明确定义的值。
XML 解析器
接下来将介绍一种用 Lua 编写的 Expat 绑定(binding)的简单实现,称为 lxp。Expat 是用 C 语言编写的开源 XML1.0 解析器,实现了 SAX,即 Simple API for XML。SAX 是一套基于事件的 API,这意味着 SAX 解析器在读取 XML 文档时会一边读取一边通过回调函数向应用上报读取到的内容。
为了在 Lua 中使用这个库,第一种方法是一种直接的方法,即简单地把所有函数导出给 Lua,另一个更好的方法是让这些函数适配 Lua。如下代码展示了如何实现这一点:
1 |
|