当前位置: BOLT界面引擎 > 知识库文章 > Lua封装的注意事项

Lua封装的注意事项

作者:李亚星

  通过公司内部众多产品上面的崩溃、开发中的一些经验和review插件代码,发现了lua封装里面存在的一些问题,通过C/C++代码来操作lua栈,需要很谨慎,大多数时候出问题了,比如luaState被破坏了,并不会立刻崩溃,但是会导致崩溃到其它时间的其它模块,导致查错的成本增加。请在开发中涉及到lua封装开发的童鞋们,花一点时间看一下下面的内容,避免类似的问题再出现

 

1、 CC++代码里面调用lua代码(一般是触发事件),一定要使用XLLRT_LuaCall,不要使用lua_calllua_pcall

l  lua_call是没有保护的调用,如果在脚本调用过程中出现错误,不是返回错误值,而是会抛出异常,并最终调用exit导致出现意外。

l  使用lua_pcall的话,那么调用过程中的lua错误信息需要自己来处理,并不会触发XLLRT_ErrorHandle设置的错误回调函数,导致有些脚本错误统计不到,尤其是迅雷7的插件,出现脚本错误后无法被迅雷7主程序捕获到。

 

2、 XLLuaRuntime有一个函数XLLRT_ErrorHandle,可以设置应用程序的自己错误回调函数。每个单独的exe程序,可以设置一个回调函数来捕获调用中的脚本错误,需要注意的是迅雷7主程序已经设置了回调函数,其它插件不要再次设置了。

 

3、 请仔细阅读每一个lua接口的说明,比较重要的是看清楚该接口是否会对当前操作的栈有影响,比如一些lua接口在执行完操作后会将操作数弹栈,有些则不会,这个影响到调用完当前接口后是否需要自己维护栈,比如是否需要一个额外的lua_pop调用。例如,luaL_ref接口,会自动将栈顶操作数pop,之后并不需要额外的lua_pop操作来将操作数弹栈。

 

4、  关于lua_toxxxluaL_checkxxx的区别,这两个函数都是从lua栈上获取一个值,但是在检查到类型不符时候,lua_toxxx只是返回null或者默认值;而luaL_check则是会抛出一个异常,下面的代码不会再继续执行;这里就需要注意了,lua里面使用的异常并不是c++的异常,只是使用了csetjumplongjump来实现到恢复点的跳转,所以并不会有C++所期望的栈的展开操作,所以在C++里面看来是异常安全的代码,此时也是“不安全”的,这种情况下即便使用RAII的,也不能保证异常安全,比如

 

Function1(lua_state state)

{

           TestClass tmp();

           luaL_checkstring(state,1);

}

 

当上面的luaL_checkstring出现异常时候,TestClass的析构函数并不会被调用,假如你需要在析构函数里面释放一些资源,可能会导致资源泄露、锁忘记释放等问题。所以在使用luaL_checkxxx时候,需要很小心,在luaL_checkxxx之前尽量不要申请一些需要之后释放的资源,尤其是加锁函数,此时也不要依赖RAII来实现异常安全

 

推荐使用lua_toxxx来代替luaL_checkxxx,然后自己来检查返回值是否正确

 

5、  lua调用中的栈平衡,在从CC++代码里面发起对lua的调用,需要非常小心的保持lua栈的平衡,调用之前push进去的参数,和调用的返回值,在调用之后, 都需要原数目的从栈里面弹出来,否则会导致栈不停的增长,可能会导致栈溢出导致崩溃

 

         一般来说,可以使用下面的方法:

1)         在往lua栈里面push参数之前,先保存栈顶位置;

2)         lua栈里面push自己需要的参数

3)         调用XLLRT_LuaCall发起调用

4)         lua栈上取出自己想要的值

5)         恢复之前的栈顶位置

 

 

    long nowTop = lua_gettop(m_luaState); //在最开始,保存栈顶位置

 

    // push参数进栈

    lua_rawgeti(m_luaState,LUA_REGISTRYINDEX,m_luaFunRef );

    lua_pushinteger(m_luaState,x);

    lua_pushinteger(m_luaState,y);

    lua_pushinteger(m_luaState,flags);

   

    // 发起lua调用

    long lCallRet = XLLRT_LuaCall(3,3,……);

   

    long lResult = 0;

    if (lCallRet == 0)

    {

       // 取出返回值,注意之前要先判断返回值是不是0,不是0的话,表示调用失败了

    }

   

    // 恢复之前的栈顶位置

lua_settop(luaState,nowTop);

 

6、 调用XLLRT_LuaCall之后,从lua栈上获取返回值,需要注意两点

l  只有返回值为0的情况下,表示调用成功,才可以栈上获取指定数目的返回值;否则的话栈上面是没有返回值的(只有一个表示错误信息的字符串,但是XLLRT_LuaCall也会将这个值弹出来)

l  返回值要从栈顶从上往下取,用-1-2来索引,假如返回了3个值,那么第一个返回值的索引就是-3,第二个返回值的索引时-2,最后一个返回值的索引是-1,例如:

 

lua里面调用了return  ret1, ret2, ret3 返回了三个值,那么在lua栈上面取这三个值的时候,获取ret1需要lua_toxxx(luaState,-3),而ret3需要lua_toxxx(luaState, -1)

 

7、 关于luaL_refluaL_unref,这两个调用一定要是对称的,比如在AttachListener里面,调用luaL_ref将函数放到注册表里面,那么在RemoveListener的时候,也需要调用luaL_unref将该函数从注册表里面移除

 

8、 关于lua_Numberlua_Integer. lua中原生的number类型都是一个lua_Number类型,就是一个double类型,无论通过封装是是传入的何种类型,在lua虚拟机中的数据结构都是一个double类型,所以一般不会出现超界溢出的问题。但是注意,lua有两个c接口,一个是lua_pushnumber ,参数类型是一个lua_Number ,一个是 lua_pushinteger,参数类型是一个lua_Integerlua_Number就是一个doublelua_Integer32位系统下是一个int32位)。所以调用这两个接口时,传入参数是隐式转型到参数类型上去的,也就是在当你把一个值通过这两个接口压入lua栈中的时候,会先被转型成一个double 或者是 int 类型。有不少接口需要操作ULONGLONG类型,这个时候请使用lua_pushnumber接口,而不是lua_pushineger,显然ULONGLONG被转型为int和可能会溢出。但是 即使是使用lua_pushnumber也只能保证不超界溢出,当ULONGLONG中的数字大于一定位时,被转型为double会造成精度损失,若干个低位数据被舍去。比如在一个C函数返回一个ULONGLONG,在它的lua封装中用了转型为一个double,在另一个C函数中接受这个返回值,在lua封装中从栈上取回这个double值并转型回ULONGLONG,就有可能导致与原本C函数返回的值不同,因为低位被舍弃了。Thunder7中比较常见的task id因为只使用到ULONGLONG的低52位,不会出现这种问题。但是更安全的做法,可以用一个结构的两个int字段分表表示ULONGLONG的高位和低位,通过userdata化该结构来通过lua交互。

 

9、 LuaRuntime的线程安全问题,注意同一个XL_LRT_RUNTIME_HANDLE和对应的lua_state不可以在多个线程里面使用,只可以在初始线程里面使用;如果有需要在线程里面操作lua代码的需要的话, 参考附件里面的文档;在开发时候,尽量使用XLLuaRuntime.dll的日志版本,这样假如出现跨线程的不正确的访问,会弹出错误提示

 

10、     关于在luaC/C++代码里面传递XLGraphic里面的对象,比如bitmaptexture等,尽量使用xlue封装的函数:

 

    XLUE_API(BOOL) XLUE_PushBitmap(lua_State* luaState, XL_BITMAP_HANDLE hBitmap);

    XLUE_API(BOOL) XLUE_PushMask(lua_State* luaState, XL_MASK_HANDLE hMask);

    XLUE_API(BOOL) XLUE_PushTexture(lua_State* luaState, XL_TEXTURE_HANDLE hTexture);

    XLUE_API(BOOL) XLUE_PushFont(lua_State* luaState, XL_FONT_HANDLE hFont);

    XLUE_API(BOOL) XLUE_PushColor(lua_State* luaState, XL_Color* lpColor);

    XLUE_API(BOOL) XLUE_PushColor2(lua_State* luaState, XL_Color color);

 

    XLUE_API(BOOL) XLUE_CheckBitmap(lua_State* luaState, int index, XL_BITMAP_HANDLE *lpBitmap);

    XLUE_API(BOOL) XLUE_CheckMask(lua_State* luaState, int index, XL_MASK_HANDLE *lpMask);

    XLUE_API(BOOL) XLUE_CheckTexture(lua_State* luaState, int index, XL_TEXTURE_HANDLE *lpTexture);

    XLUE_API(BOOL) XLUE_CheckFont(lua_State* luaState, int index, XL_FONT_HANDLE *lpFont);

    XLUE_API(BOOL) XLUE_CheckColor(lua_State* luaState, int index, XL_Color** lplpColor);

XLUE_API(BOOL) XLUE_CheckColor2(lua_State* luaState, int index, XL_Color* lpColor);

 

    其中pushxxx,在传入lua之前,会自动将引用计数增加,所以不需要自己在外面显式的先addref了。Pushxxx支持传入null代表空对象

    Checkxxx,从lua里面获取句柄后,会自动将调用addref,所以使用完毕之后,需要调用release释放

 

11、     关于XLLRT_LuaCall XLLRT_PrepareChunk的返回值的判断。比如如下代码

 

lua_State* luaState = XLLRT_GetLuaState(hRuntime);

XLLRT_PrepareChunk(hRuntime, hThisChunk);

LuaTaskRequestList* pLuaTaskRequestList = new LuaTaskRequestList();

pLuaTaskRequestList->SetObjectPtr(pTaskRequestList);

XLLRT_PushXLObject(luaState, "Xunlei.Thunder.TaskRequestList.Class", (void*)pLuaTaskRequestList);

XLLRT_LuaCall(luaState, 1, 1, L"CTaskRequestManager::CommitRequestList");

HRESULT hr = S_OK;

//取消2 使用ie下载0 成功1

int nRet = (int)lua_tonumber(luaState, -1);

 

    XLLRT 的接口本来都有返回值,一般来说返回0是正常的。如果lua虚拟机的执行中出现了错误,可能会返回非0 值。

    比如XLLRT_LuaCall ,意这个XLLRT_LuaCall的返回值是指接口本身的返回值,不等同于执行的脚本函数的返回值。如果脚本中出现了错误或者lua虚拟机本身执行时出现了异常(比如栈过大无法扩张)时,    lua虚拟机会停止执行当前脚本, 并且恢复到调用之前的状态。 跟出现脚本错误差不多的行为,就是XLLRT_LuaCall发起的脚本调用中, 异常之前已经被执行的代码会被回退恢复到执行之前的状态,剩下来  未执行的脚本代码不会被执行。如果出现异常说明这一次的脚本没有被成功执行,当前的状态和LuaCall之前的状态是一样的,不能依赖于脚本成功执行以后的任何结果。如果此时仍然认为脚本在栈上压入了  返回值的话,取到错误的脚本返回值。  

 

    XLLRT_PrepareChunk 这个也是一样的,不保证一定成功创建了一个可以被调用的闭包并且压入栈顶。如果失败接下来的XLLRT_LuaCall就会失败。如果接下来成功的压入参数,而且参数是可被调用的,就有    可能错误的调用到第一个压入的参数上的方法,并且把接下来的参数作为参数调用该方法,出现不可预期的结果。

 

    所以,简单的说安全的做法是需要判断XLLRT_LuaCall的返回值,返回值为0,表明调用没有错误,才可以取lua栈上的返回值。还需要判断XLLRT_PrepareChunk的返回值,如果为0,才可以压入参数,执行   chunk中的方法。

 

12、              还有就是曾经发生过的一个问题,希望对大家有参考价值:

 

local hr, curTaskId = gCurTaskBasic:GetId()

if gSnapStates[curTaskId] == nil then

end

 

gCurTaskBasic是当前环境内的一个tablecurTaskId是一个整形的索引,看上去很正常。但是注意,下载库返回的任务id是一个64位的整形,lua脚本中的number使用double表示的,所以这里不会有任何问题。但是lua虚拟机的实现中,如果索引是一个number会先尝试转型到int32上做直接索引,成功就做直接偏移索引,如果转型失败才在hash表中查找。就存在这样一个过程 从 int64转到double 再从double 转到int32,这样中间的double值有可能超出int32的上限,这时候会在FPU的状态字中置一个异常,正常环境下被忽略没有问题。但在某些环境下这个异常没有被忽略,导致系统抛出异常崩溃。

所以,如果要规避这个问题,可以不用numbercurTaskId做索引,将之格式化转换为相应的string再作为索引

 

---------------------------------------------------------

李亚星

 

 

 

迅雷公司 版权所有 Copyright 2003-2010 Thunder Inc.All Rights Reserved. 意见反馈:xl7doc@xunlei.com