Contents

LUA开发那些事

前言

lua是一门很小巧的脚本语言,完全是由C实现。正因为它的小巧,所以现在很多项目拿它来做嵌入执行的脚本,这中操作在游戏行业中非常普遍。

1. lua 开发过程中的坑

1.1 薛定谔的table长度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local t1 = {
    ["key1"] = "value1",
    ["key2"] = "value2",
    ["key3"] = "value3"
}
local t2 = {
    "value1",
    "value2",
    "value3"
}
local t3 = {
    ["key1"] = "value1",
    "value2"
}
local t4 = {}
local t5 = {nil, nil}

上面的5个table, 如果使用 # 或者 table.getn 运算计算table长度的话各为多少呢? 结果是:

t1 size: 0, t2 size: 3, t3 size: 1, t4 size: 0, t5 size: 0

读到这你时候可能有疑问了,t1中明明有三个值,为啥它的长度就为0呢。还有t3它不是有两个值么,为啥它的长度又为1呢

直接结论 就是,# 运算只计算数组值的个数,像key-value这种map形式的元素不参与长度计算

1.2 空与不空

对于最后两个table, 如果如果使用判空操作又会得到什么结果呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if t4 == nil then
    print("t4 is nil")
else
    print("t4 is not nil")
end
if t5 == nil then
    print("t5 is nil")
else
    print("t5 is not nil")
end

执行结果是:

t4 is not nil

t5 is not nil

t4 明明没有任何值,它为啥就不为空呢

原因就是:t4是个空table它有地址所以它不等于nil,类似于C中的分配了地址但是内容为空的情况,这时候判断此地址是否为空,那肯定不为空嘛

1.3 table比较

1
2
3
4
5
6
7
t6 = {}
t7 = {}
if t6 == t7 then
    print("t6 is equal t7")
else
    print("t6 is not equal t7")
end

执行结果是:

t6 is not equal t7

为啥明明 t6 和 t7 都是空它两就不相等呢?

原因就是: lua中如果是table比较的话,比较的两个table的地址,并不是内容

2. lua 与 C 的互相调用

因为lua是由C实现的,所以其和C的交互非常方便

2.1 C 中运行lua代码

在常用的C 开发中,都会将lua嵌入做脚本语言,方便相关业务快速开发或迭代。 查看lua doc 我们会发现有好几个接口都能执行lua脚本。

lua c doc

lua c api

函数名描述
lua_loadLoads a Lua chunk, it does not run it
luaL_dofileLoads and runs the given file
luaL_dostringLoads and runs the given string
luaL_loadfileLoads a file as a Lua chunk, it does not run it
luaL_loadstringLoads a string as a Lua chunk, it does not run it
luaL_loadbufferLoads a buffer as a Lua chunk, same as lua_load

C 中运行lua的简单例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const char *lua_str = " \
local a = 1234      \n \
local b = \"xxyy\"      \n \
print(string.format(\"a is %d, b is %s\", a, b)) \
";
int main()
{
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    if (luaL_dostring(L, lua_str) != 0)
    {
        printf("luaL_dostring failed, %s\n", lua_tostring(L, -1));
    }
    return 0;
}

执行结果:

a is 1234, b is xxyy

2.2 C 导出接口给lua中使用

在平常的开发过程中,我们可能需要导出一些C函数给到业务层的lua调用。C 函数导出后是以动态库的形态被lua加载,然后在lua中以module的形式存在

2.2.1 C 导出普通函数

C 导出函数给到lua调用有两种方式,一种是普通的导出方式,另一种也是“普通”的导出方式

普通的导出方式就是使用lua 的CAPI, luaL_register

例如导出一个简单的hello_world接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int hello_world(lua_State *L)
{
    const char *name = luaL_checkstring(L, -1);
    printf("from c function, %s tell hello world\n", name);
    lua_pushinteger(L, 18);     // 返回一个整数
    return 1; 
}
/* 导出函数列表及函数调用入口 */
static luaL_Reg func_list[] = {
    {"hello_world", hello_world},
    {NULL, NULL}
};
LUALIB_API int luaopen_test(lua_State *L)
{
    luaL_register(L, "test", func_list);
    return 1;
}

调用上述C库的lua脚本如下:

1
2
3
local test = require("test")
local output = test.hello_world("zhangshan")
print(string.format("hello_world output is %d", output))

执行结果:

from c function, zhangshan tell hello world hello_world output is 18

另一种“普通”的导出方法参考导出类部分,(●'◡'●)

2.2.2 C中导出lua类

有些操作是附加在某些对象上的,所以我们可能需要在C中导出lua的类

下面就是一个简单的读写文件的操作类的导出:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
/* key name in registry */
#define TEST_CALSS_ID  "test.class"
typedef struct file_udata {
    FILE *fp;
}file_udata_t;
int open_file(lua_State *L) {
    const char *file = luaL_checkstring(L, 1);
    FILE *fp = fopen(file, "a+");
    if (!fp) {
        lua_pushnil(L);
        return 1;
    }
    else {
        file_udata_t *ui = lua_newuserdata(L, sizeof(file_udata_t));
        ui->fp = fp;
        luaL_getmetatable(L, TEST_CALSS_ID);
        lua_setmetatable(L, -2);
    }
    return 1;
}
int close_file(lua_State *L) {
    file_udata_t *ud = (file_udata_t *)luaL_checkudata(L, 1, TEST_CALSS_ID);
    luaL_argcheck(L, ud != NULL, 1, "unexpected object class");
    int ret = fclose(ud->fp);
    lua_pushboolean(L, ret?0:1);
    return 1;
}
int append_file(lua_State *L)
{
    file_udata_t *ud = (file_udata_t *)luaL_checkudata(L, 1, TEST_CALSS_ID);
    luaL_argcheck(L, ud != NULL, 1, "unexpected object class");
    const char *str = luaL_checkstring(L, 2);
    size_t size = strlen(str);
    if (fwrite(str, 1, size, ud->fp) == size) {
        lua_pushboolean(L, 1);
    }
    else {
        lua_pushboolean(L, 0);
    }
    return 1;
}
int readall_file(lua_State *L)
{
    file_udata_t *ud = (file_udata_t *)luaL_checkudata(L, 1, TEST_CALSS_ID);
    luaL_argcheck(L, ud != NULL, 1, "unexpected object class");
        // get file size
    fseek(ud->fp, 0, SEEK_END);
    size_t size = ftell(ud->fp);
    rewind(ud->fp);
    if (size == 0) {
        lua_pushnil(L);
        return 1;
    }
    char *out = malloc(size);
    if (size != fread(out, 1, size, ud->fp)) {
        lua_pushnil(L);
        return 1;
    }
    lua_pushstring(L, out);
    free(out);
    return 1;
}
static void make_namespace(lua_State *L, const char *name)
{
    lua_getglobal(L, name);
    if (lua_isnoneornil(L, -1)) {
        lua_pop(L, 1);
        lua_newtable(L);
        lua_setglobal(L, name);
        lua_getglobal(L, name);
    }
    assert(lua_istable(L, -1));
}
static void register_function(lua_State *L, const char *name, lua_CFunction func)
{
    lua_pushstring(L, name);
    lua_pushcfunction(L, func);
    lua_settable(L, -3);  
}
LUALIB_API int luaopen_testclass(lua_State *L) {
    // regist module private function
    luaL_newmetatable(L, TEST_CALSS_ID);
    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);
    lua_settable(L, -3);            /* metatable.__index = metatable */

    register_function(L, "appendfile", append_file);
    register_function(L, "readallfile", readall_file);
    register_function(L, "closefile", close_file);
    lua_pop(L, 1);

    // registe module global function 
    make_namespace(L, "testclass");
    register_function(L, "openfile", open_file);

    lua_pushliteral(L, "__META__");
    luaL_getmetatable(L, TEST_CALSS_ID);
    lua_rawset(L, -3);
    lua_pop(L, 1);

    lua_getglobal(L, "testclass");
    return 1;
}

测试的lua脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local tc = require("testclass")
local file = tc.openfile("test.txt")
if file then
    file:appendfile("this is a test txt in file")
    local str = file:readallfile()
    print("test.txt: " .. tostring(str))
    file:closefile()
else
    print("openfile test.txt failed")
end

执行结果:

test.txt: this is a test txt in file

2.2.3 C中执行lua的闭包函数

在业务层写lua脚本的过程中,我们会经常使用到lua的闭包函数,特别是在异步执行的过程中 例如:

1
2
3
4
5
6
7
local func = function(param, callback)
    local str = "param: " .. tostring(param)
    callback(1, str)
end
func(123, function(code, msg)
    print(string.format("from callback: code is %d msg is %d", code, msg))
end)

上面例子中调用的是本地实现的func, lua中参数获取或返回还是比较简单,但是如果此 func 是在C中实现的话,参数传递又该如何操作呢?

依然是实现一个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int function_from_c(lua_State *L)
{
    int code = luaL_checkint(L, 1);
    lua_pushvalue(L, 2);            /* copy closure to top */
    lua_pushinteger(L, code);
    lua_pushstring(L, "c function");
    //call the c function, two paramter, no return value
    if (lua_pcall(L, 2, 0, 0)) {
        printf("error executing callback, error: %s\n", lua_tostring(L, -1));
    }
    return 0; 
}
static luaL_Reg func_list[] = {
    {"func", function_from_c},
    {NULL, NULL}
};
LUALIB_API int luaopen_testclosure(lua_State *L)
{
    luaL_register(L, "testclosure", func_list);
    return 1;
}

测试的lua脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local ts = require("testclosure")
local func = function(param, callback)
    callback(param, "lua function")
end
func(123, function(code, msg)
    print(string.format("from callback: code is %d msg is %s", code, msg))
end)
ts.func(123, function(code, msg)
    print(string.format("from callback: code is %d msg is %s", code, msg))
end)

结果:

from callback: code is 123 msg is lua function

from callback: code is 123 msg is c function

2.2.4 C中的lua栈平衡

由于lua调用C时传参是通过lua的 虚拟栈 实现的,所以这其中就必然涉及到栈平衡的问题。

每当 Lua 调用 C 时,被调用的函数都会获得一个新的堆栈,该堆栈独立于先前的堆栈和仍处于活动状态的 C 函数堆栈。此堆栈包含C函数的参数,并且是C函数将其结果返回给调用者的地方。

栈结构示意图如下:

/images/lua_dev_01.png

可以通过 luaL_check* 系列接口获取传递给C函数的参数, 这时候需要确保只获取指定数量的参数,不然后会引发panic。在使用完参数后可以将参数丢弃或者弹出到全局变量等其他地方,这时候也需要确保只从栈中弹出了指定数量的参数,否则也会造成panic。

处理完成后可以使用 luaL_push* 系列接口将返回值放入栈中。这时候C函数的返回值表示需要返回的参数个数,这时候就需要确保栈中有指定数量的值,否则就会引发panic错误。

2.2.5 C中栈交换的问题

a. 在一些特殊的场景我们需要交换两个栈中的值,这时候可以使用 lua_xmove 接口来交换两个栈中的数据

依然是举个栗子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const char *lua_func_str = " \
local gen_table = function(value1, value2) \n \
    return {[\"item1\"] = value1, [\"item2\"] = value2} \n \
end \n \
local gen_int = function(value) \n \
    return value + 1 \n \
end \n \
local gen_string = function(value) \n \
    return \"value is \" .. tostring(value) \n \
end \n \
return gen_table, gen_int, gen_string \n \
";
static lua_State *lua_func_state = NULL;
static void init_lua_func(void)
{
    if (!lua_func_state) {
        lua_func_state = luaL_newstate();
    }
    luaL_openlibs(lua_func_state);
    if (luaL_dostring(lua_func_state, lua_func_str) != 0) {
        printf("luaL_dostring failed, %s\n", lua_tostring(lua_func_state, -1));
        return;
    }
    // lua function is on stack
    if (!lua_isfunction(lua_func_state, 1) || !lua_isfunction(lua_func_state, 2) || !lua_isfunction(lua_func_state, 3)) {
        printf("load lua function is failed\n");
        return;
    }
    lua_setglobal(lua_func_state, "gen_string");
    lua_setglobal(lua_func_state, "gen_int");
    lua_setglobal(lua_func_state, "gen_table");
}

int xchange_int(lua_State *L)
{
    int value = luaL_checkint(L, 1);
    lua_settop(lua_func_state, 0);
    lua_getglobal(lua_func_state, "gen_int");
    lua_pushinteger(lua_func_state, (lua_Integer)value);
    if (lua_pcall(lua_func_state, 1, 1, 0) || !lua_isnumber(lua_func_state, -1)) {
        printf("execute lua function gen_int failed, %s\n", luaL_checkstring(lua_func_state, -1));
        lua_pushnil(L);
        return 1;
    }
    lua_settop(L, 0);
    // exchange value 
    lua_xmove(lua_func_state, L, 1);
    return 1;
}
int xchange_string(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    lua_settop(lua_func_state, 0);
    lua_getglobal(lua_func_state, "gen_string");
    lua_pushstring(lua_func_state, str);
    if (lua_pcall(lua_func_state, 1, 1, 0) || !lua_isstring(lua_func_state, 1)) {
        printf("execute lua function gen_int failed, %s\n", luaL_checkstring(lua_func_state, -1));
        lua_pushnil(L);
        return 1;
    }
    lua_settop(L, 0);
    // exchange value 
    lua_xmove(lua_func_state, L, 1);
    return 1;
}
int xchange_table(lua_State *L)
{
    const char *value1 = luaL_checkstring(L, 1);
    const char *value2 = luaL_checkstring(L, 2);
    lua_settop(lua_func_state, 0);
    lua_getglobal(lua_func_state, "gen_table");
    lua_pushstring(lua_func_state, value1);
    lua_pushstring(lua_func_state, value2);
    if (lua_pcall(lua_func_state, 2, 1, 0) || !lua_istable(lua_func_state, 1)) {
        printf("execute lua function gen_int failed, %s\n", luaL_checkstring(lua_func_state, -1));
        lua_pushnil(L);
        return 1;
    }
    lua_settop(L, 0);
    // exchange value 
    lua_xmove(lua_func_state, L, 1);
    return 1;
}
static luaL_Reg func_list[] = {
    {"xchange_int", xchange_int},
    {"xchange_string", xchange_string},
    {"xchange_table", xchange_table},
    {NULL, NULL}
};
LUALIB_API int luaopen_testexchange(lua_State *L)
{
    init_lua_func();
    luaL_register(L, "testexchange", func_list);
    return 1;
}

测试的lua代码如下:

1
2
3
4
5
6
7
local tx = require("testexchange")
local v1 = tx.xchange_int(10)
print("xchange_int result: " .. tostring(v1))
local v2 = tx.xchange_string("hello")
print("xchange_string result: " .. tostring(v2))
local v3 = tx.xchange_table("zhangsan", "lisi")
print(string.format("xchange_table reault: %s %s", tostring(v3["item1"]), tostring(v3["item2"])))

执行结果如下:

xchange_int result: 11

xchange_string result: value is hello

xchange_table reault: nil nil

结论就是: 可以在不同的lua_State交换栈上的普通值(int, string等), 不能交换稍微高级的值(table, userdata等)

b. 改变栈中数据的位置

在C接口编码的过程中,我们的C接口可能需要将当前栈上的参数做一些位置调换,那该怎么办呢? luac的接口也考虑到了这种需求,只需调用相关的API就可以做相应的位置变换

函数接口说明
lua_insert将栈顶的元素插入到指定的位置,其他元素相应的往前挪位置
lua_replace将栈顶元素替换指定位置的元素,其他元素相应的往前挪位置