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

Return already existing objects from C++ to Lua

本文关键字:C++ 返回 Lua 对象 存在      更新时间:2023-10-16

在C++中,假设我有一个类创建了一个类似二叉树的结构,我使用它的方式如下:

CTreeRoot* root = new CTreeRoot(/* whatever */);
CNode* leftNode = root->getLeftNode();
CNode* rightNode = root->getRightNOde();
leftNode->doSomething();
rightNode->doSomething();
// etc

并且假设左节点和右节点都有自己的左节点和右侧节点(因此,是一个二叉树)。现在我想将其公开给Lua(而不是使用luabind),这样我就可以做同样的事情:

local root = treeroot.new(/* whatever */)
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);
        // create a new copy of the CNode object inplace of the memory Lua
        // allocated for us
        new ((CNode*)lua_newuserdata(L,size)) CNode((const CNode&)*leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }
    return 1;
}

我将用户数据强制转换回一个CTreeRoot对象,调用getLeftNode(),确保它存在,然后(这是"错误的部分")我用复制构造函数创建另一个用户数据对象,复制我想要返回的对象。

这是这种情况下的"标准做法"吗?您似乎希望避免创建对象的另一个副本,因为您真正想要的只是对现有对象的引用。

听起来这将是lightuserdata的完美位置,因为我不想创建新对象,我很乐意返回已经存在的对象。然而,问题是lightuserdata没有元表,所以一旦我把它们拿回来,这些对象对我来说就毫无用处了。基本上,我想做一些类似的事情:

int MyLua::TreeRootGetLeftNode(luaState* L)
{
    CTreeRoot* root = (CTreeRoot*)luaL_checkudata(L, 1, "MyLua.treeroot");
    CNode* leftNode = root->getLeftNode();
    if (leftNode != NULL)
    {
        // "WRONG" CODE BUT SHOWS WHAT I WISH I COULD DO
        lua_pushlightuserdata(L, (void*)leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }
    return 1;
}

有人能告诉我如何让我的MyLua::TreeRootGetLeftNode方法向Lua返回已经存在的对象的副本,以便我可以将该对象用作Lua中的"对象"吗?

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

在第一级优化中,您可以记忆此调用,以便每次用户请求同一子树时,您可以简单地返回最初创建的用户数据,而不是复制和构造另一个用户数据来表示相同的东西。不过,有三种方法可以做到这一点,这取决于您是想修改Lua接口、更改C++实现,还是只是咬紧牙关。

  • MyLua.treenode用户数据当前包含CNode对象的实际数据。然而,这是不幸的,因为这意味着无论何时创建CNode对象,都必须使用placementnew在创建后立即将其存储到Lua分配的内存中。可能更好的方法是简单地将指针(CNode*)存储在MyLua.treenode的用户数据中。这确实需要修改MyLua.treenode的Lua接口,以便它现在将其数据视为指向CNode对象的指针。

  • 如果您更愿意将CNode数据直接存储在MyLua.treenode用户数据中,那么您必须确保在创建CTreeRoot时,它将使用placement new从Lua每次分配的内存中构造CNode(或者您可以使用C++标准库中使用的分配器模式?)。然而,由于CNode实现现在依赖于Lua运行时,所以这就不那么优雅了,我不建议这样做。

  • 如果以上两种解决方案都不合适,那么无论何时返回子节点,您都必须制作一个副本,尽管您仍然可以通过跟踪以前是否创建过相同的用户数据(例如,使用Lua表)来提高在同一节点上重复调用的效率。

在第二级优化中,您可以通过使复制的子树成为原始树片段的弱引用来进一步节省内存。这样,无论何时复制子树,都只是创建一个指向原始树一部分的指针。或者,如果你想让子树在原始树被破坏后仍然存在,你可以使用强引用,但你必须深入到引用计数的血腥细节中。在任何一种情况下,这种优化都是纯粹在C++级别上进行的,与Lua接口无关,从您的代码来看,我认为您已经在使用弱引用(CNode*)。

注意:除了在内部实现中使用外,最好避免使用轻用户数据。原因是轻量级用户数据本质上等同于C指针,因此几乎可以指向任何东西。如果您将轻量级用户数据暴露给Lua代码,您将不知道轻量级用户数据可能来自哪里,也不知道它包含什么类型的数据,使用它会带来安全风险(以及分割程序的可能性)。使用轻用户数据的适当方法是将其用作存储在Lua注册表中的Lua查找表的索引,该查找表可用于实现前面提到的记忆。