第二十四课 C API概述

Lua是一种嵌入式语言,即Lua不是一个单独运行的程序,而是一个可以链接到其他程序的库。通过链接就可以将Lua的功能合并入这些程序。
如果Lua不是一个独立运行的程序,那么之前我们使用的Lua程序是怎么来的呢?这个问题的答案是Lua解释器,即可执行程序“lua”。这个解释器是一个简单的应用程序,它依靠Lua库来实现主要功能。这个程序会处理 与用户的交互,它将用户的文件或字符串输入Lua库,由Lua库来完成主要的工作,例如真正地运行Lua代码等。
这种使用一个库来扩展应用程序的能力使得Lua成为一种“扩展语言”。而与此同时,一个使用了Lua的程序可以在Lua环境中注册C语言或其他语言实现的新函数,由此就可以向Lua添加某些无法直接用Lua编写的功能, 这便使Lua成为一种“可扩展的语言”。
C API是一组能使C代码与Lua交互的函数。其中包括读写Lua全局变量、调用Lua函数、运行一段代码,以及注册C函数以供Lua代码调用等。
Lua和C语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈 来完成。此外,还可以用这个栈来保存一些中间结果。栈可以解决Lua和C语言之间存在的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显示地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型。

第一个示例
通过一个简单的Lua解释器程序来开始学习C API。以下代码是一个最原始的解释器程序:
#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(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)提供的函数。它的所有定义都以luaL_开头。辅助库是一个使用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语言之间交换数据时,需要面对两个问题:
1、动态类型和静态类型之间的区别;
2、自动内存管理和手动内存管理之间的区别。
在Lua中,当用户写a[k]=v,k和v可以是任意的类型。甚至于a也有可能是其他类型,因为可以通过元表重载操作符。假设要在C语言中提供一个API函数settable。由于C函数的参数是固定类型,所以必须为各种类型的参数编写一个settable函数。
可以在C语言中声明一些联合(union)类型来解决这个问题。假设这种类型叫做lua_Value,能够表示所有的Lua值。那么settable就可以声明为:
void lua_settable(lua_Value a, lua_Value k, lua_Value v)
这种做法有两个缺点。首先,很难将这种复杂的类型映射到其他语言中。Lua的设计目标不仅仅是为了便于C/C++访问,还应该可以被Java、 Fortran、C#或其他语言访问。其次,Lua采用垃圾收集机制,如果将一个Lua table保持在一个C变量中,Lua引擎则无法搜索出,因此,它会认为这个table是垃圾文件,并回收它。
由于上述原因,Lua API中没有定义任何类似于 lua_Value的类型,而是使用了一个抽象的栈,在Lua和C语言之间交换数据。栈中的每个元素都能保存任何类型的Lua值。要获取Lua中的一个值时,只要调用一个Lua API函数,Lua就会将指定的值压入栈中。要将一个值传给Lua时,需要先将这个值压入栈,然后调用Lua API,Lua就会获取该值并将其从栈中弹出。为了将C类型的值压入栈,或者从栈中获取不同类型的值,就需要为每种类型定义一个特定的函数。但这种定义的数量远远小于上例中提到的定义settable的数量。另外,由于这个栈是Lua管理的,垃圾收集器能确定C语言使用哪些值。
Lua严格按照LIFO后进先出的规范来操作这个栈。当调用Lua时,Lua只会改变栈的顶部。不过,C代码则有更大的自由度,它可以检索栈中间的元素,甚至在栈的任意位置插入或删除元素。

压入元素
对于每种可以呈现在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);
类型 lua_Number是Lua中的数字类型。默认为 双精度浮点数,但有些 发行版本为了适应某种硬件受限的环境,会将数字类型改 为 单精度浮点数或 长整数。类型lua_Integer是一种整数类型,它足以存储大型字符串的长度,通常定义为ptrdiff_t类型。
Lua中的字符串不是以零结尾的,它们可以包含任意二进制数据。因此,它们必须同时保存一个显示的长度。将字符串压入栈的基本函数是 lua_pushlstring,它要求传入一个显示的长度参数。对于零结尾的字符串,可以使用使用函数lua_pushstring,这个函数通过strlen来计算字符串的长度。Lua不会持有指向外部字符串的指针。对于所有Lua持有的字符串,它都会生成一个内部副本,或者复用现有的内容。因此,即使在这些函数返回后立即释放或修改这些字符串,也不会出现问题。
向栈中压入一个元素时,应该确保栈中具有足够的 空间。当Lua启动时,或Lua调用C语言时,栈中至少会有20个空闲槽。这些空间对于普通的应用是足够了,所以一般我们无须顾及空间上的问题。然而有些任务会需要更多的空间,例如一个具有很多参数的函数,在这些情况中,就要调用 lua_checkstack来检查栈中是否有足够的空间:
int lua_checkstack(lua *L, int sz);

查询元素
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_isstring都返回真。
还有一个函数lua_type,它会返回栈中元素的类型。每种类型都对应于一个常量,这些常量定义在头文件lua.h中,它们是LUA_TNIL,LUA_TBOOLEAN,LUA_TNUMBER,LUA_TSTRING、LUA_TTABLE、LUA_TTHREAD、 LUA_TUSERDATA和LUA_TFUNCTION。这个函数一般可用在一个switch语句中。另外,若要检查一个元素是否为真正的字符串或数字(无须转换的),也可以使用这个函数。
lua_to*函数用于从栈中获取一个值:
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);
如果指定的元素不具有正确的类型,调用这些函数也不会有问题。在这种情况下, lua_toboolean、lua_tonumber、 lua_tointeger和 lua_objlen会返回0,而其他函数会返回 NULL。返回0并不是很有用,但ANSI C也没有提供其他可以表示错误的值。至于其他lua_to*函数,通常不先使用lua_is*函数,只需在调用它们之后测试返回结果是否为NULL就可以了。
lua_tolstring函数会返回一个指向内部字符串副本的指针,并将字符串的长度存入最后一个参数len中。这个内部副本不能修改,返回 类型中的const也说明了这点。Lua 保证只要这个对应的字符串值还在栈中,那么这个指针就是有效的。当Lua调用的一个C函数返回时,Lua就会清空它的栈。这就形成了一条规则,不要在C函数之外使用在C函数内获得的指向Lua字符串的指针。
所有 lua_tolstring返回字符串在其末尾都会有一个额外的零,不过这些字符串的中间也有可能会有零。字符串长度通过第三个参数len返回,这才是真正的字符串长度。进一步说 ,假设栈顶的值是一个字符串,如下总是为真:
size_t l;
const char *s = lua_tolstring(L, -1, &l); /*任何Lua字符串*/
assert(s[l] == '\0');
assert(strlen(s) <= l);
如果不需要长度信息,可以将第三个参数设为NULL来调用lua_tolstring。或者使用宏lua_tostring,这个宏就是用NULL作为第三个参数来调用lua_tolstring。
lua_objlen函数可以返回一个对象的“长度”。对于字符串和table,这个值是长度操作符‘#’的结果。这个函数可以用于获取一个“完全userdata”的大小。
为了演示这些函数的使用,以下代码实现了一个有用的辅助函数,它会打印整个栈的内容,这个 函数会由下而上地遍历栈,并根据每个元素的类型打印其值,字符串放在一对单引号内打印,数字使用格式“%g”来打印,其他值(table、函数等)则只打印它们的类型。其中, lua_typename可将一个类型编码转换为一个类型名。
static void stackDump (lua_State *L)
{
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; ++i)
{
//遍历所有层
int t = lua_type(L, i);
switch (t)
{
case LUA_TSTRING:
{
//字符串
printf("'%s'", lua_tostring(L, i));
break;
}
case LUA_TBOOLEAN:
{
//布尔
printf(lua_toboolean(L, i) ? "true" : "false");
break;
}
case LUA_TNUMBER:
{
//数字
printf("%g", lua_tonumber(L, i));
break;
}
default:
{
//其他值
printf("%s", lua_typename(L, i));
break;
}
}
printf(" "); //打印一个分隔符
}
printf("\n"); //列表结尾
}

其他栈操作
除了在C语言和栈之间交换数据的函数外,API还提供了以下这些用于普通栈操作的函数:
//返回栈中元素的个数,也可以说是栈顶元素的索引。
int lua_gettop (lua_State *L);

//将栈顶设置为一个指定的位置,即修改栈中元素的数量。如果之前的栈顶比新设置的更高,那么高出来的部分会被丢弃;反之,会向栈中压入nil来补足大小。有一个特例,调用lua_settop(L, 0)能清空栈。也可以用负数索引 来使用 lua_settop。另外,API根据这个函数还提供了一个宏,用于从栈中弹出n个元素:#define lua_pop(L, n) lua_settop(L, -(n) - 1)
void lua_settop (lua_State *L, int index);

//lua_pushvalue函数会将指定索引上值的副本压入栈。
void lua_pushvalue (lua_State *L, int index);

//lua_remove删除指定索引上的元素,并将该位置之上的所有元素下移以填补空缺。
void lua_remove (lua_State *L, int index);

//lua_insert会上移指定位置之上的所有元素以开辟一个槽的空间,然后将栈顶元素移到该位置。
void lua_insert (lua_State *L, int index);

//lua_replace弹出栈顶的值,并将该值设置到指定索引上,但它不会移动任何东西。
void lua_replace (lua_State *L, int index);

# include <stdio.h>
#include "lua.h"
#include "luaxlib.h"

static void stackDump (lua_State *L)
{
<如之前的示例>
}
int main (void)
{
lua_State *L = luaL_newstate();

lua_pushboolean(L, 1);
lua_pushnumber(L, 10);
lua_pushnil(L);
lua_pushstring(L, "hello");

stackDump(L); //true 10 nil 'hello'

lua_pushvalue(L, -4);
stackDump(L); //true 10 nil 'hello' true

lua_replace(L, 3);
stackDump(L); //true 10 true 'hello'

lua_settop(L, 6);
stackDump(L); //true 10 true 'hello' nil nil

lua_remove(L, -3);
stackDump(L); //true 10 true nil nil

lua_settop(L, -5);
stackDump(L); //true

lua_close(L);
return 0;
}

C API中的错误处理
C语言不同于C++和Java,它没有提供异常处理机制。为了克服这个困难,Lua使用C语言中的setjmp机制,这是一种类似与异常处理的机制。
Lua中的所有结构都是动态的,它们会根据 需要来增长,或者缩小。在Lua中有许多地方可能会发生内存分配错误。几乎所有的函数都要面对这种潜在的错误。那么在发生错误时,与其让API中的每个函数返回错误代码,不如使用异常来标记这些错误。因此,几乎所有的API函数都会抛出错误(即调用longjmp),而 不是返回错误。
当编写库代码 时(被Lua调用的C函数),使用longjmp几乎和使用异常处理机制一样方便,Lua会捕获所有可能的错误。而当编写应用程序代码(调用Lua的C代码),必须提供一种捕获错误的方式。

应用程序代码中的错误处理
通常情况下,应用程序代码是以“无保护”模式运行的。由于它们不是由Lua调用的,Lua无法设置适当的上下文来捕获错误。因此,当Lua发现了例如“内存不足”这类错误时,它基本上不会 进行太多的处理。此时,Lua会调用一个“紧急函数“,当这个函数返回后,Lua就会结束应用程序。用户可以通过函数set_atpanic来设置自己的”紧急“函数。
如果发生了内存分配错误,而又不想结束应用程序,那么有两种做法。第一种是设置一个“紧急”函数,让它不要把控制权返回给Lua。例如,可以调用longjmp转到之前setjmp所设置的位置。第二种做法是让代码在“保护模式”下运行。
大多数应用程序 (包括Lua解释器程序)都采用第二种做法,它们调用lua_pcall来运行Lua代码。因而这些Lua代码 也都是运行在保护模式中的。如果发生了内存分配错误,lua_pcall会返回一个错误代码,并将解释器 封固在 一致的状态。如果要保护那些与Lua交互的C代码,可以使用 lua_cpcall。这个函数类似于lua_pcall,但它接受一个C函数作为参数,然后调用这个C 函数 。将一个函数压入栈中不会有内存分配失败的可能。

库代码中的错误处理
Lua是一种安全的语言,无论写什么,写出来的内容是否正确,都能用Lua自身的术语来理解程序的行为。此外,错误也是通过Lua的 术语来检测和解释的。可以用C语言来做一个对比,许多C程序的错误行为只能用底层硬件的术语来解释,而错误位置则是由“程序计数器”寄存器给出的。
当将新的C函数加入Lua时,就有可能打破这种安全性。例如,添加一个函数poke,它能在任意内存地址上存储任意字节。这个函数就有可能引起各种内存破坏。因此必须确保新加入的函数对Lua是安全的,并提供良好的错误处理。
正如之前所说的,每个C程序都有各自处理错误的方法。然而,当为Lua 编写库函数时,却只有一种标准的 错误处理方法。当一个C函数检测到一个错误时,它就应该调用lua_error。lua_error函数会清理Lua中所有需要清理的东西,然后 跳转回发起执行的那个lua_pcall,并附上一条错误消息。

















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值