lua与c交互笔记

lua 5.3官方英文手册

lua 5.3runoob.com中文手册

交互栈注意事项

  • 栈不是一个全局性的结构,每次函数调用都有自己的私有交互栈。尽管出现递归调用,还是有各自的私用栈。
  • 压/弹栈中的元素个数,具体要看capi的实现来理清元素的位置,否侧参数/返回值个数不对,或类型错误引起逻辑错误。常见的规则如下
    1. lua_toXXX系列函数,都不会将栈顶元素弹出
    2. lua_call,lua_settop这类通过参数,影响弹出多个栈顶元素
    3. lua_next务必符合特定的栈结构,才能遍历元素。
    4. 不是所用capi都支持伪索引LUA_REGISTRYINDEX,lua_pcall的第四个参数异常处理函数,就不支持伪索引。
    5. 大多数capi调用出现异常,会将错误消息压入栈顶。但部分capi未在文档中说明,最好还是看源码了解是否会压入错误消息。
  • 如果相对栈底索引,第一个压入栈的元素索引为1,第二个为2, … 即栈底固定索引是1
  • 如果相对栈顶索引,第一个压入栈的元素索引为-n,… 最后一个为-1, 即栈顶固定索引是-1
  • 需要注意的是,交互栈的默认长度是20,定义在lua.h中由LUA_MINSTACK定义
  • 从Lua过压入栈的字符串,受lua gc管理。当该字符串被弹出后,不要通过缓存的指针去访问它,否侧会引起内存出。lua内部会复用短字符串,所以不要去修改它,否则会影响其它地方使用。
  • 如果在C代码的内存并不关键,尝试使用lua_newuserdata创建内存,该内存受lua gc管理。

C调用Lua

  • 写入k-v
    lua_getglobal(L, t);            // 压入需要访问的table
    lua_pushstring(L, "key");       // key
    lua_pushstring(L, "value");     // value
    lua_settable(L, -3);            //由于压入了keyvalue,所以此时的table索引为-3
    

    或者

    lua_getglobal(L, t);            // 压入需要访问的table
    lua_pushnumber(L, value);       // value
    lua_setfield(L, -2, key);       // 由于只压入value,所以此时的table索引为-2
    
  • 读取k-v
lua_getglobal(L, t);            // 压入需要访问的table
lua_pushstring(L, key);     // key
lua_gettable(L, -2);        // table索引

或者

lua_getglobal(L, t);            // 压入需要访问的table
lua_getfield(L, -1, key);  // 因为没有压入keytable的索引为1-1

注意:写入/读取k-v时,lua_settable,lua_gettable,lua_setfield和lua_getfield会触发元表,若不想触发可以使用lua_rawset,lua_rawget

  • 遍历table
int top = lua_gettop(L);    // 记录当前栈的长度,遍历结束后还原栈
lua_getglobal(L, t);        // 压入需要遍历的table
lua_pushnil(L);             // nil是为了起初k
while (lua_next(L, -2)) {   // 此时,上次的key在栈顶,弹出后。压入下一对k-v
/* 此时栈上 -1 处为value, -2 处为key */

lua_pop(L, 1);              // 弹出value,此时key在栈顶
}
lua_settop(L, top);         // 还原栈长度

lua_next() 这个函数的工作过程是: 1) 先从栈顶弹出一个 key 2) 从栈指定位置的 table 里取下一对 key-value,先将 key 入栈再将 value 入栈 3) 如果第 2 步成功则返回非 0 值,否则返回 0,并且不向栈中压入任何值 第 2 步中从 table 里取出所谓“下一对 key-value”是相对于第 1 步中弹出的 key 的。table 里第一对 key-value 的前面没有数据,所以先用 lua_pushnil() 压入一个 nil 充当初始 key。 按照本例遍历完成后不再用到table,所以在lua_getglobal(L, t)前,记录里栈的长度,lua_settop(L, top)将会同时清除table索引。

  • 调用Lua方法

以下是将两个数求和的例子,sum是lua的全局方法,lua_pcall的第二个参数表示压入了多少个参数,按照本例sum的定义这里至少需要压入2个参数。第三个参数表示有多少个返回值,本例返回了x+y的求和结果,所以会有1个返回值,并且将值压入到栈顶。第四个参数,是调用端指定的运行异常的处理函数,传人0表示无指定处理函数。最后lua_pcall的返回值等于0,表明调用成功并将本例sum的返回值压入栈顶,lua_tonumber将栈元素转化为整数。

-- lua代码
-- sum是全局方法
function sum(x, y)
    -- x = 1, y = 2
    return x + y
end

// c代码
lua_getglobal(L, "sum");    // 压入sum方法
lua_pushnumber(L, 1);       // 压入调用sum的第1个参数
lua_pushnumber(L, 2);       // 压入调用sum的第2个参数
if (lua_pcall(L, 2, 1, 0) != 0) //
{
    fprintf(stderr, "lua_pcall fail %s", lua_tostring(L, -1));
    return -1;
}
int sum = lua_tonumber(L, -1);  // 获取sum的求和值

lua_pcall会根据要求的数量来调整实际结果的数量,即压入nil或丢弃多余的结果。在压入结果前,lua_pcall 会先删除栈中的函数及其参数。如果一个函数会返回多个结果,那么第一个结果最先压入。例如,函数返回了 3个结果,第一个的索引就是-3,最后一个的索引是-1

如果在lua_pcall的运行过程中有任何错误,lua_pcall会返回一个非零值 ,并在栈中压入一条错误消息。不过 即使如此,它仍会弹出函数及其参数。然而,在压入错误消息前,如果存在一个错误处理函数,lua_pcall就会 先调用它。通过lua_pcall的最后一个参数可以指定这个错误处理函数。零表示没有错误处理函数。若传入非零 参数,那么这个参数就应该是一个错误处理函数在栈中的索引。

对于普通的错误,lua_pcall会返回错误代码LUA_ERRRUN。但有两种特殊的错误情况,不会运行错误处理函数。 第一种是内存分配错误,对于这类错误,lua_pcall总是返回LUA_ERRMEM。第二类错误则发生在Lua运行错误处理 函数时,在这种情况中,是没有必要再次调用错误处理函数的。因此lua_pcall会立即返回错误代码LUA_ERRERR.

栈不是一个全局性的结构。每个函数都有自己的局部私有栈。当Lua调用一个C函数时, 第一个参数总是这个局栈的索引1。 即使这个C函数调用了Lua代码,并且Lua代码双调用了相同的C函数,这些C函数调用只看到自己的私有栈,它们的第一个 参数都是索引1

  • 字符串缓冲

当一个C函数需要创建一个字符串返回给Lua时,C代码还必须处理字符串缓冲的分配和释放、缓冲溢出等问题。如果不想操作缓冲的GC问题,可以使用luaL_Buffer。以下是一个使用缓冲机制的例子,它是源码lstrlib.c中string.upper函数的实现

static int str_upper(lua_state* L)
{
    size_t l;
    size_t i ;
    
    luaL_Buffer b;
    const char* s = luaL_checklstr(L, 1, &l);
    luaL_buffinit(L, &b);
    for (int i = 0; i < l;  ++i)
    {
        luaL_addchar(&b, toupper((unsigned char)(s[i])));
    }

    luaL_pushresult(&b);

    return 1;
}

使用缓冲机制的第一步是声明一个luaL_Buffer变量,并用luaL_buffinit来初始化它。在初始化后,这个变量中就会保留一份状态L的副本,由此再调用其他操作缓冲的函数时,就无须传递状态参数了。宏luaL_addchar会将一个字符放入缓冲。辅助库还提供了将字符串放入缓冲的函数,对于具有显式长度的字符串有luaL_addlstring,而对”0结尾”的字符串有luaL_addstring。最后,luaL_pushresult会更新缓冲,并将最终的字符串留在栈顶。 在使用辅助库的缓冲机制时,必须注意一个细节。当向缓冲中添加东西时,缓冲会将一个些中间结果放到栈中。因此,不应假设栈顶还是和使用缓冲前一样。此外,虽然可以在使用缓冲的过程中将栈用于其他任务(甚至是创建另一个缓冲),但这些任务所调用的压入和弹出的次数必须相等。这项限制有些严格,例如将一个Lua返回的字符串放入缓冲。在这种情况中,既不能在字符串未加入缓冲前弹出它(因为无法在弹出字符串后继续使用它),又不能在弹出字符串前将它加缓冲(否则栈的层数就会不正确)。 由于这是一种很觉的情况,辅助库提供一个专门的函数来将栈顶的值加入缓冲: void luaL_addValue(luaL_Buffer*B); 如果栈顶的值不是字符串或数字的话,调用这个函数就会是一个错误。

c side缓存全局ua对象:注册表,环境表和upvalue

注册表 C与Lua间的交互只能通过索引和栈,而每进入C中的索引和栈都临时的。如果想在C中缓存Lua对象,一种做法是将lua对象缓存注册表,注册表是一个仅存在于C的lua table,不能被lua访问。注册表总是位于一个“伪索引”上,这个索引值由LUA_REGISTERYINEX定义。伪索引就像是一个栈中的索引,但它所关联的值不在栈中。Lua API中的大多数函数都能接受伪索引,但像lua_remove和lua_insert这种操作栈本身的函数却只能使用普通索引。例如,为了获取注册表中key为“key”的值,可以这样做:

lua_getfield(L, LUA_REGISTRYINDEX, "key");

注册表是一个普通的lua table.可以用任何Lua值(除了nil)来索引它。然而,由于所有的C模块共享同一个注册表,为了避免使用冲突,必须谨慎地选择key的值。如果允许其他库来访问数据,那么用字符串作为key会比较合适。因为这些库只需知道key的名字就可以了。对于key名字的选择,没有一种可以绝对避免冲突的方法。但有一些好的建议,例如,避免常用的名字,用库名或公司名作为key名字的前缀。尽量不使用像lua或lualib这样的前缀。避免冲突的另一种选择是使用”通用唯一标识符”,现在大多数系统中都有生成uuid的程序 在注册表中不应用使用数据类型的key,因为这种key是被“引用系统”所保留的。这个系统是由辅助库中的一系列函数组成的,它可以在向一个table存储value时,忽略如何创建一个唯一的key。以下调用:

lua_pushnumber(L, 10);
int r = luaL_ref(L, LUA_REGISTRYINDEX);
会在LUA_REGISTRYINDEX表中,增加对栈顶元素的映射,即[r] = 10
lua_geti(L, LUA_REGISTRYINDEX,r)
int n =lua_tonumber(L, LUA_REGISTRYINDEX)
另外使用完后要归还r
lua_unref(L, LUA_REGISTRYINDEX, r)
另一种方法
lua_pushnumber(L, 10);
lua_setfield(L, LUA_REGISTRYINDEX, "my_key");
lua_getfield(L, LUA_REGISTRYINDEX, " my_key");
int n =lua_tonumber(L, LUA_REGISTRYINDEX)

lua 5.2以前的 环境表 模块内私有存储/获取。如lua5.1里的module的实现就用到了环境表,由于环境表的不恰当使用导致给代码的理解带来混乱,lua5.2版本移除了环境表 通过伪索引LUA_ENVIRONINDEX来访问模块的环境table,以下例子使用了新的环境table int luaopen_foo(lua_Stage* L) { lua_newtable(L); lua_replace(L, LUA_ENVIRONINDEX); luaL_register(L, <库名>, <函数列表>); }

闭包私有数据 upvalues 每个闭包closure有各自的upvalue table, C代码中使用lua_pushcclosure创建lua闭包, void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n); 第二个参数是闭包函数,第三个参数表示updatevalue table的长度,所以调用void lua_pushcclosure前需要压入upvalue元素,元素只能通过lua_upvalueindex传入伪索引取值,如果索引不存在返回LUA_TNONE,也可以使用lua_isnone来测试。

static void foo(lua_Stage* L) { if (lua_isnone(L,lua_upvalueindex(1) == 0) lua_tonumber(L, lua_upvalueindex(1)); if (lua_isnone(L,lua_upvalueindex(2) == 0) lua_tostring(L, lua_upvalueindex(2)); }

lua_pushinteger(L, 0); lua_pushstring(L, “hellow”) lua_pushcclosure(L, &foo, 2); // 压入2个upvalue

Lua调用C

待续

协程 API

在Lua中的线程是用户级线程,一个基于内核线程API的协同程序。在C API的角度一个线程就是一个栈,每个栈保留着一个线程中所有未完成的函数调用信息,这些信息包括调用的函数,参数和局部变量。只要创建一个lua_State就会自动在这个状态中创建一个主线程。 lua协程提供的特性是可以通过yield挂起当前任务并且保留栈信息,等待下次resume时恢复上次栈状态执行后续指令。

// Lua代码
function foo(x) coroutine.yield(10, x) end
function foo1(x) foo(x + 1) return 3 end

local co  = coroutine.create(foo1)
print(coroutine.resume(co, 20))     -- true    10      21
print(coroutine.resume(co))         -- true    3

不过这些特性只能作用在Lua层代码,在纯C层没有等效的实现,capi接口主要是为了操作lua代码里的协程,要注意的是lua_resume不会阻塞C代码,而是阻塞Lua代码里的yield。不过从Lua5.2开始提供了另一个绕弯的办法lua_callk,用来回到yield的C代码,lua_callk的可以用法参考云风 Lua 5.2 如何实现 C 调用中的 Continuation

以下是在C层操作协程的样例:

  1. 调用lua_resume前先压入方法类似于coroutine.create
  2. 然而压入参数类似于向coroutine.resume传入参数,而参数的个数由lua_resumenarg指定。
  3. 尽管lua层那边coroutine.yield(10, x)会挂起协程,但在C层的lua_resume不会阻塞,而立即返回一个错误码。除了0或LUA_YIELD以外,则表示执行异常,使用lua_tostring输出异常信息。
  4. coroutine.yield返回值的数量,由lua_gettop获取,并且通过lua_toXXX转换。
     lua_State* L = luaL_newstate();
     luaL_openlibs(L);
        
     const char* chunk = "\
     function foo(x) coroutine.yield(10, x) end \n\
     function foo1(x) foo(x + 1) return 3 end \n\
     ";
     luaL_dostring(L, chunk);
    
     lua_getglobal(L, "foo1");
     lua_pushinteger(L, 20);
     int ret = lua_resume(L, NULL, 1); // Lua5.3的写法
     // int ret = lua_resume(L, 1);    // Lua5.1的写法
     if (!(ret == 0 || ret == LUA_YIELD))
     {
         printf("%s\n", lua_tostring(L, -1));
         return ;
     }
    
     printf("%d %d %d\n", lua_gettop(L), lua_tointeger(L, 1), lua_tointeger(L, 2));
     printf("%d\n", lua_resume(L, NULL, 0));
     printf("%d %d\n", lua_gettop(L), lua_tointeger(L, 1));
    

    如果想将栈信息从主线程中独立出来,则可以使用lua_newthread创建独立的lua_State,两者内部引用相同的Lua状态,但有各自的栈。以下是使用lua_newthread的样例

     lua_State* L = luaL_newstate();
     luaL_openlibs(L);
        
     const char* chunk = "\
     function foo(x) coroutine.yield(10, x) end \n\
     function foo1(x) foo(x + 1) return 3 end \n\
     ";
     luaL_dostring(L, chunk);
    
     lua_State* co = lua_newthread(L);
     lua_getglobal(co, "foo1");
     lua_pushinteger(co, 20);
     int ret = lua_resume(co, L, 1);     // Lua5.3的写法
     // int ret lua_resume(co, 1);       // Lua5.1的写法
     if (!(ret == 0 || ret == LUA_YIELD))
     {
         printf("%s\n", lua_tostring(L, -1));
         return ;
     }
     printf("%d %d %d\n", lua_gettop(co), lua_tointeger(co, 1), lua_tointeger(co, 2));
     printf("%d\n", lua_resume(co, L, 0));
     printf("%d %d\n", lua_gettop(co), lua_tointeger(co, 1));
    

    除主线程外,其他的线程都是GC回收对象,不要使用未被引用的协程。以下lua_pushstring可能会触发GC回收co对象,如果后续操作co会出现异常。

    lua_Stage* co = lua_newthread(L);
    lua_pop(L, 1);
    lua_pushstring(L, "hi");
    



    原文:

https://lizijie.github.io/2021/10/05/lua%E4%B8%8Ec%E4%BA%A4%E4%BA%92%E7%AC%94%E8%AE%B0.html
作者github:
https://github.com/lizijie </b>

PREVIOUS游戏开发纲领
NEXTmmap相比于fwrite写日志,是否有性能优势?