You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过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

问题原因

核心错误是类型转换不匹配:

  1. 在foo_new中,你创建了一个大小为sizeof(Foo*)的userdata,将其转换为Foo**类型,并把new Foo(state)返回的对象地址存储在这个userdata中(即userdata里存的是指向Foo对象的指针)。
  2. 但在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 05:34:07