协程的基本用法

前言

协程是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_cocreateluaB_coresumeluaB_yield,他们分别对应了coroutine.createcoroutine.resumecoroutine.yield。在此之前我们先说明一下,协程在C源码里的本质数据结构其实是lua_State,我们之前一直把它叫做lua虚拟机的东西,其实在lua_State申明上面也写了per thread' state,不过两者的创建是有区别的,我们分别来看看。

lua_newstate创建主协程(lua虚拟机)

我们之前虽然一直使用的是lua_open,但实际调用的都是此函数。

1
2
3
4
5
6
7
8
//lua.h
#define lua_open() luaL_newstate()
//lauxlib.c
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; /* mark it as unfinished state */
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) {
/* memory allocation error: free partial state */
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); /* move function to top */
lua_xmove(L, NL, 1); /* move function from L to NL */
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); /* init stack */
setobj2n(L, gt(L1), gt(L)); /* share table of globals */
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
/*
** informations about a call
*/
typedef struct CallInfo {
StkId base; /* base for this function */
StkId func; /* function index in the stack */
StkId top; /* top for this function */
const Instruction *savedpc;
int nresults; /* expected number of results from this function */
int tailcalls; /* number of tail calls lost under this entry */
} CallInfo;
struct lua_State {
***
CallInfo *ci; /* call info for current function */
CallInfo *end_ci; /* points after end of ci array*/
CallInfo *base_ci; /* array of CallInfo's */
***
};

讲解看官方注解,我们主要看调用栈改变的时候和如何改变。其实顾名思义,改变主要源于调用luaD_calllua_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' is not a function? */
func = tryfuncTM(L, func); /* check the `function' tag method */
funcr = savestack(L, func);
cl = &clvalue(func)->l;
L->ci->savedpc = L->savedpc;
if (!cl->isC) { /* Lua function? prepare its call */
CallInfo *ci;
StkId st, base;
Proto *p = cl->p;
luaD_checkstack(L, p->maxstacksize);
func = restorestack(L, funcr);
if (!p->is_vararg) { /* no varargs? */
base = func + 1;
if (L->top > base + p->numparams)
L->top = base + p->numparams;
}
else { /* vararg function */
int nargs = cast_int(L->top - func) - 1;
base = adjust_varargs(L, p, nargs);
func = restorestack(L, funcr); /* previous call may change the stack */
}
ci = inc_ci(L); /* now `enter' new function */
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; /* starting point */
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++; /* hooks assume 'pc' is already incremented */
luaD_callhook(L, LUA_HOOKCALL, -1);
L->savedpc--; /* correct 'pc' */
}
return PCRLUA;
}
else { /* if is a C function, call it */
CallInfo *ci;
int n;
luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */
ci = inc_ci(L); /* now `enter' new function */
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); /* do the actual call */
lua_lock(L);
if (n < 0) /* yielding? */
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); /* previous call may change the stack */
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; /* return false + error message */
}
else {
lua_pushboolean(L, 1);
lua_insert(L, -(r + 1));
return r + 1; /* return true + `resume' returns */
}
}

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; /* error flag */
}
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); /* move yielded values */
return nres;
}
else {
lua_xmove(co, L, 1); /* move error message */
return -1; /* error flag */
}
}

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
/* thread status; 0 is OK */
#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; /* protect stack slots below */
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); /* seed */
size_t step = (l>>5)+1; /* if string is too long, don't hash all its chars */
size_t l1;
for (l1=l; l1>=step; l1-=step) /* compute hash */
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)) {
/* string may be dead */
if (isdead(G(L), o)) changewhite(o);
return ts;
}
}
return newlstr(L, str, l, h); /* not found */
}

返回值为TString,这就是lua用来存字符串的C结构体了。GCObject *o;创建可回收对象o。

1
2
3
4
5
unsigned int h = cast(unsigned int, l);  /* seed */
size_t step = (l>>5)+1; /* if string is too long, don't hash all its chars */
size_t l1;
for (l1=l; l1>=step; l1-=step) /* compute hash */
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; /* thread */
};

仔细看看,其实就是每个基本的TString都会存一个空的GCObject指针,这其实是用于解决Hash冲突的基本方法—单向链表。TString *ts = rawgco2ts(o);将这块内存地址转化为TString类型,ts->tsv.len == lmemcmp(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'; /* ending 0 */
tb = &G(L)->strt;
h = lmod(h, tb->size);
ts->tsv.next = tb->hash[h]; /* chain new entry */
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); /* too crowded */
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]; /* chain new entry */
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);
/* temporary values (kept only if some malloc fails) */
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) /* needs more space? */
luaH_resizearray(L, h, last); /* pre-alloc it at once */
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)) { /* `t' is a table? */
Table *h = hvalue(t);
const TValue *res = luaH_get(h, key); /* do a primitive get */
if (!ttisnil(res) || /* result is no nil? */
(tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */
setobj2s(L, val, res);
return;
}
/* else will try the tag method */
}
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; /* else repeat with `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; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of `node' array */
struct Table *metatable;
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
GCObject *gclist;
int sizearray; /* size of `array' array */
} Table;

对于一份源码,读了大部分框架以后,再去细读还是挺索然无味的,因为我们大致都能预测该存储些什么了,而一些基础操作和我们平常写的也不会有太大区别。node和array分别存储hash和array部分的内容。lsizenode和sizearray存储相应部分的大小,metatable是元表信息,Node是由TKey和TValue组成的结构体。

结尾

说实话,最后两部分还是挺失望的,本以为应该会有些有趣的思想在里面,但仔细一读,发现也就不过如此,但已经写了不少,就直接附上算了,对协程的理解才是这篇文章的主要部分。“大部分程序员都是做苦力的。”我似乎也属于这一梯度,我也是时候想要突破了。