Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to store python objects in Cython C++ containers?

I would like to port an existing c++ library with cython to Python, with the C++ library employing templates. In this case, it is the adevs library.

The question is how can I store Python objects in a C++ container with Cython? I know this is somehow discouraged, for issues of reference counting, but can it be done nevertheless and if so, how?

I am aware of Gauthier Boaglios' answer to a similar question. However, this does not address the issue of reference counting, apparently, as I tried the following:

Let's say in 'cadevs.pxd' I have the following code:

cdef extern from "adevs/adevs_digraph.h" namespace "adevs":
cdef cppclass PortValue[VALUE, PORT]:
    PortValue() except +
    PortValue(PORT port, const VALUE& value) except +
    PORT port
    VALUE value

And in 'adevs.pyx':

from cpython.ref cimport PyObject
cimport cadevs

ctypedef PyObject* PythonObject

cdef class PortValue:
    cdef cadevs.PortValue[PythonObject, PythonObject]* _c_portvalue

    def __cinit__(self, object port, object value):
        self._c_portvalue = new cadevs.PortValue[PythonObject, PythonObject](
            <PyObject *>port, <PyObject *>value
        )

    def __dealloc__(self):
        del self._c_portvalue

    property port:
        def __get__(self):
            return <object>self._c_portvalue.port

    property value:
        def __get__(self):
            return <object>self._c_portvalue.value

Then I cythonize and compile

$ cython --cplus -3 adevs.pyx
$ g++ -shared -pthread -fPIC -fwrapv -O2 -Wall -I/usr/include/python3.4m -I../include -lpython3.4m -o adevs.so adevs.cpp

But running in python or ipython

import adevs
pv = adevs.PortValue((1,2), 3)
pv.port
pv.port

crashes Python as the reference to the (1, 2) tuple is lost, apparently.

like image 340
Andreas Sorge Avatar asked Nov 14 '14 19:11

Andreas Sorge


2 Answers

You are right in that you will have difficulties with running a memory-safe application by storing python objects in a C++ container with Cython. If you want to do this in Cython, and not Pybind11 (as referenced by Mike MacNeil's answer), then you have a number of options.

  1. Store the value somewhere in Cython/Python to keep the reference count above 1 while the object is in the your container. Example:
cdef class PortValue:
    cdef cadevs.PortValue[PythonObject, PythonObject]* _c_portvalue

    # Add fields to keep stored Python objects alive.
    cdef object port_ref_holder 
    cdef object value_ref_holder 


    def __cinit__(self, object port, object value):
        self._c_portvalue = new cadevs.PortValue[PythonObject, PythonObject](
            <PyObject *>port, <PyObject *>value
        )

        # Assign objects here to keep them alive.
        port_ref_holder = port
        value_ref_holder = value
  1. You can use the Python C-API along with Wrapper to manually increment and decrement the reference The Python C API reference for reference counting is here. The cython package provides this API to you in Cython automatically as cython declaration (.pxd) file that you can cimport (see here). I can add reference counting functionality in a separate C++ file, or I can add this code directly to Cython verbatim according to the Interfacing with C guide. Something like this is a start:
from cpython.ref cimport PyObject, Py_INCREF, Py_DECREF
cdef extern from *:
    """
    class PyRef {
        PyObject* obj;
    public:
        
        PyObject* get() {return obj;}
        PyRef() {obj = NULL;}
        PyRef(PyObject* set_obj) {
            Py_XINCREF(set_obj); 
            obj = set_obj;}
        
        ~PyRef() {
            Py_XDECREF(obj);obj = NULL;
        }

        PyRef(const PyRef& other)  {
            Py_XINCREF(other.obj); 
            obj = other.obj;
        }
        PyRef(PyRef&& other) {obj = other.obj; other.obj = NULL;}

        PyRef& operator=(const PyRef& other) {
            Py_XDECREF(obj); 
            Py_XINCREF(other.obj); 
            obj = other.obj;
            return *this;
        }
        PyRef& operator=(PyRef&& other) {
            Py_XDECREF(obj); 
            obj = other.obj; 
            other.obj = NULL;
            return *this;
        }
    };
    """
    cdef cppclass PyRef:
        PyRef() except +
        PyRef(PyObject* set_obj) except +
        PyObject* get() except +
    

Then you use the "PyRef" class instead of PythonObject and use its get() method to borrow a reference to the stored python object.

like image 87
Golden Rockefeller Avatar answered Sep 20 '22 07:09

Golden Rockefeller


I'm not sure about binding to Cython which may be difficult, I would recommend using Pybind11, which makes it (relatively) simple to write python wrappers for C++ (see this particular example for object oriented code).

like image 21
Mike MacNeil Avatar answered Sep 21 '22 07:09

Mike MacNeil