【Lua进阶系列】实例lua调用capi

                         【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文件的方式,准备步骤分三步:

  1. 新建一个c++控制台项目
  2. 下载lua源码,把src目录下的所有文件拷贝到新建的c++目录下
  3. 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);

流程分三步:

  1. 在c/c++中定义函数
  2. 注册在lua全局表中
  3. 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,客户只需要直接和翻译官打交道,但是一些翻译档案还是要翻译公司存放管理。

   全局状态机global_State

(2)lua线程lua_State

    lua_State是暴露给用户的数据类型,是一个lua程序的执行状态,也是lua的一个线程thread。大致分为4个主要模块,分别是独立的数据栈StkId,数据调用栈CallInfo ,独立的调试钩子以及错误处理机制。而在调用栈中我们就可以通过func域获得所在函数的源文件名,行号等诸多调试信息。

   执行状态机lua_Stack

(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文件中就能访问到该函数了。

 

今天的博客就到这里,谢谢大家~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lampard杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值