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

后端 未结 4 1295
北海茫月
北海茫月 2020-12-01 15:17

I have written a Python extension for a C library. I have a data structure that looks like this:

typedef struct _mystruct{
   double * clientdata;
   size_t          


        
4条回答
  •  北荒
    北荒 (楼主)
    2020-12-01 15:57

    The simplest solution to this is to implement __getitem__ and throw an IndexError exception for an invalid index.

    I put together an example of this, using %extend and %exception in SWIG to implement __getitem__ and raise an exception respectively:

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

    I tested it by adding to 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:

    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:

    %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 *. I tested this %typemap(out) with the exact same function as the previous method.

    You can also write a corresponding %typemap(in) and %typemap(freearg) for the reverse, something like this untested code:

    %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__. 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:

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

    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. 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. 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:

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

提交回复
热议问题