lua中的C

12k 词

了解lua的内部实现结构有助于更清楚的各个函数是怎么要操作内容及内容的,就从最开始的lua解释器开始进行查看。

Lua提供了一系列API来让宿主程序和Lua进行通信。所有的API函数和相关的类型和常量都在lua.h内声明。

虽然我们使用了函数这个术语,但是API中的某些特性可能是以宏的形式提供的。

和大多数C库一样,Lua API函数不会检查他们参数的有效性和完整性。这可以通过在编译Lua的时候加上LUA_USE_APICHECK来定义。

Lua库是完全可重入的:其没有全局变量。它把所有的信息保存在一个动态数据结构中,我们称之为Lua state

每个Lua state有一个或多个线程,每个对应每行的执行。lua_State类型(不要管名字)指向了这个线程。(可以认为这个线程也引用了与此线程相关的Lua state)。

一个指向线程的指针必须作为传递给函数的第一个参数,lua_newstate()是个例外,这个函数创建一个lua state并返回指针到主线程。

Lua使用一个virtual stack(虚拟栈)来与C进行值传递。栈中的每个元素代表了一个Lua值(nil, numbers, string等等)。API中的函数可以通过其接受的第一个Lua state参数来访问栈。

在Lua调用C的时候,被呼叫的函数获得一个新的栈,这个栈独立于上面提到的那个栈,和依然活跃的C函数的栈。这个栈初始为调用C函数的参数,C函数可以在这里存储临时的Lua值,而且必须把其结果压入这个栈来返回给调用者(lua_CFunction)。

为了方便,大多API中的查询操作不遵从一个严格的栈限制。他们可以通过index(索引)来引用栈中的任何元素:一个正索引表示一个绝对的栈位置(从1开始);一个负值索引表示从栈顶开始的相对偏移值。更特别一些,如果栈有n个元素,1代表了第一个元素(就这就说,这个元素被第一个压入栈),n代表最后一个元素;-1也代表了最后一个元素(在栈顶的元素),in-n代表了第一个元素。

栈大小

当还Lua API交互的时候,你有责任保证完整性。实际上,你需要控制栈的溢出。可以用lua_checkstack()函数来保证栈有足够的空间用来压入新的元素。

当Lua调用C的时候,要保证栈拥有最少LUA_MINSTACK(20)的额外空间。默认值是20,意味着通常情况下不需要担心栈空间,但代码中有循环往栈压入元素的情况例外。

当调用一个Lua函数而没有指定固顶返回结果个数时(lua_call),Lua保证会有足够的空间用来返回值,但不确保其他任何空间。因此,在这种调用后,在压入任何东西入栈前,必须先调用lua_checkstack()

有效和可接受的索引

API中的所有函数只能接受有效索引可接受的索引

一个valid index(有效索引)说的是一个指向存储了一个可修改Lua值的位置。其由1到栈顶(1 <= abs(index) <= top)加上一些伪索引(代表某些C代码可以访问的位置,但不在Lua栈内)。伪索引用来访问registry $4.5节和C函数的上值($ 4.4节)。

函数不需要指定一个可变的位置,需要的只是一个值(比如,查询函数),可以把这个值叫做可接受的索引。一个acceptable index(可接受索引)可以被叫做有效索引,但是其也可以是栈顶后的索引任何正值索引,但这必须保证这个索引指向的位置在为这个栈分配的内存空间中。除非特别指明,API中函数与acceptable indices工作。

在查询栈的时候,可接受的索引可用来避免额外的对栈顶的测试。具体而言,C函数可以查询其第三个参数而不用首先检查是不是已经有第三个参数,也不需要检查3是不是一个有效索引。

那些与acceptable indices相工作的函数而言,任何非有效的索引被当做LUA_TNONE类型,其表现得像一个nil值。

C闭包

当一个C函数建立,就可能把它与一些值相关联,这就创建了一个C 闭包(查看lua_pushcclosure);这些值被称做upvalues(上值),在函数被调用的时候可以被访问。

在调用一个C函数的时候,其upvalues被安排在指定的伪索引内。这些伪索引用宏lua_upvalueindex产生。与函数相关联的第一个upvalue位于索引lua_upvalueindex(1)。任何lua_upvalueindex(n)n大于当前函数的upvalues值个数,但小于256,256这个值是一个闭包拥有的upvalues值的最大值加1)会产生一个可接受但是无效的索引。

注册

Lua提供一个registry,一个予定义的表,C代码可以用来存储任何类型的Lua。注册表总是被安排在伪索引LUA_REGISTRYINDEX。所有的C库都可以在这个表内存储数据,但必须保证所使用的键不与已使用的键冲突。典型的,使用包含库名的字符串来作键。对于变量名字,以一个下划线和大写字母开始的字符键是Lua保留的。

当创建一个新的Lua state,其registry有一些预定义的值。
registry中的整数键被 索引机制(luaL_ref)和一些予定义的值使用。因此,整数键不能被用做其他目的。这些预定义在lua.h中的常量,通过整数键来进行索引。下面的常量被定义:

  • LUA_RIDX_MAINTHREAD:在这个索引中,registry拥有这个state的主线程。(主线程是和State一起创建的那个)。
  • LUA_RIDX_GLOBALS:这个索引拥有了全局环境。

错误处理

内部的,Lua使用Clongjump特性来处理错误。(当编译为C++的时候使用的不一样;在源代码内搜索LUAI_THROW来查看细节)当Lua遇到错误时(比如内存分配错误或类型错误)其会raises错误,这就是说,Lua会进行一个 long jump。一个protected environment(受保护的环境)使用setjump来设置一个恢复点;一个错误会跳转到最近活跃的恢复点。

在C函数内可以使用lua_errorraise一个错误。

大多数API函数可以raise一个错误,比如内存分配错误。每个函数的文档表明了其是否可以raise一个错误。

如果错误在受保护的环境外发生,Lua调用一个panic函数(lua_panic)后退出,也就是会退出宿主程序。panic函数可以避免不返回的退出(例如,long jump到一个Lua外的恢复点)

panic函数,和其名字一样,是最常出现的问题。程序应该避免使用它。作为一个通用规则,当Lua通过Lua state调用一个C函数时,其可以在Lua state上做任何事情,就跟它已经受保护了一样。然而,当C代码在其他Lua state上操作的时候(例如,Lua参数给函数,registry中的Lua state, lua_newthread()的结果),这是仅有的不能raise错误的情况。

panic函数运行起来就像一个消息处理器;实际上,错误对象位于栈顶。然而,这不会对栈空间有任何保证。为了压入些东西到栈内,panic函数必须首先检查可用空间。

处理C中的放弃

内部,Lua使用C的longjump来放弃一个协程。因此,如果一个C函数foo()调用一个API函数,而这个API函数放弃了(直接或非直接通过调用其他函数放弃),Lua就不能再返回到foo(),因为longjump移除了这个函数在C栈上的帧。

为了避免这个类型的问题。Lua会在API调用中试图放弃操作时产生一个错误,有三个函数是例外(lua_yieldk, lua_callk, lua_pcallk。所有这三个函数都接受一个continuation function(接续函数,参数名k)来在放弃操作后继续执行。

我们需要进行更多的解释一下continuations。我们会在Lua中调用一个C函数,我们把他称为original function(原始函数)。这个原始函数调用这三个函数中的一个,我们称之为callee(被调)函数,然后放弃当前进程。(这会在被调函数是lua_yieldk, lua_callk, lua_pcallk和函数被其自身放弃操作时发生)

假设线程在执行被调函数时放弃操作。在进程恢复时,其会继续运行被调函数。然而,这个被调函数不能再返回原始函数了,因为在C栈上的帧已经被放弃操作销毁了。作为替代,Lua调用continuation function,作为被调函数参数传递过去的。就跟名字一样,接续函数继续原始函数的工作。

作为一个模拟,考虑下面的函数:

int  (lua_State *L) {
...
status = lua_pcall(L, n, m, h); /* calls Lua */
... /* code 2 */
}

现在我们打算让Lua代码运行lua_pcall来放弃操作。首先,我们可以重写我们的函数:

int k (lua_State *L, int status, lua_KContext ctx) {
... /* code 2 */
}

int (lua_State *L) {
...
return k(L, lua_pcall(L, n, m, h), ctx);
}

在上面的代码中,函数k是一个continuation function(类型lua_KFunction),它会继续做原始函数在调用lua_pcall后的所有工作。现在,我们必须告诉Lua,在代码执行lua_pcall遇到某些形式中断(错误或放弃操作)的时候必须调用k,所以我们继续重写代码,把lua_pcall替换为lua_pcallk

int  (lua_State *L) {
...
return k(L, lua_pcallk(L, n, m, h, ctx2, k), ctx1);
}

注意外部,现式的调用了这个接续函数:Lua只会在需要的时候调用接续函数,也就是发生错误或者让出了CPU(yield)。如果被调用函数正常返回,lua_pcallk(和lua_call)也会正常返回。(当然,不用调用接续函数,你也可以在原始函数中继续工作)。

和Lua state概念相应,接续函数有两个其他参数:调用的最终状态,传递给lua_pcallk的上下文(ctx)。(Lua不会使用这个上下文,它只会把这个值从原始函数传递到接续函数)。lua_callk()而言,状态和lua_pcallk的返回值一样,在一个yield后返回LUA_YIELD是个例外。对于lua_yieldk, lua_callk,Lua调用接续函数时状态参数总是LUA_YIELD(对这两个函数,在出现错误的时候Lua不会调用接续函数,因为他们不进行错误处理)。类似地,当使用lua_callk时,你应该使用LUA_OK作为状态来调用接续函数。(对于lua_yield,没有直接调用接续函数的方法,因为它通常情况下不返回)。

lua.c 中的main()函数

int main (int argc, char **argv) {
int status, result;
lua_State *L = luaL_newstate(); /* create state */
if (L == NULL) {
l_message(argv[0], "cannot create state: not enough memory");
return EXIT_FAILURE;
}
lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */
lua_pushinteger(L, argc); /* 1st argument */
lua_pushlightuserdata(L, argv); /* 2nd argument */
status = lua_pcall(L, 2, 1, 0); /* do the call */
result = lua_toboolean(L, -1); /* get result */
report(L, status);
lua_close(L);
return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}

可以看到,main函数所做的事情就是这几样:

  1. 调用luaL_newstate()建立一个新的state(我不知道怎么去翻译了)。
  2. 把函数 pmain()压入栈
  3. 把函数参数个数argc压入栈
  4. 把函数参数数组argv压入栈
  5. 执行函数pmain()
  6. 获取结果
  7. 报告状态
  8. 关闭state。
    我们更详细的来看这个过程。

luaL_newstate()

在文件lauxlib.c中我们可以看到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;
}

其是利用 lua_newstate()这个函数的封装。
而我们 可以看到,lua_newstate()需要一个l_alloc参数,这是一个函数指针。

lua_newstate()

lua_newstate()函数定义在lstate.c中:

LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
int i;
lua_State *L;
global_State *g;
LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
if (l == NULL) return NULL;
L = &l->l.l;
g = &l->g;
L->tt = LUA_TTHREAD;
g->currentwhite = bitmask(WHITE0BIT);
L->marked = luaC_white(g);
preinit_thread(L, g);
g->allgc = obj2gco(L); /* by now, only object is the main thread */
L->next = NULL;
g->frealloc = f;
g->ud = ud;
g->mainthread = L;
g->seed = makeseed(L);
g->gcrunning = 0; /* no GC while building state */
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_INC;
g->finobj = g->tobefnz = g->fixedgc = NULL;
g->survival = g->old = g->reallyold = NULL;
g->finobjsur = g->finobjold = g->finobjrold = NULL;
g->sweepgc = NULL;
g->gray = g->grayagain = NULL;
g->weak = g->ephemeron = g->allweak = g->protogray = NULL;
g->twups = NULL;
g->totalbytes = sizeof(LG);
g->GCdebt = 0;
setgcparam(g->gcpause, LUAI_GCPAUSE);
setgcparam(g->gcstepmul, LUAI_GCMUL);
g->gcstepsize = LUAI_GCSTEPSIZE;
setgcparam(g->genmajormul, LUAI_GENMAJORMUL);
g->genminormul = LUAI_GENMINORMUL;
for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
/* memory allocation error: free partial state */
close_state(L);
L = NULL;
}
return L;
}

这个文件中还定义了结构LGLX

LX是一个线程状态和额外空间的组合。

LG是一个线程状态和全局状态的组合,就是一个 LX成员加一个 全局状态 global_State。

typedef struct LG {
LX l;
global_State g;
} LG;

LX是一个扩展的本地数据的结构:

typedef struct LX {
lu_byte extra_[LUA_EXTRASPACE];
lua_State l;
} LX;

lua_State与global_State

这两个结构在lstate.h中分别定义如下:

typedef struct global_State {
lua_Alloc frealloc; /* function to reallocate memory */
void *ud; /* auxiliary data to 'frealloc' */
l_mem totalbytes; /* number of bytes currently allocated - GCdebt */
l_mem GCdebt; /* bytes allocated not yet compensated by the collector */
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 genminormul; /* control for minor generational collections */
lu_byte genmajormul; /* control for major generational collections */
lu_byte gcrunning; /* true if GC is running */
lu_byte gcemergency; /* true if this is an emergency collection */
lu_byte gcpause; /* size of pause between successive GCs */
lu_byte gcstepmul; /* GC "speed" */
lu_byte gcstepsize; /* (log2 of) GC granularity */
GCObject *allgc; /* list of all collectable objects */
GCObject **sweepgc; /* current position of sweep in list */
GCObject *finobj; /* list of collectable objects with finalizers */
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 *protogray; /* list of prototypes with "new" caches */
GCObject *tobefnz; /* list of userdata to be GC */
GCObject *fixedgc; /* list of objects not to be collected */
/* fields for generational collector */
GCObject *survival; /* start of objects that survived one GC cycle */
GCObject *old; /* start of old objects */
GCObject *reallyold; /* old objects with more than one cycle */
GCObject *finobjsur; /* list of survival objects with finalizers */
GCObject *finobjold; /* list of old objects with finalizers */
GCObject *finobjrold; /* list of really old objects with finalizers */
struct lua_State *twups; /* list of threads with open upvalues */
lua_CFunction panic; /* to be called in unprotected errors */
struct lua_State *mainthread;
const lua_Number *version; /* pointer to version number */
TString *nfield; /* string "n" (key in vararg tables) */
TString *tmname[TM_N]; /* array with tag-method names */
struct Table *mt[LUA_NUMTAGS]; /* metatables for basic types */
TString *strcache[STRCACHE_N][STRCACHE_M]; /* cache for strings in API */
} global_State;

struct lua_State {
CommonHeader;
unsigned short nci; /* number of items in 'ci' list */
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 */
UpVal *openupval; /* list of open upvalues in this stack */
GCObject *gclist;
struct lua_State *twups; /* list of threads with open upvalues */
struct lua_longjmp *errorJmp; /* current error recover point */
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
volatile lua_Hook hook;
ptrdiff_t errfunc; /* current error handling function (stack index) */
int stacksize;
int basehookcount;
int hookcount;
unsigned short nny; /* number of non-yieldable calls in stack */
unsigned short nCcalls; /* number of nested C calls */
l_signalT hookmask;
lu_byte allowhook;
};

可以看到,lua_newstate()通过内存分配函数l_alloc的参数f先分配一个sizeof(LG)大小的结构,并强制转换为LG *,接着本地数据指针、全局数据指针分别指向这个分配结构中的LG->l.lLG->g成员。然后就做一些初始化工作。

f_luaopen()————state分配与初始化

最后,通过f_luaopen()来进行初始化,这个函数定义在lstate.c中:

/*
** open parts of the state that may cause memory-allocation errors.
** ('g->version' != NULL flags that the state was completely build)
*/
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->gcemergency = 0;
g->version = lua_version(NULL);
luai_userstateopen(L);
}

其首先通过宏#define G(L) (L->l_G)获取全局的state,然后进行初始化:

statck_init(L, L)

此函数定义在lstate.c中:
这函数,会分配内存,然后初始化为nil,并设置栈顶,栈底等信息。内存,是分配在堆中的

static void stack_init (lua_State *L1, lua_State *L) {
int i; CallInfo *ci;
/* initialize stack array */
L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, StackValue);
L1->stacksize = BASIC_STACK_SIZE;
for (i = 0; i < BASIC_STACK_SIZE; i++)
setnilvalue(s2v(L1->stack + i)); /* erase new stack */
L1->top = L1->stack;
L1->stack_last = L1->stack + L1->stacksize - EXTRA_STACK;
/* initialize first ci */
ci = &L1->base_ci;
ci->next = ci->previous = NULL;
ci->callstatus = CIST_C;
ci->func = L1->top;
setnilvalue(s2v(L1->top)); /* 'function' entry for this 'ci' */
L1->top++;
ci->top = L1->top + LUA_MINSTACK;
L1->ci = ci;
}

  1. 其通过luaM_newvector()宏来分配内存:
    其定义是:
lmem.h:
L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, StackValue);

luaM_newvector()是通过执行内存分配函数g->frealloc(g->ud, NULL, tag, size)来执行内存分配的。
在执行

lua_newstate(l_alloc, NULL);

的时候,内存分配函数被指定为l_alloc(),这函数定义在lauxlib.c中:

static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
(void)ud; (void)osize; /* not used */
if (nsize == 0) {
free(ptr);
return NULL;
}
else
return realloc(ptr, nsize);
}

这个函数只是利用realloc()来重新分配一块内存,或者在nsize为0的时候,释放内存。

init_registry(L, g);

luaS_init(L);

初始化 字符串 hash 表G(L)->strt,代码文件lstring.c:

void luaS_init (lua_State *L) {
global_State *g = G(L);
int i, j;
TString *memerrmsg;
stringtable *tb = &G(L)