Lua和C
在实际游戏业务运营过程中,经常会出现一些紧急的配置修改、bug修复等;这种规划外的变更行为希望能做到用户无感知,否则对于游戏体验和产品口碑有很大影响,在此背景下,热更新能力成为成熟上线游戏的标准配置。
一般情况下,后台逻辑热更新能力的技术方案有两种:
①基于共享内存的数据存储:通过进程状态数据存放在共享内存,使得进程重启数据不丢,可以通过重新attach到共享内存实现进程状态的恢复。通常用这种机制的是C++,优点是效率高,缺点是需要重新编译整个项目,发布操作较重。
②后台代码脚本化:利用脚本语言代码热加载的能力,实现在进程运行过程中的逻辑更新,优点是更新操作轻量,可以像更新配置一样实现逻辑更新,但是由于业务逻辑是脚本实现的,在执行效率上会有一定的损耗,为此常规的操作是底层框架和热点逻辑交给C++去实现,而经常变动的业务逻辑则用脚本去写。
C和Lua通过虚拟栈进行交互,如下图
栈的元素代表一个Lua值(table、string、nil、function等八种基本类型)
Lua和C交互的形式:Lua和C的交互分为数据交互和逻辑交互两类,再叠加两种访问方向组合出四种交互形式。下面逐一进行论述:
1. C访问Lua的数据
如图所示,Lua会为程序中所有的全局变量建立一个变量名到变量值的映射关系表,这个映射关系就存放在这样一个全局表中,当C想访问Lua中定义的全局变量的时候,调用接口lua_getglobal(),接口里面会先把这个变量名通过Lua的虚拟栈传递给Lua程序,Lua虚拟机拿到这个变量名之后去全局表里查询,查询到之后,加入到Lua虚拟栈中,C调用相关接口获取到这个值。
2. Lua访问C的数据
Lua访问C的数据本质上还是需要C程序通过接口先将数据压入到虚拟栈,并通过接口lua_setglobal(lua_State *L, const char *name)触发Lua虚拟机从虚拟栈中弹出一个值,并赋值给name这个变量(即在全局表里面建立一个KV结构)(value重复怎么办?)。
3. Lua访问C的接口
lua访问C的接口分为两种方式:
(1)lua_register
typedef int (*lua_CFunction) (lua_State *L); void lua_pushcfunction (lua_State *L, lua_CFunction f); void lua_register (lua_State *L,const char *name,lua_CFunction f); #define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
①所有能被Lua调用的C接口,都需要定义成lua_CFunction
②lua_pushcfunction用于接收一个C函数的指针作为参数,将一个Lua.function类型的对象压栈
③从lua_register定义看,本质上就是将函数压栈,然后在全局表注册
(2)luaL_newlib
#luaL_newlib用于将C函数以库的形式加载到Lua环境中 void luaL_newlib (lua_State *L,const luaL_Reg l[]); #用下列宏实现 (luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)) #luaL_newlibtable创建一张新的表,并预分配足够保存下数组l内容的空间,其中l得是一个数组 void luaL_newlibtable (lua_State *L, const luaL_Reg l[]); #luaL_setfuncs用于把数组l中的所有函数注册到栈顶的表中 #若nup≠0,所有的函数都共享nup个upvalues。这些值必须在注册之前,pushed到栈上。 #这些值在注册完毕后都会从栈弹出。 void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup);
luaL_newlib是将C语言函数以库的方式加载到Lua环境中
(3)用法示例
#用法示例 /*需要被调用的C语言函数*/ int C_Func_01(lua_State* L) { printf("C_Func_01 is run \r\n"); return 0; } /*定义函数表*/ static const luaL_Reg CFuncLib[] = { {"C_Func1",C_Func_01}, {NULL,NULL} }; /*创建一个新库*/ int luaopen_CFuncLib(lua_State* L) { luaL_newlib(L, CFuncLib); return 1; } int main(void) { lua_State* L = NULL; L = luaL_newstate(); #若CFuncLib不在package.loaded中,则调用接口luaopen_CFuncLib,将接口注册 #最后一个参数为1,表示CFuncLib注册到全局变量 luaL_requiref(L, "CFuncLib", luaopen_CFuncLib, 1); #加载并运行指定的文件 luaL_dofile(L, "test_01.lua"); lua_close(L); return 0; } --注意这里,必须以"库名.函数名()"的方式调用,还可以以"库名:函数名()"的方式调用这两种方式稍有不同,下面介绍 CFuncLib.C_Func1() --或 CFuncLib:C_Func1()
4. C访问Lua的接口
C调用Lua的接口是通过lua_pcall实现,以下面代码段为例:
lua_getglobal(L, "test_add"); int a = 10; int b = 20; lua_pushnumber(L, a); lua_pushnumber(L, b); if (lua_pcall(L, 2, 1, 0) != 0) { lua_pop(L, 1); exit(-1); } int sum = lua_tonumber(L, -1); lua_pop(L, 1);
在lua脚本中,我们定义了一个test_add的函数,这个test_add在全局表里面有一个KV映射,key为函数名,val为函数的地址,通过lua_getglobal,将函数的入口地址压到栈中,然后C通过接口lua_pushxxx将另外两个参数入栈,然后调用lua_pcall或者lua_call触发lua接口的执行,lua接口执行完毕之后参数和函数地址会自动出栈,并将返回值压栈,因此在函数调用结束之后,通过接口lua_toxxx(L,-1)从栈顶取出返回值,并转成我们期望的格式。
针对返回值的问题,需要多提一句,实际编码过程中,可能出现实际返回和预期返回值类型不一致的情况,此时应该在进行返回值转换之前,通过接口lua_checktype进行类型检查。
外一个就是lua_call和lua_pcall的差别,lua_pcall意味着保护模式,lua_call没有返回值,效率相对较高,但是主调接口无法获取函数调用状态,而lua_pcall有返回值并支持设置特定的错误处理函数。如下所示:
void lua_call (lua_State *L, int nargs, int nresults); int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);