C调用Lua

图片来源:https://www.pixiv.net/artworks/82570722

Lua官方解释器完全使用ANSI C编写,并且提供了C API,使用C/C++和Lua交互相当方便

Lua虚拟栈

C和Lua的交互基于一个栈,这个栈的每个位置都有两个索引值,这里只需要记住这个栈的栈顶索引始终为“-1”,栈底索引始终为“1”即可

基本环境加载

后续的代码示例均基于“test.lua”“main.c”这两个文件

-- Lua 5.4.4  Copyright (C) 1994-2022 Lua.org, PUC-Rio

a_bool_true = true
a_bool_false = false
a_integer = 214;
a_float_numer = 3.1415926
a_string = "hello"
a_string_has_zero = "hel\0lo"
a_int_arr = {1, 4, 9, 16, 25}

function Concatenate(str1, str2)
    return str1..","..str2;
end

rect = {
    length = 13,
    width = 17
}

function rect:Area()
    return self.length * self.width
end

dict = {}
dict[17] = "this is value, key is number 17"
dict["key1"] = "this is value, key is string \"key1\""
dict[Concatenate] = "this is value, key is fuction Concatenate"
#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

int main(void){
    //创建Lua主线程
    lua_State *L = luaL_newstate();
    if (!L) {
        fprintf(stderr, "Failed to create new Lua state");
    }

    //加载Lua标准库
    luaL_openlibs(L);

    //加载test.lua
    if (luaL_loadfile(L, "test.lua") || lua_pcall(L, 0, 0, 0)) {
        fprintf(stderr, lua_tostring(L, -1));
        lua_close(L);
        return -1;
    }

    /*
        后续的示例代码应该放在这里运行
    */

    //结束运行,释放资源
    lua_close(L);
    return 0;
}

Lua C API的函数大多第一个参数都为“lua_State *L”,即“main.c”中使用“luaL_newstate()”创建的lua住线程,后续不再说明

参考编译命令

gcc main.c `pkg-config --cflags --libs lua`

C获取Lua变量

Lua的C API提供 “lua_getglobal” 来将指定全局变量压入栈,然后提供一系列“lua_toxxx”函数来将栈中的变量转换为C类型变量,相关函数在使用前

// name参数是lua脚本中的全局变量名,返回值是变量的lua类型,在lua.h中有相关宏定义
int lua_getglobal (lua_State *L, const char *name);
#define LUA_TNONE		(-1)

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

#define LUA_NUMTYPES		9
// index参数是要转换的变量在栈中的位置
int lua_toboolean (lua_State *L, int index);
lua_Integer lua_tointeger (lua_State *L, int index);
lua_Number lua_tonumber (lua_State *L, int index);
const char *lua_tostring (lua_State *L, int index);

获取布尔值、数值、字符串

布尔值

由于C语言里没有对应布尔类型,会将布尔类型值转换为整形的“1”和“0”。使用“lua_typename”函数可以将表示lua类型的整数转换为对应C字符串

int type;
type = lua_getglobal(L, "a_bool_true"); // 将a_bool_true变量压入栈
int lua_bool_true = lua_toboolean(L, -1); // 将栈顶变量转换为布尔值,但在C中会将true转换为整数1
// 打印变量lua类型名称(boolean)和变量值(1)
printf("type:%s, value:%d\n", lua_typename(L, type), lua_bool_true); 

type = lua_getglobal(L, "a_bool_false"); // 将a_bool_false变量压入栈
int lua_bool_false = lua_toboolean(L, -1); // 将栈顶变量转换为布尔值,但在C中会将false转换为整数0
// 打印变量lua类型名称(boolean)和变量值(0)
printf("type:%s, value:%d\n", lua_typename(L, type), lua_bool_false); 

lua_pop(L,2); // 用完之后记得清理栈,第二个参数是要弹出栈的变量的数量

数值

lua脚本中不显式区分整数和浮点数,所以“lua_getglobal”返回的类型都是“LUA_TNUMBER” ,但转换为C变量时需要区分,分别使用“lua_tointeger”和“lua_tonumber

type = lua_getglobal(L, "a_integer");
lua_Integer a_lua_integer = lua_tointeger(L, -1); // 将栈顶变量转换为整数
// 打印变量lua类型名称(number)和变量值(214)
printf("type:%s, value:%lld\n", lua_typename(L, type), a_lua_integer); 

type = lua_getglobal(L, "a_float_numer");
lua_Number a_float_numer = lua_tonumber(L, -1); // 将栈顶变量转换为浮点数
// 打印变量lua类型名称(number)和变量值(3.141593)
printf("type:%s, value:%lf\n", lua_typename(L, type), a_float_numer); 

lua_pop(L,2);

字符串

获取字符串差不多,“lua_tostring”会在末尾补 '\0'

type = lua_getglobal(L, "a_string");
const char* a_lua_string = lua_tostring(L, -1);
// 打印变量lua类型名称(string)和变量值(hello)
printf("type:%s, value:%s\n", lua_typename(L, type), a_lua_string);
lua_pop(L,1);

但如果字符串本身内容也包含 '\0' 的话无法判断真实长度,此时可以使用“lua_tolstring”传递第三个参数保存字符串长度

const char *lua_tolstring (lua_State *L, int index, size_t *len);

分别使用“printf”和“fwrite”打印获取到的字符串

type = lua_getglobal(L, "a_string_has_zero");
size_t len = ;
// 将栈顶变量转换为字符串,转换后的长度保存到len中
const char* a_string_has_zero = lua_tolstring(L, -1, &len);
// 虽然长度为6,但字符串还是只打印出hel
printf("type:%s, value:%s, len:%d, ", lua_typename(L, type), a_string_has_zero, len);
// 使用fwrite打印完整的字符串,由于\0不可打印,只能看见hello
printf("complete_string:");
fwrite(a_string_has_zero, sizeof(char), len, stdout);
printf("\n");
lua_pop(L,1);

要在命令行确认是否有’\0’输出可以将输出通过管道传递给‘cat -v’命令,’\0’会以“^@”的形式显示

./a.out | cat -v
type:string, value:hel, len:6, complete_string:hel^@lo

获取表中的变量

通过变量名获取

获取表中的变量,需要先将表压入栈,然后把这个表在栈中的位置要获取的变量名传给“lua_getfield”,“lua_getfield”会将变量压入栈

// index是表在栈中的索引,k是要获取的变量名
int lua_getfield (lua_State *L, int index, const char *k);
// 先将获取表压入栈
int type_rect = lua_getglobal(L, "rect");
// 将rect.length压入栈
int type_lenght = lua_getfield(L, -1, "length");
lua_Integer rect_length = lua_tointeger(L, -1);
printf("type_rect:%s, type_lenght:%s, rect.length:%d\n", 
        lua_typename(L, type_rect), lua_typename(L, type_lenght), rect_length);
lua_pop(L,2);

通过key获取

有时候表中成员没有变量名,比如数组和字典,此时可以先将表压入栈,然后通过“lua_pushxxx”系列函数(或者其他方式)将key压入栈,之后调用“lua_gettable”将table[key]压入栈

void lua_pushboolean (lua_State *L, int b);
void lua_pushinteger (lua_State *L, lua_Integer n);
void lua_pushnumber (lua_State *L, lua_Number n);
const char *lua_pushstring (lua_State *L, const char *s);
// index是表在栈中的索引
// 该函数会把栈顶值当作key
// 调用后会自动把key弹出栈,并将table[key]压入栈
int lua_gettable (lua_State *L, int index);
// 将dict表压入栈
lua_getglobal(L, "dict");

// 将整数17压入栈
lua_pushinteger(L, 17);
// 将17弹出栈,并将dict[17]压入栈
lua_gettable(L, -2);
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);

// 将字符串"key1"压入栈
lua_pushstring(L, "key1");
// 将"key1"弹出栈,并将dict["key1"]压入栈
lua_gettable(L, -2);
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);

// 甚至函数也可以作为key, 将全局函数Concatenate压入栈
lua_getglobal(L, "Concatenate");
// 将Concatenate函数弹出栈,并将dict[Concatenate]压入栈
lua_gettable(L, -2);
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);

// 将dict表弹出栈
lua_pop(L, 1);

遍历数组

操作数组前需要先将数组,压入栈,获取数组长度使用“luaL_len”,该函数会直接返回数组长度。访问数组中的单个值使用“lua_geti”传入数组在栈中的索引和要访问的数组下标

// index是数组在栈中的索引,返回值为数组长度
lua_Integer luaL_len (lua_State *L, int index);
// index是数组在栈中的索引,i是数组下标
// 调用结束会将arr[i]压入栈
int lua_geti (lua_State *L, int index, lua_Integer i);
// 将数组压入栈
lua_getglobal(L, "a_int_arr");
// 获取数组长度
size_t arr_len = luaL_len(L, -1);
// 注意lua数组下标从1开始
for(size_t i = 1; i <= arr_len; i++){
    // 将数组第i个值压入栈
    lua_geti(L, -1, i);
    printf("%d,",lua_tointeger(L, -1));
    // 将数组第i个值弹出栈
    lua_pop(L, 1);
}
printf("\n");
// 将数组弹出栈
lua_pop(L, 1);

C调用Lua函数

在C中调用Lua函数需要先将函数压入栈,然后依次将函数参数入栈,调用“lua_pcall”设置函数参数数量返回值数量错误信息位置即可,函数返回值会依次压入栈中

// nargs是函数输入参数数量,nresults是函数返回值数量,msgh设置成0表示调用出错时直接将错误信息入栈
// 返回值为LUA_OK(0)时表示函数调用未出错
// lua_pcall执行后会把lua函数和输入参数弹出栈,然后将lua函数的返回值依次入栈
int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
#define LUA_OK		0
#define LUA_YIELD	1
#define LUA_ERRRUN	2
#define LUA_ERRSYNTAX	3
#define LUA_ERRMEM	4
#define LUA_ERRERR	5

调用全局函数

// 将全局函数Concatenate压入栈
lua_getglobal(L, "Concatenate");
// 将调用Concatenate需要的参入依次压入栈
lua_pushstring(L, "Hello");
lua_pushstring(L, "World!");
// 调用Concatenate,两个输入参数,一个返回值,0表示调用失败时直接将错误信息压入栈
if (lua_pcall(L, 2, 1, 0)) {
    fprintf(stderr, lua_tostring(L, -1));
    lua_pop(L, 1);
    lua_close(L);
    return -1;
}
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);

调用对象的成员函数

其实和调用全局函数没太大区别,就是需要先获取对象再获取对象的函数,另外成员函数定义时使用“:”语法省略了self参数,但C调用的时候不能省略

rect = {
    length = 13,
    width = 17
}

function rect:Area()
    return self.length * self.width
end
// 将rect对象压入栈
lua_getglobal(L, "rect");
// 将rect.Area压入栈, Area定义时使用':'语法省略了self参数,但我们调用时不能省略
lua_getfield(L, -1, "Area");
// 将栈顶移动到-2,即将栈顶两个元素交换位置,这就可以直接将rect作为self参数传给Area
lua_insert(L, -2);
if (lua_pcall(L, 1, 1, 0)) {
    fprintf(stderr, lua_tostring(L, -1));
    lua_pop(L, 1);
    lua_close(L);
    return -1;
}
printf("%d\n", lua_tointeger(L, -1));
lua_pop(L, 1);

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

发表评论

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