Search code examples
c++cythonshared-ptrstdvector

How to wrap std::shared_ptr and std::vector from C++ in Cython?


I am trying to wrap a C++ library into a python API with Cython. The class I want to wrap has the following template:

template<typename Value>
class ClassToWrap
{

public:
    typedef std::shared_ptr<std::vector<Value> > TypeToWrap;

    ClassToWrap(TypeToWrap data)
    {
    }
}

I'm not confident with C++ standard library. How I can wrap the TypeToWrap in Cython in a way that it can be inizialized in a simple way like an array or a multidimenstional array, for example with a for loop of assignments? Thanks for any suggestion.


Solution

  • let's assume you have a C++ header as follows:

    // cpp_class.h
    
    #include <memory>
    #include <vector>
    
    template<typename Value>
    class ClassToWrap
    {
    public:
        typedef std::shared_ptr<std::vector<Value> > TypeToWrap;
    
        ClassToWrap(TypeToWrap data) : obj(std::move(data))
        {
        }
    private:
        TypeToWrap obj;
    };
    

    you would need to expose this class to cython, this is done by a cdef extern from cython wrapping Cpp documentation.

    # my_cy_class.pyx
    
    # distutils: language = c++
    
    from libcpp.memory cimport make_shared, shared_ptr
    from libcpp.vector cimport vector
    
    cdef extern from "cpp_class.h" nogil:
        cdef cppclass ClassToWrap[T]:
            ctypedef shared_ptr[vector[T]] TypeToWrap
            ClassToWrap(TypeToWrap)
            # define anything you intend to use
    

    note that you only need to define the functions, not their implementations.

    secondly, let's define a cython class to wrap it and expose it to python, since python is going to use it, it needs to know the type of T, let's assume it is an int:

    from cython.operator cimport dereference as deref
    from libcpp.utility cimport move
    
    cdef class wrapper_class:
        cdef ClassToWrap[int]* wrapped_obj  # needs to be a defined type and heap allocated
    
        def __cinit__(self, some_list):
            cdef vector[int] v = some_list
            cdef ClassToWrap[int].TypeToWrap ptr = make_shared[vector[int]](move(v))
            self.wrapped_obj = new ClassToWrap[int](move(ptr))
            # deref(self.wrapped_obj).foo()
    
        def __dealloc__(self):
            del self.wrapped_obj
    

    you may be wondering why a pointer to an object is used ? the reason is because your object has no default zero arguments constructor, and cython requires a default zero arguments constructor to be able to stack allocate it, using a __cinit__ and a __dealloc__ guarantee no memory leaks

    note that some_list doesn't need to be a python list, it can easily be a numpy array, and knowing the type beforehand can help the compiler optimize the code for it, the following code can test it.

    import pyximport
    script_args = ["--cython-cplus"]
    setup_args = {
        "script_args": script_args,
        "include_dirs": ['.'],
    
    }
    pyximport.install(setup_args=setup_args, language_level=3,)
    
    import numpy as np
    import my_cy_class
    
    inputs = np.array([1,2,3,4,5])
    a = my_cy_class.wrapper_class(inputs)