programming in lua (6)

中断了的 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 的值修改为“栈”顶元素的值,并且将“栈”顶元素删除。

异常处理

现在还是小白,不熟悉接口也不了解机制,以后再看吧……

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注