从C++传到Python

常见的教程如这个问题及回答是将大小已知的numpy数组传入传出C++,如确定会从C++传出大小为$M \times N$的矩阵。方法简单讲就是在Python端分配一个大小为$M \times N$的矩阵,把指向这个矩阵的指针传给C++,C++负责修改矩阵的内容,结束后矩阵就自动“传回”了。

然而有时我们事先不知道从C++传回的矩阵是多大,这时我们可以用这个回答所提及的技术,即从C++传回std::vector,然后在Python端把它无拷贝地转成numpy数组。

例子:从C++传回$M \times 2$大小的矩阵,$M$在Python端未知。例子主要来源于网络,但我稍微换了一下应用,并修改了里面的谬误。

doit.h:

#ifndef _DOIT_H_
#define _DOIT_H_
#include <vector>
std::vector<long> arange2d();
#endif

doit.cpp:

#include "doit.h"
std::vector<long> arange2d() {
	std::vector<long> arr(10);
	long x = 0;
	for (auto i = arr.begin(); i != arr.end(); ++i) {
		*i = x++;
	}
	return arr;
}

fast.pyx:

from libcpp.vector cimport vector

cdef extern from 'doit.h':
    vector[long] arange2d()

cdef class ArrayWrapper:
    cdef vector[long] v
    cdef Py_ssize_t shape[2];
    cdef Py_ssize_t strides[2];

    def set_data(self, vector[long]& data):
        self.v.swap(data)  # 注(1)

    def __getbuffer__(self, Py_buffer *buf, int flags):
        self.shape[0] = self.v.size() // 2
        self.shape[1] = 2
        self.strides[0] = self.shape[1] * sizeof(long)
        self.strides[1] = sizeof(long)

        # 注(2)
        buf.buf = <char *> self.v.data()
        buf.format = 'l'  # 注(3)
        buf.internal = NULL
        buf.itemsize = <Py_ssize_t> sizeof(long)
        buf.len = self.v.size() * sizeof(long)
        buf.ndim = 2
        buf.obj = self
        buf.readonly = 0
        buf.shape = self.shape
        buf.strides = self.strides
        buf.suboffsets = NULL

def pyarange2d():
    cdef vector[long] arr = arange2d()
    cdef ArrayWrapper wrapper = ArrayWrapper()
    wrapper.set_data(arr)
    return np.asarray(wrapper)
  • 注(1):std::vector<T>::swap完成了无拷贝传值,另一种方法是用std::move,不过那需要cdef extern from '<utility>' namespace 'std' nogil: vector[long] move(vector[long]),应该是这样,不过我没试过
  • 注(2):numpy的Buffer Protocol见此处,里面讲了buf需要设置哪些属性
  • 注(3):buf.format如何设置见此处

至于从C++传回Python的多维数组有两个及以上的维度不知道的话(已知维度总数ndim),网络上没找到答案,但我是这么做的:

  1. 传给C++一个指向Py_ssize_t类型、长度为ndim的数组(即待传回数组的shape)的指针
  2. C++传回一个std::vector并修改shape元素为合适的值
  3. 按照shapestd::vector的元素类型填写buf的属性,完成std::vector到numpy数组的转换

从Python传到C++

这应该已经耳熟能详了,我就不在此赘述了。不过有一点需要注意。传double数组时没问题,各平台double都对应numpy.float64。传int数组时需注意,Windows下对应numpy.int32、Linux/Mac下对应numpy.int64。所以直接用传double数组的方法传int数组会报这个错:

Cannot assign type 'int_t *' to 'int *'

这个问题(就是我提的)。目前我还没有优雅的解决方法。我笨拙的方法(受ead的启发)(请对照着“这个问题”看)如下:把所有的int全替换为int64_t(或int32_t,一致就行),例如int * => int64_t *np.int_t => np.int64_t,然后在dotit.h包含头文件的地方加上#include <cstdint>,在q.pyx头部加上from libc.stdint cimport int64_t。应该就可以编译了。

补充一点我近期观察到的:以上workaround在Windows下(Visual Studio 2022)貌似不行,会报不能将numpyint32_t转为int32_t,类似这样的错。在Darwin和Linux下都是能通过编译的。