【问题标题】:Adding swig pythoncode to set thisown flag on Python object添加 swig pythoncode 以在 Python 对象上设置 thisown 标志
【发布时间】:2018-05-09 17:39:17
【问题描述】:

我有一个 swigged C++ 类容器,MyContainer,包含 MyObject 类型的对象,也是一个 C++ 类。

以下是C++头代码(freemenot.h)

#ifndef freemenotH
#define freemenotH
#include <vector>
#include <string>

using std::string;
class MyObject
{
    public:
                        MyObject(const string& lbl);
                        ~MyObject();
        string          getLabel();

    private:
        string          label;
};

class MyContainer
{
    public:
                    MyContainer();
                    ~MyContainer();
        void        addObject(MyObject* o);
        MyObject*   getObject(unsigned int t);
        int         getNrOfObjects();

    private:
        std::vector<MyObject*>   mObjects;
};

#endif

这是源代码(freemenot.cpp)

#include "freemenot.h"
#include <iostream>
using namespace std;

/* MyObject source */
MyObject::MyObject(const string& lbl)
:
label(lbl)
{ cout<<"In object ctor"<<endl; }

MyObject::~MyObject() { cout<<"In object dtor"<<endl; }
string MyObject::getLabel() { return label; }


/* MyContainer source */
MyContainer::MyContainer() { cout<<"In container ctor"<<endl; }

MyContainer::~MyContainer()
{
    cout<<"In container dtor"<<endl;
    for(unsigned int i = 0; i < mObjects.size(); i++)
    {
        delete mObjects[i];
    }
}

int MyContainer::getNrOfObjects() { return mObjects.size(); }
void MyContainer::addObject(MyObject* o) { mObjects.push_back(o); }
MyObject* MyContainer::getObject(unsigned int i) { return mObjects[i]; }

观察对象在向量中存储为RAW POINTERS。类就是这样设计的,因此容器负责释放其析构函数中的对象,就像在析构函数 for 循环中所做的那样。

在 C++ 代码中,如下所示,将对象 o1 添加到容器 c 中,然后返回给客户端代码

MyContainer* getAContainerWithSomeObjects()
{
  MyContainer* c = new MyContainer();
  MyObject* o1 = new MyObject();
  c.add(o1);
  return c;
}

返回的容器拥有它的对象,并负责在完成后释放这些对象。在 C++ 中,在上面的函数退出后访问容器对象就可以了。

使用 Swig 将上述类暴露给 python,需要一个接口文件。这个接口文件是这样的

%module freemenot
%{    #include "freemenot.h"    %}
%include "std_string.i"
//Expose to Python
%include "freemenot.h"

为了使用 CMake 生成 Python 模块,使用了以下 CMake 脚本。

cmake_minimum_required(VERSION 2.8)
project(freemenot)

find_package(SWIG REQUIRED)
include(UseSWIG)
find_package(PythonInterp)
find_package(PythonLibs)
get_filename_component(PYTHON_LIB_FOLDER ${PYTHON_LIBRARIES} DIRECTORY CACHE)
message("Python lib folder: " ${PYTHON_LIB_FOLDER})
message("Python include folder: " ${PYTHON_INCLUDE_DIRS})
message("Python libraries: " ${PYTHON_LIBRARIES})

set(PyModule "freemenot")
include_directories(
    ${PYTHON_INCLUDE_PATH}
    ${CMAKE_CURRENT_SOURCE_DIR}
)

link_directories( ${PYTHON_LIB_FOLDER})

set(CMAKE_MODULE_LINKER_FLAGS ${CMAKE_CURRENT_SOURCE_DIR}/${PyModule}.def)

set_source_files_properties(${PyModule}.i PROPERTIES CPLUSPLUS ON)
set_source_files_properties(${PyModule}.i PROPERTIES SWIG_FLAGS "-threads")

SWIG_ADD_LIBRARY(${PyModule}
    MODULE LANGUAGE python
    SOURCES ${PyModule}.i freemenot.cpp)

SWIG_LINK_LIBRARIES (${PyModule} ${PYTHON_LIB_FOLDER}/Python37_CG.lib    )

# INSTALL PYTHON BINDINGS
# Get the python site packages directory by invoking python
execute_process(COMMAND python -c "import site; print(site.getsitepackages()[0])" OUTPUT_VARIABLE PYTHON_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE)
message("PYTHON_SITE_PACKAGES = ${PYTHON_SITE_PACKAGES}")

install(
    TARGETS _${PyModule}
    DESTINATION ${PYTHON_SITE_PACKAGES})

install(
    FILES         ${CMAKE_CURRENT_BINARY_DIR}/${PyModule}.py
    DESTINATION   ${PYTHON_SITE_PACKAGES}
)

使用 CMake 生成 make 文件,并使用 borlands bcc32 编译器进行编译,生成 Python 模块 (freemenot) 并安装到 python3 有效的 sitepackages 文件夹中。

然后,在 Python 中,可以使用以下脚本来说明问题

import freemenot as fmn

def getContainer():
   c = fmn.MyContainer()
   o1 = fmn.MyObject("This is a label")
   o1.thisown = 0
   c.addObject(o1)
   return c

c = getContainer()
print (c.getNrOfObjects())

#if the thisown flag for objects in the getContainer function
#is equal to 1, the following call return an undefined object
#If the flag is equal to 0, the following call will return a valid object
a = c.getObject(0)
print (a.getLabel())

此 Python 代码可能看起来不错,但 无法正常工作。问题是,当函数 getContainer() 返回时,对象 o1 的内存被释放,如果 thisown 标志未设置为零。在此行之后访问对象,使用返回的容器将导致灾难。请注意,这本身并没有什么问题,因为这就是 python 垃圾收集的工作方式。

对于上述用例,如果能够在 addObject 函数中设置 python 对象 thisown 标志 inside,将使 C++ 对象在 Python 中可用。 让用户设置这个标志不是好的解决方案。 还可以使用“addObject”函数扩展 python 类,并修改该函数内部的 thisown 标志,从而对用户隐藏这个记忆技巧。

问题是,如何在不扩展类的情况下让 Swig 做到这一点? 我正在寻找使用 typemap,或者可能是 %pythoncode,但我似乎无法找到一个好的工作示例。

上述代码将由调用 Python 解释器的 C++ 程序使用并传递给该程序。 C++ 程序负责管理在 python 函数中分配的内存,即使在 PyFinalize() 之后也是如此。

以上代码可从github下载https://github.com/TotteKarlsson/miniprojects

【问题讨论】:

  • 这不是真正的 Python 代码。有一个 minimal reproducible example 会有所帮助,但在这种情况下,请查看 SWIG 的 %newobject 指令。
  • 非常好的 MCVE!但是我不确定解决方案。
  • 所以你想让addObject自动窃取Python创建的MyObject?您是否要求 MyContainer 对象可以由 C++ 和 Python 代码创建,还是只能由 Python 代码创建?如果您使用 Python 添加一个从 C++ 开始的实例,您希望发生什么?
  • 是的,确切地说,addObject 函数应该将对象的 thisown 标志设置为零,对用户隐藏。当容器被销毁或决定销毁其对象时,该对象将被销毁。由于 MyObject 和 MyContainer 都起源于 C++,python 是从 C++ 传递一个 MyContainer 还是创建它都无关紧要。
  • 好吧,我要去的地方是 a) 我可以自动复制传递给 addObject 的所有内容吗? b)我可以让 MyContainer 在添加到它的所有内容后面持有对 PyObject 的引用。 (除了调整 thisown 之外,两者都是巧妙的技术,所以我可能会尝试将所有 3 个选项都写出来)。

标签: python c++ swig


【解决方案1】:

有许多不同的方法可以解决这个问题,所以我将尝试依次解释它们,并在此过程中建立一些东西。希望这对于了解 SWIG 的选项和内部结构很有用,即使您只需要第一个示例。

添加Python代码直接修改thisown

与您提出的最相似的解决方案依赖于使用 SWIG 的 %pythonprepend 指令添加一些额外的 Python 代码。您可以根据您关心的重载的 C++ 声明来定位它,例如:

%module freemenot
%{    #include "freemenot.h"    %}
%include "std_string.i"

%pythonprepend MyContainer::addObject(MyObject*) %{
# mess with thisown
print('thisown was: %d' % args[0].thisown)
args[0].thisown = 0
%}

//Expose to Python
%include "freemenot.h"

唯一值得注意的怪癖来自于使用*args而不是命名参数传入的事实,因此我们必须通过位置编号访问它。

还有其他几个地方/方法可以在 SWIG Python documentation 中注入额外的 Python 代码(前提是您没有使用 -builtin),并且猴子补丁也始终是一种选择。

使用 Python 的 C API 来调整 thisown

这里的下一个可能选项是使用类型映射调用 Python C API 来执行等效功能。在这种情况下,我已经匹配了参数类型 和 参数名称,但这确实意味着这里的类型映射将应用于接收名为 o 的 MyObject * 的所有函数。 (这里最简单的解决方案是让名称描述标题中的预期语义,如果这会与当前过度匹配,这有利于使 IDE 和文档更清晰)。

%module freemenot
%{    #include "freemenot.h"    %}
%include "std_string.i"

%typemap(in) MyObject *o {
    PyObject_SetAttrString($input, "thisown", PyInt_FromLong(0)); // As above, but C API
    $typemap(in,MyObject*); // use the default typemap
}

//Expose to Python
%include "freemenot.h"

关于这个例子,除了类型映射匹配之外,最值得注意的一点是这里使用$typemap 来“粘贴”另一个类型映射,特别是MyObject* 的默认类型映射到我们自己的类型映射中。值得在之前/之后的示例中查看生成的包装器文件,以了解最终的外观。

使用 SWIG 运行时直接获取 SwigPyObject 结构的 own 成员

由于我们已经在编写 C++,而不是通过 Python 代码中的 setattr,我们可以调整此类型映射以使用更多 SWIG 的内部结构,并跳过从 C 到 Python 再回到 C 的往返。

在 SWIG 内部有一个结构,其中包含每个实例的详细信息,包括所有权、类型等。

我们可以自己直接从PyObject* 转换为SwigPyObject*,但这需要我们自己编写错误处理/类型检查(这个 PyObject 甚至是 SWIG 吗?)并依赖于各种不同方式的细节SWIG 可以生成 Python 接口。相反,我们可以调用一个函数来为我们处理所有这些,所以我们现在可以像这样编写我们的类型映射:

%module freemenot
%{    #include "freemenot.h"    %}
%include "std_string.i"

%typemap(in) MyObject *o {
    // TODO: handle NULL pointer still
    SWIG_Python_GetSwigThis($input)->own = 0; // Safely cast $input from PyObject* to SwigPyObject*
    $typemap(in,MyObject*); // use the default typemap
}

//Expose to Python
%include "freemenot.h"

这实际上只是先前答案的演变,但纯粹在 SWIG C 运行时中实现。

在添加之前复制构造一个新实例

还有其他方法可以解决此类所有权问题。首先,在这个特定实例中,您的 MyContainer 假设它始终可以在它存储的每个实例上调用 delete(因此拥有这些语义)。

如果我们也包装一个这样的函数:

MyObject *getInstanceOfThing() {
    static MyObject a;
    return &a;
}

这给我们之前的解决方案带来了一个问题——我们将thisown设置为0,但这里它已经是0了,所以当容器被释放时我们仍然不能合法地在指针上调用delete。

有一种简单的方法可以解决这个问题,不需要了解 SWIG 代理内部结构 - 假设 MyObject 是可复制构造的,那么您可以简单地创建一个新实例并确保无论它来自哪里容器删除它是合法的。我们可以通过稍微调整我们的 typemap 来做到这一点:

%module freemenot
%{    #include "freemenot.h"    %}
%include "std_string.i"

%typemap(in) MyObject *o {
    $typemap(in,MyObject*); // use the default typemap as before
    $1 = new $*1_type(*$1); // but afterwards call copy-ctor
}

//Expose to Python
%include "freemenot.h"

这里要注意的一点是使用了更多 SWIG 功能,这些功能让我们知道类型映射输入的类型 - $*1_type 是一次取消引用的类型映射参数的类型。我们本可以在这里写MyObject,因为这就是它所解析的,但是如果你的容器真的是一个模板,这可以让你处理模板之类的东西,或者在其他类似的容器中重新使用类型映射%apply。

现在要注意的是,如果你有一个 C++ 函数,而你故意允许返回一个实例而没有设置 thisown,则这里要注意的是,假设容器将获得现在不持有的所有权。

让容器有机会管理所有权

最后,我最喜欢使用的其他技术之一不能直接在此处使用,但对于后代来说值得一提。如果您有机会在容器中的每个实例旁边存储一些额外的数据,您可以调用Py_INCREF 并保留对底层PyObject* 的引用,无论它来自何处。如果您随后在销毁时获得回调,您还可以调用 Py_DECREF 并强制 Python 运行时保持对象与容器一样长。

即使无法保持 1-1 MyObject*/PyObject* 配对,您也可以这样做,方法是在某处也保持影子容器处于活动状态。除非您愿意将另一个对象添加到容器中,将其子类化,或者可以非常确定容器的初始 Python 实例将始终存在足够长的时间,否则这可能很难做到。

【讨论】:

  • 很好的治疗!期待测试您的答案!
  • 我无法让 args 工作的示例。不断获取 args 未定义。但是通过将 pythonprepend 函数的签名更改为 (Myobject* o),而不是 args.thisown = 0,使用 o.thisown = 0,让代码运行良好!
  • 我得到了逻辑工作,使用 pythonprepend,还有一些额外的代码。 1)添加了一个命名空间和 2)为对象使用模板类。两种添加都很好!这个额外的代码被添加到上面提到的 git 存储库中的示例代码中。
  • 很高兴你能成功 - 出于好奇,你是否推送了更新的示例?我在 git repo 中看不到它。
  • 更新后的示例现在应该可用了!再次感谢!
【解决方案2】:

您正在寻找%newobject。这是一个小例子:

%module test

%newobject create;
%delobject destroy;

%inline %{

#include <iostream>
struct Test
{
    Test() { std::cout << "create" << std::endl; }
    ~Test() { std::cout << "destroy" << std::endl; }
};

Test* create() { return new Test; }
void destroy(Test* t) { delete t; }

%}

用途:

>>> import test
>>> t1 = test.create()    # create a test object
create
>>> t2 = test.Test()      # don't really need a create function :)
create
>>> t3 = test.create()    # and another.
create
>>> test.destroy(t2)      # explicitly destroy one
destroy
>>>
>>>
>>>
>>> ^Z                   # exit Python and the other two get destroyed.

destroy
destroy

【讨论】:

  • 其实这不是我想要的。这仍然需要在 Python 中释放对象。在我的例子中,上面的 Python 函数是从 C++ 程序执行的,在它返回后,内存应该在 C++ 端进行管理。这就是为什么我想在将对象添加到容器时为其设置 thisown 标志。对于“纯”Python 代码,我猜你的答案是正确的,所以我投了赞成票。
  • @TotteKarlsson 提供minimal reproducible example。你的伪代码没有意义。如果您的代码被正确引用计数,则该对象不会被释放。
  • 我添加了一个最小且可验证的示例,因为发布的初始代码确实没有意义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-30
  • 1970-01-01
  • 1970-01-01
  • 2020-10-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多