协程的基本用法 前言 协程是lua十分有趣的一部分,它并非多线程,而是单线程,用法有点goto,但有本质区别。但我们需要注意这属于脚本动态语言,静态语言没有,也没有需要这种功能的必要。
含义 协程是一段代码的封装,一般通过一个“线程”将代码包装起来,其特点是可以中途挂起(yield)和恢复(resume)。不过在lua的线程与一般语言的线程不太一样。
协程状态 对于一个协程有四种状态,‘running’协程正在运行,‘suspended’协程处于挂起状态,‘normal’协程处于唤起另一个协程的状态中,‘dead’协程运行完毕。由‘normal’我们就可以知道,lua程序运行时,只能有一个协程处于‘running’状态,由‘dead’我们可以知道,一个协程只能运行一次。
lua里的使用 在lua里,协程的主要库函数有三个coroutine.create(f)
,coroutine.yield (···)
和coroutine.resume (co [, val1, ···])
。
coroutine.create 用于创建一个协程,传入一个函数用于封装代码,其返回值是一个协程引用,使用type
的输出是thread。
coroutine.yield(…)与coroutine.resume (co , ···) 这两个函数交替使用互相呼应,‘coroutine.resume (co , …)’用于唤起co协程,属于在一个协程里唤起另一个协程,值得注意的是我们的lua程序也是一个协程,称为主协程。…用于向协程传参,如果协程没有运行,则作为协程封装函数的参数。然后‘coroutine.yield(…)’用于挂起当前正在运行定位协程,参数用来作为唤起此协程的resume的返回值的后部分,resume的第一个返回值是bool类型,用来说明协程是否运行正常。第二次使用resume的参数则将作为yield的返回值。如果协程已经dead则直接返回false和报错信息。
官方案例 main.lua 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function foo (a) print ("foo" , a) return coroutine.yield (2 *a) end co = coroutine.create(function (a,b) print("co-body" , a, b) local r = foo(a+1 ) print("co-body" , r) local r, s = coroutine.yield(a+b, a-b) print("co-body" , r, s) return b, "end" end) print("main" , coroutine.resume(co, 1 , 10 )) print("main" , coroutine.resume(co, "r" )) print("main" , coroutine.resume(co, "x" , "y" )) print("main" , coroutine.resume(co, "x" , "y" ))
1 2 3 4 5 6 7 8 co-body 1 10 foo 2 main true 4 co-body r main true 11 -9 co-body x y main true 10 end main false cannot resume dead coroutine
这一个例子已经把协程的运行过程说透了。
其它 coroutine.running()
返回正在运行的协程,如果是主协程则返回nil,也就是说明了主协程的特殊性。coroutine.status(co)
返回协程co的运行状态,如之前所说的四种情况。coroutine.wrap(f)
在coroutine.create(f)
基础上,将协程封装为函数,调用此函数相当于调用coroutine.resume
。协程的基本用法其实挺简单的,我们主要去分析源码。
协程的源码解读 有关协程的函数我们主要分析三个,它们是在lbaselib.c
里的luaB_cocreate
,luaB_coresume
和luaB_yield
,他们分别对应了coroutine.create
,coroutine.resume
和coroutine.yield
。在此之前我们先说明一下,协程在C源码里的本质数据结构其实是lua_State
,我们之前一直把它叫做lua虚拟机的东西,其实在lua_State申明上面也写了per thread' state
,不过两者的创建是有区别的,我们分别来看看。
lua_newstate创建主协程(lua虚拟机) 我们之前虽然一直使用的是lua_open
,但实际调用的都是此函数。
1 2 3 4 5 6 7 8 #define lua_open() luaL_newstate() LUALIB_API lua_State *luaL_newstate (void ) { lua_State *L = lua_newstate(l_alloc, NULL ); if (L) lua_atpanic(L, &panic); return L; }
l_alloc
是一个用于分配内存的函数,有关内存管理是C的必修课,不多讲。lua_atpanic
用于给虚拟机挂上一个报错函数,主要在C方面,看源码的话,就输出一句*unprotected call*
,属于debug这个大坑。
lstate.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { int i; lua_State *L; global_State *g; void *l = (*f)(ud, NULL , 0 , state_size(LG)); if (l == NULL ) return NULL ; L = tostate(l); g = &((LG *)L)->g; L->next = NULL ; L->tt = LUA_TTHREAD; g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT); L->marked = luaC_white(g); set2bits(L->marked, FIXEDBIT, SFIXEDBIT); preinit_state(L, g); g->frealloc = f; g->ud = ud; g->mainthread = L; g->uvhead.u.l.prev = &g->uvhead; g->uvhead.u.l.next = &g->uvhead; g->GCthreshold = 0 ; g->strt.size = 0 ; g->strt.nuse = 0 ; g->strt.hash = NULL ; setnilvalue(registry(L)); luaZ_initbuffer(L, &g->buff); g->panic = NULL ; g->gcstate = GCSpause; g->rootgc = obj2gco(L); g->sweepstrgc = 0 ; g->sweepgc = &g->rootgc; g->gray = NULL ; g->grayagain = NULL ; g->weak = NULL ; g->tmudata = NULL ; g->totalbytes = sizeof (LG); g->gcpause = LUAI_GCPAUSE; g->gcstepmul = LUAI_GCMUL; g->gcdept = 0 ; for (i=0 ; i<NUM_TAGS; i++) g->mt[i] = NULL ; if (luaD_rawrunprotected(L, f_luaopen, NULL ) != 0 ) { close_state(L); L = NULL ; } else luai_userstateopen(L); return L; }
虽然长,但核心就几部分,L是lua虚拟机,g是挂在L上的全局状态机,l是为L和g共同分配的内存。L = tostate(l);g = &((LG *)L)->g;
将内存分配给L和g。L->tt = LUA_TTHREAD;
说明了我们的虚拟机是一个协程对象。preinit_state
初始化L,包括将g挂到L上也在这里进行。g->frealloc = f;
内存处理挂到了g上面,g->mainthread = L;
g上还存储了我们的主协程。其它都不是很重要,不过lua放了许多空接口,比如luai_userstateopen,NUM_TAGS之类的,可见lua的野心还是挺大的,都在试图变成一个可高度扩展的语言。
luaB_cocreate创建协程 1 2 3 4 5 6 7 8 static int luaB_cocreate (lua_State *L) { lua_State *NL = lua_newthread(L); luaL_argcheck(L, lua_isfunction(L, 1 ) && !lua_iscfunction(L, 1 ), 1 , "Lua function expected" ); lua_pushvalue(L, 1 ); lua_xmove(L, NL, 1 ); return 1 ; }
首先创建一个协程NL,其次检查参数是否为函数,lua为了扩展还是挺辛苦的,执行lua_pushvalue
后实际没什么变化,我们传参准确,不准确的话之前就会被测出来,再移动一遍没太多意义。记住我们之前所说,0处是我们调用函数的指针,最后将函数从L转移到NL。实际上,还有将NL放入栈顶,其实它将这放在了lua_newthread
里,我们继续看。
1 2 3 4 5 6 7 8 9 10 11 LUA_API lua_State *lua_newthread (lua_State *L) { lua_State *L1; lua_lock(L); luaC_checkGC(L); L1 = luaE_newthread(L); setthvalue(L, L->top, L1); api_incr_top(L); lua_unlock(L); luai_userstatethread(L, L1); return L1; }
luaE_newthread
真正用于创建协程,setthvalue(L, L->top, L1);api_incr_top(L);
将我们的协程入栈。我们来看看创建协程与一般的主协程有什么区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 lua_State *luaE_newthread (lua_State *L) { lua_State *L1 = tostate(luaM_malloc(L, state_size(lua_State))); luaC_link(L, obj2gco(L1), LUA_TTHREAD); preinit_state(L1, G(L)); stack_init(L1, L); setobj2n(L, gt(L1), gt(L)); L1->hookmask = L->hookmask; L1->basehookcount = L->basehookcount; L1->hook = L->hook; resethookcount(L1); lua_assert(iswhite(obj2gco(L1))); return L1; }
tostate
直接开始分配内存了,但我们从state_size(lua_State)
可以看到没有分配全局状态机的内存,也就是说一般协程没有全局状态机。然后它通过luaC_link
将此协程纳入了主协程的全局状态机的垃圾回收对象里面。stack_init(L1, L);
用来初始化我们的协程栈,L主要是用来初始化L1调用栈的,有关此我们马上也该讲讲了。setobj2n
让L1与主协程共享全局表,也就是主协程里的全局变量和函数可以在协程里使用。 至此我们已经可以看出协程与主协程的区别了,如没有全局状态机,与主协程共享全局变量表等。
CallInfo 这里我们要穿插将一下这个在lua虚拟机里俗称调用栈的东西。它的主要结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct CallInfo { StkId base; StkId func; StkId top; const Instruction *savedpc; int nresults; int tailcalls; } CallInfo; struct lua_State { *** CallInfo *ci; CallInfo *end_ci; CallInfo *base_ci; *** };
讲解看官方注解,我们主要看调用栈改变的时候和如何改变。其实顾名思义,改变主要源于调用luaD_call
的lua_precall()
函数,好久以前的坑,今天我们来看看。
ldo.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 int luaD_precall (lua_State *L, StkId func, int nresults) { LClosure *cl; ptrdiff_t funcr; if (!ttisfunction(func)) func = tryfuncTM(L, func); funcr = savestack(L, func); cl = &clvalue(func)->l; L->ci->savedpc = L->savedpc; if (!cl->isC) { CallInfo *ci; StkId st, base; Proto *p = cl->p; luaD_checkstack(L, p->maxstacksize); func = restorestack(L, funcr); if (!p->is_vararg) { base = func + 1 ; if (L->top > base + p->numparams) L->top = base + p->numparams; } else { int nargs = cast_int(L->top - func) - 1 ; base = adjust_varargs(L, p, nargs); func = restorestack(L, funcr); } ci = inc_ci(L); ci->func = func; L->base = ci->base = base; ci->top = L->base + p->maxstacksize; lua_assert(ci->top <= L->stack_last); L->savedpc = p->code; ci->tailcalls = 0 ; ci->nresults = nresults; for (st = L->top; st < ci->top; st++) setnilvalue(st); L->top = ci->top; if (L->hookmask & LUA_MASKCALL) { L->savedpc++; luaD_callhook(L, LUA_HOOKCALL, -1 ); L->savedpc--; } return PCRLUA; } else { CallInfo *ci; int n; luaD_checkstack(L, LUA_MINSTACK); ci = inc_ci(L); ci->func = restorestack(L, funcr); L->base = ci->base = ci->func + 1 ; ci->top = L->top + LUA_MINSTACK; lua_assert(ci->top <= L->stack_last); ci->nresults = nresults; if (L->hookmask & LUA_MASKCALL) luaD_callhook(L, LUA_HOOKCALL, -1 ); lua_unlock(L); n = (*curr_func(L)->c.f)(L); lua_lock(L); if (n < 0 ) return PCRYIELD; else { luaD_poscall(L, L->top - n); return PCRC; } } }
函数挺长的,我们看主要部分。cl存储函数闭包,funcr记录函数在栈上的位置。savepc是lua虚拟机当前指令的指针,L->ci->savedpc = L->savedpc;
就是将当前CallInfo的指令存储为虚拟机当前的指令,这样记录了当前函数运行到了哪里,方便回来继续执行。接下来的if用于区别是lua闭包还是c闭包,先看上一个。首先创建一个CallInfo,也就是说每次call的时候都会产生一个CallInfo用于记录信息。st用于缓存StkId,base最后会存为lua虚拟机的base,p存储当前函数的Proto。restorestack与之前的funcr相呼应,估计是防止func的指针不小心用着给用丢了。如果没有可变参数则,base指向func+1位置,top指向base+参数个数位置,以前讲过,我们回忆一下,如果ind>0返回bade+(ind-1),如果..<ind<0返回top+ind,如果ind=0返回当前函数。如果有可变参数的话,先存储总参数个数,我们在调用有可变参数的函数时,对此特性是不知的,所以会将所有参数入栈,lua_call处理时也将此作为参数处理,所以要依据此函数是否有可变参数再决定是否执行if (L->top > base + p->numparams) L->top = base + p->numparams;
来清除多余的参数,adjust_varargs
这个函数挺离谱的,它把非可变参数拷贝到栈上,返回的base就是旧top,可变参数就在base下面。我们可以看一下OP_VARAGE的执行代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 case OP_VARARG: { int b = GETARG_B(i) - 1 ; int j; CallInfo *ci = L->ci; int n = cast_int(ci->base - ci->func) - cl->p->numparams - 1 ; if (b == LUA_MULTRET) { Protect(luaD_checkstack(L, n)); ra = RA(i); b = n; L->top = ra + n; } for (j = 0 ; j < b; j++) { if (j < n) { setobjs2s(L, ra + j, ci->base - n + j); } else { setnilvalue(ra + j); } } continue ; }
取出操作数b,注意减了一,取出当前的CallInfo,接下来就是计算可变参数个数n了,base-func是所有参数个数加一(函数不做参数),然后减去固定参数加一,结果就是可变参数个数。如果b等于LUA_MULTRET即-1,则b=n即取出所有可变参数,接下来的for就是取出操作了。 我们回到原来的函数,ci = inc_ci(L);
修改虚拟机当前的ci,ci的func指向当前函数,ci的base和L的base都为我们之前申明的新base,ci的top指向当前函数可到的最大值。L->savedpc = p->code;
开始改变lua虚拟机指令了,原来的指令已经存到了原来的CallInfo里,然后存储返回值个数和tailcalls,这里的tailcalls是尾调用,在函数执行时才能获得值。后面不重要,最后返回PCRLUA。 下面是调用C闭包函数,大致差不多,而且它对可变参数熟视无睹,其实C函数本来就没这概念。n = (*curr_func(L)->c.f)(L);
执行函数,n则是返回值的个数,这个我们以前写过,其实还可以返回一个负数来表示yield的返回,一般我们也不会在自己的C函数里写协程就是了。luaD_poscall(L, L->top - n);
则用来回复运行状态,包括CallInfo改回,savepc的改变等。在lua闭包里,其实也有,不过是在上一级函数的luaV_execute(L, 1);
里执行到return系列字节码的时候。看来CallInfo也不是什么神奇的东西,只是存储函数的信息的结构体。
luaB_coresume运行协程 有的时候,弄清结构以后,读源码真的不难。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int luaB_coresume (lua_State *L) { lua_State *co = lua_tothread(L, 1 ); int r; luaL_argcheck(L, co, 1 , "coroutine expected" ); r = auxresume(L, co, lua_gettop(L) - 1 ); if (r < 0 ) { lua_pushboolean(L, 0 ); lua_insert(L, -2 ); return 2 ; } else { lua_pushboolean(L, 1 ); lua_insert(L, -(r + 1 )); return r + 1 ; } }
lua_State *co = lua_tothread(L, 1);
取出第一个协程参数,auxresume
是执行的主体,r是yield传的参数个数,错误则返回一个负数,后面的栈操作,用于注入信息,我们看这个关键函数。
lbaselib.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static int auxresume (lua_State *L, lua_State *co, int narg) { int status = costatus(L, co); if (!lua_checkstack(co, narg)) luaL_error(L, "too many arguments to resume" ); if (status != CO_SUS) { lua_pushfstring(L, "cannot resume %s coroutine" , statnames[status]); return -1 ; } lua_xmove(L, co, narg); lua_setlevel(L, co); status = lua_resume(co, narg); if (status == 0 || status == LUA_YIELD) { int nres = lua_gettop(co); if (!lua_checkstack(L, nres + 1 )) luaL_error(L, "too many results to resume" ); lua_xmove(co, L, nres); return nres; } else { lua_xmove(co, L, 1 ); return -1 ; } }
costatus(L, co);
返回co协程的状态,实现方式挺有趣的,注意协程处于初始化也属于suspended状态。后面检测参数是否超额和协程是否为suspended状态,只有suspended才能resume。lua_xmove(L, co, narg);
用于两个协程间移动narg个参数。lua_resume(co, narg);
是真正用来实现的函数,它与yield一样的,都提供了CAPI,我们搞嵌入开发时也能用的。有没有觉得lua比我们小心好多倍,一个函数套这么多皮,有点离谱哦。返回的是lua_State的状态,虽然协程是用lua_State实现的,但两者并不等价,之前创建的时候就说过了,lua_State状态如下:
1 2 3 4 5 6 #define LUA_YIELD 1 #define LUA_ERRRUN 2 #define LUA_ERRSYNTAX 3 #define LUA_ERRMEM 4 #define LUA_ERRERR 5
0表示正常,1表示挂起,其它用于表示各种错误,错误为什么分这么多,当然是为了便于调试了,以后再说。lua_resume返回0和yield,此函数正常执行,并通过lua_xmove
将返回值移动。对于lua_resume的内容,就算不看都可以猜个大概了,就是两台虚拟机互相呼叫传参而已。
luaB_yield挂起协程 这个简单得离谱
1 2 3 static int luaB_yield (lua_State *L) { return lua_yield(L, lua_gettop(L)); }
为了防止说我水段落,我就继续看看lua_yield的具体内容。
ldo.c 1 2 3 4 5 6 7 8 9 10 LUA_API int lua_yield (lua_State *L, int nresults) { luai_userstateyield(L, nresults); lua_lock(L); if (L->nCcalls > L->baseCcalls) luaG_runerror(L, "attempt to yield across metamethod/C-call boundary" ); L->base = L->top - nresults; L->status = LUA_YIELD; lua_unlock(L); return -1 ; }
luai_userstateyield
又是一个空函数,lua还真有趣。if用来报错,跳过。L->base = L->top - nresults;
移动base准备接受resume的参数了,L->status = LUA_YIELD;
修改协程为挂起状态,return -1;
还记得之前的luaD_precall吗,里面不就有一个检测是否yield的语句嘛。看来yield的实现也就如此了。
字符串的实现 字符串为何如此重要,你可能觉得在c里不就是一个char*或char[]吗,其实gcc编译器隐藏了字符串在汇编下的表现,lua的实现类似于此,字符串的存储大有学问,准确来说,可变内存的对象处理都是不易的,cpu可没有智慧来看出字符串的长度,我们来看看lua如何存储字符串的。
lua_pushlstring函数 这是向栈压入string的核心方法,其它都基于此函数。
1 2 3 4 5 6 7 LUA_API void lua_pushlstring (lua_State *L, const char *s, size_t len) { lua_lock(L); luaC_checkGC(L); setsvalue2s(L, L->top, luaS_newlstr(L, s, len)); api_incr_top(L); lua_unlock(L); }
看了这么多lua源码的你,应该一眼就发现了luaS_newlstr()
就是创建字符串的核心函数了。我们来深入了解一下它。
lstring.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { GCObject *o; unsigned int h = cast(unsigned int , l); size_t step = (l>>5 )+1 ; size_t l1; for (l1=l; l1>=step; l1-=step) h = h ^ ((h<<5 )+(h>>2 )+cast(unsigned char , str[l1-1 ])); for (o = G(L)->strt.hash[lmod(h, G(L)->strt.size)]; o != NULL ; o = o->gch.next) { TString *ts = rawgco2ts(o); if (ts->tsv.len == l && (memcmp (str, getstr(ts), l) == 0 )) { if (isdead(G(L), o)) changewhite(o); return ts; } } return newlstr(L, str, l, h); }
返回值为TString,这就是lua用来存字符串的C结构体了。GCObject *o;
创建可回收对象o。
1 2 3 4 5 unsigned int h = cast(unsigned int , l); size_t step = (l>>5 )+1 ; size_t l1; for (l1=l; l1>=step; l1-=step) h = h ^ ((h<<5 )+(h>>2 )+cast(unsigned char , str[l1-1 ]));
这一段用于计算字符串的Hash值,最终将Hash值存入h。for (o = G(L)->strt.hash[lmod(h, G(L)->strt.size)]; o != NULL;o = o->gch.next)
比较复杂,我们分解来看。G(L)->strt
是全局状态机中用于存储字符串的stringtable(字符串表),str.hash[]
即用来取出相应字符串(GCObject类型)。strt.size
是字符串表的容量,lmod
则用于对容量取模,保证数据在stringtable内的基本操作了。结合o != NULL
,我们可以知道,如果hash位置非空当然话,我们取出o->gch.next
,还非空则继续next。最终我们要取出空位置来存储数据。我们看看gch.next
是什么东东。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define CommonHeader GCObject *next; lu_byte tt; lu_byte marked typedef struct GCheader { CommonHeader; } GCheader; union GCObject { GCheader gch; union TString ts ; union Udata u ; union Closure cl ; struct Table h ; struct Proto p ; struct UpVal uv ; struct lua_State th ; };
仔细看看,其实就是每个基本的TString都会存一个空的GCObject指针,这其实是用于解决Hash冲突的基本方法—单向链表。TString *ts = rawgco2ts(o);
将这块内存地址转化为TString类型,ts->tsv.len == l
和memcmp(str, getstr(ts), l)
则是比较内存大小,看是否能存此数据,通过则进行GC标记并返回对象指针。否则执行newlstr(L, str, l, h)
来创建,具体如何,我们继续看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static TString *newlstr (lua_State *L, const char *str, size_t l, unsigned int h) { TString *ts; stringtable *tb; if (l+1 > (MAX_SIZET - sizeof (TString))/sizeof (char )) luaM_toobig(L); ts = cast(TString *, luaM_malloc(L, (l+1 )*sizeof (char )+sizeof (TString))); ts->tsv.len = l; ts->tsv.hash = h; ts->tsv.marked = luaC_white(G(L)); ts->tsv.tt = LUA_TSTRING; ts->tsv.reserved = 0 ; memcpy (ts+1 , str, l*sizeof (char )); ((char *)(ts+1 ))[l] = '\0' ; tb = &G(L)->strt; h = lmod(h, tb->size); ts->tsv.next = tb->hash[h]; tb->hash[h] = obj2gco(ts); tb->nuse++; if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2 ) luaS_resize(L, tb->size*2 ); return ts; }
创建指针ts和tb,然后判断字符串长度是否过长,也就是lua字符串还有长度限制,实际就是size_t的大小,不同系统有些差异。继续看吧,luaM_malloc
分配内存,大小是实际数据+用于封装的TString结构体,l+1
是因为要存储\\0
表示字符串结束,ts+1
则表示跳过TString所占的内存。然后一系列初始化,memcpy(ts+1, str, l*sizeof(char));((char *)(ts+1))[l] = '\0'; /* ending 0 */
进行字符串数据的拷贝。然后
1 2 3 4 5 tb = &G(L)->strt; h = lmod(h, tb->size); ts->tsv.next = tb->hash[h]; tb->hash[h] = obj2gco(ts); tb->nuse++;
则是将字符串放入全局状态机的stringtable。放入方式是更改相同Hash入口的TString,主要比较快,改改指针就行,放入末尾的话,还要遍历过去,不太方便。最后则是看nuse(个数)是否超过size(容量),超过则进行扩容,都是常规操作了。
结尾 到这里其实就没什么重要的东西了。我们可以看到,lua中string的存储结构是TString加具体内容,原来我以为它在TString存了指向数据的指针,但并没有这样,可能是为了加快访问的速度吧。
表的实现 最后再来看看,我们几乎万能的表吧。表除了创建还有各种操作,我们逐个来解读。table比较特别,不能从C数据直接得到,所以我们从创建表的函数开始。
lua_createtable函数 至于#define lua_newtable(L) lua_createtable(L, 0, 0)
不讲也罢。
1 2 3 4 5 6 7 LUA_API void lua_createtable (lua_State *L, int narray, int nrec) { lua_lock(L); luaC_checkGC(L); sethvalue(L, L->top, luaH_new(L, narray, nrec)); api_incr_top(L); lua_unlock(L); }
入栈操作,主要函数还是luaH_new(L, narray, nrec)
,来深入探究一下吧:
ltable.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Table *luaH_new (lua_State *L, int narray, int nhash) { Table *t = luaM_new(L, Table); luaC_link(L, obj2gco(t), LUA_TTABLE); t->metatable = NULL ; t->flags = cast_byte(~0 ); t->array = NULL ; t->sizearray = 0 ; t->lsizenode = 0 ; t->node = cast(Node *, dummynode); setarrayvector(L, t, narray); setnodevector(L, t, nhash); return t; }
Table *t = luaM_new(L, Table);
直接为Table分配内存并保存首地址,luaC_link()
则将Table纳入回收对象。然后一系列的初值设定,node用于存储k-v形式的值,array用于存储顺序值,最后设置array和hash各部分的大小,基本都是置为lua里的nil类型,注意这与C里面的NULL是有区别的。实际上,如果看过字节码就会知道,创建表一般还要配OP_SETLIST来初始化array元素和OP_SETTABLE来初始化hash元素,我们来看看字节码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 case OP_SETLIST: { int n = GETARG_B(i); int c = GETARG_C(i); int last; Table *h; if (n == 0 ) { n = cast_int(L->top - ra) - 1 ; L->top = L->ci->top; } if (c == 0 ) c = cast_int(*pc++); runtime_check(L, ttistable(ra)); h = hvalue(ra); last = ((c-1 )*LFIELDS_PER_FLUSH) + n; if (last > h->sizearray) luaH_resizearray(L, h, last); for (; n > 0 ; n--) { TValue *val = ra+n; setobj2t(L, luaH_setnum(L, h, last--), val); luaC_barriert(L, h, val); } continue ; }
和
1 2 3 4 case OP_SETTABLE: { Protect(luaV_settable(L, ra, RKB(i), RKC(i))); continue ; }
后一个比较简单,主要还是因为泛用性高,所有我们看前一个。ra是表在栈上的索引,n是要加入的元素个数,c的话好像用处不大,目前我见到的都是1,h用于存储要写入数据的表的指针。如果n=0,则准备将ra上的元素都写入表。h = hvalue(ra);
取出我们之前创建的表,last似乎和表原来是否有初值有关c也是一样,但这个字节码,都是在空表之后,只能说这是程序员的严谨吧,虽然不太可能发生,但还是要检测,也就是last是扩充后表array部分的大小,不够则luaH_resizearray
进行扩容。接下来就是for循环将栈上的值通过luaH_setnum
来一个个写入表,从后往前写,这样可以顺便清理一下栈。
元素获取 由上部分,我们可以知道,往array里添元素的核心函数是luaH_setnum
,我们来看看它:
1 2 3 4 5 6 7 8 9 10 TValue *luaH_setnum (lua_State *L, Table *t, int key) { const TValue *p = luaH_getnum(t, key); if (p != luaO_nilobject) return cast(TValue *, p); else { TValue k; setnvalue(&k, cast_num(key)); return newkey(L, t, &k); } }
参数好理解,t是表,key是array的索引,看到这我们可以合理猜想array的长度不能超过int的最大值。返回TValue的指针,这其实是一个获得表对应位置指针的函数,修改实际在之前的函数setobj2t
里完成的。第一步判断对应位置是否有值,没有则先创建key,是TValue类型中的number,接着通过newkey创建此key对应值的TValue,并返回。这样来看在lua里对表的索引不论array还是hash都是一样的,事实上,看过字节码的伙伴都知道,表索引使用的字节码其实都是OP_GETTABLE,我们来一看究竟吧,从字节码直接可以知道关键函数是luaV_gettable
,它的源码如下:
lvm.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) { int loop; for (loop = 0 ; loop < MAXTAGLOOP; loop++) { const TValue *tm; if (ttistable(t)) { Table *h = hvalue(t); const TValue *res = luaH_get(h, key); if (!ttisnil(res) || (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL ) { setobj2s(L, val, res); return ; } } else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX))) luaG_typeerror(L, t, "index" ); if (ttisfunction(tm)) { callTMres(L, val, tm, t, key); return ; } t = tm; } luaG_runerror(L, "loop in gettable" ); }
参数好理解,最后一个是索引后的TValue存放在栈上的相对位置。这个函数好复杂,似乎有好多功能,但这是笔者不太理解的,只知道索引的关键部分在if(ttistable(t)) {**}
里面,似乎也只会用到这部分。内容简单明了,通过luaH_get
获得TValue,没有问题则将表放入栈后,然后直接结束函数。更详细的索引也挺无聊的,根据TValue的类型不同,再分别调用不同的函数。
表结构体 我们没有看将表的移除的源码,对于array我们一般没有移除操作,实际上表是通过table库实现移除操作的,而移除操作实际是将值置为nil,实在没什么看点。我们稍微看看表的结构体吧。
lobject.h 1 2 3 4 5 6 7 8 9 10 11 typedef struct Table { CommonHeader; lu_byte flags; lu_byte lsizenode; struct Table *metatable ; TValue *array ; Node *node; Node *lastfree; GCObject *gclist; int sizearray; } Table;
对于一份源码,读了大部分框架以后,再去细读还是挺索然无味的,因为我们大致都能预测该存储些什么了,而一些基础操作和我们平常写的也不会有太大区别。node和array分别存储hash和array部分的内容。lsizenode和sizearray存储相应部分的大小,metatable是元表信息,Node是由TKey和TValue组成的结构体。
结尾 说实话,最后两部分还是挺失望的,本以为应该会有些有趣的思想在里面,但仔细一读,发现也就不过如此,但已经写了不少,就直接附上算了,对协程的理解才是这篇文章的主要部分。“大部分程序员都是做苦力的。”我似乎也属于这一梯度,我也是时候想要突破了。