lua中的C
了解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_error
来raise
一个错误。
大多数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) { |
现在我们打算让Lua代码运行lua_pcall
来放弃操作。首先,我们可以重写我们的函数:
int k (lua_State *L, int status, lua_KContext ctx) { |
在上面的代码中,函数k
是一个continuation function(类型lua_KFunction
),它会继续做原始函数在调用lua_pcall
后的所有工作。现在,我们必须告诉Lua,在代码执行lua_pcall
遇到某些形式中断(错误或放弃操作)的时候必须调用k
,所以我们继续重写代码,把lua_pcall
替换为lua_pcallk
:
int (lua_State *L) { |
注意外部,现式的调用了这个接续函数: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) { |
可以看到,main函数所做的事情就是这几样:
- 调用
luaL_newstate()
建立一个新的state(我不知道怎么去翻译了)。 - 把函数
pmain()
压入栈 - 把函数参数个数
argc
压入栈 - 把函数参数数组
argv
压入栈 - 执行函数
pmain()
- 获取结果
- 报告状态
- 关闭state。
我们更详细的来看这个过程。
luaL_newstate()
在文件lauxlib.c
中我们可以看到luaL_newstate()
的定义 :
LUALIB_API lua_State *luaL_newstate (void) { |
其是利用 lua_newstate()
这个函数的封装。
而我们 可以看到,lua_newstate()
需要一个l_alloc
参数,这是一个函数指针。
lua_newstate()
lua_newstate()
函数定义在lstate.c
中:
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { |
这个文件中还定义了结构LG
,LX
。
LX
是一个线程状态和额外空间的组合。
LG
是一个线程状态和全局状态的组合,就是一个 LX
成员加一个 全局状态 global_State。
typedef struct LG { |
LX
是一个扩展的本地数据的结构:
typedef struct LX { |
lua_State与global_State
这两个结构在lstate.h
中分别定义如下:
typedef struct global_State { |
可以看到,lua_newstate()
通过内存分配函数l_alloc
的参数f
先分配一个sizeof(LG)
大小的结构,并强制转换为LG *
,接着本地数据指针、全局数据指针分别指向这个分配结构中的LG->l.l
和LG->g
成员。然后就做一些初始化工作。
f_luaopen()————state分配与初始化
最后,通过f_luaopen()
来进行初始化,这个函数定义在lstate.c
中:
/* |
其首先通过宏#define G(L) (L->l_G)
获取全局的state,然后进行初始化:
statck_init(L, L)
此函数定义在lstate.c
中:
这函数,会分配内存,然后初始化为nil,并设置栈顶,栈底等信息。内存,是分配在堆中的
static void stack_init (lua_State *L1, lua_State *L) { |
- 其通过
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) { |
这个函数只是利用realloc()
来重新分配一块内存,或者在nsize
为0的时候,释放内存。
init_registry(L, g);
luaS_init(L);
初始化 字符串 hash 表G(L)->strt
,代码文件lstring.c
:
void luaS_init (lua_State *L) { |