SWIG将C库连接到Python(从C'序列'结构创建'可迭代'Python数据类型)

时间:2012-01-08 08:38:24

标签: python c swig

我为C库编写了一个Python扩展。我有一个如下所示的数据结构:

typedef struct _mystruct{
   double * clientdata;
   size_t   len;
} MyStruct;

此数据类型的目的直接映射到Python中的列表数据类型。因此,我想为导出的结构创建“类似列表”的行为,因此使用我的C扩展编写的代码更像是'Py​​thonic'。

特别是,这是我想要做的(来自python代码) 注意:py_ctsruct是在python中访问的ctsruct数据类型。

我的要求可以归结为:

  1. list(py_ctsruct)返回一个python列表,其中包含从c struct
  2. 复制的所有内容
  3. py_cstruct [i]返回 ith 元素(最好在无效索引上抛出IndexError)
  4. 对于py_ctsruct中的elem:枚举的能力
  5. 根据PEP234一个对象可以用“for”迭代,如果它实现的话    _ iter _()或_ getitem _()。然后使用该逻辑,我认为通过将以下属性(通过rename)添加到我的SWIG接口文件,我将获得所需的行为(除了上面的请求#1 - 我仍然不知道如何实现):

    __len__
    __getitem__
    __setitem__
    

    我现在能够在python中索引C对象。我还没有实现Python异常抛出,但是如果超出数组边界,则返回一个幻数(错误代码)。

    有趣的是,当我尝试使用'for x in'语法迭代结构时,例如:

    for i in py_cstruct:
        print i
    

    Python进入一个无限循环,它只是在控制台上打印上面提到的魔术(错误)数字。这告诉我索引有问题。

    最后但并非最不重要,我如何实施要求1?这涉及(据我所知):

    • 处理来自python的函数调用 list()
    • 从C代码
    • 返回Python(列表)数据类型

    [[更新]]

    我有兴趣看一下我需要在我的接口文件中放入什么(如果有的话)声明的一些代码片段,以便我可以从Python迭代c结构的元素。

4 个答案:

答案 0 :(得分:17)

最简单的解决方案是实现__getitem__并为无效索引抛出IndexError例外。

我汇总了一个示例,使用SWIG中的%extend%exception来实现__getitem__并分别引发异常:

%module test

%include "exception.i"

%{
#include <assert.h>
#include "test.h"
static int myErr = 0; // flag to save error state
%}

%exception MyStruct::__getitem__ {
  assert(!myErr);
  $action
  if (myErr) {
    myErr = 0; // clear flag for next time
    // You could also check the value in $result, but it's a PyObject here
    SWIG_exception(SWIG_IndexError, "Index out of bounds");
  }
}

%include "test.h"

%extend MyStruct {
  double __getitem__(size_t i) {
    if (i >= $self->len) {
      myErr = 1;
      return 0;
    }
    return $self->clientdata[i];
  }
}

我通过添加到test.h来测试它:

static MyStruct *test() {
  static MyStruct inst = {0,0};
  if (!inst.clientdata) {
    inst.len = 10;
    inst.clientdata = malloc(sizeof(double)*inst.len);
    for (size_t i = 0; i < inst.len; ++i) {
      inst.clientdata[i] = i;
    }
  }
  return &inst;
}

运行以下Python:

import test

for i in test.test():
  print i

打印哪些:

python run.py
0.0
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0

然后结束。


使用类型地图直接将MyStruct映射到PyList的替代方法也是可行的:

%module test

%{
#include "test.h"
%}

%typemap(out) (MyStruct *) {
  PyObject *list = PyList_New($1->len);
  for (size_t i = 0; i < $1->len; ++i) {
    PyList_SetItem(list, i, PyFloat_FromDouble($1->clientdata[i]));
  }

  $result = list;
}

%include "test.h"

这将创建一个PyList,其中包含返回MyStruct *的任何函数的返回值。我使用与前一个方法完全相同的函数测试了这个%typemap(out)

您还可以为相反的内容编写相应的%typemap(in)%typemap(freearg),类似于未经测试的代码:

%typemap(in) (MyStruct *) {
  if (!PyList_Check($input)) {
    SWIG_exception(SWIG_TypeError, "Expecting a PyList");
    return NULL;
  }
  MyStruct *tmp = malloc(sizeof(MyStruct));
  tmp->len = PyList_Size($input);
  tmp->clientdata = malloc(sizeof(double) * tmp->len);
  for (size_t i = 0; i < tmp->len; ++i) {
    tmp->clientdata[i] = PyFloat_AsDouble(PyList_GetItem($input, i));
    if (PyErr_Occured()) {
      free(tmp->clientdata);
      free(tmp);
      SWIG_exception(SWIG_TypeError, "Expecting a double");
      return NULL;
    }
  }
  $1 = tmp;
}

%typemap(freearg) (MyStruct *) {
  free($1->clientdata);
  free($1);
}

使用迭代器对于链接列表等容器更有意义,但为了完整起见,您可以使用MyStruct__iter__执行此操作。关键是你得到SWIG为你包装另一种类型,它提供所需的__iter__()next(),在这种情况下MyStructIter使用{同时定义和包装%inline {1}}因为它不是普通C API的一部分:

%module test

%include "exception.i"

%{
#include <assert.h>
#include "test.h"
static int myErr = 0;
%}

%exception MyStructIter::next {
  assert(!myErr);
  $action
  if (myErr) {
    myErr = 0; // clear flag for next time
    PyErr_SetString(PyExc_StopIteration, "End of iterator");
    return NULL;
  }
}

%inline %{
  struct MyStructIter {
    double *ptr;
    size_t len;
  };
%}

%include "test.h"

%extend MyStructIter {
  struct MyStructIter *__iter__() {
    return $self;
  }

  double next() {
    if ($self->len--) {
      return *$self->ptr++;
    }
    myErr = 1;
    return 0;
  }
}

%extend MyStruct {
  struct MyStructIter __iter__() {
    struct MyStructIter ret = { $self->clientdata, $self->len };
    return ret;
  }
}

iteration over containers的要求是容器需要实现__iter__()并返回一个新的迭代器,但除了next()之外还返回下一个项并递增迭代器迭代器本身也必须提供__iter__()方法。这意味着容器或迭代器可以相同地使用。

MyStructIter需要跟踪当前的迭代状态 - 我们在哪里以及我们剩下多少。在这个例子中,我通过保持指向下一个项目的指针和我们用来告诉我们到达结束时的计数器来做到这一点。您还可以通过保持指向迭代器正在使用的MyStruct的指针以及其中位置的计数器来跟踪状态,例如:

%inline %{
  struct MyStructIter {
    MyStruct *list;
    size_t pos;
  };
%}

%include "test.h"

%extend MyStructIter {
  struct MyStructIter *__iter__() {
    return $self;
  }

  double next() {
    if ($self->pos < $self->list->len) {
      return $self->list->clientdata[$self->pos++];
    }
    myErr = 1;
    return 0;
  }
}

%extend MyStruct {
  struct MyStructIter __iter__() {
    struct MyStructIter ret = { $self, 0 };
    return ret;
  }
}

(在这个实例中,我们实际上可以使用容器本身作为迭代器作为迭代器,通过提供返回容器的副本__iter__()和{{1类似于第一种类型。我没有在我的原始答案中这样做,因为我认为这不会比有两种不同的类型 - 容器和该容器的迭代器那样清晰。

答案 1 :(得分:1)

  1. 使用%typemap swig命令查找。 http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn25 memberin typemap可能会做你想要的。 http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn35 我有一个我在Python部分找到的类型映射,它允许我将char **数据作为Python字符串列表传输到C ++中。我猜想会有类似的功能。
  2. 此外,您可以在swig“i”文件中的struct内的接口中定义%pythoncode。这将允许您在为结构创建的对象中添加python方法。还有另一个命令%addmethod(我认为),它允许您向结构或类添加方法。然后,您可以创建用于在C ++或C中索引对象的方法(如果需要)。有很多方法可以解决这个问题。
  3. 对于我正在处理的接口,我使用了一个类对象,它有一些方法可以访问我的代码中的数据。这些方法是用C ++编写的。然后我在“i”文件里面的类中使用了%pythoncode指令,并在使用公开C ++方法的Python代码中创建了“ getitem ”和“ setitem ”方法使它看起来像字典样式访问。

答案 2 :(得分:1)

我遇到了与 Python 2.6 相同的问题,并且感谢@aphex回复解决了这个问题。 但我想避免任何魔术值,或额外的布尔值来传递列表结束条件。果然,我的迭代器有一个 atEnd()方法,告诉我我已经过了列表的末尾。

事实上,SWIG异常处理相当容易。我只需添加以下魔法:

%ignore MyStructIter::atEnd();
%except MyStructIter::next {
    if( $self->list->atEnd() ) {
        PyErr_SetString(PyExc_StopIteration,"End of list");
        SWIG_fail;
    }
    $action
}

重点是,一旦超过列表末尾,snipet将完全跳过next()调用。

如果你坚持自己的习语,它应该看起来像:

%except MyStructIter::next {
    if( $self->pos >= $self->list->len ) {
        PyErr_SetString(PyExc_StopIteration,"End of list");
        SWIG_fail;
    }
    $action
}

注意 PYTHON 3.x

您应使用神奇的“__”前缀和后缀名称命名 next()函数。一个选项只是添加:

%rename(__next__) MyStructIter::next;

答案 3 :(得分:0)

你说你还没有实现Python异常抛出 - 这就是问题所在。来自PEP 234:

  

定义了一个新的异常StopIteration,它可以用来表示迭代的结束。

您必须在迭代结束时设置此异常。由于您的代码不会执行此操作,因此您遇到了您所描述的情况:

  1. 解释器遍历列表的自定义iternext函数
  2. 您的函数到达数组的末尾,而不是正确设置StopIteration异常,只需返回您的“幻数”。
  3. 解释器,没有理由停止迭代,只是继续打印iternext ...你的幻数返回的值。对于口译员来说,这只是另一个名单成员。
  4. 幸运的是,这是一个非常简单的修复,但可能看起来并不那么简单,因为C没有异常功能。 Python C API只使用在引发异常情况时设置的全局错误指示符,然后API标准规定您在堆栈中一直返回NULL到解释器,然后解释器查看{{1}的输出查看是否设置了错误,如果是,则打印相关的异常和回溯。

    所以在你的函数中,当你到达数组的末尾时,你只需要这个:

    PyErr_Occurred()

    以下是进一步阅读此问题的另一个很好的答案:How to create a generator/iterator with the Python C API?