通过Lua包装器访问C++类状态时返回垃圾值的问题排查
Lua C API暴露C++类时get_state返回垃圾值的问题排查与修复
问题描述
我试图通过Lua C API将一个简单的C++类暴露给Lua,不使用第三方库以理解底层原理。以下是我的代码,但调用foo:get_state()时返回的是垃圾值而非预期的1:
#define LUA_LIB #include <cassert> extern "C" { #include "lua.h" #include "lauxlib.h" } namespace { class Foo { public: Foo(int state) : _state(state) {} int get_state() const { return _state; } private: int _state; }; int foo_new(lua_State *L) { assert(lua_gettop(L) == 2); luaL_checktype(L, 1, LUA_TTABLE); lua_newtable(L); lua_pushvalue(L, 1); lua_setmetatable(L, -2); lua_pushvalue(L, 1); lua_setfield(L, 1, "__index"); int state = lua_tointeger(L, 2); auto **__obj = static_cast<Foo **>(lua_newuserdata(L, sizeof(Foo *))); *__obj = new Foo(state); lua_setfield(L, -2, "__self"); return 1; } int foo_get_state(lua_State *L) { assert(lua_gettop(L) == 1); luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__self"); auto *__obj = static_cast<Foo *>(lua_touserdata(L, -1)); lua_pushinteger(L, __obj->get_state()); return 1; } luaL_Reg const foo_functions[] = { {"new", foo_new}, {"get_state", foo_get_state}, {nullptr, nullptr} }; } // namespace extern "C" { LUALIB_API int luaopen_foo(lua_State *L) { luaL_newlib(L, foo_functions); lua_setglobal(L, "Foo"); return 1; } } // extern "C"
Lua调用代码:
local foo = Foo:new(1) print(foo:get_state()) -- 返回垃圾值而非1
问题原因
核心错误是类型转换不匹配:
- 在
foo_new中,你创建了一个大小为sizeof(Foo*)的userdata,将其转换为Foo**类型,并把new Foo(state)返回的对象地址存储在这个userdata中(即userdata里存的是指向Foo对象的指针)。 - 但在
foo_get_state中,你直接将userdata转换为Foo*,相当于把userdata自身的内存地址(而非内部存储的Foo对象地址)当成了Foo对象的地址,访问了错误的内存区域,因此得到垃圾值。
此外,当前代码缺少Foo对象的析构逻辑,会导致内存泄漏;同时用表包装userdata的方式,无法让Lua自动触发垃圾回收(Lua仅对userdata调用__gc元方法)。
修复方案
方案1:修正类型转换(保留原表包装方式)
仅修复类型转换问题,并添加内存泄漏处理:
// 修改foo_get_state函数 int foo_get_state(lua_State *L) { assert(lua_gettop(L) == 1); luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__self"); // 先转成Foo**,再解引用得到真正的Foo对象指针 auto **obj_ptr = static_cast<Foo **>(lua_touserdata(L, -1)); Foo *obj = *obj_ptr; lua_pushinteger(L, obj->get_state()); return 1; } // 添加__gc元方法释放Foo对象 int foo_gc(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__self"); auto **obj_ptr = static_cast<Foo **>(lua_touserdata(L, -1)); delete *obj_ptr; *obj_ptr = nullptr; return 0; } // 更新函数注册表 luaL_Reg const foo_functions[] = { {"new", foo_new}, {"get_state", foo_get_state}, {"__gc", foo_gc}, {nullptr, nullptr} };
注意:这种方式下,Lua不会自动触发表的__gc方法,需要手动管理对象生命周期,因此更推荐方案2。
方案2:直接用userdata存储Foo对象(推荐)
这是Lua C API暴露C++类的标准写法,直接在userdata内存上构造Foo对象,让Lua自动管理生命周期:
#define LUA_LIB #include <cassert> #include <new> // 用于placement new extern "C" { #include "lua.h" #include "lauxlib.h" } namespace { class Foo { public: Foo(int state) : _state(state) {} int get_state() const { return _state; } private: int _state; }; // 从userdata中获取Foo对象的辅助函数 Foo* get_foo(lua_State *L) { return static_cast<Foo*>(luaL_checkudata(L, 1, "Foo")); } int foo_new(lua_State *L) { // 兼容点号调用(Foo.new(1))和冒号调用(Foo:new(1)) int state = lua_istable(L, 1) ? luaL_checkinteger(L, 2) : luaL_checkinteger(L, 1); // 分配容纳Foo对象的userdata Foo *obj = static_cast<Foo*>(lua_newuserdata(L, sizeof(Foo))); // 在userdata内存上构造Foo对象 new(obj) Foo(state); // 设置元表 luaL_getmetatable(L, "Foo"); lua_setmetatable(L, -2); return 1; } int foo_get_state(lua_State *L) { Foo *obj = get_foo(L); lua_pushinteger(L, obj->get_state()); return 1; } // 析构Foo对象 int foo_gc(lua_State *L) { Foo *obj = get_foo(L); obj->~Foo(); return 0; } luaL_Reg const foo_methods[] = { {"get_state", foo_get_state}, {"__gc", foo_gc}, {nullptr, nullptr} }; } // namespace extern "C" { LUALIB_API int luaopen_foo(lua_State *L) { // 创建元表并设置__index指向自身 luaL_newmetatable(L, "Foo"); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); // 注册元方法 luaL_setfuncs(L, foo_methods, 0); // 创建Foo表,暴露new方法 luaL_newlib(L, (luaL_Reg[]){{"new", foo_new}, {nullptr, nullptr}}); lua_setglobal(L, "Foo"); return 1; } } // extern "C"
Lua调用方式不变,此时print(foo:get_state())会正确输出1。
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

