7

In another question I learnt how to expose a function returning a C++ object to Python by copying the object. Having to perform a copy does not seem optimal. How can I return the object without copying it? i.e. how can I directly access the peaks returned by self.thisptr.getPeaks(data) in PyPeakDetection.getPeaks (defined in peak_detection_.pyx)?

peak_detection.hpp

#ifndef PEAKDETECTION_H
#define PEAKDETECTION_H

#include <string>
#include <map>
#include <vector>

#include "peak.hpp"


class PeakDetection
{
    public:
        PeakDetection(std::map<std::string, std::string> config);
        std::vector<Peak> getPeaks(std::vector<float> &data);

    private:
        float _threshold;               
};

#endif

peak_detection.cpp

#include <iostream>
#include <string>

#include "peak.hpp"
#include "peak_detection.hpp"


using namespace std;


PeakDetection::PeakDetection(map<string, string> config)
{   
    _threshold = stof(config["_threshold"]);
}

vector<Peak> PeakDetection::getPeaks(vector<float> &data){

    Peak peak1 = Peak(10,1);
    Peak peak2 = Peak(20,2);

    vector<Peak> test;
    test.push_back(peak1);
    test.push_back(peak2);

    return test;
}

peak.hpp

#ifndef PEAK_H
#define PEAK_H

class Peak {
    public:
        float freq;
        float mag;

        Peak() : freq(), mag() {}
        Peak(float f, float m) : freq(f), mag(m) {}
};

#endif

peak_detection_.pyx

# distutils: language = c++
# distutils: sources = peak_detection.cpp

from libcpp.vector cimport vector
from libcpp.map cimport map
from libcpp.string cimport string

cdef extern from "peak.hpp":
    cdef cppclass Peak:
        Peak()
        Peak(Peak &)
        float freq, mag


cdef class PyPeak:
    cdef Peak *thisptr

    def __cinit__(self):
        self.thisptr = new Peak()

    def __dealloc__(self):
        del self.thisptr

    cdef copy(self, Peak &other):
        del self.thisptr
        self.thisptr = new Peak(other)

    def __repr__(self):
        return "<Peak: freq={0}, mag={1}>".format(self.freq, self.mag)

    property freq:
        def __get__(self): return self.thisptr.freq
        def __set__(self, freq): self.thisptr.freq = freq

    property mag:
        def __get__(self): return self.thisptr.mag
        def __set__(self, mag): self.thisptr.mag = mag


cdef extern from "peak_detection.hpp":
    cdef cppclass PeakDetection:
        PeakDetection(map[string,string])
        vector[Peak] getPeaks(vector[float])

cdef class PyPeakDetection:
    cdef PeakDetection *thisptr

    def __cinit__(self, map[string,string] config):
        self.thisptr = new PeakDetection(config)

    def __dealloc__(self):
        del self.thisptr

    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)

        retval = []

        for peak in peaks:
            new_peak = PyPeak()
            new_peak.copy(peak) # how can I avoid that copy?
            retval.append(new_peak)

        return retval
Community
  • 1
  • 1
jul
  • 36,404
  • 64
  • 191
  • 318
  • You could wrap the object you don't want copied in another object which has value semantics but does not copy it's held value, like shared_ptr. Not sure if that is a godd idea though; you could also look at what boost::python does to solve this. – stijn Nov 12 '15 at 17:13

2 Answers2

7

If you have a modern C++ compiler and can use rvalue references, move constructors and std::move it's pretty straight-forward. I think the easiest way is to create a Cython wrapper for the vector, and then use a move constructor to take hold of the contents of the vector.

All code shown goes in peak_detection_.pyx.

First wrap std::move. For simplicity I've just wrapped the one case we want (vector<Peak>) rather than messing about with templates.

cdef extern from "<utility>":
    vector[Peak]&& move(vector[Peak]&&) # just define for peak rather than anything else

Second, create a vector wrapper class. This defines the Python functions necessary to access it like a list. It also defines a function to call the move assignment operator

cdef class PyPeakVector:
    cdef vector[Peak] vec

    cdef move_from(self, vector[Peak]&& move_this):
        self.vec = move(move_this)

    def __getitem__(self,idx):
        return PyPeak2(self,idx)

    def __len__(self):
        return self.vec.size()

Then define the class the wraps the Peak. This is slightly different to your other class in that it doesn't own the Peak it wraps (the vector does). Otherwise, most of the functions remain the same

cdef class PyPeak2:
    cdef int idx
    cdef PyPeakVector vector # keep this alive, since it owns the peak rather that PyPeak2

    def __cinit__(self,PyPeakVector vec,idx):
        self.vector = vec
        self.idx = idx

    cdef Peak* getthisptr(self):
        # lookup the pointer each time - it isn't generally safe
        # to store pointers incase the vector is resized
        return &self.vector.vec[self.idx]

    # rest of functions as is

    # don't define a destructor since we don't own the Peak

Finally, implement getPeaks()

cdef class PyPeakDetection:
    # ...    
    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)

        retval = PyPeakVector()
        retval.move_from(move(peaks))

        return retval

Alternative approaches:

If Peak was nontrivial you could go for an approach where you call move on Peak rather that on the vector, as you construct your PyPeaks. For the case you have here move and copy will be equivalent for `Peak.

If you can't use C++11 features you'll need to change the interface a little. Instead of having your C++ getPeaks function return a vector have it take an empty vector reference (owned by PyPeakVector) as an input argument and write into it. Much of the rest of the wrapping remains the same.

DavidW
  • 29,336
  • 6
  • 55
  • 86
  • Thanks again for your help! I'm not familiar with `move` semantics so I first need to read about it before investigating your solution. – jul Nov 13 '15 at 09:12
  • very nice. how to ensure in cython that the C++ compiler supports move ? would it be possible to do conditional compilation about it ? – Stephane Martin Nov 16 '15 at 10:53
  • @StephaneMartin I don't think conditional compilation in Cython is very easy. You might be better creating a small C++ shim testing the version with the `__cplusplus` macro, then either using `std::move` or making a copy if move isn't supported. I'm increasingly inclined to assume just that compilers do support C++11 (and thus move) though. – DavidW Nov 16 '15 at 12:33
  • Can the getthisptr method be made as a property called thisptr so they don't need to edit their existing methods in pyPeak? – Eney Aug 04 '22 at 16:14
  • 1
    @Eney Probably not. Generally properties are expected to return Python objects (not pointers). Cython has limited support for `cdef` properties, but I think it's for extern classes only. – DavidW Aug 04 '22 at 16:32
  • @DavidW I'm wondering if theres a good way to avoid modifying/copying a lot of code to accomplish this. One thought was to make `pyPeak2` a subclass of `pyPeak`. Since python properties aren't a good option yet, you could use custom `getthisptr()` and `setthisptr()` methods everywhere in the baseclass and then just override them in the subclass. But you'd have to use `__init__` probably instead `__cinit__`to change the args. Also you'd have to override `__dealloc__` to do nothing in the subclass, but the baseclass method will always be called right? Is there no better option? – Eney Aug 08 '22 at 21:45
  • You could definitely use inheritance to do this. Maybe have them both inherit from an abstract common base class? – DavidW Aug 09 '22 at 05:13
-2

There are two projects that accomplish interfacing with C++ code into Python that have withstood the test of time Boost.Python and SWIG. Both work by adding additional markup to pertinent C/C++ code and generating dynamically loaded python extension libraries (.so files) and the related python modules.

However, depending on your use case there may still be some additional markup that looks like "copying." However, the copying should not be as extensive and it will all be exposed in the C++ code rather than being explicitly copied verbatim in Cython/Pyrex.

XD573
  • 92
  • 4
  • 2
    I believe Cython "has withstood the test of time" too. I am sure there is a way to do this, it seems to me like a basic feature. I'm not familiar with wrappers, so maybe I'm wrong... – jul Nov 12 '15 at 17:40