前言
最近一直在探索Lua的C API编程部分,上次实现了一个函数执行时间统计库:lfunctimer,这次就果断写了一个lnodelist
来玩玩。在这期间,遇到了许多纠结的问题,因此果断做下分享~
测试用例现在贼少= =想要试用的同学可以走lnodelist
的Github传送门,或者luarocks install lnodelist
,就可以开始干起~
在Lua里,table充当着array以及hashmap两个角色,提供了简单的insert、remove、concat等功能。lnodelist
则是独立于table之外建立一个崭新的list/array
数据结构,暂时是一个双向链表。API的需求上,则兼并java的LinkedList跟js的array两种体系,列表如下:
1 | static const luaL_Reg lnodelist_f[] = { |
踩坑记录
数据结构与元表设计
lnodelist
本身要实现为一个非lua的自定义的数据结构,在Lua里,这种数据结构属于userdata
的类型,是一个可以被lua的GC(garbage collection,倒垃圾)机制检测回收的struct
结构体数据块。从双向链表的设计上来看,初始化的userdata
可以包含size
、*head
以及*tail
三方面。通过size
、*head
以及*tail
三个信息,我们就可以简单管理一个双向链表。而链表的每个结点内存则需要自己管理,其数据结构包含type
、*value
、*prev
以及*next
四个信息,分别是结点数据的类型、值跟链表里边上一个、下一个结点的指针。因此,lnodelist
的userdata
结构体跟结点结构体定义可以如下:
1 | /* struct of node */ |
lnodelist
作为一个用户自定义数据,我们需要通过一个叫元表(metatable)的东西对其基本属性以及在lua运行过程中发生特定事件下的行为进行描述。了解过元数据(metadata)的同学都会知道,它是描述数据的数据,其实元表,也不离其宗。在lnodelist
的元表中,我们必须得重新定义这个数据树结构被lua标记GC时的行为(事件__gc
)。
GC(Garbage Collection)行为可以回收数据的内存,但是我们在lua虚拟机定义lnodelist
的过程中,值定义了刚开始的userdata
,而后面结点的内存则暂时是一个个自己操作的,并不受lua虚拟机管辖。因此,GC时候,需要注意回收结点内存。
lnodelist
采用Lua最新版5.3.5的C API编写。相对于某些旧版本(如5.1),其注册lib的方式(luaopen函数里边的逻辑)不一样了。经过一番探究,参考了lua源码某些地方的骚操作,现在的写法可以这样:
1 | LUAMOD_API int luaopen_lnodelist(lua_State *L) { |
而后,每次新建list
时,注册一下元表即可。
1 | /* push a new empty list on lua stack */ |
数据存取
在lua中,采用TValue
表示lua的数据
1 | // lobject.h |
为此,在结点l_node
中,也同样采用type
加value
的形式,表示每一个存到链表的数据。
获取数据的类型可以通过lua_type
函数获得,根据数据的类型,我们可以做switch跳表去走相应的分支存数据。
值得一提的是,部分数据类型(比如函数、table),在API里我们无法直接获得相应数据结构的struct定义,因此只能通过lua的注册表Registry找到这些数据唯一索引,然后把索引值存在value
中,这样通过type
跟value
就能找到对应的数据了。
数据存储的逻辑如下:
1 | /* assign value to value pointer, may modify type */ |
反过来,取数据push到lua栈上的逻辑如下:
1 | /* get value of node and push to lua stack */ |
销毁数据(free)时,特殊的数据结构暂时需要在lua注册表里unref
来取消索引。其中的优化方案,还要后续有空探究。
遍历数据
lua本身遍历数据采用pairs
与ipairs
两种方式,但是对于lnodelist
的链表来说,存在两个问题——首先,pairs
跟ipairs
是无状态的。对于列表类型,我们采用ipairs
遍历时,通常为索引自增后再调用ipair
函数执行,但对于链表,根据索引寻找会增加性能开销,并且l_node
结点本身并非lua原生的数据类型。因此,现在暂时的解决方案是采用Javascript
的模式call回调函数进行遍历。比如foreach
的实现,可以如下:
1 | /* foreach loop for stateful iteration */ |
这样就可以实现一次遍历,map
等方法同理
总结
近期刷题少了,在写lnodelist
的过程中也渐渐找回了一点感觉。总的来说能有个小demo也不错,要把它做得更好的话,还有很多方面需要调研与改进:
- 内存管理:应当兼容lua的内存管理,并且配合lua的GC机制
- 错误控制:应当有更细的错误声明跟回滚机制
- 功能改进:完善功能,进一步兼容lua内置数据结构,比如table
- 协程支持:既然要JS Style,那还是得增加对coroutine的支持
要解决这些问题,还得再深入去研究呀!