SWIG将C库接口连接Python(从C 'sequence'结构创建可迭代的Python数据类型)

9 投票
4 回答
5143 浏览
提问于 2025-04-17 09:47

我写了一个Python扩展,用来和一个C语言库配合使用。我有一个数据结构,长得像这样:

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

这个数据类型的目的和Python里的列表类型直接对应。因此,我想为导出的结构体创建一种“像列表一样”的行为,这样用我的C扩展写的代码就会更符合Python的风格。

具体来说,我希望能在Python代码中做到以下几点: 注意:py_cstruct是一个在Python中访问的ctstruct数据类型。

我的需求可以总结为:

  1. list(py_cstruct) 返回一个Python列表,里面包含从C结构体中复制出来的所有内容
  2. py_cstruct[i] 返回第i个元素(如果索引无效,最好能抛出IndexError错误)
  3. for elem in py_cstruct: 能够遍历元素

根据PEP234一个对象可以通过“for”循环进行迭代,如果它实现了 _iter_() 或 _getitem_() 方法。按照这个逻辑,我认为通过在我的SWIG接口文件中添加以下属性(通过rename)可以实现我想要的行为(除了上面提到的第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 个回答

1

你提到还没有实现Python的异常抛出,这就是问题所在。根据PEP 234的说明:

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

你必须在迭代结束时设置这个异常。因为你的代码没有做到这一点,所以你遇到了你描述的情况:

  1. 解释器在你的列表的自定义 iternext 函数中循环。
  2. 你的函数到达数组的末尾,而不是正确地设置 StopIteration 异常,而是简单地返回了你的“魔法数字”。
  3. 解释器看到没有理由停止迭代,就继续打印 iternext 返回的值……你的魔法数字。对解释器来说,这只是列表中的另一个成员。

幸运的是,这个问题的解决方法相对简单,尽管看起来可能不那么直接,因为C语言没有异常处理的机制。Python的C API使用一个全局错误指示器,当出现异常情况时,你需要设置这个指示器,然后根据API标准,你需要一路返回NULL到解释器,解释器会查看 PyErr_Occurred() 的输出,看看是否设置了错误,如果有,就会打印相关的异常和回溯信息。

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

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

这里还有一个很好的答案,可以进一步阅读这个问题: 如何使用Python C API创建生成器/迭代器?

1
  1. 可以使用 %typemap 这个 swig 命令来查找相关信息。你可以在这个链接找到详细的说明:http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps。在这个文档中,有一个叫 memberin 的 typemap,可能正好能满足你的需求。还有一个链接是关于 Python 部分的 typemap,里面有一个可以把 char** 数据转成 Python 字符串列表的例子。我猜应该还有类似的功能可以用。
  2. 另外,你可以在 swig 的 "i" 文件中,在结构体里面定义 %pythoncode。这可以让你在创建的结构体对象中添加 Python 方法。还有一个命令叫 %addmethod(我记得是这个),可以让你给结构体或类添加方法。这样的话,你就可以在 C++ 或 C 中创建方法来索引这些对象。解决这个问题的方法有很多种。

在我正在做的一个接口中,我使用了一个类对象,这个对象有一些方法可以访问我代码中的数据。这些方法是用 C++ 写的。然后我在 "i" 文件中的类里面使用了 %pythoncode 指令,创建了 "getitem" 和 "setitem" 这两个 Python 方法,利用暴露的 C++ 方法,让它看起来像字典那样可以访问。

19

最简单的解决办法是实现一个叫做 __getitem__ 的功能,并在索引无效时抛出一个 IndexError 异常。

我做了一个示例,使用 %extend%exception 在 SWIG 中实现 __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 同时定义和包装,因为它不是正常 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;
  }
}

关于 容器的迭代 的要求是,容器需要实现 __iter__() 并返回一个新的迭代器,此外,迭代器本身也必须提供一个 __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__() 的方法和一个类似于第一种类型的 next()。我在最初的回答中没有这样做,因为我觉得这样会比有两个不同的类型——一个容器和一个为该容器服务的迭代器——更不清晰。)

撰写回答