如何在 Python 中的 SWIG Director 方法中处理空指针

How to handle void pointers in SWIG director methods in Python

本文关键字:方法 处理 空指针 Director SWIG Python 中的      更新时间:2023-10-16

我正在使用SWIG将C++库包装为Python库。 C++库公开抽象类供用户从中继承,因此我们使用 SWIG 中的控制器来处理。它基本上可以正常工作(经过一些调整)。

一个问题是这个C++类有两个方法,如下所示:

class Base {
void* getObject();
void  doSomething(void* o);
}

用户应该实现这些方法,然后用户在getObject()中返回的对象传递给doSomething()方法。

问题是,在通过SWIG时,Python中的doSomething()方法会收到一个包装类型为"void*"的SwigPyObject,因此我们不能像我们希望的那样使用原始的Python对象方法。 而且投射不是一种选择,因为它是 Python(或者是吗?

有人有什么见解吗?

我在这里和那里发现了一些相关的问题,但似乎没有一个能准确地解决我的情况,我已经尝试了很多事情来解决它,但没有任何成功。

如果您需要更多详细信息,请告诉我,我会提供。

多谢!

首先,我们希望将你的代码变成真实且可运行的东西。我根据您展示的小代码编写了自己的 test.hh,让我们可以稍微练习这个设计:

class Base {
public:
void runMe() {
std::cerr << "Getting objectn";
void *result = getObject();
std::cerr << "Got: " << result << "n";
doSomething(result);
std::cerr << "Did a thingn";
}
virtual ~Base() {}
protected:
virtual void* getObject() = 0;
virtual void  doSomething(void* o) = 0;
};

我们最初可以将其包装为如下所示:

%module(directors="1") test
%{
#include "test.hh"
%}
%feature("director") Base;
%include "test.hh"

并生成一个测试用例来展示我们希望它在 Python 中的工作方式:

import test
class Foobar(test.Base):
def getObject(self):
return [1,2,3]
def doSomething(self, thing):
print(thing)
f=Foobar()
f.runMe()

但这在现阶段还行不通,因为我们还没有告诉SWIG如何在Python内部有意义地处理void*

这里的总体思路是,我们希望将void*用作界面内部的PyObject*。我们可以通过导演和导演输出类型图配对来做到这一点。从广义上讲,我们需要解决两个问题:

  1. 我们如何使引用计数正常工作而不泄漏?
  2. 如果我们得到的void*不是真正的PyObject*,会发生什么?

如果我们首先假设getObject()调用和doSomething()调用之间存在 1:1 映射,那么引用计数相当简单,我们可以在界面中编写两个类型图,保留对PyObject的引用,然后在需要时将其从void*中转换回来(请注意,我们还通过添加 1:1 限制在这里完全回避了问题 #2)。

因此,使用这两个类型图,我们的界面变为:

%module(directors="1") test
%{
#include "test.hh"
%}
%feature("director") Base;
%typemap(directorout) void *getObject %{
Py_INCREF($1);
$result = $1;
%}
%typemap(directorin) void *o %{
$input = static_cast<PyObject*>($1);
// Director call will decref when we're done here - it assumes ownership semantics, not borrowed
%}
%include "test.hh"

当我们像这样测试它时:

swig -Wall -python -py3 -c++ test.i
g++ -Wall -Wextra  -shared -o _test.so -I/usr/include/python3.5 test_wrap.cxx -std=c++11 -fPIC
python3 run.py 
Getting object
Got: 0x7fce97b91c48
[1, 2, 3]
Did a thing

但是,如果我们将这里的语义更改为不完全是 1:1,那么我们就会遇到问题,例如runMe是这样的:

void runMe() {
std::cerr << "Getting objectn";
void *result = getObject();
std::cerr << "Got: " << result << "n";
doSomething(result);
std::cerr << "Second timen";
doSomething(result);
std::cerr << "Did a thingn";
}

现在出现段错误,因为在第一次调用doSomething完成后引用会递减。

在这个阶段,显而易见的事情是在 directorin typemap 中添加对Py_INCREF的调用,但这还不是故事的全部 - 我们现在永远不会将发布称为getObject()的结果,它只是在runMe()结束时超出了范围。

我倾向于解决这个问题的方法是在您的Base界面中添加另一个调用:

virtual void cleanupThing(void* o) {} // Default nothing, not mandatory

有了这个,我们可以让你的SWIG接口实现(如果需要,可以隐藏)完全在Python控制器中调用。做到这一点的方法是通过一些%rename%ignore以及一些宏观技巧:

因此,通过对SWIG接口的以下调整,我们现在可以在runMe的第二个化身上正常工作:

%module(directors="1") test
%{
#include "test.hh"
%}
%feature("director") PyBase;
%typemap(directorout) void *getObject %{
Py_INCREF($1);
$result = $1;
%}
%typemap(directorin) void *o %{
$input = static_cast<PyObject*>($1);
Py_INCREF($input); // Not borrowed now
// Director call will decref when we're done here
%}
// Python won't even know cleanupThing existed because we use it internally in the Python binding    
%ignore PyBase::cleanupThing;
%feature("nodirector") PyBase::cleanupThing;
// This is a sleight of hand trick with SWIG so we can add another type into the hierarchy without anyone really noticing
%rename(Base) PyBase;
%{
class PyBase : public Base {
void cleanupThing(void *o) {
Py_DECREF(o);
}
};
%}
#define Base PyBase
%include "test.hh"

runMe打电话给cleanupThing

void runMe() {
std::cerr << "Getting objectn";
void *result = getObject();
std::cerr << "Got: " << result << "n";
doSomething(result);
std::cerr << "Second timen";
doSomething(result);
std::cerr << "Did a thingn";
cleanupThing(result);
}

现在运行时确实会得到:

Getting object
Got: 0x7ff65dccfd08
[1, 2, 3]
Second time
[1, 2, 3]
Did a thing

(存在其他可能的解决方案,特别是如果语义比简单地来回传递到同一实例的局部变量更复杂)。