Lua CAPI 使用

2017-09-23 Saturday    


Lua 是扩展性非常良好的语言,虽然核心非常精简,但是用户可以依靠 Lua 库来实现大部分工作,还可以通过与 C 函数相互调用来扩展程序功能。

在 C 中嵌入 Lua 脚本既可以让用户在不重新编译代码的情况下修改 Lua 代码更新程序,也可以给用户提供一个自由定制的接口,这种方法遵循了机制与策略分离的原则;在 Lua 中调用 C 函数可以提高程序的运行效率。

Lua 与 C 的相互调用在工程中相当实用,这里就简单讲解 Lua 与 C 相互调用的方法。

Lua 与 C 交互的栈是一个重要的概念,作为一种嵌入式语言,所有的 Lua 程序最后都需要通过 Lua 解释器将其解析成字节码的形式才能执行。

可在应用程序中嵌入 Lua 解释器,此时使用 Lua 的目的是方便扩展这个应用程序,用 Lua 实现相应的工作;另一方面,在 Lua 程序中也可以使用那些用 C 语言实现的函数,比如 string.find()

为了方便在两个语言之间进行交换数据,Lua 引入了一个虚拟栈,同时提供了一系列的 API ,通过这些 API C 语言就可以方便从 Lua 中获取相应的值,也可以方便地把值返回给 Lua,当然,这些操作都是通过栈作为桥梁来实现的。

API

Lua 提供了大量的 API 操作栈,用于向栈中压入元素、查询栈中的元素、修改栈的大小等操作。

1. 向栈中压入元素

通常都是以 lua_push 开头来命名,比如 lua_pushnunberlua_pushstringlua_pushcfunctionlua_pushcclousre 等函数都是向栈顶中压入一个 Lua 值。

2. 获取栈中的元素

从栈中获取一个值的函数,通常都是以 lua_to 开头来命名,比如 lua_tonumberlua_tostringlua_touserdatalua_tocfunction 等函数都是从栈中指定的索引处获取一个值,可在 C 函数中用这些接口获取从 Lua 中传递给 C 函数的参数。

源码解析

程序中为了加载执行 Lua 脚本,首先需要调用 luaL_newstate() 初始化 Lua 虚拟机,该函数会创建 Lua 与 C 交互的栈,返回指向 lua_State 类型的指针,后面几乎所有 API 都需要 lua_State* 作为入参,这样就使得每个 Lua 状态机是各自独立的,不共享任何数据。

这里的 lua_State 就保存了一个 Lua 解析器的执行状态,它代表一个新的线程 (同上,非操作系统中的线程),每个线程拥有独立的数据栈、函数调用链、调试钩子和错误处理方法。

实际上几乎所有的 API 操作,都是围绕这个 lua_State 结构来进行的,包括把数据压入栈、从栈中取出数据、执行栈中的函数等操作。

struct lua_State 在 lstate.h 头文件中定义,其代码如下:

struct lua_State {
	CommonHeader;
	lu_byte status;
	StkId top;                     /* 指向数据栈中,第一个可使用的空间*/
	global_State *l_G;
	CallInfo *ci;                  /* 保存着当前正在调用的函数的运行状态 */
	const Instruction *oldpc;
	StkId stack_last;              /* 指向数据栈中,最后一个可使用的空间 */
	StkId stack;                   /* 指向数据栈开始的位置 */
	int stacksize;                 /* 栈当前的大小,注意并不是可使用的大小*/
	unsigned short nny;
	unsigned short nCcalls;
	lu_byte hookmask;
	lu_byte allowhook;
	int basehookcount;
	int hookcount;
	lua_Hook hook;
	GCObject *openupval;
	GCObject *gclist;
	struct lua_longjmp *errorJmp;
	ptrdiff_t errfunc;
	CallInfo base_ci;              /* 保存调用链表的第一个节点*/
};

线程

线程作为 Lua 中一种基本的数据类型,代表独立的执行线程 (independent threads of execution),这也是实现协程 (coroutines) 的基础,注意这里的线程类型不要与操作系统线程混淆,Lua 中的线程类型是 Lua 虚拟机实现一种数据类型。

从 Lua 脚本来看,一个协程就是一个线程类型,准确来说,协程是一个线程外加一套良好的操作接口,比如:

local co = coroutine.create(function() print("hello world") end)
print(co)  --output: thread: 0038BEE0

从实现角度来看,一个线程类型数据就是一个 Lua 与 C 交互的栈,每个栈包含函数调用链和数据栈,还有独立的调试钩子和错误信息,线程类型数据与 table 数据类型类似,它也是需要 GC 来管理。

新建栈

为了加载执行 Lua 脚本,首先要调用 lua_newstate() 函数来初始化虚拟机,该函数在初始化虚拟机状态的同时,还会创建整个虚拟机的第一个线程,也就是主线程,或者说是第一个交互栈。

在已经初始化的全局状态中创建一个新的线程可以调用函数 lua_newthread(),声明如下:

lua_State *lua_newthread (lua_State *L);

创建一个线程就拥有一个独立的执行栈了,但是它与其线程共用虚拟机的全局状态;Lua 没有提供 API 关闭或者销毁一个线程,类似其它 GC 对象一样,由虚拟机管理。

也就是说,一个 Lua 虚拟机只有一个全局的状态,但可以包含多个执行环境 (或者说多个线程、交互栈,从脚本角度来说,也可以说是多个协程)。

源码解析

全局状态的结构体 global_State 的代码如下:

/*
** `global state', shared by all threads of this state
*/
typedef struct global_State {
	lua_Alloc frealloc;  /* function to reallocate memory */
	void *ud;         /* auxiliary data to `frealloc' */
	lu_mem totalbytes;  /* number of bytes currently allocated - GCdebt */
	l_mem GCdebt;  /* bytes allocated not yet compensated by the collector */
	lu_mem GCmemtrav;  /* memory traversed by the GC */
	lu_mem GCestimate;  /* an estimate of the non-garbage memory in use */
	stringtable strt;  /* hash table for strings */
	TValue l_registry;
	unsigned int seed;  /* randomized seed for hashes */
	lu_byte currentwhite;
	lu_byte gcstate;  /* state of garbage collector */
	lu_byte gckind;  /* kind of GC running */
	lu_byte gcrunning;  /* true if GC is running */
	int sweepstrgc;  /* position of sweep in `strt' */
	GCObject *allgc;  /* list of all collectable objects */
	GCObject *finobj;  /* list of collectable objects with finalizers */
	GCObject **sweepgc;  /* current position of sweep in list 'allgc' */
	GCObject **sweepfin;  /* current position of sweep in list 'finobj' */
	GCObject *gray;  /* list of gray objects */
	GCObject *grayagain;  /* list of objects to be traversed atomically */
	GCObject *weak;  /* list of tables with weak values */
	GCObject *ephemeron;  /* list of ephemeron tables (weak keys) */
	GCObject *allweak;  /* list of all-weak tables */
	GCObject *tobefnz;  /* list of userdata to be GC */
	UpVal uvhead;  /* head of double-linked list of all open upvalues */
	Mbuffer buff;  /* temporary buffer for string concatenation */
	int gcpause;  /* size of pause between successive GCs */
	int gcmajorinc;  /* how much to wait for a major GC (only in gen. mode) */
	int gcstepmul;  /* GC `granularity' */
	lua_CFunction panic;  /* to be called in unprotected errors */
	struct lua_State *mainthread;
	const lua_Number *version;  /* pointer to version number */
	TString *memerrmsg;  /* memory-error message */
	TString *tmname[TM_N];  /* array with tag-method names */
	struct Table *mt[LUA_NUMTAGS];  /* metatables for basic types */
} global_State;

一个 Lua 虚拟机只有一个全局的 global_State,在调用 lua_newstate() 时,会创建和初始化这个全局结构,这个全局结构管理着 Lua 中全局唯一的信息。

线程对应的数据结构 lua_State 的定义如下:

/*
** `per thread' state
*/
struct lua_State {
	CommonHeader;
	lu_byte status;
	StkId top;  /* first free slot in the stack */
	global_State *l_G;
	CallInfo *ci;  /* call info for current function */
	const Instruction *oldpc;  /* last pc traced */
	StkId stack_last;  /* last free slot in the stack */
	StkId stack;  /* stack base */
	int stacksize;
	unsigned short nny;  /* number of non-yieldable calls in stack */
	unsigned short nCcalls;  /* number of nested C calls */
	lu_byte hookmask;
	lu_byte allowhook;
	int basehookcount;
	int hookcount;
	lua_Hook hook;
	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) */
	CallInfo base_ci;  /* CallInfo for first level (C calling Lua) */
};

可以看到,struct lua_State 跟其它可回收的数据类型一样,结构体带用 CommonHeader 的头,也即是说它也是 GC 回收的对象之一。

C 库导入

导入全局性的库到 Lua 中,这些库由 C 实现:

/*
** these libs are loaded by lua.c and are readily available to any Lua
** program
*/
static const luaL_Reg loadedlibs[] = {
	{"_G", luaopen_base},
	{LUA_LOADLIBNAME, luaopen_package},
	{LUA_COLIBNAME, luaopen_coroutine},
	//....
	{NULL,NULL}
};
LUALIB_API void luaL_openlibs (lua_State *L) {
	const luaL_Reg *lib;
	/* "require" functions from 'loadedlibs' and set results to global table */
	for (lib = loadedlibs; lib->func; lib++) {
		luaL_requiref(L, lib->name, lib->func, 1);
		lua_pop(L, 1);  /* remove lib */
	}
}

每一个库封装了很多函数, 且每个库都由库名和 open 函数导入,以协程库为例:

{LUA_COLIBNAME, luaopen_coroutine},

通过看协程的库的创建过程可以知道如何将 C 函数写的库导入 Lua :

/* 下面是协程库的lua函数名和对应的C函数 */
static const luaL_Reg co_funcs[] = {
	{"create", luaB_cocreate},
	{"resume", luaB_coresume},
	{"running", luaB_corunning},
	{"status", luaB_costatus},
	{"wrap", luaB_cowrap},
	{"yield", luaB_yield},
	{"isyieldable", luaB_yieldable},
	{NULL, NULL}
};

/* 每个库必须有的open函数,newlib的实现就是一个table */
LUAMOD_API int luaopen_coroutine (lua_State *L) {
	luaL_newlib(L, co_funcs);
	return 1;
}

单个 C 函数组成的库的 open 函数里, 调用的是 luaL_newlib(L, co_funcs); 函数,其实现如下:

/*
 * 根据库函数数组luaL_Reg的大小创建的table, 这里的createtable()的实现就是在栈中创建
 * 一个哈希表, 表元素个数为sizeof(l)/sizeof((l)[0]) - 1
 */
#define luaL_newlibtable(L,l)  \
	lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)

/* 库的实现就是以l的大小创建了一个table */
#define luaL_newlib(L,l)  \
	(luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))

如下是上面调用的 luaL_setfuncs() 函数的实现代码, 由于当前的栈顶是刚才 newlibtable 出来的 table, 所以现在是将库函数名 set 到 table 中;

/*
** set functions from list 'l' into table at top - 'nup'; each
** function gets the 'nup' elements at the top as upvalues.
** Returns with only the table at the stack.
*/
LUALIB_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
	/* nup是闭包元素的个数,如果空间不够会自动扩展栈空间 */
	luaL_checkstack(L, nup, "too many upvalues");
	for (; l->name != NULL; l++) {  /* fill the table with given functions */
		int i;
		for (i = 0; i < nup; i++)  /* copy upvalues to the top */
			/* 压入所有的闭包, 当前栈顶(新的table)下的元素是nup个的闭包 */
			lua_pushvalue(L, -nup);
		lua_pushcclosure(L, l->func, nup);  /* closure with those upvalues */
		lua_setfield(L, -(nup + 2), l->name);
	}
	lua_pop(L, nup);  /* remove upvalues */
}

下面看下 luaL_checkstack 调用的 check 函数,ci 为当前栈中的调用的函数帧,可以看成函数的局部空间,ci->func 为底,ci->top 为空间顶,两者之间就是当前函数的局部空间:

/*  const int extra = LUA_MINSTACK;  5个额外的空间
 * 调用的是该: lua_checkstack(L, space + extra)) ..
 */
LUA_API int lua_checkstack (lua_State *L, int n) {
	int res;
	CallInfo *ci = L->ci;   /* 当前的函数调用帧, ci->func为函数调用点 */
	lua_lock(L);
	api_check(L, n >= 0, "negative 'n'");
	if (L->stack_last - L->top > n)  /* stack large enough? */
		res = 1;  /* yes; check is OK 空间足够 */
	else {  /* no; need to grow stack 空间不够,需要增加栈空间 */
		int inuse = cast_int(L->top - L->stack) + EXTRA_STACK;
		if (inuse > LUAI_MAXSTACK - n)  /* can grow without overflow? */
			res = 0;  /* no */
		else  /* try to grow stack */
			res = (luaD_rawrunprotected(L, &growstack, &n) == LUA_OK);
	}
	if (res && ci->top < L->top + n)
		ci->top = L->top + n;  /* adjust frame top 调用帧顶为栈顶+所需空间 */
	lua_unlock(L);

	return res;
}

前面的库导入过程中 luaL_requiref() 是真正的导入函数。

/*
** Stripped-down 'require': After checking "loaded" table, calls 'openf'
** to open a module, registers the result in 'package.loaded' table and,
** if 'glb' is true, also registers the result in the global table.
** Leaves resulting module on the top.
*/
LUALIB_API void luaL_requiref (lua_State *L, const char *modname, lua_CFunction openf, int glb) {
	/* 全局注册表找到_loaded表 */
	luaL_getsubtable(L, LUA_REGISTRYINDEX, "_LOADED");
	lua_getfield(L, -1, modname);  /* _LOADED[modname] */
	if (!lua_toboolean(L, -1)) {  /* package not already loaded? */
		lua_pop(L, 1);  /* remove field */
		lua_pushcfunction(L, openf);
		lua_pushstring(L, modname);  /* argument to open function */
		/* 调用库的open函数,在栈中创建了一个table */
		lua_call(L, 1, 1);  /* call 'openf' to open module */
		/* 复制一份以保存到_loaded里面取 */
		lua_pushvalue(L, -1);  /* make copy of module (call result) */
		lua_setfield(L, -3, modname);  /* _LOADED[modname] = module */
	}
	lua_remove(L, -2);  /* remove _LOADED table */
	if (glb) {
		/* 复制一份保存到_G里面去 */
		lua_pushvalue(L, -1);  /* copy of module */
		lua_setglobal(L, modname);  /* _G[modname] = module */
	}
}

API 简介

Lua 和 C 之间的数据交互通过堆栈进行,栈中的数据通过索引值进行定位,从栈底向上是从 1 开始递增的正整数,从栈顶向下是从 -1 开始递减的负整数,栈的元素按照 FIFO 的规则进出。也就是说,栈顶是 -1,栈底是 1,其中第 1 个入栈的在栈底。

lua_State* luaL_newstate();
    脚本编译执行相互独立,该函数申请一个虚拟机,后续的API都以此指针作为第一个参数。
void lua_close(lua_State *L);
    清理状态机中所有对象。

lua_State* lua_newthread(lua_State *L)

int lua_gettop(lua_State *L);
    取得栈的高度。
void lua_settop(lua_State *L, int idx);
    设置栈的高度,如果之前的栈顶比新设置的更高,那么高出来的元素会被丢弃,反之压入nil来补足大小。

void lua_pushvalue(lua_State *L, int idx);
    将指定索引上值的副本压入栈。
        for (int i = 1; i <= 3; ++i)
            lua_pushnumber(i);
        // bottom->top 1 2 3
        lua_pushvalue(L, 2)
        // bottom->top 1 2 3 2
void lua_remove(lua_State *L, int idx);
    删除指定索引上的元素,并将该位置之上的所有元素下移以补空缺。
        for (int i = 1; i <= 3; ++i)
            lua_pushnumber(i);
        // bottom->top 1 2 3
        lua_remove(L, 2)
        // bottom->top 1 3
void lua_insert(lua_State *L, int idx);
    移指定位置上的所有元素以开辟一个空间槽的空间,然后将栈顶元素移到该位置。
        for (int i = 1; i <= 5; ++i)
            lua_pushnumber(i);
        // bottom->top 1 2 3 4 5
        lua_insert(L, 3)
        // bottom->top 1 2 5 4 3
void lua_replace(lua_State *L, int idx);
    弹出栈顶的值,并将该值设置到指定索引上,但它不会移动任何东西。
        for (int i = 1; i <= 5; ++i)
            lua_pushnumber(i);
        // bottom->top 1 2 3 4 5
        lua_replace(L, 3)
        // bottom->top 1 2 5 4


如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^