快速掌握Lua 5.3 —— 从Lua中调用C函数

Posted VermillionTear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速掌握Lua 5.3 —— 从Lua中调用C函数相关的知识,希望对你有一定的参考价值。

Q:Lua调用C函数的两种方式?

A:
1、程序主体在C中运行,C函数注册到Lua中。C调用Lua,Lua调用C注册的函数,C得到函数的执行结果。
2、程序主体在Lua中运行,C函数作为库函数供Lua使用。
第一种方式看起来很罗嗦,也很奇怪。既然程序主体运行在C中,而且最终使用的也是C中定义的函数,那么为何要将函数注册给Lua,然后再通过Lua调用函数呢?
相比于第一种方式,第二种方式使用的更加普遍。
一个Lua库(Lua本身所提供的库)实际上是一个定义了若干Lua函数的”chunk”,这些函数通常作为”table”的域来保存。一个C库(C语言编写,注册给Lua使用的库)的实现方式类似于Lua库的实现方式。首先C库中定义提供给Lua使用的函数,其次还需要一个“特殊函数”,它的作用是注册所有C库中的函数,并将它们存储在适当的位置(类似于Lua库中的函数作为”table”的域来保存)。
Lua可以调用C库中的函数,就是通过这个注册的过程实现的。一旦C函数注册到Lua中,Lua就可以直接通过C函数的引用获取到C函数的地址(这也是我们注册的意义,将C函数的地址提供给Lua)。换句话说,一旦C函数注册,Lua调用他们不依赖于函数名,”package”位置,或者是可见规则。
以上两种方式下面都会列举对应的例子,理解第一种方式,将有助于你理解第二种方式的实现流程。

Q:从Lua中调用C所遵循的规则?

A:当C调用Lua函数的时候,必须遵循一些简单的协议来传递参数和获取返回结果。同样的,从Lua中调用C函数,也必须遵循一些协议来传递参数和获得返回结果。此外,从Lua调用C函数我们必须注册函数,也就是说,我们必须把C函数的地址以一个适当的方式传递给Lua解释器。
任何在Lua中注册的C函数必须有同样的原型,
typedef int (*lua_CFunction) (lua_State *L); // 定义在"lua.h"中。
被注册的C函数接收一个单一的lua_State类型的参数,同时返回一个表示返回值个数的数字。函数在将返回值入栈之前无需清理栈,在函数返回之后,Lua会自动清除栈中返回结果下面的所有内容。

Q:如何在C中调用注册给Lua的C函数?

A:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_sin(lua_State *L)

    // 如果给定虚拟栈中索引处的元素可以转换为数字,则返回转换后的数字,否则报错。
    double d = luaL_checknumber(L, 1);
    lua_pushnumber(L, sin(d));  /* push result */

    /* 这里可以看出,C可以返回给Lua多个结果,
     * 通过多次调用lua_push*(),之后return返回结果的数量。
     */
    return 1;  /* number of results */


int main(void)

    lua_State *L = luaL_newstate();    // 创建Lua状态机。
    luaL_openlibs(L);    // 打开Lua状态机"L"中的所有Lua标准库。

    /* 这两句话还有更简单的方法:
     * lua_register(L, "mysin", l_sin)
     * 将C函数"l_sin"定义为Lua的全局变量"mysin"。
     * 其实现是如下宏:
     * #define lua_register(L,n,f) \\
     *      (lua_pushcfunction(L, f), lua_setglobal(L, n))
     */
    lua_pushcfunction(L, l_sin);    // 将C函数转换为Lua的"function"并压入虚拟栈。
    lua_setglobal(L, "mysin");    // 弹出栈顶元素,并在Lua中用名为"mysin"的全局变量存储。

    const char* testfunc = "print(mysin(3.14 / 2))";

    if(luaL_dostring(L, testfunc))    // 执行Lua命令。
        printf("Failed to invoke.\\n");
    lua_close(L);    // 关闭Lua状态机。

    return 0;
prompt> gcc main.c -llua -ldl -lm -Wall
prompt> ./a.out
0.99999968293183

另一个例子(假定我们的系统符合”POSIX”标准)的功能类似于ls,将指定目录中的所有文件以数组的形式返回。当有错误发生时,返回nil加上一个描述错误信息的字符串。

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <dirent.h>
#include <errno.h>

static int l_dir(lua_State *L)

    DIR *dir;
    struct dirent *entry;
    int i = 0;
    // 如果给定虚拟栈中索引处的元素可以转换为字符串,则返回转换后的字符串,否则报错。
    const char *path = luaL_checkstring(L, 1);

    /* open directory */
    dir = opendir(path);
    if(dir == NULL) 
        // 出错返回"nil"加上一个描述错误信息的字符串。
        lua_pushnil(L);
        lua_pushstring(L, strerror(errno));
        return 2;    // "nil"加上字符串,共两个返回值。
    

    /* create result table */
    lua_newtable(L);
    i = 1;
    while((entry = readdir(dir)) != NULL)    // 逐一读取目录中的文件。
    
        lua_pushnumber(L, i++);  /* push key */
        lua_pushstring(L, entry->d_name);  /* push value */
        lua_settable(L, -3);    // t[k] = v
    

    closedir(dir);

    return 1;    // 返回值只有一个,"table"。


int main(void)

    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // 将C函数"l_dir"定义为Lua的全局变量"mydir"。
    lua_register(L, "mydir", l_dir);

    // 打印"/home/"目录下的所有文件。
    const char* testfunc = "for i, v in pairs(mydir('/home')) do print(i, v) end";

    if(luaL_dostring(L, testfunc))    // 执行Lua命令。
        printf("Failed to invoke.\\n");

    lua_close(L);

    return 0;
prompt> gcc main.c -llua -ldl -lm -Wall
prompt> ./a.out
1   vermilliontear
2   git
3   .
4   ..
5   lost+found

Q:如何在Lua中调用作为库函数提供给Lua的C函数?

A:我们使用上面的第一个例子,将其改造为C库的方式。
“mylib.c”文件中:

#include <stdio.h>
#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

/* 所有注册给Lua的C函数具有
 * "typedef int (*lua_CFunction) (lua_State *L);"的原型。
 */
static int l_sin(lua_State *L)
   
    // 如果给定虚拟栈中索引处的元素可以转换为数字,则返回转换后的数字,否则报错。
    double d = luaL_checknumber(L, 1);
    lua_pushnumber(L, sin(d));  /* push result */

    /* 这里可以看出,C可以返回给Lua多个结果,
     * 通过多次调用lua_push*(),之后return返回结果的数量。
     */
    return 1;  /* number of results */


/* 需要一个"luaL_Reg"类型的结构体,其中每一个元素对应一个提供给Lua的函数。
 * 每一个元素中包含此函数在Lua中的名字,以及该函数在C库中的函数指针。
 * 最后一个元素为“哨兵元素”(两个"NULL"),用于告诉Lua没有其他的函数需要注册。
 */
static const struct luaL_Reg mylib[] = 
    "mysin", l_sin,
    NULL, NULL
;

/* 此函数为C库中的“特殊函数”。
 * 通过调用它注册所有C库中的函数,并将它们存储在适当的位置。
 * 此函数的命名规则应遵循:
 * 1、使用"luaopen_"作为前缀。
 * 2、前缀之后的名字将作为"require"的参数。
 */
extern int luaopen_mylib(lua_State* L)

    /* void luaL_newlib (lua_State *L, const luaL_Reg l[]);
     * 创建一个新的"table",并将"l"中所列出的函数注册为"table"的域。
     */ 
    luaL_newlib(L, mylib);

    return 1;

将”mylib.c”编译为动态连接库,

prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c    mylib.so    a.lua

“a.lua”文件中:

--[[ 这里"require"的参数对应C库中"luaopen_mylib()"中的"mylib"。
     C库就放在"a.lua"的同级目录,"require"可以找到。]]
local mylib = require "mylib"

-- 结果与上面的例子中相同,但是这里是通过调用C库中的函数实现。
print(mylib.mysin(3.14 / 2))    --> 0.99999968293183

附加:

1、每一个与Lua通信的C函数都有其独有的虚拟栈。
2、在极端情况下,打印指定目录中文件的例子可能会造成小小的内存泄漏。在内存空间不足的情况下,l_dir()中的lua_newtable()lua_pushstring()lua_settable()都会立即抛出错误并终止程序的运行,这将导致closdir()无法被调用。
3、通常C库中“特殊的函数”都被定义为公有的(使用extern修饰),而其他函数均被定义为私有的(使用static修饰)。
4、当你想要使用C函数扩展你的Lua程序时,即使只有一个C函数,也最好使用C库的方式。因为在不久的将来(通常来说会很快),你将需要其他的C函数。

以上是关于快速掌握Lua 5.3 —— 从Lua中调用C函数的主要内容,如果未能解决你的问题,请参考以下文章

快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧

快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧

快速掌握Lua 5.3 —— 调试库

快速掌握Lua 5.3 —— userdata

快速掌握Lua 5.3 —— userdata

快速掌握Lua 5.3 —— 字符串库