在Lua中检测过期的C++引用

我是 Bitfighter 的首席开发者,这是一款主要使用 C++ 编写,但使用 Lua 脚本编写机器人玩家的游戏。我们使用Lunar(Luna 的变体)将各部分粘合在一起。

我现在开始思考如何让 Lua 脚本知道它们引用的对象已被 C++ 代码删除。

以下是一些示例机器人代码(使用 Lua):

if needTarget then                             -- needTarget => 全局布尔值?
    ship = findClosest(findItems(ShipType))      -- ship => 全局 lightUserData obj
end

if ship ~= nil then
    bot:setAngleToPoint(ship:getLoc())
    bot:fire()
end

请注意,只有在 needTarget 为 true 时才会设置 ship,否则将使用上一次迭代的值。很可能(甚至很可能,如果机器人正在发挥它的作用 ?),自变量最后一次设置后,船只已被击败(并且其对象已被 C++ 删除)。如果是这样,当我们调用 ship:getLoc()时,C++ 将会崩溃。

因此,问题是如何最优雅地处理这种情况,并限制程序员出错时的损失。

我有一些想法。首先,我们可以创建一些 Lua 函数,C++ 代码可以在船只或其他项目死亡时调用:

function itemDied(deaditem)
    if deaditem == ship then
        ship = nil
        needTarget = true
    end
end

其次,我们可以实现某种引用计数智能指针以“神奇地”解决问题。但我不知道从哪里开始。

第三个,我们可以有某种死亡检测器(不确定如何工作),机器人可以这样调用:

if!isAlive(ship)then
    needTarget = true
    ship = nil              -- 这里只是为了本例子的清晰性,超级多余
end

if needTarget then                                -- needTarget => 全局布尔值?
    ship = findClosest(findItems(ShipType))      -- ship => 全局 lightUserData obj
end

<...像以前一样...>

第四,我只能保留船舶的 ID,而不是引用,并使用该 ID 在每个周期中获取船舶对象,例如:

local ship = getShip(shipID)                 -- shipID => 全局 ID

if ship == nil then
    needTarget = true
end

if needTarget then                                -- needTarget => 全局布尔值?
    ship = findClosest(findItems(ShipType))      -- ship => 全局 lightUserData obj
    shipID = ship:getID()
end

<...像以前一样...>

我的理想情况也会智能地引发错误。如果我在一艘死去的船上运行 getLoc()方法,我希望触发错误处理代码,以便让机器人有机会恢复,或者至少允许系统杀死机器人并记录问题,希望我更加小心编写机器人的方式。

这是一个常见的问题。您对这些想法有何看法,并且有更好的想法吗?

谢谢!

原文链接 https://stackoverflow.com/questions/1047212

点赞
stackoverflow用户121674
stackoverflow用户121674

我们的公司选择了第四种解决方案,并且它对我们效果很好。我建议采用它。但是,出于完整性的考虑:

第一种方案是可行的。让船的破坏者调用一些月球代码(或标记它应该被调用),然后如果找不到它就抱怨。这样做意味着你必须非常小心,并且如果你想在游戏引擎和机器人中分别运行,在 Lua 运行时方面可能需要进行一些修改。

第二种方案不像你想像的那么难:在 C++ 端编写或借用一个引用计数指针,如果你的 Lua/C++ 粘合剂习惯于处理 C++ 指针,它很可能可以在没有进一步干预的情况下工作,除非你通过检查运行时符号表来生成绑定。然而问题是,它将强制你进行相当深刻的更改;如果你使用引用计数的指针来引用船只,那么你必须在所有地方使用它们——使用裸指针和智能指针混用所带来的风险是显而易见的。所以我不会采用这种方式,特别是在项目已经很晚的时候。

第三种方案很棘手。你需要一种方法来确定一个给定的船只对象在内存释放后是否仍然活着。所有我能想到的解决这个问题的方法基本上都会降解成第四种方案:你可以让死亡的船只留下一种被复制到 Lua 对象中并可用于检测死亡的令牌(你将死亡的对象保留在 std::set 或类似的东西中),但为什么不直接用令牌来引用船只呢?

总的来说,你不能检测一个特定的 C++ 指针是否指向了一个已经被删除的对象,因此没有简单的魔法方法来解决你的问题。只有在析构函数中采取特殊措施时,才有可能捕获调用已删除船只的 ship:getLoc() 的错误。没有完美的解决方案,所以祝你好运。

2009-06-26 04:20:08
stackoverflow用户15416
stackoverflow用户15416

我认为您在 Lua 方面没有问题,不应该在那里解决。

您的 C++ 代码正在删除仍在引用的对象。不管它们是如何引用的,这都是不好的。

简单的解决方案可能是让 Lunar 清理所有对象。它已经知道哪些对象必须保持活动状态,因为脚本正在使用它们,让它也能为随机的C++对象做 GC(当然,假设在C++端使用了智能指针 - 每个智能指针都会增加 Lunar 的引用计数)。

2009-06-26 10:18:02
stackoverflow用户110672
stackoverflow用户110672

我同意 MSalters 的看法,我真的认为你不应该从 C++ 端释放内存。Lua userdata 支持 ___gc 元方法,让你有机会清理一些东西。如果垃圾回收不够积极,你可以稍微调整一下,或者通过更小的步骤、更频繁地手动运行它。Lua 的垃圾回收是不确定的,所以如果你需要释放资源,那么你需要有一个函数来释放这些资源(这也会由 __gc 调用,并进行适当的检查)。

你可能还想研究一下使用 弱表 来管理你的飞船引用,这样你就不必为了释放内存而将每个引用都赋值为 nil。有一个强引用(比如,放在所有活动飞船的列表中),然后其他的都是弱引用。当飞船被摧毁时,在飞船上设置一个标志,标记它已被摧毁,然后在活动飞船表中将引用设置为 nil。然后,当另一个飞船想要进行交互时,你的逻辑相同,只不过你要检查:

if ship==nil or ship.destroyed then
  ship = findClosest(findItems(ShipType))
end
2009-06-26 15:08:29
stackoverflow用户736571
stackoverflow用户736571

这是一个老问题,但我认为正确的解决方案是使用lua_newuserdata()通过boost::shared_ptr/ boost::weak_ptr或者C++11std::shared_ptr/ std::weak_ptr创建一个shared_ptrweak_ptr。然后在需要时创建引用,或者如果weak_ptr无法获得lock()一个shared_ptr,则失败。例如(在此示例中使用Boost的shared_ptr,因为这是一个旧问题,你可能尚未支持C++11,但对于新项目,我会建议使用C++11的shared_ptr):

using MyObjectPtr = boost::shared_ptr<MyObject>;
using MyObjectWeakPtr = boost::weak_ptr<MyObject>;

auto mySharedPtr = boost::make_shared<MyObject>();
auto userdata = static_cast<MyObjectWeakPtr*>(lua_newuserdata(L, sizeof(MyObjectWeakPtr)));
new(userdata) MyObjectWeakPtr(mySharedPtr);

然后在需要获取C++对象时:

auto weakObj = *static_cast<MyObjectWeakPtr*>(
    luaL_checkudata(L, 1, "MyObject.Metatable"));
luaL_argcheck(L, weakObj != nullptr, 1, "'MyObjectWeakPtr' expected");
  
// 如果你使用的是weak_ptr,则这是必需的!!!如果userdata是shared_ptr,那么您可以在luaL_argcheck()之后直接操作shared_ptr
if (auto obj = weakObj.lock()) {
  // 你有一个有效的shared_ptr,C++对象还活着,你可以像普通shared_ptr一样引用
} else {
  // C++对象消失了,你可以安全进行垃圾回收userdata
}

你不要忘记在你的lua __gc 元方法中释放weak_ptr

static int
myobject_lua__gc(lua_State* L) {
  auto weakObj = *static_cast<MyObjectWeakPtr*>(
      luaL_checkudata(L, 1, "MyObject.Metatable"));
  luaL_argcheck(L, weakObj != nullptr, 1, "'MyObjectWeakPtr' expected");
  weakObj.~MyObjectWeakPtr();
}

不要忘记使用宏或模板元编程来避免代码重复的情况,如static_cast<>luaL_argcheck()等。

当你需要保持C++对象和lua对象同时存活时,请使用shared_ptr。当C++可能回收对象并且它可以在lua底下消失时,使用weak_ptr。当对象的生命周期未知且需要由引用计数自动管理时,始终使用shared_ptrweak_ptr

提示:让你的C++类从boost::enable_shared_from_thisstd::enable_shared_from_this继承,因为它使shared_from_this()可用。

2013-08-14 19:15:16