Lua是一种嵌入式语言,这就意味着Lua并不是一个独立运行的应用,而是一个库,它可以链接到其他应用程序,将Lua的功能融入这些应用。
因为能够当作库来扩展某个应用程序,所以Lua是一种嵌入式语言。同时,使用了Lua语言的程序也可以在Lua环境中注册新的函数,比如用C语言实现函数,从而增加一些无法直接用Lua语言编写的功能。因此Lua也是一种可扩展的语言。 上述两种对Lua语言的定位分别对应C语言和Lua语言之间的两种交互形式。在第一种形式中,C语言拥有控制权,而Lua语言被用作库,这种交互形式中的C代码被称为应用代码。在第二种形式中,Lua语言拥有控制权,而C语言被用作库,此时的C代码被称为库代码。应用代码和库代码都适用相同的API与Lua语言通信,这些API被称为C API。 C API是一个函数、常量和类型组成的集合,有了它,C语言代码就能与Lua语言交互。C API包括读写Lua全局变量的函数、调用Lua函数的函数、运行Lua代码段的函数,以及注册C函数的函数等。通过调用CAPI,C代码几乎可以做Lua代码能够做的所有事情。 CAPI遵循C语言的操作模式,与Lua的操作模式由很大的区别。在使用C语言编程时,我们必须注意类型检查、错误恢复、内存分配错误和其他一些复杂的概念。CAPI中的大多数函数都不会检查其参数的正确性,我们必须在调用函数前确保参数的合法性,一旦出错,程序会直接崩溃而不会收到规范的错误信息。此外,CAPI强调的是灵活性和简洁性,某些情况下会以牺牲易用性为代价,即便是常见的需求,也可能需要调用好几个API。这么做虽然有些繁琐,但我们却可以完全控制所有细节。
第一个示例
首先来学习一个简单的应用程序的例子:一个独立的解释器。
代码语言:javascript复制一个简单地额独立解释器
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "luaxlib.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_loadstring(L,buff)||lua_pcall(L,0,0,0);
if(error){
fprintf(stderr,"%sn",lua_tostring(L,-1));
lua_pop(L,1); /*从栈中弹出错误信息*/
}
}
lua_close(L);
return 0;
}
代码语言:javascript复制头文件lua.h声明了Lua提供的基础函数,其中包括创建新Lua环境的函数、调用Lua函数的函数、读写环境中的全局变量的函数,以及注册供Lua语言调用新函数的函数等等。lua.h中声明的所有内容都有一个前缀lua。 头文件luaxlib.h声明了辅助库所提供的函数,其中所有的声明均以luaL开头。辅助库使用lua.h提供的基础API来提供更高层次的抽象,特别是对标准库用到的相关机制进行抽象。基础API追求经济性和正交性,而辅助库则追求对常见任务的实用性。当然,要在程序中创建其他所需的抽象也是非常简单的。请记住,辅助库不能访问Lua的内部元素,而只能通过lua.h中声明的官方基础API完成所有工作。辅助库能实现什么,你的程序就能实现什么。 Lua标准库没有定义任何C语言全局变量,它将其所有的状态都保存在动态的结构体lua_State中,Lua中的所有函数都接收一个指向该结构的指针作为参数。这种设计使得Lua是可重入的,并且可以直接用于编程多线程代码。 顾名思义,函数luaL_newstate用于创建一个新的Lua状态。当它创建一个新状态时,新环境中没有包含预定义的函数,甚至连print也没有。为了保持Lua语言的精炼,所有的标准库都被组织成不同的包,这样我们在不需要使用某些包时可以忽略它们。头文件lualib.h中声明了用于打开这些库的函数。函数luaL_openlibs用于打开所有的标准库。 当创建好一个状态并且在其中加载标准库以后,就可以处理用户的输入了。程序会首先调用函数luaL_loadstring来编译用户输入的每一行内容。如果没有错误,则返回零,并向栈中压入编译后得到的函数。然后,程序调用函数lua_pcall从栈中弹出编译后的函数,并以保护模式运行。与函数lua_loadstring类似,如果没有错误发生,函数lua_pcall则返回零;当发生错误时,这两个函数都会向栈中压入一条错误信息。随后我们可以通过函数lua_tostring获取错误信息,并在打印错误信息后使用函数lua_pop将从栈中删除。 在C语言中,真是的错误处理可能会相当复杂,并且如何处理错误取决于应用的性质。Lua核不会直接向任何输出流写入数据,它只会通过返回错误信息来提示错误。每个应用可以用其所需的最恰当的方式来处理这些错误信息。为了简化讨论,假设一下示例使用如下简单的错误处理函数,即打印一条错误信息,关闭Lua状态并结束整个应用:
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
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);
}
代码语言:javascript复制由于Lua既可以作为C代码来编译,也可以作为C 代码来编译,因此lua.h中并没有包含以下这种在C标准库中的常见的写法:
#ifdef __cplusplus
extern "C"{
#endif
...
#ifdef __cplusplus
}
#endif
代码语言:javascript复制如果将Lua作为C代码编译出来后又要在C 中使用,那么可以引入lua.hpp来替代lua.h,定义如下:
extern "C"{
#include "lua.h"
}
栈
Lua和C之间通信的主要组件是无处不在的虚拟栈,几乎所有的API调用都是在操作这个栈中的值,Lua与C之间所有的数据交换都是通过这个栈完成的。此外,还可以利用栈保存中间结果。 当我们想在Lua和C之间交换数据时,会面对两个问题:第一个问题是动态类型和静态类型体系之间不匹配;第二个问题是自动内存管理和手动内存管理之间不匹配。 在Lua中,如果我们写t[k]=v,k和v都可以是几种不同类型;由于元表的存在,甚至t也可以有不同的类型。然而,如果要在C语言中提供这种操作,任意给定的settable函数都必须有一个固定的类型。为了实现这样的操作,我们就需要好几十个不同的函数。 可以通过在C语言中声明某种联合体类型来解决这个问题,假设这种类型叫lua_Value,它能够表示Lua语言中所有的值,然后,可以把settable声明为:
代码语言:javascript复制void lua_settable (lua_Value a,lua_Value k,lua_Value v);
这种方法有两个缺点。首先,我们很难将如此复杂的类型映射到其他语言中;而在设计Lua时,我们又要求Lua语言不仅能方便地与C/C 交互,而且还能与Java
、Fortran
、C#
等其他语言方便地交互。其次,Lua语言会做垃圾收集:由于Lua预压引擎并不知道Lua中的一个表可能会被保存在一个C语言变量中,因此它可能会认为这个表是垃圾并将其收回。
因此,LuaAPI中灭有定义任何类似于lua_Value的类型,而是使用栈在Lua和C之间交换数据。栈中的每个元素都能保存Lua中任意类型的值。当我们想要从Lua中获取一个值时,只需要调用Lua,Lua就会将指定的值压入栈中。当想要将一个值传给Lua时,首先要将这个值压入栈,然后调用Lua将其中栈中弹出即可。尽管我们仍然需要一个不同的函数将每种C语言类型的值压入栈,还需要另一个不同函数从栈中弹出每种C语言类型的值,但是避免了过多的组合。另外,由于这个栈是Lua状态的一部分,因此垃圾收集器知道C语言正在使用哪些值。
几乎CAPI中的所有函数都会用到栈。正如第一个示例,函数luaL_loadstring将其结果留在栈中;函数lua_pcall从栈中取出要调用的函数,并且也会将错误消息留在栈中。
Lua严格地按照LIFO的规则来操作栈。在调用Lua时只有栈顶部的部分会发生改变,而C语言代码则有更大的自由度。更具体地说,C语言可以检视栈中的任何一个元素,甚至可以在栈的任意位置插入或删除元素。
压入元素
针对每一种能用C语言直接表示的Lua数据类型,CAPI中都有一个对应的亚栈函数:常量nil使用lua_pushnil;布尔值使用lua_pushboolean;双精度浮点数使用lua_pushnumber;整型使用lua_pushinteger;任意字符串使用lua_pushlstring;以 终止的字符串使用lua_pushstring。
代码语言:javascript复制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相当于Lua语言的浮点数类型,默认为double,但可以在编译时配置Lua,让Lua_Number为float甚至long double。类型lua_Integer相当于Lua语言中的整型,通常被定义为long long ,既有符号64位整型。同样,要把Lua语言中的lua_Integer配置为使用int或long也很容易。如果使用float-int组合,也就是32浮点数类型和整型,即我们所说的精简Lua,对于资源受限的机器和硬件而言,相当高效。
Lua语言中的额字符串不是以 结尾的,它们可以包含任意二进制数据。因此,将字符串压栈的基本函数lua_pushlstring需要一个明确的长度作为参数。对于以 结尾的字符串,也可以使用函数lua_pushstring,该函数通过strlen来计算字符串的长度。Lua语言不会保留指向外部字符串的指针。对于不得不保留的字符串,Lua要么生成一个内部副本,要么复用已有的字符串。因此,一旦上述函数返回,即使立即释放或修改缓冲区也不会出现问题。
无论何时向栈内压入一个元素,我们都应该确保栈中有足够的空间。请注意,现在你是一个C语言程序员,Lua语言不会宠着你。当Lua启动时,以及Lua调用C语言时,栈中至少有20个空闲的位置。对于大多数情况,这个空间是完全够用,所以我们一般无须考虑栈空间的问题。不过,有些任务可能会需要更多的栈空间,特备是循环向栈中压入元素时。在这些情况下,就需要调用哈数lua_checkstack来检查栈中是否有足够的空间:
代码语言:javascript复制int lua_checkstack (lua_State *L, int sz);
这里,sz是我们所需要的额外栈位置的数量。如果可能,函数lua_checkstack会增加栈的大小,以容纳所需的额外空间;否则,该函数返回零。
辅助库也提供了一个高层函数来检查栈空间:
代码语言:javascript复制void luaL_checkstack (lua_State *L, int sz, const char *msg);
该函数类似于函数lua_checkstack,但是如果栈空间不能满足请求,该函数会使用指定的错误信息抛出异常,而不是返回错误码。
查询元素
CAPI使用索引来引用栈中的元素。第一个被压入栈的元素索引为1,第二个被压入的元素索引为2,依次类推。我们还可以以栈顶为参照,使用负数索引来访问栈中的元素,此时,-1表示栈顶元素,-2表示在它之前被压入栈的元素,依次类推。例如,调用lua_tostring(L,-1)会将栈顶的值作为字符串返回。正如你接下来要看到的,有些情况下从栈底对栈进行索引更加自然,而有些情况下则使用负数索引更好。 要检查栈中的一个元素是否为特定的类型,CAPI提供了一系列名为lua_is的函数,其中可以是任意一种Lua数据类型。这些函数包括lua_isnil、lua_isnumber、lua_isstring和lua_istable等。所有这些函数都有同样的原型:
代码语言:javascript复制int lua_is* (lua_State *L, int index);
实际上,函数lua_isnumber不会检查某个值是否为特定类型,而是检查该值是否能被转换为特定类型。函数lua_isstring与之类似,特别之处在于,它接受数字。
还有一个函数lua_type,用于返回栈中元素的类型,每一种类型都由一个对应的常量表示,包括LUA_INIT、LUA_TBOOLEAN、LUA_TUMBER、LUA_TSTRING等。还函数一般与switch语句连用。当需要检查字符串和数值是否存在潜在的强制类型转换时,该函数也同样有用。
函数lua_to*用于从栈中获取一个值:
代码语言:javascript复制int lua_toboolean(lua_State *L, int index);
const char *lua_tolstring(lua_State *L, int index, size_t *len);
lua_State *lua_tothread(lua_State *L, int index);
lua_Number lua_tonumber(lua_State *L, int index);
lua_Integer lua_tointeger(lua_State *L, int index);
即使指定的元素的类型不正确,调用这些函数也不会有问题。函数lua_toboolean适用于所有类型,它可以按照如下的规则将任意Lua值转换为C的布尔值:nil和false转换为0,所有其他的Lua值转换为1.对于类型不正确的值,函数lua_tolstring和lua_tothread返回NULL。不过,数值相关的函数都无法提示数值的类型错误,因此只能简单地返回0。以前我们需要调用函数lua_isnumber来检查类型,但是Lua5.2引入了如下的新函数:
代码语言:javascript复制lua_Number lua_tonumberx(lua_State *L, int idx, int *isnum)
lua_Integer lua_tointegerx(lua_State *L,int idx, int *isnum)
出口参数isnum返回了一个布尔值,来表示Lua值是否被强制转换为期望的类型。
函数lua_tolstring返回一个指向该字符串内部副本的指针,并将字符串的长度存入到参数len指定的位置。我们无法修改这个内部副本。Lua语言保证,只要对应的字符串还在栈中,那么这个指针就是有效的。当Lua调用的一个C函数返回时,Lua就会清空栈。因此,作为规则,永远不要指向Lua字符串的指针存放到获取该指针的函数之外。
函数lua_tolstring返回的所有字符串在其末尾都会有一个额外的 ,不过这些字符串中也可能会有 ,因此可以通过第三个参数len获取字符串的真实长度。特别的,假设栈顶的值是一个字符串,那么如下推断永远成立:
代码语言:javascript复制size_t len;
const char *s = lua_tolstring(L, -1 ,&len); /*任意Lua字符串*/
assert(s[len] == '