前言
最近一直在探索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的支持
要解决这些问题,还得再深入去研究呀!