将已经存在的C++对象从C++返回给Lua

在 C++ 中,假设我有一个创建类似二叉树结构的类,并且我像这样使用它:

CTreeRoot* root = new CTreeRoot(/*随便什么*/);
CNode* leftNode = root->getLeftNode();
CNode* rightNode = root->getRightNOde();

leftNode->doSomething();
rightNode->doSomething();

// 等等

并且假设左右节点都有自己的左右节点(因此是二叉树)。现在我想将其暴露给 Lua(使用 luabind),以便我可以执行类似的操作:

local root = treeroot.new(/*随便什么*/);
local left = root:getLeftNode();
local right = root:getRightNode();

left:doSomething();
right:doSomething();

我已经大多数工作都完成了。然而,对于 getLeftNode() 和 getRightNode() 方法,我相当确定我做错了。以下是我如何在 C++ 中实现 getLeftNode():

int MyLua::TreeRootGetLeftNode(luaState* L)
{
    CTreeRoot* root = (CTreeRoot*)luaL_checkudata(L, 1, "MyLua.treeroot");
    CNode* leftNode = root->getLeftNode();

    if (leftNode != NULL)
    {
        int size = sizeof(CNode);

        // 在 Lua 为我们分配的内存处创建 CNode 对象的新副本
        new ((CNode*)lua_newuserdata(L,size)) CNode((const CNode&)*leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }

    return 1;
}

我将 userdata 转换回 CTreeRoot 对象,调用 getLeftNode(),确保它存在,然后(这是“错误的部分”)再创建另一个 userdata 数据对象,其中包含复制构造函数,将我想要返回的对象进行复制。

对于这种情况,这是“标准做法”吗?似乎您希望避免创建对象的另一个副本,因为您真正想要的只是对已经存在的对象的引用。

听起来这是 lightuserdata 的完美场所,因为我不想创建新对象,我将很乐意返回已经存在的对象。然而,问题在于 lightuserdata 没有元表,因此我得到它们后对象对我是无用的。基本上,我想做的事情是:

int MyLua::TreeRootGetLeftNode(luaState* L)
{
    CTreeRoot* root = (CTreeRoot*)luaL_checkudata(L, 1, "MyLua.treeroot");
    CNode* leftNode = root->getLeftNode();

    if (leftNode != NULL)
    {
        // “错误的”代码,但显示我希望能做的事情
        lua_pushlightuserdata(L, (void*)leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }

    return 1;
}

请问有谁能告诉我如何使我的 MyLua::TreeRootGetLeftNode 方法将已经存在的对象的副本返回给 Lua,以便我可以在 Lua 中将该对象用作“对象”?

点赞
用户440302
用户440302

这里可以执行两个级别的内存优化。在您的第一个功能齐备但效率低下的解决方案中,当用户调用 getLeftNode() 时,它必须创建一个 CNode 的副本以将其存储在 Lua userdata 中。此外,每当用户反复在相同的树上调用 getLeftNode() 时,它都会继续创建新的userdata来表示 CNode,即使它以前已经创建过。

在第一级优化中,您可以对此调用进行备忘录,以便每次用户请求相同的子树时,您可以简单地返回最初创建的userdata,而不是复制并构造另一个userdata来表示同一物。不过,这取决于要修改 Lua 接口、修改 C++ 实现还是忍耐一下,有3种方法可供选择。

  • MyLua.treenode userdata 目前包含了一个 CNode 对象的实际数据。然而,这是不幸的,因为这意味着每当您创建 CNode 对象时,您必须使用就地 new 将其存储到 Lua 立即在创建时分配的内存中。更好的做法可能是在 MyLua.treenode 的 userdata 中仅存储一个指针(CNode*)。这确实需要您修改 MyLua.treenode 的 Lua 接口,以便它现在将其数据视为指向 CNode 对象的指针。

  • 如果您希望直接在 MyLua.treenode 的 userdata 中存储 CNode 数据,则必须确保在创建 CTreeRoot 时,每次都使用就地 new 从 Lua 分配的内存中构造 CNode(或者您可以使用 C++ 标准库中使用的 分配器模式?)。然而,这并不那么优雅,因为您的 CNode 实现现在取决于 Lua 运行时,我不建议这样做。

  • 如果以上两种解决方案都不适用,则每当返回子节点时都必须进行复制,但是您仍然可以通过跟踪是否创建了相同的userdata(例如使用 Lua 表)来提高_在相同节点上重复调用_的效率。

在第二个级别的优化中,您可以通过使复制的子树成为原始树的弱引用来进一步节省内存。这样,每当您复制子树时,您只是创建对原始树的一部分的指针。或者,如果您希望子树在原始树被销毁后仍然存在,则可以使用强引用,但是那么您必须了解引用计数的细节。无论哪种情况,此优化纯粹在 C++ 级别上,与 Lua 接口无关,并且从您的代码中判断,我认为您已经在使用弱引用(CNode*)了。

注意:最好避免使用轻量级 userdata,除非用于内部实现。原因是轻量级 userdata 基本上等同于 C 指针,因此可以指向几乎任何东西。如果将轻量级 userdata 暴露给 Lua 代码,您将不知道轻量级 userdata 可能来自哪里或其中包含何种类型的数据,使用它会产生安全风险(以及可能导致段错误)。使用轻量级用户数据的_适当_方法是将其用作存储在 Lua 注册表中的 Lua 查找表的索引,该表可用于实现前面提到的备忘录。

2013-02-09 19:06:04