lua5.3 栈学习
lua 与c进行交互,或者lua函数执行,都需要用到栈,接下来就先了解下 lua 栈的内部实现。
了解栈的实现,可以从2个方面出发,栈结构在哪定义,栈存储的元素是什么。
lua 栈主要放在 lua_State 结构上,摘要 lua_State 一些和栈相关的字段:
/*
** 'per thread' state
*/
struct lua_State {
StkId top; /* first free slot in the stack */
CallInfo *ci; /* call info for current function */
StkId stack_last; /* last free slot in the stack */
StkId stack; /* stack base */
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
int stacksize;
...
};
typedef TValue *StkId; /* index to stack elements */
/*
** Union of all Lua values
*/
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;
#define TValuefields Value value_; int tt_
typedef struct lua_TValue {
TValuefields;
} TValue;
通过对 lua_State 分析,对栈的理解,可以分为数据栈,调用栈两部分。
数据栈:
- stack 指向栈的起始地址。
- top 指向栈顶(和我们平时学数据结构栈一样,都会有一个 top指针,指向入栈顶数据的下一个位置)。
- stack_last 指向最后可用的位置,但是会留空 EXTRA_STACK=5 个位置,用于元表调用或错误处理的栈操作。
- stacksize 栈大小,初始化时,大小为40
通过查看 stack 的类型,我们知道,栈中存储的每一个元素都是 TValue 类型,只不过给它起了个别名 StkId,目的是为了方便内部一些函数传参时,能一眼看出来,就是对栈中的数据进行操作,TValue 可不止在栈中使用,在其他地方,比如 table 的 key/value,Proto 函数原型的常量表k等地方也有使用到这个结构。TValue 它包含了一个类型标识字段 tt_(标识 Value 当前存的值是整型还是浮点型,又或者其他类型值),以及一个真正存储数据的 Value 字段。
lua 数据分为值类型和引用类型。值类型可以直接复制,比如整数,浮点数等。而引用类型共享同一份数据,由 GC 维护生命周期,比如,table,string 等。在 lua 中,用 TValue 保存数据。
调用栈:
- 用 CallInfo(简称ci)结构体来描述调用栈信息,我们在函数调用时,都需要通过 ci 来存储函数地址,以及记录函数在栈中使用到的栈顶位置。
- L->base_ci 指向第一个 ci。也就是所有 lua 函数调用的第一层,这个 base_ci 可以理解为指向 main 函数的 ci。
简单看下调用栈 ci 结构,摘要其中比较重要的字段分析:
typedef struct CallInfo {
StkId func; /* function index in the stack */
StkId top; /* top for this function */
struct CallInfo *previous, *next; /* dynamic call link */
short nresults; /* expected number of results from this function */
...
} CallInfo;
- func 指向栈中存储函数的那个 TValue 位置。(我们前面说过,栈是由 TValue 构成的,TValue 可以存储函数)
- top 指向本函数在栈中可使用的最后一个元素的位置+1。
- previous, next 是把所有的 ci 串连起来,构成双向链表。就好比我们平时写的函数调用,A call B call C,那么调用栈顺序:ci(A) -> ci(B) -> ci(C),C执行完了,返回上一级函数B,B执行完了,返回A继续执行。函数能正确返回,都是因为 ci 记录了每个函数在栈中的位置。
- nresult 返回值个数,本次函数调用,调用方期待被调用函数能返回的值的个数。
初始化栈时,结构大致示意图如下:

下面举个例子,来说明下 c 函数是如何和 lua 栈进行交互的:
int f2(lua_State *L) {
printf("f2() called\n");
return 0;
}
int f1(lua_State *L) {
printf("f1() called start\n");
printf("args: %lld %s\n", lua_tointeger(L, 1), lua_tostring(L, -1));
lua_pushcfunction(L, f2);
lua_pcall(L, 0, 0, 0);
lua_pushstring(L, "a1");
lua_pushstring(L, "a2");
printf("f1() called end\n");
return 2;
}
int main(int argc, char const *argv[]) {
lua_State *L = luaL_newstate();
lua_pushcfunction(L, f1);
lua_pushinteger(L, 12);
lua_pushstring(L, "hello world");
lua_call(L, 2, 2);
printf("main() recv: %s %s\n", lua_tostring(L, 1), lua_tostring(L, 2));
lua_close(L);
return 0;
}
/**
输出结果:
f1() called start
args: 12 hello world
f2() called
f1() called end
main() recv: a1 a2
**/
在 main 函数中,先把 f1 压栈,接下来再压入一个整型12,以及一个字符串 "hello world",接下来会调用 lua_call,lua_call 会调用我们最先开始压入的函数f1,在函数 f1 中,打印栈中参数,然后接着通过 lua_call 调用 f2 函数,f2 执行完后,就退回到 f1,接着把字符串 "a1","a2" 压栈返回,最后 main 函数里打印 "a1","a2"。
如果忽略 lua_call API的包装,单纯从我们自己写的函数,调用顺序为:main() -> f1() -> f2(),通过使用 lua_pushxxx(), lua_call,lua_pcall 等API,我们可以在函数之间传递多个参数,以及返回多个值。看起来,有点像 lua 代码中的可变参数使用。
接下来分析具体实现原理:
当我们在 main 函数中 push f1函数地址,12,"hello world" 三个值,栈的变化如下:

当我们调用 lua_call 时,会创建一个新的 CallInfo 对象,并且 L->ci 指向它。用它来记录 f1 在栈中的位置,以及在栈中默认可以使用 LUA_MINSTACK(20)个空位。此时,L->base_ci 的 next 会指向当前 L->ci,构成一个双向链表。
紧接着,lua_call 会通过 func 指向的 f1 函数地址,调用 f1函数。在 main 函数中,我们不仅 push 了 f1 函数地址, 还 push 了两个值:12,"hello world",那么我们是怎么在 f1 函数中获得这两个值的呢。
在代码中,我们是通过在 lua_tointeger(L, 1) 和 lua_tostring(L, 2) 来获取参数值的,还记得我们说过栈存储的是 TValue 吗,我们在 main 函数中 push 数据的时候,lua 就会根据不同的api,设置 TValue.tt_ 类型。在获取值的时候,我们也要自己知道参数的类型,根据不同的类型,来获取对应 TValue.value_ 的值。
#define lua_tointeger(L,i) lua_tointegerx(L,(i),NULL)
LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) {
lua_Integer res;
const TValue *o = index2addr(L, idx);
int isnum = tointeger(o, &res);
if (!isnum)
res = 0; /* call to 'tointeger' may change 'n' even if it fails */
if (pisnum) *pisnum = isnum;
return res;
}
static TValue *index2addr (lua_State *L, int idx) {
CallInfo *ci = L->ci;
if (idx > 0) {
TValue *o = ci->func + idx;
...
else return o;
}
else if (!ispseudo(idx)) { /* negative index */
...
return L->top + idx;
}
...
}
我们可以通过简单分析一下其中一个 lua_tointeger 的实现。lua_tointeger 是一个宏,它的原型是 lua_tointegerx 函数,在 lua_tointegerx 函数中,我们通过给定的 idx,先去 index2addr 函数中查找栈中对应位置的数据。
在 index2addr 函数中的查找方式主要有2种大的方式查找,一种是正数,一种是负数,正数是从 L->ci->func 开始偏移查找,负数是从 L->top 开始往下偏移。(当然,还有其他情况,比如上值,全局表等,这里先不讨论)。也就是说,获取参数值,可以通过基于 ci->func 当前函数正向偏移查找,也可以通过栈顶反方向偏移查找,具体看怎么方便怎么来。
在例子 f1 函数中,lua_tointeger(L, 1) 表示相对当前 L->ci->func(也就是 f1)在栈中的位置偏移1,指向栈中第2个位置,然后返回正数12。lua_tostring(L, -1) 表示相对于栈顶 L->top 向下偏移1,指向栈中的第3个位置,然后返回的是 "hello world"。
在例子11-12行中,我们再 push 一个函数 f2 进栈,并执行它。最终的效果如下

最终我们看到,所有的 ci 构成双向链表,base_ci 指向第一个起始 CallInfo(相对于 main 函数),L->ci 指向当前正在执行的 f2 函数。每个 CallInfo 都记录一个函数在栈中的起始地址,以及参数最大地址 top(当然上图没画 top指向)。再 f2 调用完后,继续 push 两个字符 "a1", "a2" 进栈。然后返回2,标识有两个返回值,这样我们就能在 main 函数中获取到 "a1","a2"。
我们再进一步分析,f1 函数 push 两个字符到栈上后,main 函数又是怎么拿到的,我们又应该偏移 L->base_ci->func 多少去获取呢。通过源码分析,lua_call -> lua_callk() -> luaD_precall() -> luaD_callnoyield() -> luaD_call() -> luaD_precall() -> luaD_poscall() -> moveresults() 最终在 moveresults() 函数中,根据 f1 的返回个数2,来移动栈顶的2个数据到 f1函数位置上,可以理解为: stack[1] = stack[4]; stack[2] = stack[5]; L->top = stack[3],具体可以看下 luaD_poscall() -> moveresults()实现。在 f1 执行完后,最终效果如下:

函数最终返回几个值,主要和 lua_pcall 调用时,第3个参数(nresults)有关。 情况如下:
1. nresults 大于函数的返回值,例如:
int f1(lua_State *L) {
lua_pushstring(L, "a1");
lua_pushstring(L, "a2");
return 2;
}

此时,会从 a1位置开始移动两个元素到 f1处 ,不够时,补nil,L->top 指向 nresults (3)个元素的下一个位置,最终变成右图的栈。
2. nresults 小于或者等于函数的返回值,例如:
int f1(lua_State *L) {
lua_pushstring(L, "a1");
lua_pushstring(L, "a2");
return 2;
}

此时,会从 a1位置开始移动一个元素到 f1处 ,L->top 指向 nresults (1)个元素的下一个位置,最终变成右图的栈。
3. nresults 为 LUA_MULTRET(-1),表示函数返回多少就接收多少,例如:
int f1(lua_State *L) {
lua_pushstring(L, "a1");
lua_pushstring(L, "a2");
return 2;
}

此时,会从 a1位置开始移动两个元素到 f1处 ,L->top 指向 f1返回值(2)个元素的下一个位置,最终变成右图的栈。
最后,我们就可以在 main 函数中,调用 lua_tostring(L, 1) 和 lua_tostring(L, 2) 来相对 L->base_ci->func 偏移1位,获取字符串 "a1",偏移2位,获取字符串 "a2" 。
总结
总结下,我们的栈分为数据栈,和调用栈,数据栈stack 本质是一个可变长的数组(在堆中申请),数组中的每个数据存储的类型是 StkId,它的原型是 TValue,包括了一个标识当前值类型的 tt_ 字段,以及一个存储值的 value_ 字段。当我们调用函数层级越多时,需要手动调用 lua_checkstack(L, n),告诉栈,我们当前需要至少 n 个位置来存放数据,栈就会检测是否有 n 个多余空位,不足时,就会扩容。调用栈,即 CallInfo,记录了函数地址在栈中的位置,以及如何维护函数之间调用层级关系的,最后我们还介绍函数返回时,上一层函数是如何获取多个返回值。

浙公网安备 33010602011771号