如何正确使用Lua C API
moon使用lua作为脚本语言,避免不了使用Lua C API来交互,经过多年的使用,发现有一些容易出错的用法,这里主要做一些总结。
1. 不要在非保护模式下,调用Lua C API
Lua手册列出了所有API,并标注了这些API的是否会抛出异常。
如果在非保护模式下触发Lua异常就会调用panics
, 然后调用abort()
退出进程。
保护模式是指最外层由lua_pcall 或 lua_resume 调用。
Example, bad
local m = {}
function m.__index(tbl, key)
error("Boo!")
end
setmetatable(_G, m)
int main(int argc, char* argv []) {
auto L = luaL_newstate();
luaL_openlibs(L); // may raise error
lua_getglobal(L, "EXAMPLE"); // may raise error
const char *c = lua_tostring(L, -1); // may raise error
std::string result = c ? c : ""; //
lua_pop(L, 1);
lua_close(L);
return 0;
}
lua_getglobal
和lua_tostring
都可能抛出异常,例如上面使用元表改写了_G表的默认行为。在非保护模式下就会调用 abort() 退出进程。
Example, good
int protected_call(lua_State* L)
{
luaL_openlibs(L); // may raise error
lua_getglobal(L, "EXAMPLE"); // may raise error
const char* c = lua_tostring(L, -1); // may raise error
std::string result = c ? c : "";
lua_pop(L, 1);
return 0;
}
int main(int argc, char* argv []) {
auto L = luaL_newstate();
lua_pushcfunction(L, protected_call);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
printf("error: %s\n", lua_tostring(L, -1));
lua_close(L);
return -1;
}
lua_close(L);
return 0;
}
另一种选择是使用lua_atpanic
注册一个处理函数,但不建议这样做,作者原话是The panic function, as its name implies, is a mechanism of last resort. Programs should avoid it.
。其中一个原因是会中断lua函数调用后的栈清理工作,会在lua栈上遗留未确定的东西。
2. 尽量使用Cpp编译Lua
Lua使用C编译时,Lua使用longjmp
处理异常,通常longjmp
不会确保CPP对象的析构函数会被调用,这样就会造成内存-资源泄露。
Cpp编译Lua时Lua会使用cpp的 try catch
来处理异常。
Memory Leak Example
int TestFunction(lua_State* L){
luaL_openlibs(L);
std::string script = "print('hello')";
int r = luaL_dostring(L, script.data());
if (r != LUA_OK)
{
//script对象的析构函数可能未被调用,造成内存泄露
return luaL_error(L, lua_tostring(L, -1));
}
return 0;
};
注意 longjmp不同平台行为并不一致,如windows平台:
https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/longjmp?view=msvc-170
Microsoft Specific
In Microsoft C++ code on Windows, longjmp uses the same stack-unwinding semantics as exception-handling code. It is safe to use in the same places that C++ exceptions can be raised. However, this usage is not portable, and comes with some important caveats.
下面代码在windows平台会触发double free,估计是longjmp造成的undefined behavior
#include "lua.hpp"
#include <string>
struct xstring
{
std::string data_;
xstring(std::string&& d)
:data_(std::move(d))
{
printf("object create: %p\n", this);
}
~xstring()
{
printf("object delete: %p\n", this);
}
void append(std::string&& str)
{
data_.append(std::move(str));
}
const char* data() const
{
return data_.data();
}
};
int main(int, char* []) {
auto L = luaL_newstate();
auto protect_init = [](lua_State* L) -> int {
luaL_openlibs(L);
int r = LUA_OK;
{
xstring initialize = std::string("bad");
initialize.append("hello");
r = luaL_dostring(L, initialize.data());
}//let object destruct
if (r != LUA_OK)
{
return luaL_error(L, "initialize");
}
return 0;
};
lua_pushcfunction(L, protect_init);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
printf("error: %s\n", lua_tostring(L, -1));
return -1;
}
return 0;
}
如果无法避免使用C编译的Lua,那就不要在触发Lua Error的地方创建CPP对象,编写这种代码通常需要非常小心。
3. 不要让CPP异常传播穿越Lua调用栈
如果 Lua 调用我们的C++函数,则存在异常可能被抛出并通过 Lua 源代码向上传播的风险。这是一个问题,因为Lua是用C语言编写的,因此不能假定C++意义上是异常安全的。虽然我们无法确定通过Lua抛出异常是不安全的,但我们应该谨慎行事,并假设除非另有证明,否则它是不安全的。
Example Bad
int MyFunction(lua_State *lua)
{
throw std::runtime_error("Boo!");
}
void Test(lua_State *lua)
{
lua_pushcfunction(lua, &MyFunction);
lua_pcall(lua, 0, 0, 0);
}
Example Good
int MyFunction(lua_State *lua)
{
// This function is to be exposed to Lua
try {
// insert code here
} catch (std::exception &e) {
//注意这里调用luaL_error的前提是使用CPP编译Lua
luaL_error(lua, "C++ exception thrown: %s", e.what());
} catch (...) {
luaL_error(lua, "C++ exception thrown");
}
}
https://blog.codingnow.com/2015/05/lua_c_api.html
Lua 在内部发生异常时,VM 会在 C 的 stack frame 上直接跳至之前设置的恢复点,然后 unwind lua vm 层次上的 lua stack 。lua stack (CallInfo 结构)在捕获异常后是正确的,但 C 的 stack frame 的处理未必如你的宿主程序所愿。也就是 RAII 机制很可能没有被触发。
btw ,Lua 的 stack frame 并不一一对应 C 的 stack frame ,即并不是一次 Lua 层的函数调用就对应一层 C 函数调用,当你在 Lua 层上 pcall 一个 lua 函数中再 pcall 一个 lua 函数,也不是直觉上的做两层 try catch 。Lua 的这种实现和 Lua 的语言特性,尾递归以及 coroutine 有关。如果想在 pcall 的内部 coroutine.yield 回 C 层,就绝对不能让 Lua 的函数调用对应到 C 函数调用上,否则 coroutine 就无法 resume (因为在 C 层上跳回恢复点,就破坏了 C 层的 stack frame ,无法重建)。这也是为什么不能简单的让 Lua 内部实现的异常机制简单兼容宿主语言的缘故。
换句话说,即使你用 try catch 重新编译了 lua 库。当你在 lua_pushstring 这种可能抛出异常的 lua api 外主动 try catch ,这个异常你可以捕获到(因为指定 lua vm 的实现也使用它),但却会破坏 lua vm 本身的工作。
强调:你不能用 throw 代替 lua_error 来抛出异常,也不能用 try catch 来取代 lua_pcall 。在 Lua VM 实现层面换成 C++ 的异常机制,并不等于 lua 和 C++ 拥有了等价的异常传播系统。当你明白有些 lua api 会抛出异常,并且这个异常是以 C++ 的 throw 抛出的;你同时也应该明白,自行用 C++ 的异常捕获机制来包起这些 lua api 的调用,试图捕获异常是错误的做法。
参考
https://blog.codingnow.com/2015/05/lua_c_api.html
http://lua-users.org/wiki/ErrorHandlingBetweenLuaAndCplusplus
http://www.knightsgame.org.uk/blog/2012/09/03/notes-on-luac-error-handling/
https://groups.google.com/g/lua-l/c/mhKt6Yd1aQ0