SWIG将C库与Python接口(从C“序列”结构创建“可迭代” Python数据类型)

SWIG interfacing C library to Python (Creating 'iterable' Python data type from C 'sequence' struct)

我已经为C库编写了Python扩展。我有一个看起来像这样的数据结构:

1
2
3
4
typedef struct _mystruct{
   double * clientdata;
   size_t   len;
} MyStruct;

此数据类型的目的直接映射到Python中的列表数据类型。因此,我想为导出的结构创建"类似列表"的行为,以便使用我的C扩展编写的代码更加" Pythonic"。

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

我的要求可以概括为:

  • list(py_ctsruct)返回python列表,其中所有内容均从c结构中复制出来
  • py_cstruct [i]返回第i个元素(最好在无效索引上抛出IndexError)
  • py_ctsruct中的元素:枚举能力
  • 根据PEP234,如果对象实现,则可以使用" for"进行迭代
    _iter_()或_getitem_()。然后,使用该逻辑,我认为通过将以下属性(通过重命名)添加到我的SWIG接口文件中,我将具有所需的行为(除了上面的要求#1-我仍然不知道如何实现):

    1
    2
    3
    __len__
    __getitem__
    __setitem__

    我现在能够在python中索引C对象。我尚未实现Python异常抛出,但是如果超出数组范围,则会返回一个幻数(错误代码)。

    有趣的是,当我尝试使用" for x in"语法对结构进行迭代时:

    1
    2
    for i in py_cstruct:
        print i

    Python进入一个无限循环,该循环只是在控制台上打印上述的魔术(错误)数字。这向我暗示索引存在问题。

    最后但并非最不重要的一点,我该如何实施要求1?这涉及(据我了解):

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

    [[更新]]

    我很想在我的接口文件中看到一些声明(如果有)的代码片段,以便可以从Python遍历c结构的元素。


    最简单的解决方案是实现__getitem__并为无效索引引发IndexError异常。

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

    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
    %module test

    %include"exception.i"

    %{
    #include
    #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中进行了测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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:

    1
    2
    3
    4
    import test

    for i in test.test():
      print i

    哪些打印:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的替代方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    %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"

    这将使用返回MyStruct *的任何函数的返回值创建PyList。我用与以前方法完全相同的功能测试了此%typemap(out)

    您也可以为相反的内容编写相应的%typemap(in)%typemap(freearg),例如以下未经测试的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    %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);
    }

    对于链表之类的容器,使用迭代器会更有意义,但是出于完整性考虑,这就是使用__iter__MyStruct进行处理的方式。关键在于,您可以让SWIG为您包装另一种类型,它提供了所需的__iter__()next(),在本例中为MyStructIter,它是同时使用%inline定义和包装的,因为它不是一部分普通的C API:

    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
    42
    43
    44
    45
    46
    47
    48
    49
    %module test

    %include"exception.i"

    %{
    #include
    #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;
      }
    }

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

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

    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
    %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__()和类似于第一种类型的next()来将容器本身用作迭代器,作为迭代器。我没有这样做在我的原始答案中,因为我认为这要比两种不同的类型(容器和该容器的迭代器)不清楚


    我在Python 2.6中遇到了同样的问题,并通过@aphex答复解决了它。
    但是我想避免任何不可思议的值,或者避免额外的布尔值来传递列表结尾条件。果然,我的迭代器有一个atEnd()方法,告诉我我已经超出列表末尾了。

    因此,实际上,使用SWIG异常处理非常容易。我只需要添加以下魔术:

    1
    2
    3
    4
    5
    6
    7
    8
    %ignore MyStructIter::atEnd();
    %except MyStructIter::next {
        if( $self->list->atEnd() ) {
            PyErr_SetString(PyExc_StopIteration,"End of list");
            SWIG_fail;
        }
        $action
    }

    关键是,一旦您超过列表的末尾,此摘要将完全跳过next()调用。

    如果您遵循惯用语,则应如下所示:

    1
    2
    3
    4
    5
    6
    7
    %except MyStructIter::next {
        if( $self->pos >= $self->list->len ) {
            PyErr_SetString(PyExc_StopIteration,"End of list");
            SWIG_fail;
        }
        $action
    }

    PYTHON 3.x的注意事项:

    您应使用神奇的" __"前缀和后缀名称来命名next()函数。一种选择是简单地添加:

    1
    %rename(__next__) MyStructIter::next;


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


    您说您尚未实现Python异常抛出-这就是问题。从PEP 234:

    A new exception is defined, StopIteration, which can be used to signal the end of an iteration.

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

  • 解释器遍历列表的自定义iternext函数
  • 您的函数到达数组的末尾,而不是正确设置StopIteration异常,而只是返回您的"幻数"。
  • 解释器没有很好的理由停止迭代,只是继续打印iternext ...您的幻数返回的值。对于解释器,它只是另一个列表成员。
  • 幸运的是,这是一个非常简单的修复程序,尽管可能看起来并不简单,因为C没有异常功能。 Python C API仅使用引发异常情况时设置的全局错误指示符,然后API标准指示您从栈顶一直返回NULL到解释器,然后解释器将PyErr_Occurred()的输出查看是否设置了错误,如果已设置,则显示相关的异常和回溯。

    因此,在函数中,当到达数组末尾时,您只需要这样做:

    1
    2
    PyErr_SetString(PyExc_StopIteration,"End of list");
    return NULL;

    这是进一步阅读此问题的另一个好答案:如何使用Python C API创建生成器/迭代器?