关于SWIG:基于C++返回类型返回继承的Python类

Return inherited Python class based on C++ return types

假设我已经封装了我的C++类EDOCX1,0和EDCOX1,1,并且可以通过SWIG生成模块EDOCX1 2来访问Python。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C++

class Bar
{
    int i;
    Bar(int i) { this.i = i; }
}

class Foo
{
public:
    Foo(Bar* bar) { this.bar = bar; }
    Bar* GetBar() { return this.bar; }
private:
    Bar* bar;
}

在python中,我创建了一个面向用户的类,它是一个浅代理,主要添加docstrings,并允许IDE在参数名上完成制表符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Python
class Bar(wrap_py.Bar):
    '''Some description ...
    Args:
       i (int): ...
    '''

    def __init__(self, i):
        super(Bar, self).__init__(i)

class Foo(wrap_py.Foo):
    '''Some description ...
    Args:
       bar (instance of Bar): ...
    '''

    def __init__(self, bar):
        super(Foo, self).__init__(bar)

这里的问题是,从C++类自动生成的EDCOX1(3),返回EDCOX1(4)类型的SWIG实例,它不具有文档字符串,也不显示参数名称(SWIG将所有参数暴露为EDCOX1×5)。相反,我希望它能提供我自己的肤浅代理Bar

那么,我怎样才能告诉Swig,自动返回Bar,而不是直接返回wrap_py.Bar

编辑:理想情况下,这对于返回类型Bar是可能的,并且不仅对于具体的函数签名:


%feature("shadow") Foo::GetBar() %{
def bar(*args):
result = $action
result.__class__ = Bar
return result
%}

编辑2:我想出了下面的装饰器,我需要把它放在返回swig类型的每个函数/方法前面:

def typemap(f):
from functools import wraps
@wraps(f)
def wrapper(*args, **kwds):
typemap = {
wrap_py.Bar: Bar,
# more types to come...
}
result = f(*args, **kwds)
if isinstance(result, (tuple, list, set)):
for r in result:
r.__class__ = typemap.get(r.__class__, r.__class__)
elif isinstance(result, dict):
for k,v in result.items():
k.__class__ = typemap.get(k.__class__, k.__class__)
v.__class__ = typemap.get(v.__class__, v.__class__)
else:
result.__class__ = typemap.get(result.__class__, result.__class__)
return result
return wrapper
当然,这不好,需要省略。


你提出的两个解决方案都有问题。考虑以下测试用例:

1
2
3
4
5
6
b=Bar(1)
b.woof=2
print(b.woof)
g=(Foo(b).GetBar())
print(type(g))
print(g.woof)

在这个例子中,我们期望最终的print语句的"woof"属性值与我们创建的原始bar对象的值相同。也就是说,我们不仅希望类型匹配,而且希望它是相同的实例。使用阴影和装饰器的方法来包装事物,每次返回相同的基础C++条形码实例时,仍在创建新的Python对象。

为了解决这个问题,您可能需要设置一个字典,将原始C++对象1:1映射到Python代理对象上,并使用它返回的条形码对象。

作为说明这一点的起点,我建立了以下示例。你的C++有固定的多个问题,变成了Test.H.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Bar
{
    int i;
public:
    Bar(int i) { this->i = i; }
};

class Foo
{
public:
    Foo(Bar* bar) { this->bar = bar; }
    Bar* GetBar() { return this->bar; }
private:
    Bar* bar;
};

我编写了一个测试。我用Cu+对象的地址,将扩展条包装器提供EDOCX1 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
%module test
%{
#include"test.hh"
%}

%include <stdint.i>
%include"test.hh"

%extend Bar {
  intptr_t __hash__() {
    return reinterpret_cast<intptr_t>($self);
  }
}

最后,wrap.py从python扩展来实现对象映射和实例查找,包括重写GetBar以使用此机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import test as wrap_py

class Bar(wrap_py.Bar):
    '''Some description ...
    Args:
       i (int): ...
    '''

    def __init__(self, i):
        super(Bar, self).__init__(i)
        Bar._storenative(self)

    _objs={}

    @classmethod
    def _storenative(cls, o):
        print('Storing: %d' % hash(o))
        cls._objs[hash(o)]=o

    @classmethod
    def _lookup(cls, o):
        print('Lookup: %d' % hash(o))
        return cls._objs.get(hash(o), o)

class Foo(wrap_py.Foo):
    '''Some description ...
    Args:
       bar (instance of Bar): ...
    '''

    def __init__(self, bar):
        super(Foo, self).__init__(bar)

    def GetBar(self):
        return Bar._lookup(super(Foo, self).GetBar())

if __name__=='__main__':
  b=Bar(1)
  b.woof=2
  print(b.woof)
  g=(Foo(b).GetBar())
  print(type(g))
  print(g.woof)

不过,第一次切割有一些问题。首先,正如您所指出的,我们仍然需要手动重写每个可能返回bar实例的函数,以添加额外的查找调用。其次,查找字典可能会导致Python代理对象超出其C++对象(在最坏的情况下,错误地将Python Bar代理映射到C++对象上,而该对象实际上不是由任何Python派生对象代理的)。为了解决后一个问题,我们可以查看弱引用,但这也有缺陷(相反,Python对象可能过早地被销毁)。

要使它透明地适用于返回BAR实例的所有方法,您可以沿着以下两条道路之一进行操作:

  • 在代理类中实现__getattribute__,让它返回一个根据返回类型进行适当包装和查找的函数。
  • 在Sug中写出一个与上面类似的类型映射,但是基于C++代码而不是Python代码。
  • 要实现2,您只需要编写一个单独的%typemap(out) Bar*,它查看这是否是我们第一次在给定地址看到bar的实例,并返回对相同对象的引用(如果以前看到过),或者创建一个新对象。请注意,如果您还没有阻止中间代理对象使其变得比需要的更困难,则需要使用swig -builtin。因此我们的界面可以简单地变成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    %module test
    %{
    #include"test.hh"
    #include <map>

    namespace {
      typedef std::map<Bar*,PyObject*> proxy_map_t;
      proxy_map_t proxy_map;
    }
    %}

    %typemap(out) Bar* {
      assert($1);
      const auto it = proxy_map.find($1);
      if (it != proxy_map.end()) {
        $result = it->second;
      }
      else {
        $result = SWIG_NewPointerObj(SWIG_as_voidptr($1), $1_descriptor, $owner);
        proxy_map.insert(std::make_pair($1, $result));
      }
      Py_INCREF($result);
    }

    %include"test.hh"

    然后用上面未修改的python编译和运行。

    1
    2
    3
    swig3.0 -python -c++ -Wall -builtin test.i
    g++ -shared -o _test.so test_wrap.cxx -Wall -Wextra -fPIC -I/usr/include/python2.7/ -std=c++11
    python wrap.py

    仍然有一个突出的问题:当EDCOX1的5个实例被删除时,我们无法看到,所以我们最终会遇到一个情况,我们在多个C++的生命周期中意外地回收了Python代理对象。根据您的目标,您可以在地图中使用弱引用来解决这个问题,或者(ab)使用operator new()来钩住Bar*实例的创建。