最近在进入 Lua 编程的状态,一度令我困惑的是,Lua 提供的功能少的可怜,跟自备电池的 python 相比,可说是简陋了。连 table 的打印,都需要自己实现,也因此有了一打的第三方方案。后来我想明白了,以 Lua 和 C 如此紧密的关系,只需要建立 Lua 的 binding,那么丰富而性能强大的 C 库资源完全可以为 Lua 所用,这样就不愁功能缺失了。

关于 C 调用 Lua,前段时间已经写过一篇短文了:《多语言协作与二进制交互》,现在补上从 Lua 调用 C 的这一篇。先上一段 Lua 代码:

#!/usr/bin/lua
require("power")
print(square(1.414213598))
print(cube(5))

在这段代码里,我引用了一个 power 模块,并且调用了 power 模块里的 square 函数和 cube 函数。由此可知,在 C 语言编写的 power 模块里面,需要有相关的函数可以注册本模块提供的函数,并且让 Lua 可以感知到自身是一个模块。回忆一下 Lua 的路径搜索,可以看到除了后缀名为 *.lua 的文件之外,还有 *.so 文件,所以 C 扩展是编译为 .so 的。实际情况是,在执行 require 语句的时候,系统会调用 luaopen_power 函数,这个函数名是通过 luaopen_ 与 power 这个模块名拼接得到的。

现在看看 luaopen_power 函数的定义:

int luaopen_power(lua_State *L){
    lua_register(
            L,               /* Lua 状态机 */
            "square",        /*Lua 中的函数名 */
            isquare          /* 当前文件中的函数名 */
            );  
    lua_register(L,"cube",icube);
    return 0;
}

可以看到,通过调用 lua_register 函数,我们在 L 这个 lua 虚拟机里面注册了两个函数,一个是 square,一个是 cube,他们分别对应到 isquare 和 icube 这两个 C 函数。

回过头来看一看,lua 脚本里,这条简单的 require 语句,执行了两个步骤:一是先把名字为 power.so 的文件加载起来,二是调用其中的 luaopen_power 函数。下面来看一下具体的函数如何定义:

static int isquare(lua_State *L){              /* C 中的函数名 */
    float rtrn = lua_tonumber(L, -1);      /* 从 Lua 虚拟机里取出一个变量,这个变量是 number 类型的 */
    printf("Top of square(), nbr=%f\n",rtrn);
    lua_pushnumber(L,rtrn*rtrn);           /* 将返回值压回 Lua 虚拟机的栈中 */
    return 1;                              /* 这个返回值告诉 lua 虚拟机,我们往栈里放入了多少个返回值 */
}

注释的讲解比较详细了,可以看到为 Lua 定义的 C 函数格式都是比较统一的,首先接受一个 Lua 虚拟机变量 L,然后从 L 里取出相应的参数(需要指定数据类型),最后将返回值再次压回虚拟机里面,通过返回 int 告诉 Lua 虚拟机,自己的返回值有多少个。

好,到这里就只差最后一步了,现在把这段 C 代码编译成 .so 文件。使用如下的编译参数:

ubuntu@ubuntu:~$ gcc -Wall -shared -fPIC -o power.so  -I/usr/include/lua5.1 -llua5.1   hellofunc.c

(hellofunc.c 记得要 include lua.h, lauxlib.h, lualib.h 三个头文件哦)

这里解释一下两个参数的意思,-shared 是告诉 gcc,需要编译成 .so 文件,并且这个源文件里面不会有 main 函数,不要大惊小怪。另一个 -fPIC,是 Position Independent Code 的意思,具体的含义可以参考 这篇 ,主要用来避免同一份代码因为重定位位置不同而在内存中存在多个实例。

调用的结果:

ubuntu@ubuntu:~$ ./hellofunc.lua
Top of square(), nbr=1.414214
2.0000002687177
Top of cube(), number=5.000000
125

当然,如果机器安装有多个版本的 Lua,需要指定执行 hellofunc.lua 的 lua 解释器版本。因为本篇教程是针对 Lua5.1 的,所以需要指定 Lua5.1 来执行,例如 lua5.1 hellofunc.lua,否则用 lua5.2 调用会引起 core dump