Simon_lb
Simon_lb

Reputation: 33

Support for std::tuple in swig?

When calling a swig generated function returning std::tuple, i get a swig object of that std::tuple.

Is there a way to use type-maps or something else to extract the values? I have tried changing the code to std::vector for a small portion of the code, and that works. (using %include <std_vector.i> and templates) But i don't want to make too many changes in the C++ part.

Edit: here is a minimal reproducible example:

foo.h

#pragma once

#include <tuple>

class foo
{
private:
    double secret1;
    double secret2;
public:
    foo();
    ~foo();
    std::tuple<double, double> return_thing(void);
};

foo.cpp

#include "foo.h"
#include <tuple>

foo::foo()
{
    secret1 = 1;
    secret2 = 2;
}

foo::~foo()
{
}

std::tuple<double, double> foo::return_thing(void) {
    return {secret1, secret2};
}

foo.i

%module foo

%{
#include"foo.h"
%}

%include "foo.h"

When compiled on my linux using

-:$ swig -python -c++ -o foo_wrap.cpp foo.i
-:$ g++ -c foo.cpp foo_wrap.cpp '-I/usr/include/python3.8' '-fPIC' '-std=c++17' '-I/home/simon/Desktop/test_stack_overflow_tuple'
-:$ g++ -shared foo.o foo_wrap.o -o _foo.so

I can import it in python as shown: test_module.ipynb

import foo as f

Foo = f.foo()
return_object = Foo.return_thing()
type(return_object)
print(return_object)

Outputs is

SwigPyObject
<Swig Object of type 'std::tuple< double,double > *' at 0x7fb5845d8420>

Hopefully this is more helpful, thank you for responding

To clarify i want to be able to use the values in python something like this:

main.cpp

#include "foo.h"
#include <iostream>
//------------------------------------------------------------------------------'
using namespace std;

int main()
{
    foo Foo = foo();
    auto [s1, s2] = Foo.return_thing();
    cout << s1 << " " << s2 << endl;
}

//------------------------------------------------------------------------------

Github repo if anybody is interested
https://github.com/simon-cmyk/test_stack_overflow_tuple

Upvotes: 2

Views: 755

Answers (1)

Our goal is to make something like the following SWIG interface work intuitively:

%module test

%include "std_tuple.i"
%std_tuple(TupleDD, double, double);

%inline %{
   std::tuple<double, double> func() {
       return std::make_tuple(0.0, 1.0);
   }
%}

We want to use this within Python in the following way:

import test
r=test.func()
print(r)
print(dir(r))

r[1]=1234

for x in r:
    print(x)

i.e. indexing and iteration should just work.

By re-using some of the pre-processor tricks I used to wrap std::function (which were themselves originally from another answer here on SO) we can define a neat macro that "just wraps" std::tuple for us. Although this answer is Python specific it should in practice be fairly simple to adapt for most other languages too. I'll post my std_tuple.i file, first and then annotate/explain it after:

// [1]
%{
#include <tuple>
#include <utility>
%}

// [2]    
#define make_getter(pos, type) const type& get##pos() const { return std::get<pos>(*$self); }
#define make_setter(pos, type) void set##pos(const type& val) { std::get<pos>(*$self) = val; }
#define make_ctorargN(pos, type) , type v##pos
#define make_ctorarg(first, ...) const first& v0 FOR_EACH(make_ctorargN, __VA_ARGS__)

// [3]
#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1) action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1) action(1,a2) action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1) action(1,a2) action(2,a3) action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1) action(1,a2) action(2,a3) action(3,a4) action(4,a5)

#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
  GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef

// [4]
%define %std_tuple(Name, ...)
%rename(Name) std::tuple<__VA_ARGS__>;
namespace std {
    struct tuple<__VA_ARGS__> {
        // [5]
        tuple(make_ctorarg(__VA_ARGS__));
        %extend {
            // [6]
            FOR_EACH(make_getter, __VA_ARGS__)
            FOR_EACH(make_setter, __VA_ARGS__)
            size_t __len__() const { return std::tuple_size<std::decay_t<decltype(*$self)>>{}; }
            %pythoncode %{
                # [7]
                def __getitem__(self, n):
                    if n >= len(self): raise IndexError()
                    return getattr(self, 'get%d' % n)()
                def __setitem__(self, n, val):
                    if n >= len(self): raise IndexError()
                    getattr(self, 'set%d' % n)(val)
            %}
        }
    };
}
%enddef
  1. This is just the extra includes we need for our macro to work
  2. These apply to each of the type arguments we supply to our %std_tuple macro invocation, we need to be careful with commas here to keep the syntax correct.
  3. This is the mechanics of our FOR_EACH macro, which invokes each action per argument in our variadic macro argument list
  4. Finally the definition of %std_tuple can begin. Essentially this is manually doing the work of %template for each specialisation of std::tuple we care to name inside of the std namespace.
  5. We use our macro for each magic to declare a constructor with arguments for each element of the correct type. The actual implementation here is the default one from the C++ library which is exactly what we need/want though.
  6. We use our FOR_EACH macro twice to make a member function get0, get1, getN of the correct type of each tuple element and the correct number of them for the template argument size. Likewise for setN. Doing it this way allows the usual SWIG typemaps for double, etc. or whatever types your tuple contains to be applied automatically and correctly for each call to std::get<N>. These are really just an implementation detail, not intended to be part of the public interface, but exposing them makes no real odds.
  7. Finally we need an implementation of __getitem__ and a corresponding __setitem__. These simply look up and call the right getN/setN function on the class and call that instead. We take care to raise IndexError instead of the default exception if an invalid index is used as this will stop iteration correctly when we try to iterate of the tuple.

This is then sufficient that we can run our target code and get the following output:

$ swig3.0 -python -c++ -Wall test.i && g++ -shared -o _test.so test_wrap.cxx -I/usr/include/python3.7 -m32 && python3.7 run.py
<test.TupleDD; proxy of <Swig Object of type 'std::tuple< double,double > *' at 0xf766a260> >
['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'get0', 'get1', 'set0', 'set1', 'this']
0.0
1234.0

Generally this should work as you'd hope in most input/output situations in Python.

There are a few improvements we could look to make:

  1. Implement repr
  2. Implement slicing so that tuple[n:m] type indexing works
  3. Handle unpacking like Python tuples.
  4. Maybe do some more automatic conversions for compatible types?
  5. Avoid calling __len__ for every get/setitem call, either by caching the value in the class itself, or postponing it until the method lookup fails?

Upvotes: 2

Related Questions