Lua与C交互相关
C语言API总览。
C语言和Lua语言的两种交互形式:
- C语言拥有控制权,而Lua语言被用作库,这种交互形式中的C代码被称为应用代码。
- Lua语言拥有控制权,而C语言被用作库,此时的C代码被称为库代码。
应用代码和库代码都适用相同的API和Lua语言通信,这些API称为C API。
一个简单的独立解释器:
1 |
|
头文件lua.h声明了Lua提供的基础函数,其中包括创建新Lua环境的函数、调用Lua函数的函数、读写环境中的全局变量的函数,以及注册供Lua语言调用的新函数的函数,等等。其中声明的所有内容都有一个前缀lua_。
头文件lauxlib.h声明了辅助库所提供的函数,其中所有的声明均以luaL_开头。
头文件lualib.h中声明了用于打开这些库的函数。
栈:
Lua和C之间通信的主要组件是虚拟栈,几乎所有的API的调用都是在操作这个栈中的值,Lua和C之间所有的数据交换都是通过这个栈完成的,此外,还可以利用栈保存中间结果。
为了解决动态类型和静态类型体系之间不匹配,以及自动内存管理和手动内存管理之间不匹配:Lua API中没有定义任何类似于lua_Value的类型,而是使用栈在Lua和C之间交换数据。栈中的每个元素都能保存Lua中任意类型的值。当我们想要从Lua中获取一个值时,只需调用Lua,Lua就会将指定的值压入栈中。当想要将一个值传给Lua时,首先要将这个值压入栈,然后调用Lua将其从栈中弹出即可。这需要每个C语言类型都有一个函数将其压入栈,还需要每个类型都有一个弹出的函数,但是避免了过多的组合,另外由于这个栈是Lua状态的一部分,因此垃圾收集器知道C语言正在使用哪些值。
压入元素:
针对每一种能用C语言直接表示的Lua数据类型,C API中都有一个对应的压栈函数。
对栈空间的检查可以使用int lua_checkstack(lua_State *L,int sz);这里,sz是我们所需的额外栈位置的数量,如果可能,函数 lua_checkstack 会增加栈的大小,以容纳所需的额外空间;否则该函数返回0。
查询元素:
第一个被压入栈的元素索引为1,第二个被压入的元素索引为2,依此类推。也可以使用负数索引来访问栈中的元素,栈顶的元素为-1,-2表示在它之前被压入栈中的元素。
与Lua栈相关的函数游一系列,在此不做赘述。
使用C API进行错误处理:
处理应用程序中的错误:
Lua语言通常通过长跳转来提示错误,但是如果没有相应的setjmp,解释器就无法进行长跳转。此时,API中的任何错误都会导致Lua调用紧急函数。当这个函数返回后,应用就会退出。
要正确的处理应用代码中的错误,就必须通过Lua语言调用我们自己的代码,即在setjmp的上下文中运行代码。
我们可以把C代码封装到一个函数F中,然后使用lua_pcall调用这个函数F,通过这种方式,我们的C代码会在保护模式下运行。即便发生内存分配失败,函数lua_pcall也会返回一个对应的错误码,使解释器能够保持一致的状态:
1 | static int foo(lua_State *L) |
无论发生什么,调用secure_foo时都会返回一个布尔值,来表示foo执行是否成功。
处理库代码中的错误:
当C语言库中的函数检测到错误时,只需简单的调用lua_error即可。
内存分配:
Lua语言核心对内存分配不进行任何假设,只会通过一个分配函数来分配和释放内存,当用户创建lua状态时必须提供该函数。
luaL_newstate是一个用默认分配函数来创建Lua状态的辅助函数。该默认分配函数使用了来自C语言标准函数库的标准函数malloc-realloc-free,对于大多数应用程序来说已经够了,但是,要完全控制Lua的内存分配也很容易,使用原始的lua_newstate来创建我们自己的Lua状态即可:
1 | lua_State *lua_newstate(lua_Alloc f,void *ud) |
该函数有两个参数:一个是分配函数,另一个是用户数据。用这种方式创建的Lua状态会通过调用f完成所有的内存分配和释放,甚至结构lua_State也是由f分配的。
分配函数f必须满足lua_Alloc的类型声明:
1 | typedef void *(*lua_Alloc)(void *ud, // 为lua_newstate所提供的用户数据 |
如果ptr不是NULL,则lua会保证其之前被分配的大小就是osize。
当nsize为0时,分配函数必须释放ptr指向的块并返回NULL,对应于所要求的大小为零的块。当ptr时NULL时,该函数必须分配并返回一个指定大小的块;如果无法分配指定的块,则必须返回NULL。
pcall:
在调用函数lua_pcall时,第二个参数表示传递的参数数量,第三个参数是期望的结果数量,第四个参数代表错误处理函数。就像Lua语言的赋值一样,函数lua_pcall会根据所要求的数量来调整返回值的个数,即压入nil或丢弃多余的结果。在压入结果前,lua_pcall会把函数和其参数从栈中移除。当一个函数返回多个结果时,那么第一个结果最后被压入。
在Lua中调用C语言:
当Lua调用C函数时,我们必须注册该函数,即必须以一种恰当的方式为Lua提供该C函数的地址。Lua调用C函数时,也使用了一个与C语言调用Lua函数时相同类型的栈,C函数从栈中获取参数,并将结果压入栈中。
这个栈不是一个全局结构;每个函数都有其私有的局部栈。当Lua调用一个C函数时,第一个参数总是位于这个局部栈中索引为1的位置。即使一个C函数调用了Lua代码,而且Lua代码又再次调用了同一个的C函数,这些调用每一个都只会看到本次调用自己的私有栈,其中索引为1的位置上就是第一个参数。
所有在Lua中注册的函数都必须使用一个相同的原型,该原型就是定义在lua.h中的lua_CFunction:
1 | typedef int (*lua_CFunction)(lua_State *L) |
在Lua中,调用这个函数前,还必须通过lua_pushcfunction注册该函数。函数lua_pushcfunction会获取一个指向C函数的指针,然后在Lua中创建一个“function”类型,代表待注册的函数。一旦完成注册,C函数就可以像其他Lua函数一样行事了。