Lua与C交互相关

C语言API总览。

C语言和Lua语言的两种交互形式:

  1. C语言拥有控制权,而Lua语言被用作库,这种交互形式中的C代码被称为应用代码。
  2. Lua语言拥有控制权,而C语言被用作库,此时的C代码被称为库代码。

应用代码和库代码都适用相同的API和Lua语言通信,这些API称为C API。

一个简单的独立解释器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

int main(void)
{
char buff[256];
int error;
lua_State *L = luaL_newstate(); /* 打开Lua */
luaL_openlibs(L); /* 打开标准库 */

while(fgets(buuff,sizeof(buff),stdin) != NULL)
{
error = luaL_loadstring(L,buff) || lua_pcall(L,0,0,0);
if(error)
{
fprintf(stderr,"%s\n",lua_tostring(L,-1));
lua_pop(L,1); /* 从栈中弹出错误信息 */
}
}

lua_close(L);
return 0;
}

头文件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
2
3
4
5
6
7
8
9
10
11
static int foo(lua_State *L)
{
//code to run in protected mode
return 0;
}

int secure_foo(lua_State *L)
{
lua_pushcfunction(L,foo);
return (lua_pcall(L,0,0,0) == 0)
}

无论发生什么,调用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
2
3
4
typedef void *(*lua_Alloc)(void *ud,						// 为lua_newstate所提供的用户数据
void *ptr, // 正要被分配或者释放的块的地址
size_t osize, // 原始块的大小
size_t nsize); // 请求的块大小

如果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函数一样行事了。