【Lua进阶系列】实例lua调用capi
大家好,我是Lampard~~
欢迎来到Lua进阶系列的博客
首先祝大家2021新年好~工作顺利节节高
前文再续,书接上一回。今天和大家实战一下lua调用c++的API
(一)前言
今天是要和大家分享关于luaDebug库的一些内容,但是我在研究luaDebug库的时候,发现它调用了许多的luaAPI,对于没有研究过lua与c/c++交互的我可以说是看到满头大汉,一脸懵逼。所以我就决定从最原始入手,研究lua和c/c++是如何相互调用。今天分享的流程主要是通过举两个c++和lua相互调用的栗子,然后研究底层的实现,紧接着对我们的lua_debug库进行介绍,最后再尝试打印一些堆栈信息。
大家都知道,lua和c/c++之间是通过一个lua_Stack进行交互的,关于lua_Stack,网上对它的叫法有很多,有的说它是一个lua的堆栈,有的说它是lua状态机,也有的将它叫做lua的线程(注意这里的thread是lua的一种数据类型,与操作系统的线程需要区分开),【lua数据类型】anyway关于它的介绍我会另外出一篇博客进行探讨,我们可以简单的把lua_Stack当作一个翻译官,负责在c/c++与lua之间翻译,把正确的信息保存并传达给对方。
(二)先看两个小栗子
要让lua文件与c/c++文件进行交互有两种方式,其一是把我们的CAPI给打包成一个动态链接库dll,然后在运行的时候再加载这些函数。其二是把CAPI给编译到exe文件中。为了方便,以下是测试例子使用的是编译成一个exe文件的方式,准备步骤分三步:
- 新建一个c++控制台项目
- 下载lua源码,把src目录下的所有文件拷贝到新建的c++目录下
- include需要用到的lua库函数,生成解决方案即可
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
需要注意的是,因为我们创建的是c++的程序(cocos,u3d,ue4的底层都是c++代码),但是lua的库函数中使用的是纯c的接口,所以我们要extern "C"让编译器帮我们修改一下函数的编译和连接规约。具体的分析可以看以下这篇博客:【extern以及extern的用法】
(1)创建lua_Stack
前文提及lua_Stack是c/c++与lua的翻译官,所以在它们交互之前我们首先需要生成一个lua_Stack:
lua_State *L = luaL_newstate();
然后我们需要打开lua给我们提供的标准库:
luaL_openlibs(L);
其实lua早已经在我们不经意间调用了c的api
(2)第一个栗子:c++调用lua的函数
我们首先需要新建一个lua文件,名称随意我这里使用的是luafile.lua。然后我们在lua文件中定义一个function,举一个最简单的减法吧。
然后就是使用luaL_dofile方法让我们的lua_Stack编译并执行这个文件,我们在打lua引用其他文件的时候知道loadfile是只编译,dofile是编译且每次执行,require是在package.loaded中查找此模块是否存在,不存在才执行,否则返回该模块。luaL_dofile和luaL_loadfile和上述原理相似,luaL_loadfile是仅编译,luaL_dofile是编译且执行。
然后通过lua_getglobal方法可以通过lua的全局表拿到lua的全局函数,并将它压入栈底(我们可以把lua_Stack的存储结构理解为下图的样子,实际上肯定没有那么简单,我们往下看)。
lua数据栈的抽象图:
我们可以通过两种索引来获取lua_Stack的调用栈所指向的数据
static TValue *index2addr (lua_State *L, int idx) {
CallInfo *ci = L->ci;
if (idx > 0) {
TValue *o = ci->func + idx;
api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index");
if (o >= L->top) return NONVALIDVALUE;
else return o;
}
else if (!ispseudo(idx)) { /* negative index */
api_check(L, idx != 0 && -idx <= L->top - (ci->func + 1), "invalid index");
return L->top + idx;
}
else if (idx == LUA_REGISTRYINDEX)
return &G(L)->l_registry;
else { /* upvalues */
idx = LUA_REGISTRYINDEX - idx;
api_check(L, idx <= MAXUPVAL + 1, "upvalue index too large");
if (ttislcf(ci->func)) /* light C function? */
return NONVALIDVALUE; /* it has no upvalues */
else {
CClosure *func = clCvalue(ci->func);
return (idx <= func->nupvalues) ? &func->upvalue[idx-1] : NONVALIDVALUE;
}
}
}
然后把两个参数按顺序压入栈中(不同类型压栈的函数接口大家可以查阅文档),然后调用pcall函数执行即可
/* c++调用lua函数 */
luaL_dofile(L, "luafile.lua");
lua_getglobal(L, "l_sub");
lua_pushnumber(L, 1);
lua_pushnumber(L, 2);
lua_pcall(L, 2, 1, 0);
cout << lua_tostring(L, 1) << endl;
为了更方便看出栈中的数据,我写了个函数,遍历输出栈中所有的数据。
static int stackDump(lua_State *L)
{
int i = 0;
int top = lua_gettop(L); // 获取栈中元素个数。
cout << "当前栈的数量:" << top << endl;
for (i = 1; i <= top; ++i) // 遍历栈中每个元素。
{
int t = lua_type(L, i); // 获取元素的类型。
switch (t)
{
case LUA_TSTRING: // strings
cout << "参数" << i << " :" << lua_tostring(L, i);
break;
case LUA_TBOOLEAN: // bool
cout << "参数" << i << " :" << lua_toboolean(L, i) ? "true" : "false";
break;
case LUA_TNUMBER: // number
cout << "参数" << i << " :" << lua_tonumber(L, i);
break;
default: // other values
cout << "参数" << i << " :" << lua_typename(L, t);
break;
}
cout << " ";
}
cout << endl;
return 1;
}
然后我们再看看输出的结果:
因为c++比起lua更接近底层语言,编译速度更快,所以一般来讲c++调用lua的接口只是配置一些全局数据,传递一些触摸,点击事件给lua而已。
(3)第二个栗子:lua调用c++的函数
来到今天关键的部分,就是lua调用c/c++的API。上一个栗子我们有提及,我们是通过全局表拿到lua的函数,那么我们要给lua传递一个函数,同样要通过这个全局表进行注册,然后才被lua进行调用。
void lua_register (lua_State *L, const char *name, lua_CFunction f);
流程分三步:
- 在c/c++中定义函数
- 注册在lua全局表中
- lua文件中调用
我们举一个简单加法的栗子:
static int c_add(lua_State *L)
{
stackDump(L);
double arg1 = luaL_checknumber(L, 1);
double arg2 = luaL_checknumber(L, 2);
lua_pushnumber(L, arg1 + arg2);
return 1;
}
...
int main() {
...
lua_register(L, "c_add", c_add);
}
注意这里的返回值并不是直接return答案,答案我们需要同样压入栈中,给lua_Stack这个翻译官"翻译",return的是答案的个数(lua支持多返回值)
(三)分析这两个栗子
举栗子总是开心的,看底层总是痛苦的。虽然现在已经是凌晨三点,但是我还是要说一句:问题不大。
我们回顾刚才的代码,一切的一切是从创建一个lua_Stack,也就是调用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;
}
可以看到luaL_newstate除了生成一个lua_Stack之外,还包装了一层错误预警,处理lua保护环境以外的报错,我们可以查阅以下文档lua_atpanic的作用。
我们继续往下看lua_newstate方法:
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
int i;
lua_State *L;
global_State *g;
/* 分配一块lua_State结构的内容块 */
LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
if (l == NULL) return NULL;
L = &l->l.l;
g = &l->g;
L->next = NULL;
L->tt = LUA_TTHREAD;
g->currentwhite = bitmask(WHITE0BIT);
L->marked = luaC_white(g);
/* 初始化一个线程的栈结构数据 */
preinit_thread(L, g);
g->frealloc = f;
g->ud = ud;
g->mainthread = L;
g->seed = makeseed(L);
g->gcrunning = 0; /* no GC while building state */
g->GCestimate = 0;
g->strt.size = g->strt.nuse = 0;
g->strt.hash = NULL;
setnilvalue(&g->l_registry);
g->panic = NULL;
g->version = NULL;
g->gcstate = GCSpause;
g->gckind = KGC_NORMAL;
g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL;
g->sweepgc = NULL;
g->gray = g->grayagain = NULL;
g->weak = g->ephemeron = g->allweak = NULL;
g->twups = NULL;
g->totalbytes = sizeof(LG);
g->GCdebt = 0;
g->gcfinnum = 0;
g->gcpause = LUAI_GCPAUSE;
g->gcstepmul = LUAI_GCMUL;
for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { //f_luaopen函数中调用了 stack_init 函数
/* memory allocation error: free partial state */
close_state(L);
L = NULL;
}
return L;
}
lua_newstate主要做了3件事情:
- 新建一个global_state和一个lua_State
- 初始化默认值,创建全局表等
- 调用f_luaopen函数,初始化栈、字符串结构、元方法、保留字、注册表等重要部件
(1)全局状态机global_state
global_State 里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数,有GC 需要的把所有对象串联起来的相关信息,以及一切 lua 在工作时需要的工作内存。我们以为的是c/c++ 和 lua之间只通过一个翻译官lua_Stack,但其实还有一个负责数据存放,回收的翻译公司global_State,客户只需要直接和翻译官打交道,但是一些翻译档案还是要翻译公司存放管理。
(2)lua线程lua_State
lua_State是暴露给用户的数据类型,是一个lua程序的执行状态,也是lua的一个线程thread。大致分为4个主要模块,分别是独立的数据栈StkId,数据调用栈CallInfo ,独立的调试钩子以及错误处理机制。而在调用栈中我们就可以通过func域获得所在函数的源文件名,行号等诸多调试信息。
(3)f_luaopen函数
f_luaopen函数,非常重要,主要作用:初始化栈、初始化字符串结构、初始化原方法、初始化保留字实现、初始化注册表等。
static void f_luaopen (lua_State *L, void *ud) {
global_State *g = G(L);
UNUSED(ud);
stack_init(L, L); /* init stack */
init_registry(L, g); //初始化注册表
luaS_init(L); //字符串结构初始化
luaT_init(L); //元方法初始化
luaX_init(L); //保留字实现
g->gcrunning = 1; /* allow gc */
g->version = lua_version(NULL);
luai_userstateopen(L);
可以先看看注册表是怎么样初始化的:会把当前的线程设置为注册表的第一个元素,全局表设置位第二个元素
static void init_registry (lua_State *L, global_State *g) {
TValue temp;
/*创建注册表,初始化注册表数组部分大小为LUA_RIDX_LAST*/
Table *registry = luaH_new(L);
sethvalue(L, &g->l_registry, registry);
luaH_resize(L, registry, LUA_RIDX_LAST, 0);
/*把这个注册表的数组部分的第一个元素赋值为主线程的状态机L*/
setthvalue(L, &temp, L); /* temp = L */
luaH_setint(L, registry, LUA_RIDX_MAINTHREAD, &temp);
/*把注册表的数组部分的第二个元素赋值为全局表,即registry[LUA_RIDX_GLOBALS] = table of globals */
sethvalue(L, &temp, luaH_new(L)); /* temp = new table (global table) */
luaH_setint(L, registry, LUA_RIDX_GLOBALS, &temp);
}
在得到一个初始化后的lua_Stack之后,要想lua能拿到CAPI,我们会对c/c++的函数进行注册。
lua_register(L, "c_add", c_add);
那么我们继续往下看看究竟这个函数做了什么。
#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
分成两部分:首先把c/c++的函数弄成一个闭包push到lua_Stack数据栈中,判断是否溢出并对栈顶元素自增
然后就是把这个函数给注册在注册表中
LUA_API void lua_setglobal (lua_State *L, const char *name) {
Table *reg = hvalue(&G(L)->l_registry);
lua_lock(L); /* unlock done in 'auxsetstr' */
// LUA_RIDX_GLOBALS是全局环境在注册表中的索引
auxsetstr(L, luaH_getint(reg, LUA_RIDX_GLOBALS), name);
}
我们知道lua把所有的全局表里存放在一个_G的表中,而LUA_RIDX_GLOBALS就是全局环境在注册表中的索引。至此我们就把我们的c/c++的API注册在lua的全局表中,所以lua文件中就能访问到该函数了。