通过lua栈了解lua与c的交互

7.4k 词


其中分析、执行部分都是c语言实现的。

lua与c的关系

lua的虚拟机是用c语言实现的,换句话说一段lua指令最终在执行时都是当作c语言来执行的,lua的global表,函数调用栈也都是存在c语言中的一个叫lua_State的结构体中的。
举个例子,来看下lua中的加指令 OP_ADD a b c 是如何实现的:
lua在运行时,会在c语言中的一个叫luaV_excute的函数中不断执行翻译后的lua指令,OP_ADD就是其中的一条指令(luaV_excute函数太长了,所以只在这里截取OP_ADD的部分,有兴趣可以直接去看lua的源码)

1
2
3
4
case OP_ADD: {
arith_op(luai_numadd, TM_ADD);
continue;
}

相关的一些宏定义:

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



#define arith_op(op,tm) {
//获取b,c的值
TValue *rb = RKB(i);
TValue *rc = RKC(i);
//判断是否是b,c数字
if (ttisnumber(rb) && ttisnumber(rc)) {
//从b,c指向的TValue中将数字字段取出来
lua_Number nb = nvalue(rb), nc = nvalue(rc);
//进行op运算后放入a中
setnvalue(ra, op(nb, nc));
}
//如果不是数字,则尝试调用ra,rb的元方法
else
Protect(Arith(L, ra, rb, rc, tm));
}

//判断一个Tvalue是否是数字
#define ttisnumber(o) (ttype(o) == LUA_TNUMBER)
#define ttype(o) ((o)->tt)

//根据b的类型获取b对应的TValue
#define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK,
ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))

可以看到,OP_ADD其实就是把b,c(b,c是需要进行加运算的两个数字在函数常量中表中的位置中或调用栈中的位置上的TValue)的值加到了a中。
也就是说,每个lua指令最终的实现还是在通过执行c语言语句实现的。

lua中进行函数调用时栈的状态

lua和c的交互完全通过栈来进行交互,为了了解lua与c的交互一定要先了解lua的栈。
我们常说的lua栈有两种:
1.一个总的数据栈:每个lua环境唯一的栈,所有的调用信息都存在这上面。
2.每个函数的调用栈:函数的调用栈,其实并不是一个独立的栈,只是数据栈上的一小段。

与栈相关的结构体

lua_State

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
struct  {
CommonHeader;
lu_byte status;
StkId top; /* first free slot in the stack */
StkId base; /* base of current function */
global_State *l_G;
CallInfo *ci; /* call info for current function */
const Instruction *savedpc; /* `savedpc' of current function */
StkId stack_last; /* last free slot in the stack */
StkId stack; /* stack base */
CallInfo *end_ci; /* points after end of ci array*/
CallInfo *base_ci; /* array of CallInfo's */
int stacksize;
int size_ci; /* size of array `base_ci' */
unsigned short nCcalls; /* number of nested C calls */
lu_byte hookmask;
lu_byte allowhook;
int basehookcount;
int hookcount;
lua_Hook hook;
TValue l_gt; /* table of globals */
TValue env; /* temporary place for environments */
GCObject *openupval; /* list of open upvalues in this stack */
GCObject *gclist;
struct lua_longjmp *errorJmp; /* current error recover point */
ptrdiff_t errfunc; /* current error handling function (stack index) */
};

lua_State保存lua运行相关所有信息的结构体,就是lua的运行环境,lua的栈和栈相关的信息都存在这个结构体里。这里说明几个和lua栈紧密相关的几个变量:
stack: lua栈的实体,在每个lua环境中是唯一的,每个函数的调用栈只是stack上的一小段。
top,base: 指向当前调用栈的栈顶和栈底。
ci: 当前函数的调用信息,具体结构下面会讲到。
base_ci: 函数调用信息的列表,用来记录和恢复当前的调用信息。

CallInfo

1
2
3
4
5
6
7
8
9
10
11
/*
** 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;

CallInfo是每一次函数调用的调用信息,这里也说明几个和栈紧密相关的变量。
base: 当前调用栈的栈底
top: 当前调用栈的栈顶
func: 当前调用的函数在stack中的位置
nresults: 当前函数预期会返回的结果个数,如果返回的数量不够,会用nil补齐

通过一个例子说明一下

我们这里在c语言中操作lua来调用c写的函数进行举例,因为用c语言去操作lua的流程更接近lua编译成指令后的过程,看的更清晰一些,又不至于像直接阅读虚拟机指令那么吃力。

使用的代码

供lua调用的c函数库(addlib.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
#include <stdio.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>

static int addc(lua_State *L)
{
//输出一下当前调用栈的元素个数
printf("get top in addc: %dn",lua_gettop(L));
int a,b,c;
a = lua_tonumber(L,-1);
b = lua_tonumber(L,-2);
c = a + b;
//压入结果
lua_pushnumber(L,c);
//输出压入结果后的调用栈的元素个数
printf("get top in addc,after push result: %dn",lua_gettop(L));
return 1;
}

static const struct luaL_Reg lib[] =
{
{"addc",addc},
{NULL,NULL}
};

int luaopen_addlib(lua_State *L)
{
luaL_register(L,"testadd",lib);
return 1;
}

调用代码:

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
#include <stdio.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>

int main()
{
lua_State* luaEnv = lua_open();
//载入基础库
luaopen_base(luaEnv);
luaL_openlibs(luaEnv);
//输出一下载入库之后的栈中元素个数,lua_gettop(luaEnv)输出luaEnv->top - luaEnv->base,也就是当前调用栈中元素的个数
printf("get top after openlibs: %dn",lua_gettop(luaEnv));
//载入addlib库
lua_getglobal(luaEnv,"require");
lua_pushstring(luaEnv,"addlib");
lua_pcall(luaEnv,1,0,0);
//输出载入addlib库后的栈中元素个数
printf("get top after require addlib: %dn",lua_gettop(luaEnv));
//压入需要调用的函数和参数
lua_getglobal(luaEnv,"testadd");
lua_getfield(luaEnv,-1,"addc");
lua_pushinteger(luaEnv,10);
lua_pushinteger(luaEnv,12);
//输出压入后的栈中元素个数
printf("get top after push function and args: %dn",lua_gettop(luaEnv));
//调用addc函数
lua_pcall(luaEnv,2,1,0);
//输出调用后的栈中元素的个数
printf("get top after pcall addc: %dn",lua_gettop(luaEnv));
int result = lua_tonumber(luaEnv,-1);
//输出结果
printf("addc's result is : %dn",result);
return 0;
}

调用结果:

过程说明

在c语言中调用一个lua函数的流程:

栈的详细变化过程

1.通过lua_open创建lua_State,并进行栈的初始化,栈的初始化操作在state_init()函数中。在state_init()中,创建栈的实例和函数调用列表实例,并设置了初始的ci信息,和初始ci的调用栈信息。此时栈是空的,base,top都指向栈的第一个元素。

2.打开基本的库后的栈(打开基本库后,栈中会剩余两张表格,但不影响之后的流程,所以先无视掉),因为在打开基本库的时候载入了两张表,所有top增加了2。


3.载入addlib库,用于函数调用(因为调用了返回0个返回值的pcall,所以对栈的内容没有影响,后面说),栈的状态完全没变。
4.压入要调用的函数及参数,准备进行调用。


5.通过lua_pcall(真正对addc函数的调用是在luaD_precall中)调用addc函数,在luaD_precall函数中会创建新的ci来用于保存addc的调用信息,将base移动到func+1的位置,作为新的ci的栈底。top不动,这样新的调用栈中就有addc的参数信息了。


6.在addc中读取调用栈中的两个参数,计算出结果,并压入栈中。


7.在addc调用结束后,lua会调用luaD_poscall回归到上一层,在回归时lua会根据return n的个数将addc调用栈栈顶的n个元素拷贝到,从addc位置开始的nresults个位置中,若n < nresults,少的部分补nil,并重新计算上一层栈顶。


小结:
①lua用于调用,交互的栈只有一个,每次进行函数调用时并不会新开一个栈来存储调用信息,而是新创建一个ci用于保存被调用函数的信息,并根据被调用函数的位置、参数个数将stack上的一段作为新的ci的调用栈,并将当前的调用信息(当前的函数位置,当前使用的栈的区间等信息)保存为一个callinfo,用于调用后的恢复。
②每次在lua中调用一个c函数时,会以函数在栈中的位置加1作为被调用函数callinfo的base,top位置保持不变作为新的调用栈的栈顶(调用lua函数略有不同,调用lua函数会将函数的参数新复制一份,但原理跟调用c函数差不多,所以不多做说明)。
③一次函数调用结束后,栈会根据之前保存的callinfo恢复栈的状态,并将函数调用的结果复制到当前栈顶的位置。

lua与c的交互

c调用lua函数

我们写一个接收一个参数,返回两个结果的PrintHello函数供c语言调用。
被调用的lua函数(PrintHello.lua):

1
2
3
4
5
6
7
8
9
10
--接收一个参数,返回两个结果的函数
function PrintHello(name)
--输出Hello
print("Hello "..name);
--给要返回的两个结果复制
result1 = "the name : "..name;
result2 = "something else...";
--返回结果
return result1,result2;
end

测试用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
# include <lua5.1/lua.h>
# include <lua5.1/lualib.h>
# include <lua5.1/lauxlib.h>

int main()
{
//创建lua运行环境
lua_State* luaEnv = lua_open();
//打开基础库
luaopen_base(luaEnv);
luaL_openlibs(luaEnv);
//载入PrintHello.lua
luaL_loadfile(luaEnv,"PrintHello.lua");
//执行PrintHello.lua,将PrintHello加入Global表中
lua_pcall(luaEnv,0,0,0);
//将PrintHello函数、参数压栈,准备调用
lua_getglobal(luaEnv,"PrintHello");
lua_pushstring(luaEnv,"bard");
//调用PrintHello函数
lua_pcall(luaEnv,1,2,0);
//取出返回的两个结果并输出
char* result1 = lua_tostring(luaEnv,-2);
printf("%sn",result1);
char* result2 = lua_tostring(luaEnv,-1);
printf("%sn",result2);
return 1;
}

调用结果:

小结:
①没有看到PrintHello函数有显式从栈中取参数和压入结果的操作,那么取参数和将结果压栈的操作是在哪里进行的:
取参数: 在luaD_precall函数中,会创建新的ci供PrintHello使用,同时将栈顶的1个参数PrintHello的调用栈中,然后luaV_execute执行对应的PrintHello对应的一段指令,这些指令中会把name参数对应到调用栈的第一个位置去,这样就可以使用这个参数了。
压栈: PrintHello中的return会被翻译成两个OP_MOVE指令和一条OP_RETURN指令,进行结果的压栈和栈的恢复。
②其他操作就和前面说明调用栈的操作差不多了,这里就不做重复的解释。

lua调用c函数

我们写一个接收一个参数,返回两个结果的函数
供lua调用的c代码(testlib.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
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>
#include <stdio.h>

static int printHelloInC(lua_State* L)
{
//取出栈中的参数
//如果想实现函数重载,可以对栈中的参数数量和参数类型进行判断后进行分别处理,这里只是为了展示lua对c的调用,
//所以没有进行参数检查
char* arg0 = lua_tostring(L,-1);
//输出Hello
printf("Hello %sn",arg0);
//压入两个要返回的值
char* result1 = "this is result1";
lua_pushstring(L,result1);
char* result2 = "this is result2";
lua_pushstring(L,result2);
//表示这个函数有2个返回值
return 2;
}

static const struct luaL_Reg lib[] =
{
{"printHelloInC",printHelloInC},
{NULL,NULL}
};

int luaopen_testlib(lua_State *L)
{
luaL_register(L,"testlib",lib);
return 1;
}

lua测试代码:

1
2