1

Trying to use swig to pass a python list as input for c++ class with a (one of many) constructor taking a std::vector<double> * as input. Changing the C++ implementation of the codebase is not possible.

<EDIT> : What I am looking for is a way to "automatically" process a python list to a vector<double> * or say for example:

fake_class.cpp

class FakeClass
{
public:
   std::vector<double>* m_v;
   FakeClass(std::vector<double>* v) : m_v {v} {}
   void SomeFunction(); // A function doing some with said pointer (m_v)
   [...]
};

and then using this in python (say the compiled extension is fakeExample:

import fakeExample as ex
a = ex.FakeClass([1.2, 3.1, 4.1])
print(a.m_v)
a.SomeFunction()

without crashing. </EDIT>


What I've tried:

Code:

example.hpp

#include <vector>
#include <iostream>


class SampleClass
{
public:
    std::vector<double>* m_v;
    SampleClass(std::vector<double>* v) : m_v {v} {
     std::cout << "\nnon default constructor!\n";}

    SampleClass() {std::cout << "default constructor!\n";}
    void SampleMethod(std::vector<double>* arg);
    void SampleMethod2(std::vector<double>* arg);
    void print_member();
};

example.cpp

#include "example.hpp"
#include <iostream>


void SampleClass::SampleMethod(std::vector<double>* arg)
{
    for (auto x : (*arg)) std::cout << x << " ";

};

void SampleClass::SampleMethod2(std::vector<double>* arg)
{
    auto vr = arg;
    for (size_t i = 0; i < (*vr).size(); i++)
    {
        (*vr)[i] += 1;
    }
    for (auto x : (*vr)) std::cout<< x << "\n";
}

void SampleClass::print_member() {
    for (auto x : (*m_v)) {
        std::cout << x << " ";
    }
}

example.i

%module example

%{
    #include "example.hpp"
%}

%include "typemaps.i"
%include "std_vector.i"


%template(doublevector) std::vector<double>;

/* NOTE: Is this required? */
%naturalvar Sampleclass;


/* NOTE: This mostly works but not for constructor */
%apply std::vector<double> *INPUT {std::vector<double>* };

%include "example.hpp"

A Makefile (s_test.py is a simple test script I am not including here).

all: clean build run

clean:
        rm -rf *.o *_wrap.* *.so __pycache__/ *.gch example.py

build:
        swig -python -c++ example.i
        g++ -c -fPIC example.cpp example_wrap.cxx example.hpp -I/usr/include/python3.8
        g++ -shared example.o example_wrap.o -o _example.so
run:
        python s_test.py

build_cpp:
        g++ example.cpp example.hpp main.cpp -o test_this.o

And finally after compiling etc :

>>> import example as ex
>>> a = ex.SampleClass()
default constructor!
>>> a.SampleMethod([1.2, 3.1, 4.1])
1.2 3.1 4.1
>>> a.SampleMethod2([3.1, 2.1])
4.1
3.1    # Works fine(or at least as expected) until here.
>>> b = ex.SampleClass([1.2]) 
                                                                                                                                                                                                                     non default constructor!
>>> b.m_v
<example.doublevector; proxy of <Swig Object of type 'std::vector< double > *' at SOME_ADDRESS> >
>>> b.m_v.size()
17958553321119670438
>>> b.print_member()
>>> [... Lots of zeroes here ...]0 0 0 0[1]    <some_number> segmentation fault  python

And exits.

Thank you :)

LateBird
  • 25
  • 4

2 Answers2

0

The Python list passed into the non-default constructor gets converted to a temporary SWIG proxy of a vector<double>* and that pointer is saved by the constructor into SampleClass's m_v member, but the pointer no longer exists when the constructor returns. If you create a persistent doublevector and make sure it stays in scope, the code works.

I built the original code in the question for the following example:

>>> import example as ex
>>> v = ex.doublevector([1,2,3,4])
>>> s = ex.SampleClass(v)

non default constructor!
>>> s.m_v.size()
4
>>> list(s.m_v)
[1.0, 2.0, 3.0, 4.0]

As long as v exists, s.m_v will be valid.

Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • Hey Mark, and thanks for your answer! Truth is, the whole reason how I ran into this issue was while trying to "automate" the step you described above (i.e. passing just a list through python and having it work with the whole c++ class taking a pointer to a vector as an argument). I will update my question to reflect this. – LateBird Nov 29 '22 at 21:28
  • @LateBird The directly passed list does get automatically converted to a vector, but it is freed immediately after the function returns and the pointer saved in the class becomes invalid. As long as `v` above exists, `s` will be valid. `del v` will make `s` invalid. It’s like in C++ if you create a local vector, pass it to the class and then the local vector goes out of scope. Undefined behavior ensues – Mark Tolonen Nov 30 '22 at 08:06
  • thank you for the clarification, this makes total sense. So I suppose the only way to make something like what I had in mind would be to create a python wrapper which interfaces with the module produced by swig. I also looked a bit into typemaps, but I suppose my problem is a bit more fundamental and a temporary object (or another operation) won't fix this. I don't really have much experience in swig and especially typemaps so that's just a guess. Anyway, marking this as answered - thanks again! :) – LateBird Nov 30 '22 at 14:39
0

Just in case anyone is looking for another way to solve this, what I ended up doing was extending the class with a new constructor like this (inspired by this answer):

%extend SampleClass {
    SampleClass(std::vector<double>* vec) {
        auto tmp_buff = new std::vector<double>;
        for (auto x : (*vec)) tmp_buff->push_back(x);
        return new SampleClass(tmp_buff);
}
};

This still throws a redefinition warning which I am not sure how to solve.

Then in python:

>>> import example as ex
>>> a = ex.SampleClass([1.2, 3.1, 2.2])
>>> a.print_member()
1.2 3.1 2.2 
>>>
LateBird
  • 25
  • 4