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,当然,这些操作都是通过栈作为桥梁来实现的。
Lua 提供了大量的 API 操作栈,用于向栈中压入元素、查询栈中的元素、修改栈的大小等操作。
通常都是以 lua_push
开头来命名,比如 lua_pushnunber
、lua_pushstring
、lua_pushcfunction
、lua_pushcclousre
等函数都是向栈顶中压入一个 Lua 值。
从栈中获取一个值的函数,通常都是以 lua_to
开头来命名,比如 lua_tonumber
、lua_tostring
、lua_touserdata
、lua_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 回收的对象之一。
导入全局性的库到 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 */
}
}
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
如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^