简体   繁体   English

SWIG 将 C 库连接到 Python(从 C 'sequence' 结构创建'iterable' Python 数据类型)

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

I have written a Python extension for a C library.我为 C 库编写了一个 Python 扩展。 I have a data structure that looks like this:我有一个如下所示的数据结构:

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

The purpose of this datatype maps directly to the list data type in Python.此数据类型的用途直接映射到 Python 中的列表数据类型。 I therefore, want to create 'list-like' behavior for the exported struct, so that code written using my C extension is more 'Pythonic'.因此,我想为导出的结构创建“类似列表”的行为,以便使用我的 C 扩展编写的代码更加“Pythonic”。

In particular, this is what I want to be able to do (from python code) Note: py_ctsruct is a ctsruct datatype being accessed in python.特别是,这是我想要做的(从 python 代码) 注意:py_ctsruct 是在 python 中访问的 ctsruct 数据类型。

My requirements can be sumarized as:我的要求可以概括为:

  1. list(py_ctsruct) returns a python list with all contents copied out from the c struct list(py_ctsruct) 返回一个 python 列表,其中所有内容都从 c 结构中复制出来
  2. py_cstruct[i] returns ith element (preferably throws IndexError on invalid index) py_cstruct[i] 返回第 i 个元素(最好在无效索引上抛出 IndexError)
  3. for elem in py_ctsruct: ability to enumerate对于 py_ctsruct 中的 elem:枚举能力

According to PEP234 , An object can be iterated over with "for" if it implements _ iter _() or _ getitem _() .根据PEP234如果对象实现了 _ iter _() 或 _ getitem _()则可以使用“for”对其进行迭代 Using that logic then, I think that by adding the following attributes (via rename ) to my SWIG interface file, I will have the desired behavior (apart from req. #1 above - which I still dont know how to achieve):然后使用该逻辑,我认为通过将以下属性(通过rename )添加到我的 SWIG 接口文件中,我将拥有所需的行为(除了上面的 req. #1 - 我仍然不知道如何实现):

__len__
__getitem__
__setitem__

I am now able to index the C object in python.我现在可以在 python 中索引 C 对象。 I have not yet implemented the Python exception throwing, however if array bounds are exceeded, are return a magic number (error code).我还没有实现 Python 异常抛出,但是如果超出数组边界,则返回一个幻数(错误代码)。

The interesting thing is that when I attempt to iterate over the struct using 'for x in' syntax for example:有趣的是,当我尝试使用 'for x in' 语法遍历结构时,例如:

for i in py_cstruct:
    print i

Python enters into an infinite loop that simply prints the magic (error) number mentioned above, on the console. Python 进入一个无限循环,在控制台上简单地打印上面提到的魔法(错误)数字。 which suggests to me that there is something wrong with the indexing.这向我表明索引有问题。

last but not the least, how can I implement requirement 1?最后但并非最不重要的一点是,我如何实现要求 1? this involves (as I understand it):这涉及(据我所知):

  • handling' the function call list() from python处理'来自python的函数调用列表()
  • Returning a Python (list) data type from C code从 C 代码返回 Python(列表)数据类型

[[Update]] [[更新]]

I would be interested in seeing a little code snippet on what (if any) declarations I need to put in my interface file, so that I can iterate over the elements of the c struct, from Python.我很想看到一些关于我需要在接口文件中放置什么(如果有)声明的代码片段,以便我可以从 Python 迭代 c 结构的元素。

The simplest solution to this is to implement __getitem__ and throw an IndexError exception for an invalid index.最简单的解决方案是实现__getitem__并为无效索引抛出IndexError异常。

I put together an example of this, using %extend and %exception in SWIG to implement __getitem__ and raise an exception respectively:我整理了一个例子,在 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];
  }
}

I tested it by adding to test.h:我通过添加到 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;
}

And running the following Python:并运行以下 Python:

import test

for i in test.test():
  print i

Which prints:哪个打印:

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

and then finishes.然后完成。


An alternative approach, using a typemap to map MyStruct onto a PyList directly is possible too:另一种方法,使用一个类型映射来映射MyStructPyList直接也是可能的:

%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"

This will create a PyList with the return value from any function that returns a MyStruct * .这将创建一个PyList ,其返回值来自任何返回MyStruct *函数。 I tested this %typemap(out) with the exact same function as the previous method.我使用与前一种方法完全相同的功能测试了这个%typemap(out)

You can also write a corresponding %typemap(in) and %typemap(freearg) for the reverse, something like this untested code:您还可以编写相应的%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);
}

Using an iterator would make more sense for containers like linked lists, but for completeness sake here's how you might go about doing it for MyStruct with __iter__ .使用迭代器对于链表之类的容器更有意义,但为了完整起见,您可以使用__iter__MyStruct执行此MyStruct The key bit is that you get SWIG to wrap another type for you, which provides the __iter__() and next() needed, in this case MyStructIter which is defined and wrapped at the same time using %inline since it's not part of the normal C API:关键是你让 SWIG 为你包装另一种类型,它提供了__iter__()next() ,在这种情况下MyStructIter使用%inline同时定义和包装,因为它不是正常的一部分CAPI:

%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;
  }
}

The requirements for iteration over containers are such that the container needs to implement __iter__() and return a new iterator, but in addition to next() which returns the next item and increments the iterator the iterator itself must also supply a __iter__() method. 对容器迭代的要求是容器需要实现__iter__()并返回一个新的迭代器,但除了next()返回下一项并递增迭代器之外,迭代器本身还必须提供一个__iter__()方法. This means that either the container or an iterator can be used identically.这意味着可以相同地使用容器或迭代器。

MyStructIter needs to keep track of the current state of iteration - where we are and how much we have left. MyStructIter需要跟踪迭代的当前状态——我们在哪里以及我们还剩下多少。 In this example I did that by keeping a pointer to the next item and a counter that we use to tell when we hit the end.在这个例子中,我通过保持一个指向下一个项目的指针和一个计数器来做到这一点,我们用它来告诉我们何时到达终点。 You could also have kept track of the sate by keeping a pointer to the MyStruct the iterator is using and a counter for the position within that, something like:您还可以通过保持指向迭代器正在使用的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;
  }
}

(In this instance we could actually have just used the container itself as the iterator as an iterator, by supplying an __iter__() that returned a copy of the container and a next() similar to the first type. I didn't do that in my original answer because I thought that would be less clear than have two distinct types - a container and an iterator for that container) (在这种情况下,我们实际上可以将容器本身用作迭代器作为迭代器,通过提供返回容器副本__iter__()和类似于第一种类型的next() 。我没有这样做在我原来的答案中,因为我认为这比有两种不同的类型更不清晰 - 一个容器和一个容器的迭代器)

I encountered the very same problem with Python 2.6 , and solved it thank to @aphex reply.我在Python 2.6 上遇到了同样的问题,感谢@aphex 回复解决了这个问题。 But I wanted to avoid any magic value, or extra boolean to pass end-of-list condition.但我想避免任何魔法值或额外的布尔值来传递列表结束条件。 Sure enough, my iterator have an atEnd() methods that tells me I am past the end of the list.果然,我的迭代器有一个atEnd()方法,它告诉我我已经超过了列表的末尾。

So in fact, it is fairly easy with SWIG exception handling.所以实际上,使用 SWIG 异常处理相当容易。 I just had to add the following magic:我只需要添加以下魔法:

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

The point is this snipet skips the next() calls completly once you are past the end of list.关键是一旦你超过列表的末尾,这个 snipet 就会完全跳过 next() 调用。

If you stick to your idioms, it should look like:如果你坚持你的习惯用法,它应该是这样的:

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

NOTE FOR PYTHON 3.x : PYTHON 3.x 的注意事项:

You shall name your next() function with the magic "__ " prefix&postfix name.你应该用神奇的“__”前缀和后缀名称命名你的next()函数。 One option is simply to add:一种选择是简单地添加:

%rename(__next__) MyStructIter::next;
  1. Look up using the %typemap swig command.使用 %typemap swig 命令查找。 http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn25 The memberin typemap might do what you want. 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 I have a typemap that I found in the Python section that allows me to transfer char** data into the C++ as a list of Python strings. http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn35我在 Python 部分找到了一个类型映射,它允许我将 char** 数据作为 Python 字符串列表传输到 C++ 中。 I would guess there would be similar functionality.我猜会有类似的功能。
  2. Also, you can define %pythoncode in your interface inside the struct inside the swig "i" file.此外,您可以在 swig "i" 文件内的结构内的界面中定义 %pythoncode。 This will allow you to add python methods in the object that gets created for the struct.这将允许您在为结构创建的对象中添加 python 方法。 There is another command %addmethod (I think) that allows you to add methods to the struct or a class as well.还有另一个命令 %addmethod (我认为)允许您向结构或类添加方法。 Then you can create methods for indexing the objects in C++ or C if you want.然后,您可以根据需要创建用于在 C++ 或 C 中索引对象的方法。 There are a lot of ways to solve this.有很多方法可以解决这个问题。

For an interface I am working on I used a class object that has some methods for accessing the data in my code.对于我正在处理的接口,我使用了一个类对象,该对象具有一些用于访问代码中数据的方法。 Those methods are written in C++.这些方法是用 C++ 编写的。 Then I used the %pythoncode directive inside the class inside of the "i" file and created " getitem " and " setitem " methods in Python code that uses the expose C++ methods to make it look like a dictionary style access.然后,我在“i”文件内的类中使用了 %pythoncode 指令,并在 Python 代码中创建了“ getitem ”和“ setitem ”方法,这些方法使用公开 C++ 方法使其看起来像字典样式的访问。

You say you have yet to implement Python exception throwing - that's the problem.你说你还没有实现 Python 异常抛出 - 这就是问题所在。 From PEP 234:来自 PEP 234:

A new exception is defined, StopIteration, which can be used to signal the end of an iteration.定义了一个新的异常 StopIteration,它可用于表示迭代结束。

You must set this exception at the end of your iteration.您必须在迭代结束时设置此异常。 Since your code doesn't do this, you're running into the situation you've described:由于您的代码没有执行此操作,因此您遇到了您所描述的情况:

  1. The interpreter loops through your list's custom iternext function解释器循环遍历列表的自定义iternext函数
  2. Your function gets to the end of the array, and rather than correctly setting the StopIteration exception, simply returns your 'magic number'.您的函数到达数组的末尾,而不是正确设置StopIteration异常,只是返回您的“幻数”。
  3. The interpreter, seeing no good reason to stop iterating, simply continues to print the value returned by iternext ... your magic number.解释器看不到停止迭代的充分理由,只是继续打印iternext返回的值……你的幻数。 To the interpreter, it's just yet another list member.对于解释器来说,它只是另一个列表成员。

Fortunately, this is a pretty simple fix, though may not seem as straightforward, because C has no exception facility.幸运的是,这是一个非常简单的修复,虽然看起来可能不那么简单,因为 C 没有异常功能。 The Python C API simply uses a global error indicator that you set when an exception situation is raised, and then the API standards dictate you return NULL all the way up the stack to the interpreter, which then looks at the output of PyErr_Occurred() to see if an error is set, and if it is, prints the relevant exception and traceback. Python C API 仅使用您在引发异常情况时设置的全局错误指示符,然后 API 标准规定您将 NULL 一直向上返回到解释器,然后解释器查看PyErr_Occurred()的输出以查看是否设置了错误,如果是,则打印相关的异常和回溯。

So in your function, when you reach the end of the array, you just need this:所以在你的函数中,当你到达数组的末尾时,你只需要这样:

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

Here's another great answer for further reading on this issue: How to create a generator/iterator with the Python C API?这是进一步阅读这个问题的另一个很好的答案: How to create a generator/iterator with the Python C API?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM