C API是一组能使C代码与Lua交互的函数,其中,包括读写Lua全局变量、调用Lua函数、运行一段Lua代码,以及注册C函数以供Lua代码调用。
Lua和C语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈来完成。此外,还可以用这个栈来保存一些中间结果。栈可以解决Lua和C语言之间存在的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显示地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型。
一、Lua解释器
Lua解释器是学习C API的最好例子,下面给出最原始的解释器程序:
#include
#include
#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(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); //从栈中弹出错误消息
}
}
lua_close(L);
return 0;
}
头文件lua.h定义了Lua提供的基础函数,包括创建Lua环境、调用Lua函数(如lua_pcall)、读写Lua环境中全局变量,以及注册供Lua调用的新函数等。lua.h中定义所有内容都有一个lua_前缀。
头文件lauxlib.h定义了辅助库(auxiliary library, auxlib)提供的函数。它的所有定义都以lua)L开头(如luaL_loadbuffer)。辅助库是一个使用lua.h中API编写出的一个较高的抽象层。Lua的所有标准库编写用到了辅助库。基础API的设计保持原子性和正交性,而辅助库则侧重于解决具体的任务。辅助库并没有直接访问Lua的内部,它都是用官方的基础API来完成所有工作的。
Lua库中没有定义任何全局变量,它将所有的状态都保存在动态结构lua_State中,所有的C API都要求传入一个指向该结构的指针。这种实现使得Lua可以重入,稍加修改即可用于多线程的代码中。
luaL_newstate函数用于创建一个新环境(或状态)。当luaL_newstate创建一个新的环境时,新环境中没有包含预定义的函数,甚至没有print。为了使Lua保持小巧,所有的标准库都被组织到了不同的包中。这样便可以忽略那些不需要的包。在头文件lualib.h中定义了打开这些库的函数,而辅助库函数luaL_openlibs则可以打开所有的标准库。
当创建好一个状态,并在其中加载了标准库后,就可以解释用户的输入了。程序调用luaL_loadbuffer来编译用户输入的每行内容。如果没有错误,此调用返回0,并向栈中压入编译后的程序块。然后,程序调用lua_pcall,这个函数会将程序块从栈中弹出,并在保护模式中运行它。与luaL_loadbuffer类似,lua_pcall返回0表示没有错误。若发生错误,那么这些函数就会向栈中压入一条错误消息。用lua_tostring可以获取这条消息,打印后可以用lua_pop把它从栈中删除。
Lua可以同时作为C代码或C++代码来编译。在某些C程序库中常会出现以下这种调节代码,而在lua.h中并没有包含它们:
#ifdef --cplusplus
extern "C" {
#endif
...
#ifdef --cplusplus
}
#endif
如果将Lua作为C代码来编译,并在C++中使用它,那么可以包含lua.hpp中代替lua.h。lua.hpp定义为:
extern "C" {
#include "lua.h"
}
二、栈
在Lua和C语言之间交换数据时,需要面对两个问题。首先是动态类型和静态类型之间的区别,其次是自动内存管理和手动内存管理之间的区别。
Lua使用一个抽象的栈来在Lua和C语言之间交换数据。栈中的每个元素都能保存任何类型的Lua值。要获取Lua中的一个值时,只要调用一个Lua API函数,Lua就会将指定的值压入栈中。要将一个值传给Lua时,需要先将这个值压入栈,然后调用Lua API,Lua就会获取该值并将其从栈中弹出。这个栈由Lua来管理,垃圾收集器能确定C语言使用哪些值。
几乎所有的API函数都会用到这个栈,比如:前面解释器中,luaL_loadbuffer将它的结果(编译好的程序块或错误消息)留在栈中;lua_pcall会调用栈中的一个函数,若发生错误则将错误消息留在栈中。
Lua严格地按LIFO(Last in, First out, 后进先出)规范来操作这个栈。当调用Lua时,Lua只会改变栈的顶部。不过,C代码则有更大自由度,它可以检索中间的元素,甚至在栈的任意位置插入或删除元素。
1、压入元素
对于每种可以呈现在Lua中的C类型,API都有一个对应的压入函数:常量nil可使用lua_pushnil,双精度浮点数可使用lua_pushnumber,整数可使用lua_pushinteger,布尔(C语言中的整数)可使用lua_pushboolean,任意字符串(char*及长度)可使用lua_pushlstring,以及零结尾的字符串可使用lua_pushstring:
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函数和userdata值的函数,在此不表。
由于Lua中的字符串不是以零结尾的,它们可以包含任意二进制,所以会有两种类型的压栈函数。
向栈中压入一个元素时,应该确保栈中具有足够的空间。当Lua启动时,或Lua调用C语言时,栈中至少会有20个空闲的槽(这个常量由lua.h中的LUA_MINSTACK定义)。这些空间对于普通应用足够了,但是,有些任务会需要更多的栈空间,例如:调用一个具有很多参数的函数。在这种情况下,就需要调用lua_checkstack来检查栈中是否有足够的空间:
int lua_checkstack(lua_State *L, int sz);
2、查询元素
API使用“索引”来引用栈中的元素。第一个压入栈中的元素索引为1;第二个压入的元素索引为2,依此类推直到栈顶。还可以以栈顶为参考物,使用负数的索引来访问栈中的元素,-1为栈顶元素,-2为栈顶下面的元素,依此类推。比如:调用lua_tostring(L, -1)会将栈顶的值作为一个字符串返回。
为了检查一个元素是否为特定的类型,API提供了一系列的函数lua_is*,其中,*可以是任意Lua类型。这些函数有lua_isnumber、lua_isstring和lua_istable等,所有这些函数都有同样的原型:
int lua_is*(lua_State *L, int index);
实际上,lua_isnumber只是检查值是否能转换为数字类型。lua_isstring也具有同样的行为,因此,对于任意数字,lua_issting都返回真。
函数lua_type会返回栈中元素的类型。每种类型都对应一个常量,这些常量定义在头文件lua.h中,它们是LUA_TNIL、LUA_TBOOLEAN、LUA_TNUMBER、LUA_TSTRING、LUA_TTABLE、LUA_TTHREAD、LUA_TUSERDATA和LUA_TFUNCTION。
lua_to*函数用于从栈中获取一个值:
int lua_tobollean(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);
3、其它栈操作
除了在C语言和栈之间交换数据的函数外,API还提供了以下这些用于普通栈操作的函数:
int lua_gettop(lua_State *L); //返回栈顶元素索引或者栈中元素个数
void lua_settop(lua_State *L, int index); //将栈顶设置为指定位置,即修改栈中元素数量,丢弃超高的元素,反之,向栈中压入nil来补足大小
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); //弹出栈顶的值,并将该值设置到指定索引上,但不会移动任何东西
三、错误处理
(1)应用程序代码中的错误处理
Lua在发现内存不足一类的错误时,会调用一个紧急函数(Panic Function),当这个函数返回后,Lua就会结束应用程序。用户可以通过函数lua_atpanic来设置自己的紧急函数。如果发生了内存分配错误,而又不想结束应用程序,那么有两种做法:第一种是设置一个紧急函数,让它不要把控制权返回给Lua,如可以在调用longjmp转到之前setjmp所设置的位置;第二种做法是让代码在保护模式下运行。
大多数应用程序(包括Lua解释器)都采用第二种做法,它们调用lua_pcall来运行Lua代码。如果发生了内存分配错误,lua_pcall会返回一个错误代码,并将解释器封固在一致的状态。如果要保护那些与Lua交互的C代码,可以使用lua_cpcall。这个函数类似于lua_pcall,但它接受一个C函数作为参数,然后调用这个C函数。将一个函数压入栈中不会有内存分配失败的可能。
(2)库代码中的错误处理
每个C程序都有其各自处理错误的方法,但是,当为Lua编写库函数时,却只有一种标准的错误处理方法。当一个C函数检测到一个错误时,它就应该调用lua_error。lua_error函数会清理Lua中的所有需要清理的东西,然后跳回发起执行的那个lua_pcall,并附上一条错误消息。
四、应用实例
Lua一个很好的应用是扩展宿主程序,本节以Lua脚本作为C程序的配置文件。假设Lua脚本作为配置文件包含如下内容:
--定义窗口大小
width = 200
height = 300
可以用Lua API来分析该文件并获取全局变量width和height的值,具体代码如下:
void load(lua_State *L, const char *fname, int &w, int &h)
{
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
{
error(L, "cannot run config.file: %s", lua_tostring(L, -1));
}
lua_getglobal(L, "width");
lua_getglobal(L, "height");
if (!lua_isnumber(L, -2))
{
error(L, "'width' should be a number\n");
}
if (!lua_isnumber(L, -1))
{
error(L, "'height' should be a number\n");
}
w = lua_tointeger(L, -2);
h = lua_tointeger(L, -1);
}
假设已经创建了一个Lua状态。这个函数调用luaL_loadfile从文件fname加载程序块,然后调用lua_pcall运行编译好的程序块。若发生错误(例如配置文件中的语法错误),这两个函数都会把错误消息压入栈,并返回一个非零的错误代码。此时,程序就调用lua_tostring从栈顶获取该消息。
当运行完程序块后,程序需要获取全局变量的值。程序调用了lua_getglobal两次,这个函数除了第一个常规的lua_State参数外,还需要变量名称。每次调用这个函数,它都会将相应的全局变量压入栈中。因此,width处于索引-2上,height位于索引-1上(栈顶)。另外,由于栈事先是空的,也可以从栈底进行索引,第一个值使用索引1,第二个值使用2。接下来程序使用lua_isnumber来检查两个值是否为数字,最后调用lua_tointeger将这些值转换为整型,并赋予对应的参数变量。
阅读(4377) | 评论(1) | 转发(0) |