中断了的 lua 学习再次回来了……之前学完基本语法后就不想看接下来的标准库讲解了,因为看了不用基本等于白看,没几天就忘了;但是不看的话连 lua 有什么功能都不知道,更不用说知道把 lua 用在什么地方。最近想学习用 lua 来扩展程序,因此又捡起来了,不过跳过了书中的 part II 和 part III,这里是第 24 章。顺便说一下,这里使用的 lua 版本也从 5.1.4 升到 5.2.0,不过区别应该不大。
lua 是一个嵌入式语言,就是说它不是一个单独的程序,而是一套可以在其它语言中使用的库,在前面使用过的 lua 交互程序其实是利用 lua 提供的库所实现的一个解析器。lua 可以作为 c 语言的扩展,反过来也可以用 c 语言编写模块来扩展 lua,这两种情况都使用同样的 api 进行交互。lua 与 c 主要是通过一个虚拟的“栈”来交换数据。
简单的 lua 解析器
下面是利用 lua 提供的库来开发的一个简单的解析器。它从标准输入中读入用户的输入,然后输出执行的结果:
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void)
{
int err;
lua_State* l;
char buf[BUFSIZ];
l = luaL_newstate();
luaL_openlibs(l);
while (fgets(buf, BUFSIZ, stdin) != NULL) {
err = luaL_loadbuffer(l, buf, strlen(buf), "line");
if (err) {
fprintf(stderr, "luaL_loadbuffer err: %s", lua_tostring(l, -1));
lua_pop(l, 1);
continue;
}
err = lua_pcall(l, 0, 0, 0);
if (err) {
fprintf(stderr, "lua_pcall err: %s", lua_tostring(l, -1));
lua_pop(l, 1);
}
}
lua_close(l);
return 0;
}
例如上面的代码文件为 $HOME/workspace/src/cmd.c,而 lua 源代码在 $HOME/workspace/lua-5.2.0/。编译这个程序前要先编译 lua 代码,在 $HOME/workspace/lua-5.2.0/src 中会生成一个 liblua.a 的静态链接文件。编译的时候要指定头文件和静态库的路径。那么编译上面代码的命令应该为
cd $HOME/workspace/src
gcc cmd.c -I../lua-5.2.0/src -L../lua-5.2.0/src -llua -lm
“-l”选项要放在编译命令的最后,原因可以参考《C 专家编程》第五章的相关内容。
因为 lua.h 中没有使用“extern C”来为 c++ 导出函数接口,因此如果要在 c++ 中使用 lua,include 的时候使用 lua.hpp 而不是 lua.h。
头文件 lua.h 中定义了 lua 提供的基本函数,包括创建环境,读写变量,注册函数等,这些函数都以“lua_”为前缀。lauxlib.h 定义了一些辅助函数,这些函数使用了 lua.h 中提供的基本功来来提供一些更高层次的功能,它们都以“luaL_”作为前缀。lua 函数并没有定义全局变量,所有的变量都在 lua_State 中,lua 的函数都是可重入的。
在 c 语言中使用函数
lua_State *luaL_newstate (void);
创建一个新的 lua 执行环境。这个环境里什么都没有,因此需要使用函数
void luaL_openlibs (lua_State *L);
加载所有的标准库。之后可以使用
int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);
加载并解析读到的内容。最后的参数 name 是调试的时候输出错误信息用的。如果解析成功,luaL_loadbuffer() 会把结果压到“栈”里。然后就使用函数
int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
在 lua 保护模式下运行语句。如果运行出错,函数会把错误信息压到“栈”里。我们可以通过
const char *lua_tostring (lua_State *L, int index);
来获取错误信息。在调用函数前,用户应该先把语句所需的参数都压到栈里,参数 nargs 说明有多少个参数,nresults 表示返回值有多少个。如果 msgh 值为 0 表示“栈”里返回的是原始的错误信息。
在函数执行完后,lua 会把函数以及其使用的参数从栈里删掉,并且把结果压入栈里。lua 函数并不会打印任何信息,它只会返回一个错误代码,然后由调用者对错误进行适当的处理。
最后把错误信息弹出:
void lua_pop (lua_State *L, int n);
参数 n 表示要弹出元素的个数。
最后使用
void lua_close (lua_State *L);
释放所有的资源。
“栈”的基本操作
在 lua 和 c 之间交换数据面临两个难题:一个是动态类型和静态类型的区别,一个是动态内存管理和人工内存管理的区别。例如在 lua 中的语句“a[k]=v”,其中 a,k,v 都可能有不同的类型。在 c 语言中我们可以定义一个联合 lua_value,但是对于其它语言来说却没有这样的数据类型,而且 lua 无法得知在 c 语言中使用的变量是不是动态分配的,因此可能会错误地回收变量所占的资源。因此 lua 使用一个栈来和其它语言交换数据。lua 把数据压栈,其它语言从栈里弹出数据;或者其它语言把数据压到栈里,lua 从其中获取数据。几乎所有的 lua 函数都使用这个栈来与其它语言进行数据交换。
lua 为 c 语言提供了一系列函数来把 lua 中不同类型的数据压到栈里:
void lua_pushnil (lua_State *L); /* nil */
void lua_pushboolean (lua_State *L, int bool); /* bool */
void lua_pushnumber (lua_State *L, lua_Number n); /* double by default */
void lua_pushinteger (lua_State *L, lua_Integer n); /* int */
void lua_pushlstring (lua_State *L, const char *s, size_t len); /* s and len */
void lua_pushstring (lua_State *L, const char *s); /* null-terminated string */
lua_Number 默认是 double 类型的,但是在某些平台上可能是 float 或者 long。lua 中的字符串并不是以“\0”作为结束标志,因此通常还需要一个表示长度的域。lua 会复制一份压到栈里的内容,因此可以在这些函数外释放参数的资源(例如动态分配的内存)。
在把参数压到栈里之前需要检查栈里是否还有足够的空间:
int lua_checkstack (lua_State *L, int expected);
lua 会自动扩充栈的大小为 expected,如果扩容失败或超出限制则返回错误编号。如果栈足够大,lua 并不会缩小栈的大小。
api 中使用索引来获取栈的内容。以栈底为参考点,第一个压进栈里的元素的索引为 1,第二个为 2,以此类推。除此之外还可以以栈顶为参考点,那么栈顶的元素索引为 -1,前一个元素索引为 -2,等等。在上面的例子里,lua_tostring(l, -1) 就是获取栈顶的内容并且把它转换成一个字符串。
lua 还提供了一些函数来检查栈的某个位置的元素的类型。例如 lua_isnumber(),lua_isstring(),lua_istable() 等。这些函数都有一个相似的函数原型:
int lua_is* (lua_State *L, int index);
事实上 lua 并不是检查该位置的元素是否就是对应的类型,而是判断该元素能否转换成对应的类型,例如 lua_isstring() 对于任何数值都会返回真值,也就是说数字可以转换成对应的字符串。
另外还有一个函数
int lua_type (lua_State *L, int index);
用来判断某个位置的元素类型(类型定义在 lua.h 中),这在 switch 语句中很方便,也避免了字符和数值之间的相互转换。
下面的函数用来从“栈”里取出数据:
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);
这里并不要求 index 指向的元素就是所需的类型。如果 index 指向的元素不能转换成所需的类型,函数会返回 0 或 NULL,因此转换前不需要判断类型,而可以通过 lua_to* 系列函数的返回值来判断是否转换成功。
lua_tolstring() 的返回值指向的是 lua 内部的字符串,字符串的长度放在第三个参数 len 中。如果需要保存字符串的内容需要用 memcpy() 之类的函数复制一份,因为 lua 会把“栈”的内容给清空(没搞清楚什么时候清空,原文是“When a C function called by Lua returns, Lua clears its stack;”)。
lua_tolstring() 返回的字符串最后都有一个“\0”,因为不能确定字符串中间有没有“\0”,所以返回值的真实长度要看 len 的值。如果不需要长度值,len 可以为 NULL,或者直接使用
const char *lua_tostring (lua_State *L, int index);
lua_objlen() 获取一个 object 的长度,这个 object 可以是字符串,表或用户自定义的数据。如果是字符串和表,函数的返回值和取长度的操作符“#”结果是一样的。
“栈”的其它操作
下面是其它的一些操作函数:
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() 改变“栈”的大小:如果原来的大小比 index 大,则位于 index 上的元素都被删除;如果原来的大小比 index 小,则往“栈”里填充 nil。特别地,lua_settop(l, 0) 清空整个“栈”。index 也可以为负数(即以“栈”顶为基准),因此下面的宏
#define lua_pop(L,n) lua_settop(L, -(n) - 1)
用来从“栈”里弹出 n 个元素。
lua_pushvalue() 把位于 index 的元素复制一份,然后将其放入“栈”顶;lua_remove() 删除位于 index 的元素,并且将 index 之上的元素依次往下移;lua_insert() 将“栈”顶的元素移到位置 index,并且将原来 index 及其以上的元素依次往上移;lua_replace() 将位于 index 的值修改为“栈”顶元素的值,并且将“栈”顶元素删除。
异常处理
现在还是小白,不熟悉接口也不了解机制,以后再看吧……