Part IV: The C API
Chapter 24: An Overview of the C API
无处不在的virtual stack 是lua 和C 之间通信的主要主件。lua 到C,C到lua 所有的数据交换都通过这个栈。
24.1 A First Example
头文件lua.h 定义了由lua 提供的基本函数。有创建lua 环境的函数,有调用lua 函数的函数(如lua_pcall),有在lua 环境中读和写全局变量函数,有注册新函数以拱lua 调用的函数,等等。lua.h 所定义的任何东西都有“lua_”前缀。
头文件lauxlib.h 的所有定义以luaL_开始(如,luaL_loadbuffer)。辅助库使用由lua.h 提供的API去实现更高的抽象层。所有lua 标准库都使用auxlib。
Lua 解释器
#include
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
int main (void) {
char buff[256];
int error;
lua_State *L = luaL_newstate(); /* opens Lua */
luaL_openlibs(L); /* opens the standard libraries */
while (fgets(buff, sizeof(buff), stdin) != NULL) {
error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop(L, 1); /* pop error message from the stack */
}
}
lua_close(L);
return 0;
}
lua 库完全没有定义全局变量。它将所有状态保存在动态结构lua_State,并且将这个结构的一个指针作为参数传给所有lua 中的函数。这种实现使lua 成为reentrant,并且为在多线程环境中使用作好了准备。
luaL_newstate 函数创建一个新environment(或state)。当luaL_newstate 建立一个新环境,这个环境不包含预定义,甚至没有print 函数。为保持lua 的小巧,所有标准库以独立的packages 的形式提供,所以如果你不需要它们就不要把它们加入程序。头文件lualib.h 定义打开库的函数。luaL_openlibs 函数打开所有标准库。
在建立了环境和引入了需要的标准库后,是时侯去解析用户的输入了。对用户输入的每一行,程序首先调用luaL_loadbuffer 去编译代码。如果代码中没有错误,调用返回0,并且将resulting chunk 压栈(这个“magic”栈我们将在下一节详细讨论)。然后,程序调用lua_pcall 函数,它将chunk 从栈上弹出,并将它以保户模式返回。和luaL_loadbuffer 一样,如果没有错误lua_pcall 返回0。如果有错误,两个函数都将错误消息压栈。我们使用lua_tostring 获得这个消息,将它打印后,我们使用lua_pop 将它从栈上移除。
注意,这种程序只是简单的将错误消息打印到标准错误流。C 代码中的真实的错误处理可以非常复杂。为方便起见,我们假定使用下面的错处理器,它打印错误消息,关闭lua 状态,并且退出整个应用程序:
#include
#include
#include
void error (lua_State *L, const char *fmt, ...) {
va_list argp;
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
lua_close(L);
exit(EXIT_FAILURE);
}
24.2 The Stack
在lua 和C 之间交换数据值会面临两个问题:动态和静态类型系统不匹配,手动和自动内存管理不匹配。
lua 代码a[k] = v,其中k 和v 都可以有多种不同的类型; 如果想要在C 中提供这种操作,任何给定的settable 函数必须有固定类型。对这单个操作,我们将需要一堆不同函数。
我们可以解决这个问题,通过声明一些C的联合类型,称为lua_Value,用它来表示所有lua 的值。然后,我们可以将settable 声明为:
void lua_settable (lua_Value a, lua_Value k, lua_Value v);
这个方案有两个缺点。第一,难以适应那些具有复杂类型的语言;Lua 被设计成不只和C/C++ 易于互操作,同时也可以和Java,Fortran,C#,等语言互操作。第二,Luas 进行垃圾收集:如果我们将lua 表保存在C 变量中,Lua 引擎没有办法知道它的使用情况,所以可能(错误地)将其做为垃圾回收掉。
所以,Lua API 没有定义任何像lua_Value 这种类型。实际上,lua 使用一个抽象栈来在Lua 和C之间交换数据。这个栈上的每一个槽可以装任意Lua 值。每当你要从Lua 获取一个值(如一个全局变量的值),你对Lua发出请求,然后它会在栈上压入一个值。当你想要传一个值给Lua,首先将这个值压栈,然后通知Lua(它会将值弹出)。我们仍然需要两个不同的函数,其中一个函数将每一个C 类型值压栈,另一个函数从栈上获得每一个值,但我们避免组合爆炸。更进一步,因为这个栈是由Lua 管理的,垃圾回收器知道哪一个C 值正在使用中。
几乎所有API 函数都使用这个栈。就像我们从第一个例子看到的,luaL_loadbuffer 将其返回值放在栈中(包括编译过的chunk 和错误消息)。Lua_pcall 从栈上获得要调用的函数,任何相关的错误消息也放在这里。
这个栈执行严格的LIFO(后进,先出)。然你调用Lua,它只改变栈顶部分。你的C 代码有更多的自由。特别地,它能检视栈中的任意元素,而且还能在任意位置插入和删除元素。
Pushing elements
每一种类型都有一种对应的Push 函数:lua_pushnil 压入nil,lua_pushnumber 压入doubles,lua_pushinteger 压入integers,lua_pushboolean 压入booleans(c 是integers),lua_pushlstring 压入任意strings(char * 加上长度),lua_pushstring 压入以0 结尾的strings:
void lua_pushnil (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, lua_Number n);
void lua_pushinteger (lua_State *L, lua_Integer n);
void lua_pushlstring (lua_State *L, const char *s, size_t len);
void lua_pushstring (lua_State *L, const char *s);
也有将C 函数和用户定义值压栈的函数。稍后会讨论。
lua_Number 是数值类型,默认是double,但根椐lua 的编译选项的不同,也可能是float 甚至是long integer。lua_Integer 是有符号整数,对存储大strings来说,它是足够大的。通常,它也被定义成ptrdiff_t 类型。
Lua 的Strings 不是以0 结尾的,而且可以包含任意二进制数据。它们必须依赖于一个显示的长度信息。lua_pushlstring 需要一个长度作为参数。对于以0 结尾的strings,你可以使用lua_pushstring,它使用strlen 来计算stirng 的长度。lua 不会保存string 的指针,它总是copy 份或重用一个。因此,这些函数一旦返回,你就可以释放或修改你的buffer。
当你要在栈上压入一个元素,确保栈有足够的空间是你的责任。记住,现在你是C 程序员。当lua 启动或调用C 时,栈中至少有20 个空槽(这个常量定义于lua.h 的LUA_MINSTACK)。这个空间通常对绝大多数情形够用了,所以我们一般不用去关心它。但是,有些任务可能需要更大的栈空间(例如,调用一个有大量参数的函数)。这时,可以调用lua_checkstack 函数,用来检查栈空间是否够用:
获取检查栈空间的大小
int lua_checkstack (lua_State *L, int sz);
Querying elements
第一个入栈的元素的索引是1,第二个是2,等等。也可以用负索引,栈顶元素的索引是-1,它之前是-2 等等。例如,lua_tostring(L,-1) 返回作为string 的栈顶元素。
检查栈中元素的类型:lua_is*,如lua_isnumber,lua_isstring,lua_istable,等。它们有共同的原型:
int lua_is* (lua_State *L, int index);
只要那个类型能够转成这个类型,就认为是这个类型。例如,lua_isnuber 会检查那个值能否转成number;所有的number 都满足lua_isstring。
lua_type 函数返回栈中元素的类型。每种类型都以lua.h 中定义的常量表示:LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, LUA_TTHREAD, LUA_TUSERDATA, and LUA_TFUNCTION。此函数一般用于开关语句中。
从栈中获取值
int lua_toboolean (lua_State *L, int index);
lua_Number lua_tonumber (lua_State *L, int index);
lua_Integer lua_tointeger (lua_State *L, int index);
const char *lua_tolstring (lua_State *L, int index, size_t *len);
size_t lua_objlen (lua_State *L, int index);
这些函数可以随意调用,既使栈中没有正确类型的值也OK。这种情况下,lua_toboolean, lua_tonumber, lua_tointeger, 和lua_objlen 全都返回0。对其它函数,我们通常不需要调用相应的lua_is* 函数:只需调用lua_to*,然后检查返回值是不是NULL。
lua_tolstring 函数返回一个指针,指向那个串的内部copy,并且将串的长度保存在以len 给定的位置。你不能改变这个内部拷贝(它是常量)。Lua 确保只要栈中相应的string 有效,这个指针就有效。当一个由Lua 调用的C 函数返回,Lua 会清除它的栈。因此,作为一个准则,你不应该在获得strings 的指针后将它们存储在函数外部。
lua_tolstring 所返回的string 在尾部总有一个0,但在string 的内部也可能有其它的0。size 作为第三个参数返回,len 是stirng 的真实长度。特别的,假设栈顶的元素是一个string,以下断言总是成立的:
size_t l;
const char *s = lua_tolstring(L, -1, &l); /* any Lua string */
assert(s[l] == '/0');
assert(strlen(s) <= l);
你可以以NULL 作为第三个参数调用lua_tolstring,如果你不需要获得长度信息。更好的方式是,使用宏lua_tostring,它代劳了前面提到的工作。
lua_objlen 函数返回一个object 的“length”。对strings 和tables,这个值是操作符‘#’的结果。此函数也可以被用来获得userdata 的size(我们将在28.1 节讨论userdata)。
将整个栈dump 下来
static void stackDump (lua_State *L) {
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; i++) { /* repeat for each level */
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING: { /* strings */
printf("'%s'", lua_tostring(L, i));
break;
}
case LUA_TBOOLEAN: { /* booleans */
printf(lua_toboolean(L, i) ? "true" : "false");
break;
}
case LUA_TNUMBER: { /* numbers */
printf("%g", lua_tonumber(L, i));
break;
}
default: { /* other values */
printf("%s", lua_typename(L, t));
break;
}
}
printf(" "); /* put a separator */
}
printf("/n"); /* end the listing */
}
此函数自底向上访问栈。对应其类型,打印每一个元素。
Other stack operations
int lua_gettop (lua_State *L);
void lua_settop (lua_State *L, int index);
void lua_pushvalue (lua_State *L, int index);
void lua_remove (lua_State *L, int index);
void lua_insert (lua_State *L, int index);
void lua_replace (lua_State *L, int index);
lua_gettop 返回栈中元素的个数,也是栈顶元素的索引。lua_settop 将栈中元素的个数设为指定值,如果指定值比原值小,顶部的若干元素被丢弃。否则,压入nil 去填充新增出来的空间。lua_settop(L,0) 清空栈。也可以使用负索引作为lua_settop 的参数。
弹出n 个元素的宏
#define lua_pop(L,n) lua_settop(L, -(n) - 1)
lua_pushvalue 函数将指定位置的那个元素拷贝一份压入栈中。lua_remove 在指定的位置移除元素,其它元素顺序下移以填满间隙。lua_insert 在指定的位置插入元素,上半部元素顺序上移以让出空间。lua_replace 弹出顶部元素,并在原索引处设置新值,此过程没有移动任何东西。
没有任何效果的栈操作
lua_settop(L, -1); /* set top to its current value */
lua_insert(L, -1); /* move top element to the top */
24.3 Error Handling with the C API
C 语言没有提供异常处理机制。Lua 使用C 的setjmp 来获得类似异常处理的效果。
在lua中所有结构都是动态的:它们根椐需要增长,甚至有可能再次缩减。这意味着Lua 中内存分配失败的可能性是普遍存在的。几乎任何操作都要面对这种不可预料性。Lua 使用异常通知这些错误,而不是在API 的每个操作中使用错误码。这意味几乎所有API 函数都可能抛出一个错误(就是调用longjmp),来代替函数返回。
当我们编写库代码(从lua 调用C 函数),长跳转的使用几乎作为实际上的处理设施,因为Lua 捕获所有相关的错误。当我们编程应用程序代码(C 代码调用Lua),我们必须提供一种方法去捕获这些错误。
Error handling in application code
通常,你的应用程序运行用“未保护”状态。因为其代码不是由lua 调用,lua 不能为其设置合适的context 来处理错误(既不能调用setjmp)。在这种环境下,当lua 面对如“内存不足”的错误时,它也不能做什么。它调用panic 函数,如果函数返回就退出应用程序。你可以使用lua_atpanic 函数设置你自已的panic 函数。
不是所有的API 函数都会抛出异常。luaL_newstate,lua_load,lua_pcall,和lua_close 都是安全的。其它大多数函数仅当内存分配失败时抛出异常,如lua_loadfile 在没有足够内存来拷贝文件名时会失败。许多函数当出现内存不足时什么也不做,所以它们忽略这此异常。对这些程序,如果lua 用完了内存,它对panic 是OK的。
如果你不想你的程序退出,既使内存分配失败,你有两种选择:第一是设置panic 函数,这个函数不返回lua,例如使用一个longjmp 去到你自已的setjmp。第二是将你的代码运行于保护模式。
许多程序(包括独立解释器)通过调用lua_pcall 运行lua 代码。因此,你的代码将运行于保护模式。既使内存分配失败,lua_pcall 返回一个错误码。如果你想保护所有与lua 交互的C 代码,可以使用lua_cpcall。此函数与lua_pcall 类似,但它将被调用的C 函数做为参数,所以内存分配失败没有危险,当将给定的函数压栈时。
Error handling in library code
Lua 是安全的语言。意思是不管你写什么代码,不管发生什么错误,你总是可以理解程序的行为。
当你为lua 加入新的C 函数,可能会打破这种安全性。例如,一个像poke 的函数,它在任意地址存储任意字节,可能引起所有种类的内存错误。你应该努力确保你的添加对lua 是安全的,并提供良好的错误处理。
当你为lua 编写库,这里有一种标准的方法来处理错误。每当C 函数检测到一个错误,它简单的调用lua_error(或调用luaL_error 更好,它会格式化错误消息,然后调用lua_error)。lua_error 函数清除lua 需要清除的东西,并且jump 回lua_pcall,这是执行起始的地方,错误消息也跟着传过来。