3

I'd like to be able to pass a buffer (ie something implementing the buffer protocol, such as a numpy array) of numeric data into c++ from Python:

>>> import mymod
>>> import numpy
>>> mymod.some_func(numpy.array([1,2,3]))

and receive it in c++ in some way:

void some_func([something] array) {
    for (int ii : array) {
       cout << ii << endl;
    }
}

prints

1 
2
3

I don't really care what the [something] is (pointer, std::vector, whatever). Does anyone know how to do this? There's surprisingly little information on it...

1 Answer 1

5

OK <get_ready_for_this.mp3> here's what I did to solve this.

First, I created a type representing the buffer I wanted, along with some helpers functions to cast a buffer of data to the target format. You could easily amend this to be more flexible, but I just want an array of complex floating point values.

// vector of complex values
typedef vector<cfloat> cbuffer;


// helper to copy data
template<typename T>
void cbuffer_copy_from(cbuffer& cbuf, void *ptr, ssize_t len, ssize_t stride) {
    cbuf.reserve(len);

    // convert elements into buffer
    char* cptr = (char*)ptr;
    for (ssize_t ii=0; ii < len; ii++) {
        cbuf.emplace_back(*reinterpret_cast<T*>(cptr));
        cptr += stride;
    }
};


// populate vector from source
template<typename T>
void cbuffer_from(cbuffer& cbuf, void *ptr, ssize_t len, ssize_t stride) {
    cbuffer_copy_from<T>(cbuf, ptr, len, stride);
}


// fast path for data that's already cfloat
template <>
void cbuffer_from<cfloat>(cbuffer& cbuf, void *ptr, ssize_t len, ssize_t stride) {
    // if stride is right, we can just copy the data
    if (stride == sizeof(cfloat)) {
        cbuf.resize(len);
        memcpy(&cbuf[0], ptr, len*sizeof(cfloat));
    } else {
        cbuffer_copy_from<cfloat>(cbuf, ptr, len, stride);
    }
}

Then, I built a custom converter from python to my cbuffer type:

// python -> cbuffer conversion
struct python_to_cbuffer {
    // register converter 
    python_to_cbuffer() {
        converter::registry::push_back(
            &convertible,
            &construct,
            type_id<cbuffer>()
        );
    }

    // does python object implement buffer protocol?
    static void* convertible(PyObject* object) {
        return PyObject_CheckBuffer(object) ? object : nullptr;
    }

    // convert object into a complex number
    static void construct(
        PyObject* object,
        converter::rvalue_from_python_stage1_data* data
    ) {
        // grab pointer to memory into which to construct the new value
        void* storage = ((converter::rvalue_from_python_storage<cbuffer>*)data)->storage.bytes;

        // create buffer object from export source, require format
        Py_buffer view;
        if (PyObject_GetBuffer(object, &view, PyBUF_FORMAT | PyBUF_STRIDES) < 0) {
            return;
        }

        // make sure it's a one dimensional array
        if (view.ndim != 1) {
            PyBuffer_Release(&view);
            throw std::runtime_error("Array object is not one dimensional");
        }

        // build new cbuffer to store data
        new (storage) cbuffer;
        cbuffer* buffer = static_cast<cbuffer*>(storage);

        // try to convert view data into cfloat format
        string type(view.format);
             if (type == "f")  cbuffer_from<float>  (*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "d")  cbuffer_from<double> (*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "Zf") cbuffer_from<cfloat> (*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "Zd") cbuffer_from<cdouble>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "b")  cbuffer_from<int8_t> (*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "h")  cbuffer_from<int16_t>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "i")  cbuffer_from<int32_t>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "l")  cbuffer_from<int32_t>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "q")  cbuffer_from<int32_t>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else if (type == "n")  cbuffer_from<ssize_t>(*buffer, view.buf, view.shape[0], view.strides[0]);
        else {
            buffer->~cbuffer();
            throw std::runtime_error("Unable to marshall '" + string(view.format) + "' data format");
        }

        // Stash the memory chunk pointer for later use by boost.python
        data->convertible = storage;
    }
};

The convertible() function checks that the Python object implements the buffer protocol. Then the construct() function actually extracts a buffer from the object, and converts it to the desired format via the above helper functions. If we fail at any step, cleanup and throw a runtime exception.

Lastly we instantiate the converter in the module:

// define python module
BOOST_PYTHON_MODULE(module) {
    // register python -> c++ converters
    python_to_cbuffer();

    def("test", test);
}

And, if we create a test function:

void test(cbuffer buf) {
    for (cfloat val : buf) {
        printf("(%f, %f)\n", val.re, val.im);
    }
}

Then in python:

>>> module.test(numpy.array([1+2j,3+4j],dtype=numpy.complex64))
(1.000000, 2.000000)
(3.000000, 4.000000)
>>> module.test(numpy.array([1,2],'b'))
(1.000000, 0.000000)
(2.000000, 0.000000)
>>> module.test(numpy.array([1,2],'i'))
(1.000000, 0.000000)
(2.000000, 0.000000)
>>> module.test(numpy.array([1,2],'l'))
(1.000000, 0.000000)
(2.000000, 0.000000)

Enjoy!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.