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

Posted VermillionTear

tags:

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

Q:如何在C库函数中灵活的操作Lua的”table”?

A:

--[[ void lua_settable(lua_State *L, int index);
     从虚拟栈中"index"处获得"table",栈顶获得"value",
     栈顶下面一个元素获得"key"。相当于在Lua环境中执行"table[key] = value"命令,
     设置"table[key]"的过程有可能触发"metamethods"(__newindex)。
     函数在执行结束后,会弹出"key""value"。]]

--[[ int lua_gettable(lua_State *L, int index);
     从虚拟栈中"index"处获得"table",栈顶获得"key",
     从Lua环境中获取"table[key]"的值。
     获取"table[n]"的过程有可能触发"metamethods"(__index)。
     函数在执行结束后,会弹出"key",并将结果放在虚拟栈上。]]

--[[ void lua_rawset(lua_State *L, int index);
     功能类似于"lua_settable()",但过程中不会触发"metamethods"(__newindex)。]]

--[[ int lua_rawget(lua_State *L, int index);
     功能类似于"lua_gettable()",但过程中不会触发"metamethods"(__index)。]]

--[[ void lua_rawseti(lua_State *L, int index, lua_Integer i);
     从虚拟栈中"index"处获得"table",栈顶获得"value",
     相当于在Lua环境中执行"table[i] = value"命令。
     设置"table[i]"的过程不会触发"metamethods"(__newindex)。
     函数在执行结束后,会弹出"value"。
     其实现相当于:
         lua_pushnumber(L, i);
         lua_insert(L, -2);    -- 将"key"放到"value"的下面。
         lua_rawset(L, index);    -- 当"index"是正索引时。
         lua_rawset(L, index - 1);    -- 当"index"是负索引时(因为"key"入栈了)。]]

--[[ int lua_rawgeti(lua_State *L, int index, lua_Integer n);
     从虚拟栈中"index"处获得"table",从Lua环境中获取"table[n]"的值,
     并将结果放在虚拟栈上。获取"table[n]"的过程不会触发"metamethods"(__index)。
     其实现相当于:
         lua_pushnumber(L, n);
         lua_rawget(L, index);]]

下面来看一个例子,该C库函数接收两个参数,第一个参数为一个”table”,第二个参数为一个函数。其功能将”table”中每一个元素作为调用函数的参数,之后将函数的返回结果替换”table”中对应的元素。
“mylib.c”文件中:

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int l_map(lua_State *L)

    int i = 0, n = 0;

    // 因为具有独立的虚拟栈,所以检查"index"为1的位置是否是"table"。
    luaL_checktype(L, 1, LUA_TTABLE);

    // 因为具有独立的虚拟栈,所以检查"index"为2的位置是否是函数。
    luaL_checktype(L, 2, LUA_TFUNCTION);

    /* 获取"index"为1的位置的元素("table")的长度。
     * 对于"table",它在不触发"metamethod"(__index)的情况下,
     * 获取"table"中元素的个数。
     */
    n = lua_rawlen(L, 1);
    for(i = 1; i <= n; ++i)
    
        // 以下三步为C中调用Lua函数的规则。
        lua_pushvalue(L, 2);    // 将Lua函数(f)入栈。
        lua_rawgeti(L, 1, i);    // 将Lua函数的参数(t[i])入栈。
        lua_call(L, 1, 1);    // (以非保护模式)调用Lua函数(f(t[i]))。

        // 将函数的返回值存储到"table"中对应的位置(t[i] = result)。
        lua_rawseti(L, 1, i);
    

    return 0;    // C库函数本身没有返回值。


static const struct luaL_Reg mylib[] = 
    "mymap", l_map,
    NULL, NULL
;

extern int luaopen_mylib(lua_State* L)

    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”文件中:

local mylib = require "mylib"    -- 加载C库。

local t = 1, 3, 5
local f = function (n)    -- 计算传入参数的平方值。
    return n * n
end

mylib.mymap(t, f)    -- 调用C库中的函数。函数的执行结果会替换"table"中对应的元素。
for k, v in pairs(t) do
    print(k, v)
end
--[[ results: 
1    1
2    9
3    25
]]

Q:如何在C库函数中灵活的操作Lua的字符串?

A:在“快速掌握Lua 5.3 —— Lua与C之间的交互概览”的“如何将元素入栈与出栈”问题中已经介绍了几个操作Lua字符串的函数。
接下来再介绍一个在C库函数中判定参数是否为字符串的函数,以及一个在C库函数中快速连接Lua传递的字符串的函数,

/* 检查虚拟栈中索引"arg"处的元素是否为字符串,如果是则返回字符串,否则返回"NULL"。
 * 函数内部通过"lua_tolstring()"获取结果。
 */
const char *luaL_checkstring(lua_State *L, int arg);

/* 按照Lua中".."的功能,连接从栈顶开始的"n"个值。
 * 函数会将被连接的值出栈,之后将结果入栈。如果"n"0,则将一个空串入栈。
 */
void lua_concat(lua_State *L, int n);

下面来看一个例子。Lua程序提供碎片化的字符串,C库函数负责拼接这些字符串,然后按照Lua程序指定的分隔符重新分割字符串,最后将结果传递给Lua。
“mylib.c”文件中:

#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_concat(lua_State *L)

    int i = 0, n = 0;

    luaL_checktype(L, 1, LUA_TTABLE);    // 检查第一个参数是否为"table"。

    n = lua_rawlen(L, 1);    // 获取"table"中元素的个数。
    for(i = 1; i <= n; ++i)
    
        lua_rawgeti(L, 1, i);    // 逐一将"table"中的元素入栈。
    
    // 将从栈顶开始的"n"个元素("table"中的元素)连接为一个字符串。
    lua_concat(L, n);

    return 1;    // 返回字符串。


static int l_split(lua_State *L)

    // 检查前两个参数是否为字符串,并获得它们(待分割字符串以及分隔符)。
    const char *s = luaL_checkstring(L, 1);
    const char *sep = luaL_checkstring(L, 2);

    const char *e = NULL;
    int i = 1;

    /* 创建一个Lua的"table"并入栈(用于存储结果)。
     * 由于函数的参数在虚拟栈"index"为1和2的位置,
     * 所以新创建的"table"将在虚拟栈"index"为3的位置。
     */
    lua_newtable(L);

    // 获取每一个分割出来的字符串,并将他们存储上面创建的"table"中。
    while((e = strchr(s, *sep)) != NULL)
    
        /* 将被分割出来的字符串入栈。
         * 由于字符串没有结束符('\\0'),
         * 所以使用"lua_pushlstring()"而非"lua_pushstring()"。
         */
        lua_pushlstring(L, s, (e - s));
        lua_rawseti(L, -2, i++);    // "t[i] = 字符串"。
        s = e + 1;    // 跳过分隔符。
    
    // 将最后一个分隔符后面的字符串入栈。
    lua_pushstring(L, s);
    lua_rawseti(L, -2, i);

    return 1;    // 返回存储结果的"table"。


static const struct luaL_Reg mylib[] = 
    "myconcat", l_concat,
    "mysplit", l_split,
    NULL, NULL
;

extern int luaopen_mylib(lua_State* L)

    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”文件中:

local mylib = require "mylib"

local t = "hi,,there", ",", "ax", "bd,xx,y", "y"
local s = mylib.myconcat(t)    -- 将"t"中的所有字符串拼接为一个字符串。
print(string.format("\\"%s\\"", s))
print()    -- 换行。
t = mylib.mysplit(s, ',')    -- 使用','分割字符串。
for k, v in pairs(t) do
    print(string.format("\\"%s\\"", v))
end
--[[ results: 
"hi,,there,axbd,xx,yy"

"hi"
""
"there"
"axbd"
"xx"
"yy"
]]

Q:如何在C库函数中使用缓存来操作字符串?

A:当我们连接少量的字符串时,lua_concatlua_pushfstring非常有用。然而,如果我们需要连接大量的字符串(或者字符),这种一个接一个的连接方式的效率将会非常低,正如我们在“快速掌握Lua 5.3 —— 数据结构”,“附加3”中看到的那样。
我们可以使用字符串缓存来解决这个问题,Lua的辅助库为我们提供了luaL_Buffer以及相关的函数,

/* 专门用于在C库函数中缓存零碎的字符串。
 * 有两种使用方式:
 * 1、结果字符串的长度未知:
 * (1) 定义一个"luaL_Buffer"类型的缓存(例如,luaL_Buffer b;)。
 * (2) 调用"luaL_buffinit"初始化缓存(例如,luaL_buffinit(L, &b);)。
 *     初始化之后,"buffer"保留了一份状态"L"的拷贝,
 *     因此当我们调用其他操作"buffer"的函数的时候不需要再传递"L"。
 * (3) 调用"luaL_add*"这一组函数逐一的将字符串放入缓存(例如,luaL_addstring(L, s);)。
 * (4) 最后调用"luaL_pushresult"结束对缓存的使用,
 *     并将结果字符串入栈(例如,luaL_pushresult(&b);)。
 * 2、结果字符串的长度已知:
 * (1) 定义一个"luaL_Buffer"类型的缓存(例如,luaL_Buffer b;)。
 * (2) 调用"luaL_buffinitsize",为缓存预分配以及初始化一个指定大小的缓冲区
 *    (例如,char *p = luaL_buffinitsize(L, &b, sz);)。
 * (3) 逐一的将字符串放入缓冲区(例如,p[0] = 字符串;)。
 * (4) 最后调用"luaL_pushresultsize"将缓冲区中的字符串放入缓存,
 *     结束对缓存的使用,并将结果字符串入栈(例如,luaL_pushresultsize(&b, sz))。
 *     这里的"sz"是指已经复制到缓存中的字符串长度。
 */
typedef struct luaL_Buffer luaL_Buffer;

// 向缓存"B"中添加一个字节"c"。
void luaL_addchar(luaL_Buffer *B, char c);

// 向缓存"B"中添加一个以'\\0'结尾的字符串"s"。
void luaL_addstring(luaL_Buffer *B, const char *s);

// 向缓存"B"中添加一个长度为"l"的字符串"s"。"s"中可以包含'\\0'。
void luaL_addlstring(luaL_Buffer *B, const char *s, size_t l);

// 向缓存"B"中添加一个在缓冲区中长度为"n"的字符串。
void luaL_addsize(luaL_Buffer *B, size_t n);

// 将栈顶的值放入缓存"B",随后将该值出栈。
void luaL_addvalue(luaL_Buffer *B);

/* 为缓存"B"分配一段大小为"sz"的缓冲区。函数返回缓冲区的地址。
 * 你可以向缓冲区中存入字符串,之后必须调用"luaL_addsize"才能真正的将字符串放入缓存。
 */
char *luaL_prepbuffsize(luaL_Buffer *B, size_t sz);

// 等价于"luaL_prepbuffsize",预定义的缓冲区大小为"LUAL_BUFFERSIZE"(在哪儿定义的!!!)。
char *luaL_prepbuffer(luaL_Buffer *B);

/* 初始化缓存"B"。
 * 这个函数不会分配任何空间,缓存必须以一个变量的形式声明。
 */
void luaL_buffinit(lua_State *L, luaL_Buffer *B);

/* 
 * 等价于先调用"luaL_buffinit",再调用"luaL_prepbuffsize"。
 */
char *luaL_buffinitsize(lua_State *L, luaL_Buffer *B, size_t sz);

// 结束对缓存"B"的使用,将结果字符串入栈。
void luaL_pushresult(luaL_Buffer *B);

/* 将于缓存"B"关联的缓冲区中长度为"sz"的字符串放入缓存"B",
 * 之后结束对缓存"B"的使用,将结果字符串入栈。
 * 等价于先调用"luaL_addsize",再调用"luaL_pushresult"。
 */
void luaL_pushresultsize(luaL_Buffer *B, size_t sz);

接下来我们来看一个例子,Lua标准库中string.upper的实现(”lstrlib.c”文件中),其中就用到了luaL_Buffer的第二种使用方式,

static int str_upper(lua_State *L)

    size_t l;
    size_t i;
    luaL_Buffer b;    // 申请一个缓存。
    /* 检查传递的参数是否是字符串,"l"获得字符串的长度。
     *(因为不会修改字符串,所以"l"就是结果字符串的长度)
     */
    const char *s = luaL_checklstring(L, 1, &l);
    char *p = luaL_buffinitsize(L, &b, l);    // 为缓存预分配以及初始化一个指定大小的缓冲区。
    for(i = 0; i < l; i++)
        // 将字符强转为"unsigned char"型,然后转为大写,放入缓冲区。
        p[i] = toupper(uchar(s[i]));
    // 将缓冲区中的字符串放入缓存,结束对缓存的使用,并将结果字符串入栈。
    luaL_pushresultsize(&b, l);

    return 1;    // 返回结果字符串。

附加:

1、当Lua调用C函数时,Lua为其调用的每一个C函数提供独立的虚拟栈。
2、安全模式:遇到错误不报错,而是返回错误码和错误信息。
非安全模式:遇到错误直接报错。
3、当C函数接收到Lua传递的字符串参数时,有两个规则必须要遵守:
(1) 不要将字符串参数出栈(函数返回后,Lua会负责将函数的参数以及函数本身出栈,之后将函数的返回值入栈)。
(2) 不要修改该字符串参数。
而当C程序创建的字符串要向Lua传递时,需要注意的东西就会更多:
(1) 字符串缓存空间的分配以及释放。
(2) 字符串缓存溢出。
等等。
4、Lua辅助库所提供的缓存在被使用的过程中会在虚拟栈上存放一些中间结果,因此你不能假设栈顶一直保持在你使用缓存之前时的位置。此外,虽然你可以在使用缓存的过程中继续使用虚拟栈,但是在你每次访问缓存之前,这些入栈和出栈的操作一定要保持平衡(两种操作的数量一样多)。
5、当你需要将Lua传递的字符串放入缓存时,需要使用特殊的函数luaL_addvalue
你不能在将字符串放入缓存之前将其出栈,因为一旦你从栈中将Lua传递的字符串移出,你将再也不能使用该字符串。同时,你也不能在将字符串出栈之前将其放入缓存中,例如如下的代码,

luaL_addstring(&b, lua_tostring(L, 1));   /* BAD CODE!!! */

因为这样做会导致缓存异常(中间结果之间加入了一个你入栈的值)。
上述这两种情况正好构成了一对儿矛盾,所以Lua辅助库提供了luaL_addvalue帮助我们完成这项工作。

以上是关于快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧的主要内容,如果未能解决你的问题,请参考以下文章

快速掌握Lua 5.3 —— userdata

快速掌握Lua 5.3 —— 资源管理

快速掌握Lua 5.3 —— 扩展你的程序

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

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

快速掌握Lua 5.3 —— userdata