Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SWIG in typemap works, but argout does not

Tags:

python

swig

I have this file foobar.h

class Foobar {
public: void method(int arg[2]) {};
};

After compiling SWIG interface to Python, if I try to run this method from Python it says

TypeError: in method 'Foobar_method', argument 2 of type 'int [2]'

Certainly. So I write this SWIG typemap:

%typemap(in) int [2] {}

and when I compile this, Python runs this method without complaining. So I think, I understand how to write a typemap.

But, if I change the typemap to argout:

%typemap(argout) int [2] {}

Now, Python goes back to the previous error.

I just do this directly from the SWIG manual, this should work without that error, just like in typemap.

What am I doing wrong???

like image 496
Mark Galeck Avatar asked Jan 27 '14 04:01

Mark Galeck


1 Answers

What's wrong?

In short it's not an either/or proposition with these typemaps.

The key bit of information you're missing is the way multiple typemaps cooperate to wrap a single function.

argout gets inserted in the generated wrapper after the call has happened. It's your opportunity to copy the (now modified) input back to Python in a sensible way.

That doesn't address the issue of how the argument gets created and passed in before the call however.

You can see this quite clearly by inspecting the code generated by this interface:

%module test

%{
#include "test.h"
%}

%typemap(in) int[2] {
  // "In" typemap goes here
}

%typemap(argout) int[2] {
  // "argout" goes here
}

%include "test.h"

Which, when test.h is your example produces:

  // ... <snip>
  arg1 = reinterpret_cast< Foobar * >(argp1);
  {
    // "In" typemap goes here
  }
  (arg1)->method(arg2);
  resultobj = SWIG_Py_Void();
  {
    // "argout" goes here
  }
  return resultobj;
  // ... <snip>

In those typemaps the goal of the "in" typemap is to make arg2 a sensible value before the call and the "argout" typemap should do something sensible with the values after the call (possibly by changing the return value if you want).


What should be in the typemaps?

Typically for a function like that you might want to have the input typemap populate a temporary array from some Python inputs.

To do that we're going to need to change the input typemap first, asking SWIG to create a temporary array for us:

It's important that we get SWIG to do this for us, using the notation of adding parenthesis after the type instead of adding it inside the body of the typemap so that the scope is correct for the variable. (If we didn't the temporary wouldn't be accessible from the "argout" typemap still and would be cleaned up before the call itself was made even).

%typemap(in) int[2] (int temp[2]) {
  // If we defined the temporary here then it would be out of scope too early.
  // "In" typemap goes here
}

The code generated by SWIG now includes that temporary array for us, so we want to use the Python C API to iterate over our input. That might look something like:

%typemap(in) int[2] (int temp[2]) {
  // "In" typemap goes here:
  for (Py_ssize_t i = 0; i < PyList_Size($input); ++i) {
    assert(i < sizeof temp/sizeof *temp); // Do something smarter
    temp[i] = PyInt_AsLong(PyList_GetItem($input, i)); // Handle errors
  }
  $1 = temp; // Use the temporary as our input
}

(We could have chosen to use Python iterator protocol instead if we preferred).

If we compile and run the interface now we have enough to pass in an input, but nothing comes back yet. Before we write the "argout" typemap there's one thing still to notice in the generated code. Our temporary array in the generated code actually looks like int temp2[2]. That's not a mistake, SWIG has by default renamed the variable to be derived from the argument position in order to permit the same typemap to be applied multiple times to a single function call, once per argument if needed.

In my "argout" typemap I'm going to return another Python list with the new values. This isn't the only sane choice by a long way though - there are other options if you prefer.

%typemap(argout) int[2] {
  // "argout" goes here:
  PyObject *list = PyList_New(2);
  for (size_t i = 0; i < 2; ++i) {
    PyList_SetItem(list, i, PyInt_FromLong(temp$argnum[i]));
  }
  $result = list;
}

The two points of note in this are firstly that we need to write temp$argnum explicitly to match the transformation that SWIG did on our temporary array and secondly that we're using $result as the output.

Purely output arguments

Often we have an argument that is just used for output, not input. For these it makes no sense to force the Python user to supply a list that's just going to be ignored.

We can do that by modifying the "in" typemap, using numinputs=0 to indicate that no input is expected from Python. You'll need to take care of initializing the temporary appropriately here too. The typemap now becomes simply:

%typemap(in,numinputs=0) int[2] (int temp[2]) {
  // "In" typemap goes here:
  memset(temp, 0, sizeof temp);
  $1 = temp;
}

So now the "in" typemap doesn't actually take any input from Python at all. It can be seen as simply preparing the input to the native call.

By way of an aside you can avoid the name mangling that SWIG applies (with the cost of not being able to use the same typemap multiple times on the same function, or with another typemap that has a name clash) by using noblock=1 in the "in" typemap. I wouldn't recommend that though.

Non-fixed array length?

Finally it's worth noting that we can write all of these typemaps to be more generic and work for any, fixed, size array. To do that we change 2 to "ANY" in the typemap matching and then use $1_dim0 instead of 2 inside the typemap bodies, so the whole interface at the end of that becomes:

%module test

%{
#include "test.h"
%}

%typemap(in,numinputs=0) int[ANY] (int temp[$1_dim0]) {
  // "In" typemap goes here:
  memset(temp, 0, sizeof temp);
  $1 = temp;
}

%typemap(argout) int[ANY] {
  // "argout" goes here:
  PyObject *list = PyList_New($1_dim0);
  for (size_t i = 0; i < $1_dim0; ++i) {
    PyList_SetItem(list, i, PyInt_FromLong(temp$argnum[i]));
  }
  $result = list;
}

%include "test.h"
like image 154
Flexo Avatar answered Nov 18 '22 02:11

Flexo