如何在 Python 中处理 SWIG 导向器方法中的 void 指针

Posted

技术标签:

【中文标题】如何在 Python 中处理 SWIG 导向器方法中的 void 指针【英文标题】:How to handle void pointers in SWIG director methods in Python 【发布时间】:2018-01-06 23:26:16 【问题描述】:

我正在使用 SWIG 将 C++ 库包装为 Python 库。 C++ 库公开了抽象类供用户继承,因此我们使用 SWIG 中的导向器来处理。它工作正常(稍作调整)。

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

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

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

问题在于,当通过 SWIG 时,Python 中的 doSomething() 方法接收到一个包装类型为“void*”的 SwigPyObject,因此我们不能像希望的那样使用原始 Python 对象方法。 并且强制转换不是一种选择,因为它是 Python(或者是它?)。

有人有什么见解吗?

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

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

非常感谢!

【问题讨论】:

我不明白你在这里的设计 - 为什么是 void* 而不是类型层次结构。在实际使用中,您实际使用哪些类型? 我们提供的是一个库,由用户定义 getObject() 返回的对象的类型。我们考虑过创建某种空的 Object 类,并让 getObject 返回一个指向该 Object 类实例的指针。然后,用户必须创建继承自该 Object 类的类。但是,我们仍然有一个问题,在 doSomething() Python 方法中,传入参数的对象只会暴露 Object 类的方法,而不是用户定义的派生类方法。 所以 Python 代码可以传入任何内容,然后期望稍后再将其取回,但只有 python 实现需要了解它实际上是什么? 没错! C++ 库不必知道传递给它的对象的任何信息。 【参考方案1】:

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

class Base 
public:
  void runMe() 
    std::cerr << "Getting object\n";
    void *result = getObject();
    std::cerr << "Got: " << result << "\n";
    doSomething(result);
    std::cerr << "Did a thing\n";
  

  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*。我们可以通过directorin 和directorout 类型映射配对来做到这一点。总的来说,我们需要解决两个问题:

    我们怎样才能使引用计数正常工作而不泄漏? 如果我们得到的 void* 不是真正的 PyObject* 会怎样?

如果我们一开始就假设getObject() 调用和doSomething() 调用之间存在 1:1 映射,那么引用计数相当简单,我们可以在接口中编写两个类型映射来保留对 @ 的引用987654331@,然后在需要时将其从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 object\n";
  void *result = getObject();
  std::cerr << "Got: " << result << "\n";
  doSomething(result);
  std::cerr << "Second time\n";
  doSomething(result);
  std::cerr << "Did a thing\n";

现在哪个段错误是因为在第一次调用 doSomething 完成后引用会减少。

在这个阶段,显而易见的事情是在directorin 类型映射中添加对Py_INCREF 的调用,但这还不是全部 - 我们将从不调用释放@ 的结果现在是 987654339@,它只是在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 object\n";
  void *result = getObject();
  std::cerr << "Got: " << result << "\n";
  doSomething(result);
  std::cerr << "Second time\n";
  doSomething(result);
  std::cerr << "Did a thing\n";
  cleanupThing(result);

现在运行时确实给出:

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

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

【讨论】:

你好,我今晚试过了。首先,我必须实现 PyBase 构造函数才能使其工作。但是后来我遇到了一个问题,因为动态库没有某些符号。特别是,我的“Base”类在“List”之类的模板类中使用,但是当我编译 Python SWIG 包装器时,我在链接时缺少符号“List”,我不确定如何解决这个问题。看起来“#define Base PyBase”行没有完全工作。有什么提示吗? 我在这里看到了问题。最简单的答案是使cleanupThing 成为Base 内的一个完全成熟的虚拟成员函数,并使用#define` 跳过整个解决方法 抱歉,您能否详细说明一下。实际上,我的 Base 类中已经有一个 viurtalcleanupThing 方法。但是我应该删除#define 行还是代码示例第二部分的所有行?我仍然需要在某个地方进行清理。

以上是关于如何在 Python 中处理 SWIG 导向器方法中的 void 指针的主要内容,如果未能解决你的问题,请参考以下文章

使用 SWIG 控制器将 python 中的 numpy 函数传递给 C++

如何在带有 Swig 的 Python 中使用 float **?

SWIG:将异常从 Python 抛出到 C++

SWIG 如何在 Python 中包装 map<string,string>?

如何编写一个函数来接受 SWIG 中的 Fraction 对象?

如何在 Python + SWIG 中接收引用和指针参数?