我有一个饱受困扰的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]; }
观察到对象在 vector 中存储为 RAW POINTERS 。类是这样设计的,因此容器负责释放其析构函数中的对象,就像在析构函数中进行循环一样。
在C++代码中,如下所示,对象o1被添加到容器c中,该容器被返回给客户端代码
MyContainer* getAContainerWithSomeObjects()
{
MyContainer* c = new MyContainer();
MyObject* o1 = new MyObject();
c.add(o1);
return c;
}
返回的容器拥有其对象,并在完成后负责取消分配这些对象。在C++中,函数在上面退出后即可访问容器对象。
使用Swig将上述类暴露给python,将需要一个接口(interface)文件。该接口(interface)文件如下所示
%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代码可能看起来不错,但不能按预期工作。问题是,如果此own标志未设置为零,则当函数getContainer()返回时,将释放对象o1的内存。在此行之后访问对象,使用返回的容器将导致灾难。观察到,这本身没有任何问题,因为这就是python垃圾回收的工作方式。
对于上述用例,能够在addObject函数内设置python对象 thisown 标志,将使C++对象在Python中可用。
让用户设置此标志不是一个好的解决方案。
也可以使用“addObject”函数扩展python类,并在此函数内修改thisown标志,从而向用户隐藏此内存技巧。
问题是,如何使Swig在不扩展类(class)的情况下做到这一点?
我正在寻找使用类型图或%pythoncode ,但是我似乎找不到合适的示例。
上面的代码将由调用Python解释器的C++程序使用并传递给它。即使在PyFinalize()之后,C++程序仍然负责管理在python函数中分配的内存。
上面的代码可以从github https://github.com/TotteKarlsson/miniprojects下载
最佳答案
您可以通过多种方法来解决此问题,因此,在此过程中,我将依次尝试解释每种方法。希望这对于了解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中注入(inject)额外的Python代码(假设您未使用
-builtin
),并且猴子修补也是一种选择。使用Python的C API调整
thisown
这里的下一个可能的选择是使用调用Python C API的类型映射来执行等效的功能。在这种情况下,我在参数类型和参数名称上进行了匹配,但这确实意味着此处的类型映射将应用于所有接收到名为
MyObject *
的o
的函数。 (这里最简单的解决方案是使名称在头文件中描述预期的语义,如果这可能当前不匹配的话,这具有使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接口(interface)的各种不同方式的细节。相反,我们可以调用一个函数来为我们处理所有这些,因此我们现在可以像这样编写我们的类型图:%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功能,这些功能使我们知道了typemap输入的类型-
$*1_type
是一次取消引用的typemap参数的类型。我们可能只是在这里编写了MyObject
,因为它可以解决这个问题,但是如果您的容器确实是模板,那么它可以让您处理诸如模板之类的事情,或者使用%apply
在其他类似容器中重用typemap。现在要注意的是,如果您有一个C++函数,并且有意允许返回一个实例而没有设置
thisown
,则该泄漏是在假设容器将拥有现在不再拥有的所有权的前提下发生的。给容器一个管理所有权的机会
最后,我目前不喜欢直接使用其中一种经常使用的其他技术,但值得一提。如果您有机会在容器中的每个实例旁边存储一些其他数据,则可以调用
Py_INCREF
并保留对基础PyObject*
的引用,无论它来自何处。如果在销毁时获得回调,则还可以调用Py_DECREF
并强制Python运行时将对象保持与容器一样长的时间。即使在无法使1-1
MyObject*
/ PyObject*
配对保持 Activity 的情况下,也可以通过使影子容器保持 Activity 的状态来保持这种状态。除非您愿意将另一个对象添加到容器中,或者将其子类化,或者可以非常确定容器的初始Python实例将始终存在足够长的时间,否则这很难做到。
关于python - 添加swig pythoncode以在Python对象上设置thisown标志,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50259152/