Lua数据结构 — TValue(一)
http://blog.aliyun.com/761?spm=0.0.0.0.SxNpqW
作者:罗日健
数据结构的设计,在一定程度上奠定了整个系统的设计,所以决定写一个对Lua主要数据结构的分析文章,本来打算写一篇就好了,但是每个数据类型其实都有点复杂,一篇的话篇幅太长,所以就拆开几篇来写了。
为什么是从TValue说起,TValue是实现Lua弱数据类型的主要数据结构,不但在脚本中的值使用了TValue,连Lua的实现中,很多数据结构也依赖于TValue,TValue一定程度上贯穿了整个Lua。先说一下Lua里面的数据类型:(lua.h :69)
从上面的定义中可以看到,Lua的值类型有9种,其中LUA_TNONE是用于判断这个变量是否等于为空使用的,这个是Lua内部使用的,后面再详细说明。现在来看Lua里面的TValue数据结构:(lobject.h 71-75)
在Lua里面,一个变量使用TValue这个类型来存储的,int tt就是上面宏的类型值(4个字节),而Value则是一个union(8个字节)。在这个union中,其实分工也十分明确:
在Value中,void* p、lua_Number n、int b都是不用回收的值类型,而GCObject* gc则都是需要回收的对象,下面是GCObject数据结构:(lstate.h 133-145)
GCObject也是一个union,存储了一个GCheader,这个GCHeader主要用于GC回收机制使用,GC回收机制超出了这次讨论话题,暂时先忽略。真正存储值的结构是TString、Udata、Closure等等,每个存储数据的结构都会有GCheader,接下来几篇文章将会开始逐个数据类型进行解释。
Lua数据结构 — TString(二)
http://blog.aliyun.com/768?spm=0.0.0.0.TJ9Hr5
作者:罗日健
存储lua里面的字符串的TString数据结构:(lobject.h 196-207)
其它结构中也会有L_Umaxalign dummy这个东西,来看看L_Umaxaliagn:
从字面意思上就是保证内存能与最大长度的类型进行对齐,事实上也是做这件事,这里感觉lua想给各种不同设备做一种嵌入式脚本,这里要保证与最大的长度对齐能保证CPU运行高效不会罢工。
tsv才是TString的主要数据结构:
- CommonHeader:这个是和GCObject能对应起来的GCheader
- reserved:保留位
- hash:每个字符串在创建的时候都会用有冲突的哈希算法获取哈希值以提高性能
- len:字符串长度
哈希是lua里一个很重要的优化手段,具体的哈希算法相关知识在文章最后会补充说明一下,字符串的hast表放在L->l_G->strt中,这个成员的类型是stringtable,我们再来看看stringtable数据结构:(lstate.h 38-42)
stringtable结构很简单:
- hash:一个GCObject的表,在这里其实是个TString*数组
- nuse:已经有的TString个数
- size:hash表的大小(可动态扩充)
接下来看看stringtable是怎么动态调整大小的:
1 动态扩充stringtable:(lstring.c 60-70)
每次newlstr的之后,都会判断nuse是否已经大于table的size,如果是的话就会重新resize这个stringtable的大小为原来的2倍。
2 动态浓缩stringtable:(lgc.c 433-436)
在gc的时候,会判断nuse是否比size/4还小,如果是的话就重新resize这个stringtable的大小为原来的1/2倍。
3 resize算法:(lstring.c 22-47)
resize时,需要根据每个节点的哈希值重新计算新位置,然后放到newhash里。
字符串在哪里? 看完TString和stringtable,大家都没有发现究竟字符串放在哪里,从内存上看其实字符串直接放在了TString后面,这样还能省掉一个成员:(lstring.c 56)
性能问题:
在这里说一点lua的性能问题,虽然不在这个主题的讨论范围。由上面可以知道lua的字符串是带hash值的,所以我们拿着一个字符串去做比较、查询、传递等操作都是非常高效的。
但是我们也可以看到每次创建一个新的字符串都会做很多操作,所以这里不建议频繁做字符串创建、连接、销毁等操作,最好能缓存一下。
补充:字符串的哈希算法:
常用的字符串哈希函数比较如下:
其中数据1为100000个字母和数字组成的随机串哈希冲突个数。数据2为100000个有意义的英文句子哈希冲突个数。数据3为数据1的哈希值与1000003(大素数)求模后存储到线性表中冲突的个数。数据4为数据1的哈希值与10000019(更大素数)求模后存储到线性表中冲突的个数。
经过比较,得出以上平均得分。平均数为平方平均数。可以发现,BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的。APHash也是较为优秀的算法。DJBHash,JSHash,RSHash与SDBMHash各有千秋。PJWHash与ELFHash效果最差,但得分相似,其算法本质是相似的。
在Lua中使用到的是JSHash算法:
具体JS Hash算法的冲突性解决和性能上面,我也不懂,具体要找paper看看,但是从数据比较上看,JSHash是属于较好的
Lua数据结构 — Table(三)
http://blog.aliyun.com/787?spm=0.0.0.0.Uu9IXA
作者: 罗日健
前面(一)、(二)里面其实已经把一些常用的数据类型(数值、布尔、字符串)说明了,这次要描述的是Table,Table在Lua里是一种常用的数据类型,是Lua里的精髓之一,其效率必须得到保证,而实现这种支持任意类型key和value的Table也是较为复杂的。
一, Table的设计思想:
1, 首先,讲一下Lua要设计的Table是怎么样子的:
Lua就是想做这种支持任意类型的key和任意类型val的table,并且要高效和节约内存。
2, 基本的实现(基于链表的实现):
基于链表的实现是最简单的,其实map就可以了,这样实现是最容易的。但当遇到很多key的数组(如t[0]、t[1]、t[2]。。这种数值索引大数组)时,明明可以用O(1)查找的,却要O(n)去查找。
3, 区分数字key和其它类型的key
经过改良的Table,除了有key链表之外,还有一个数组array专门存放key为数值的val。但是这种情况下,要保证数值部分是连续且从0开始的,如果出现t[100000000] = 1,则把这个离散的数据放到链表中:
4, 利用哈希表再度优化
区分了array和head之后,始终有个问题,对于链表部分的数据,查找始终是O(n)的,有没有办法优化这部分代码呢,在Lua里,利用哈希表再对这部分的Node进行查找。
每次计算一个key的哈希值是非常快的,哈希后直接映射到hashlist的某个位置。这里已经很接近Lua Table的最终设计,但是这种方法仍然有个弊端,哈希表的大小无法较好地估计,hashlist的长度可能是一个固定的长度,无法动态扩容。
5, 动态扩容的Table设计
下面用例子展示一下动态扩容的Table设计
1) 如下图,现在初始状态下,只有[0]被使用了,里面放着A,其它都是空:
2) 现在要插入一个新的元素B,计算出其哈希值是0,即是说应该插入到节点[0]。这个时候发现节点[0]已经被使用,则会分配最后一个空闲块lastfree给这个元素B,然后node[0]的next指向node[3],即:
3) 然后再插入一个新的nodeC,计算出其哈希值是3,即是说应该插入到node[3]。这个时候发现node[3]已经被使用,但是元素B的哈希值是0,即本来应该插入到node[0]的,于是把node[3]的内容移到lastfree,然后再在node[3]插入新的nodeC,即:
4) 这是如果再往Table插入一个元素D,那么必然最后一个空闲块会被使用完,那么就会把nodelist的大小扩大一倍,并且重新计算每个元素的哈希值并重新插值,可能的结果如下:
在最后一步的重新计算哈希值,不仅重新计算nodelist的哈希值,也会重新计算arraylist的哈希值,arraylist也是会动态扩大的,这就是lua中table的设计。
二, Lua里面的实现:
Table相关数据结构关系图如下:
上图中有Table、Node、TKey这3个数据结构,不用急,我们先从简单的入手,看看Node数据结构:(lobject.h 332-335)
Node就是设计思想里的key、value数据结构,包含ikey和ival两个成员,这2个成员很好理解,一个就是table的key,另一个就是这个key的value。ival是一般值的TValue类型,而ikey的TKey类型的。可以看到Node并没有next指针,其实它把next指针藏在TKey下面了,请看TKey数据结构:(lobject.h 319-329)
可以看到TKey其实是一个支持TValue的数据结构外,还多了一个next指针。这个next指针就是用作同一个hash值下冲突时的链表指针。明白了Node结构之后,接下来看看Table数据结构:(lobject.h 338-348)
每个字段意义如下:
- CommonHeader:与TValue中的GCHeader能对应起来的部分
- flags:用于元表元方法的一些优化手段,一共有8位用于标记是否没有某个元方法
- lsizenode:用于表示node的长度,如下图所示
node成员其实是上面讨论的hashlist成员,是一个固定长度大小的数组,但是lsizenode的数据类型是lu_byte,只有一个字节长,表示范围只有0~255,一般数组大小都会很大,所以这里lsizenode用于表示整体长度的log2值,同时也表明了,hashlist的长度是2的幂,每次增长都会×2.
- metatable:元表指针
- array:这个成员就是上面讨论的array,用于给数值的索引
- node:上面提到的hashlist成员
- lastfree:lastfree就是链表的最后一个空元素
- gclist:用于gc的,以后会有专门对GC的详细讨论
- sizearray:array数组的大小
离散数值key存储的实现:
在luaH_getnum(ltable.c 432-449)函数中,实现了对数值key的获取,可以看到第一个判断:
即如果key在sizearray的范围内,则直接用t->array成员来存储,如果不是则计算key的哈希值,然后放到node里。
还有一种情况,就是如果对某个连续数值的table赋值:t[2] = nil,那是否从2到后面的key都会马上放到哈希表里呢?答案是否定的,不会马上做,等到做完gc后,会进行table的resize。
Table的Rehash(重新计算大小):
1) rehash的时机:
在newkey(ltable.c 399-429)函数中可以看到
n是hashlist中的一个没使用的节点,当没有空余节点的时候,就会调用rehash进行grow table,这个可以参考本文上面说到的动态扩容章节。
2) rehash函数(ltable.c 333-349)
table的这个rehash操作,代码不多,但是却十分复杂,接下来我们分解一下它所做的事:
a. 计算使用数值作为key的元素数量na、计算实际为数组申请的空间大小nasize、计算hashlist的元素数量nhsize。 这里有点模糊,na和nasize的关系,下面写个例子更好说明一下:
没错,nasize一定要是2的幂,computesizes(ltable.c 189-208)通过特定算法,高效地计算出实际要使用的数组大小,举下面例子说明一下:
lua其实是用了一个条件来决定数组部分大小的:
如果数值key的元素个数大于对应个数幂大小的一半,则生成对应幂长度的数组链表。
很抽象,还是拿上面的例子来说明:
整体算法如上图所示,还是挺精致的,不太懂用语言描述,可以想象一个元素如果拥有tbl[10]到tbl[50],那么这个arraylist的长度是64,中间可能会多生成1~10和50~64这个区间的数组,但是这种方法既能动态扩容,又能提升效率,牺牲一点点还是值得的。
b. resize(ltable.c 300-327)函数,根据前面计算出来的nasize和nhsize,realloc对应数组的大小,并对其中的元素重新计算哈希值和赋值。
哈希的实现:
主要可以看到mainposition(ltable.c 96-113)函数,用于计算哈希然后快速定位到某个Node上面,可以看到它根据不同类型有不同的哈希计算:
元表的实现:
元表是metatable,可以绑定metatable的对象在lua中只有table和userdata。这里讨论的是table中的metatable,在userdata中的其实也一样。我们看到Table数据结构里的struct Table* metatable指针,下面以index操作为例,其它的话其实也一样:
看luaV_gettable(lvm.c 108-131),我们可以看到在取一个对应key后会有判断:
这个判断其实就是看看返回结果如果是空,就会去取元表的__index对象,取回来之后,下次循环就再次用这个tm来取key,如果在tm上找不到对应key,而且tm又有metatable,就会一直循环下去。
这里fasttm做了一些优化,其实就是先用h->metatable的flags成员去判断是否存在__index元方法,如果不存在马上返回。flags只有8位,用于存储常用的元操作,可以在ltm.h 18-37看到,快速操作的常用元方法是index 、newindex、gc、mode、__eq,说明flags还有3位没用到。
循环有个MAXTAGLOOP,这里其实限制了元表的深度不能超过100(其实超过5个深度的元表已经很恐怖了)。元操作对象的获取方法其实是luaTgettm(ltm.c 50-58)和luaTgettmbyobj(ltm.c 61-74)
总结:
对于Table,还有个弱表的特性,这个留待在说gc的时候再详细讨论。其实Table的实现还是挺多细节的,不过主要的思想和
Lua数据结构 — 闭包(四)
http://blog.aliyun.com/845?spm=0.0.0.0.n73zxw
作者:罗日健
前面几篇文章已经说明了Lua里面很常用的几个数据结构,这次要分享的也是常用的数据结构之一 – 函数的结构。函数在Lua里也是一种变量,但是它却很特殊,能存储执行语句和被执行,本章主要描述Lua是怎么实现这种函数的。
在脚本世界里,相信闭包这个词大家也不陌生,闭包是由函数与其相关引用环境组成的实体。可能有点抽象,下面详细说明:
一、 闭包的组成
闭包主要由以下2个元素组成:
- 函数原型:上图意在表明是一段可执行代码。在Lua中可以是lua_CFunction,也可以是lua自身的虚拟机指令。
- 上下文环境:在Lua里主要是Upvalues和env,下面会有说明Upvalues和env。 在Lua里,我们也从闭包开始,逐步看出整个结构模型,下面是Closure的数据结构:(lobject.h 291-312)
不难发现,Lua的闭包分成2类,一类是CClosure,即luaC函数的闭包。另一类是LClosure,是Lua里面原生的函数的闭包。下面先讨论2者都有相同部分ClosureHeader:
- CommonHeader:和与TValue中的GCHeader能对应起来的部分
- isC:是否CClosure
- nupvalues:外部对象个数
- gclist:用于GC销毁,超出本章话题,在GC章节将详细说明
- env:函数的运行环境,下面会有补充说明
对于CClosure数据结构:
- lua_CFunction f:函数指针,指向自定义的C函数
- TValue upvalue[1]:C的闭包中,用户绑定的任意数量个upvalue
对于LClosure数据结构:
- Proto *p:Lua的函数原型,在下面会有详细说明
- UpVal *upvals:Lua的函数upvalue,这里的类型是UpVal,这个数据结构下面会详细说明,这里之所以不直接用TValue是因为具体实现需要一些额外数据。
二、 闭包的UpVal实现
究竟什么是UpVal呢?先来看看代码:
分析一下上面这段代码,最终testB的值显然是3+5+10=18。当调用testA(5)的时候,其实是在调用FuncB(5),但是这个FuncB知道a = 3,这个是由FuncA调用时,记录到FuncB的外部变量,我们把a和c称为FuncB的upvalue。那么Lua是如何实现upvalue的呢? 以上面这段代码为例,从虚拟机的角度去分析实现流程:
1) FuncA(3)执行流程
- 把3这个常量放到栈顶,执行FuncA
虚拟机操作:(帮助理解,与真实值有差别)
LOADK top 3 //把3这个常量放到栈顶 CALL top FuncA nresults //调用对应的FuncA函数
- 虚拟机的pc已经在FuncA里面了,FuncA中的局部变量都是放到栈中的,所以第一句loacl c = 10是把10放到栈顶(这里假设先放到栈顶简化一些复杂细节问题,下同)
虚拟机操作:
LOADK top 10 //local c = 10
- 遇到Function FuncB这个语句,会生成FuncB的闭包,这个过程同时会绑定upval到这个闭包上,但这是值还在栈上,upval只是个指针。
上面生成一个闭包之后,因为在Lua里,函数也是一个变量,上面的语句等价于local FuncB = function() … end,所以也会生成一个临时的FuncB到栈顶。
虚拟机操作:
- 最后return FuncB,就会把这个闭包关闭并返回出去,同时会把所有的upval进行unlink操作,让upval本身保存值。
虚拟机操作:
2) FuncB的执行过程
到了FuncB执行的时候,参数b=5已经放到栈顶,然后执行FuncB。语句比较简单和容易理解,return a+b+c 虚拟机操作如下:
到这里UpVal的创建和使用也在上面给出事例说明,总结一下UpVal的实现:
- UpVal是在函数闭包生成的时候(运行到function时)绑定的。
- UpVal在闭包还没关闭前(即函数返回前),是对栈的引用,这样做的目的是可以在函数里修改对应的值从而修改UpVal的值,比如:
lua code:
- 闭包关闭后(即函数退出后),UpVal不再是指针,而是值。 知道UpVal的原理后,就只需要简要叙述一下UpVal的数据结构:(lobject.h 274 – 284)
- CommHeader: UpVal也是可回收的类型,一般有的CommHeader也会有
- TValue* v:当函数打开时是指向对应stack位置值,当关闭后则指向自己
- TValue value:函数关闭后保存的值
- UpVal* prev、UpVal* next:用于GC,全局绑定的一条UpVal回收链表
三、 函数原型
之前说的,函数原型是表明一段可执行的代码或者操作指令。在绑定到Lua空间的C函数,函数原型就是lua_CFunction的一个函数指针,指向用户绑定的C函数。下面描述一下Lua中的原生函数的函数原型,即Proto数据结构(lobject.h 231-253):
引用内容:
- CommonHeader:Proto也是需要回收的对象,也会有与GCHeader对应的CommonHeader
- TValue* k:函数使用的常量数组,比如local d = 10,则会有一个10的数值常量
- Instruction *code:虚拟机指令码数组
- Proto **p:函数里定义的函数的函数原型,比如funcA里定义了funcB,在funcA的5. Proto中,这个指针的[0]会指向funcB的Proto
- int *lineinfo:主要用于调试,每个操作码所对应的行号
- LocVar *locvars:主要用于调试,记录每个本地变量的名称和作用范围
- TString **upvalues:一来用于调试,二来用于给API使用,记录所有upvalues的名称
- TString *source:用于调试,函数来源,如c:\t1.lua@ main
- sizeupvalues: upvalues名称的数组长度
- sizek:常量数组长度
- sizecode:code数组长度
- sizelineinfo:lineinfo数组长度
- sizep:p数组长度
- sizelocvars:locvars数组长度
- linedefined:函数定义起始行号,即function语句行号
- lastlinedefined:函数结束行号,即end语句行号
- gclist:用于回收
- nups:upvalue的个数,其实在Closure里也有nupvalues,这里我也不太清楚为什么要弄两个,nups是语法分析时会生成的,而nupvalues是动态计算的。
- numparams:参数个数
- is_vararg:是否参数是”…”(可变参数传递)
- maxstacksize:函数所使用的stacksize
Proto的所有参数都是在语法分析和中间代码生成时获取的,相当于编译出来的汇编码一样是不会变的,动态性是在Closure中体现的。
四、 闭包运行环境
在前面说到的闭包数据结构中,有一个成员env,是一个Table*指针,用于指向当前闭包运行环境的Table。
什么是闭包运行环境呢?以下面代码举例:
上面代码中的d = 20,其实就是在环境变量中取env[“d”],所以env一定是个table,而当定义了本地变量之后,之后的所有变量都对从本地变量中操作。
五、 函数调用信息
函数调用相当于一个状态信息,每次函数调用都会生成一个状态,比如递归调用,则会有一个栈去记录每个函数调用状态信息,比如说下面这段没有意义的代码:
那么每次调用将会生成一个调用状态信息,上面代码会无限生成下去:
究竟一个CallInfo要记录哪些状态信息呢?下面来看看CallInfo的数据结构:
- Instruction *savedpc:如果这个调用被中断,则用于记录当前闭包执行到的pc位置
- nresults:返回值个数,-1为任意返回个数
- tailcalls:用于调试,记录尾调用次数信息,关于尾调用下面会有详细解释
- base、func、top:如下:
六、 函数调用的栈操作
上面描述的CallInfo信息,具体整个流程是怎么走的,结合下面代码详细地叙述整个调用过程,栈是怎么变化的:
假设现在走到了funcA(30, 40)这个语句,在执行前已经存在了global这个闭包和funcA这个闭包,在调用global这个闭包时,已经生成了一个global的CallInfo。
1) 函数调用的栈操作:(OP_CALL lvm.c 582-601)
- global的CallInfo信息记录,并把funcA放到栈顶
当前虚拟机的pc指针,指向global函数原型中的CALL指令,这时global的CallInfo的savedpc就会保存当前pc。然后会把要执行的funcA的闭包放到栈顶。 – 参数分别放到栈顶(从左到右分别进栈),生成funcA的CallInfo,并把完成对应CallInfo栈操作
- 设置虚拟机pc到funcA闭包第一条虚拟机Instruction,并继续执行虚拟机
2) 函数返回的栈操作:(OP_RETURN lvm.c 635-648)
- 记录第一个返回值的位置到firstResult,把栈中的funcA位置设置为base和top
- 把返回值根据nresult参数重新push到栈
- 从全局CallInfo栈弹出funcA,并还原虚拟机pc到global的savedpc和栈信息
- 继续执行虚拟机
七、 尾调用(TAILCALL)
尾调用是一种对函数解释的优化方法,对于上面代码,改造成下面代码后,则不会出现stack overflow:
上面的Recursion方法不会出现stack overflow错误,也能顺利算出Recursion(20000) = 200010000。尾调用的使用方法十分简单,就是在return后直接调用函数,不能有其它操作,这样的写法即会进入尾调用方式。
那究竟lua是如何实现这种尾调用优化的呢?尾调用是在编译时分析出来的,有独立的操作码OP_TAILCALL,在虚拟机中的执行代码在lvm.c 603-634,具体原理如下:
1)首先像普通调用一样,准备调用Recursion函数
2)关闭Recursion1的调用状态,把Recursion2的对应栈数据下移,然后重新执行
本质优化思想:先关闭前一个函数,销毁CallInfo,再调用新的CallInfo,这样就会避免全局CallInfo栈溢出。
八、 总结
本文讨论了闭包、UpVal、函数原型、环境、栈操作、尾调用等相关知识,基本上把大部分的知识点和细节也囊括了,另外还有2大块知识:函数原型的生成和闭包GC可能迟些再分享。
Lua数据结构 — Udata(五)
http://blog.aliyun.com/789?spm=0.0.0.0.TJF6LI作者:罗日健
Udata负责存储userdata的数据,这部分其实很简单,但是为了保证系列文章的完整性,还是写一篇出来补全。
下面是Udata的数据结构:
意义:
- CommonHeader:和与TValue中的GCHeader能对应起来的部分
- metatable:userdata的元表,和table的元表一样的
- env:创建userdata时,会把当前执行语句的curenv赋给userdata的env,可修改
- len:使用userdata的时候绑定对象申请的空间大小
和TString类似,用户绑定的C对象或数据内存紧跟在Udata后面,在luaS_newudata中(lstring.c 96 – 110)有如下代码:
Udata元表(metable)的作用:
如果userdata没有元表,那是使用起来将会很麻烦,有元表,可以在脚本这样写:
从C语言层面来看,myuserdata这个变量其实只是个指针,不像table那样有子元素。但是因为有metatable,由此可以把成员\函数放到这个metatable中,在脚本中可以利用它来实现这个类似table的访问方法。
Udata环境(env)的作用:
env这个成员,默认是存储创建userdata时的环境table,而参考Lua官方的文档后,其实这个env成员在Lua中并没有使用,它的值时什么并不影响Lua的运行。
这就说明这个成员目前来说是一个用户可以自由操作的table,在UserDataRefinement文章中,告诉了我们一些使用的手段。另外一篇文章http://lua-users.org/lists/lua-l/2005-08/msg00709.html也对它的使用方法提出了一些建议。
我比较赞成后一篇文章的看法,对于Udata来说,metatable是一种静态的类型数据(type-common data),而env则是实例相关的数据(instance-local data)。当然了,怎么用取决于使用者。
Lua数据结构 — lua_State(六)
http://blog.aliyun.com/795?spm=0.0.0.0.JmUCgF作者:罗日健
前面各种Lua的数据类型基本都说得差不多了,剩下最后一个数据类型:lua_State,我们可以认为是”脚本上下文”,主要是包括当前脚本环境的运行状态信息,还会有gc相关的信息。
Lua这门语言考虑了多线程的情况,在脚本空间中能够开多个线程相关脚本上下文,而大家会共用一个全局脚本状态数据,如下:
全局数据global_state的数据结构如下:
global_state主要是用于GC的数据链表,下面简要说明几个:
- stringtable strt:这个是在TString那章说到的全局字符串哈希表
- TValue lregistry:对应LUAREGISTRYINDEX的全局table.
- TString *tmname[TM_N]:元方法的名称字符串。
- Table *mt[NUM_TAGS]:基本类型的元表,这是Lua5.0的特性。
mt成员在作者介绍文章中说到:
在上面代码中,我们看到a支持一个tostring的方法,a是数值类型,我们可以为数值类型添加任意的方法。Lua文章中说到一个用途,就是对于unicode和gbk的字符串的len方法能自己实现。
其它成员就不一一介绍了,下面来介绍与线程相关的脚本上下文lua_State:
我们看到,luaState也带有CommonHeader头,在第一章中也提到了GCObject中有luaState th这个成员,由此可见lua_State也会是被回收的对象之一。
考虑回一个线程中的脚本上下文,我们再来逐个分析每个成员:
- lu_byte status:线程脚本的状态,线程可选状态如下:
- StkId top:指向当前线程栈的栈顶指针,typedef TValue *StkId
- StkId base:指向当前函数运行的相对基位置,具体可参考第四章的闭包
- globalState *lG:指向全局状态的指针
- CallInfo *ci:当前线程运行的函数调用信息
- const Instruction *savedpc:函数调用前,记录上一个函数的pc位置
- StkId stack_last:栈的实际最后一个位置(栈的长度是动态增长的)
- StkId stack:栈底
- CallInfo *end_ci:指向函数调用栈的栈顶
- CallInfo *base_ci:指向函数调用栈的栈底
- int stacksize:栈的大小
- int size_ci:函数调用栈的大小
- unsigned short nCcalls:当前C函数的调用的深度
- unsigned short baseCcalls:用于记录每个线程状态的C函数调用深度的辅助成员
- lu_byte hookmask:支持哪些hook能力,有下列可选的
- lu_byte allowhook:是否允许hook
- int basehookcount:用户设置的执行指令数(LUA_MASKCOUNT下有效)
- int hookcount:运行时,跑了多少条指令(LUA_MASKCOUNT下有效)
- lua_Hook:用户注册的hook回调函数
- TValue l_gt:当前线程的全局的环境表
- TValue env:当前运行的环境表
- GCObject *openupval、gclist:用于gc,详细将会在GC一章细说
- struct lua_longjmp *errorJmp:发生错误的长跳转位置,用于记录当函数发生错误时跳转出去的位置。
本系列总结:
整个系列文章回答了我们对Lua中最基本的一个问题:“一个Lua变量究竟是什么?”。由此我们深入并引申出各种知识,在脚本中我们觉得弱类型变量用起来很痛快,而其实它的内部实现其实是如此的复杂。
对于实现一门脚本语言,必须实现的是解释器、虚拟机、上下文数据3大部分:
上下文数据这一层是脚本最基础,最底层的东西,它决定了这门脚本究竟能做什么。抛开解释器和虚拟机,我们依然可以单纯地通过C接口,在C++这一层就能操作脚本的上下文数据。
有空再研究一下Lua的GC,解释器等等。