Ctypes:分配 double** ,将其传递给 C,然后在 Python 中使用它

Ctypes: allocate double** , pass it to C, then use it in Python

编辑 3

我有一些从 python 访问的 C++ 代码(外部为 C)。 我想在python中分配一个double**,传给C/C++代码复制一个class内部数据的内容,然后在[=]中使用35=] 类似于我使用列表列表的方式。

不幸的是,我无法指定 python 最内部数组的大小,因此在遍历它和程序段错误时它会读取无效内存。

我无法更改 C++ 中的内部数据结构,我希望 python 为我进行绑定检查(就像我使用 c_double_Array_N_Array_M 而不是指针数组)。

test.cpp(用g++ -Wall -fPIC --shared -o test.so test.cpp编译)

#include <stdlib.h>
#include <string.h>

class Dummy
{
    double** ptr;
    int e;
    int i;
};

extern "C" {
    void * get_dummy(int N, int M) {
        Dummy * d = new Dummy();
        d->ptr = new double*[N];
        d->e = N;
        d->i = M;
        for(int i=0; i<N; ++i)
        {
            d->ptr[i]=new double[M];
            for(int j=0; j <M; ++j)
            {
                d->ptr[i][j] = i*N + j;
            }
        }
        return d;
    }

    void copy(void * inst, double ** dest) {
        Dummy * d = static_cast<Dummy*>(inst);
        for(int i=0; i < d->e; ++i)
        {
            memcpy(dest[i], d->ptr[i], sizeof(double) * d->i);
        }
    }

    void cleanup(void * inst) {
        if (inst != NULL) {
            Dummy * d = static_cast<Dummy*>(inst);
            for(int i=0; i < d->e; ++i)
            {
                delete[] d->ptr[i];
            }
            delete[] d->ptr;
            delete d;
        }
    }


}

Python(这个段错误。把它放在 test.so 所在的同一个目录中)

import os
from contextlib import contextmanager
import ctypes as ct

DOUBLE_P = ct.POINTER(ct.c_double)
library_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test.so')
lib = ct.cdll.LoadLibrary(library_path)
lib.get_dummy.restype = ct.c_void_p
N=15
M=10

@contextmanager
def work_with_dummy(N, M):
    dummy = None
    try:
        dummy = lib.get_dummy(N, M)
        yield dummy
    finally:
        lib.cleanup(dummy)

with work_with_dummy(N,M) as dummy:
    internal = (ct.c_double * M)
    # Dest is allocated in python, it will live out of the with context and will be deallocated by python
    dest = (DOUBLE_P * N)()
    for i in range(N):
        dest[i] = internal()
    lib.copy(dummy, dest)

#dummy is not available anymore here. All the C resources has been cleaned up
for i in dest:
    for n in i:
        print(n) #it segfaults reading more than the length of the array

我可以在我的 python 代码中更改什么,以便我可以将数组视为列表? (我只需要阅读它)

3 种将 int** 数组从 Python 传递到 C 并返回

的方法

使得Python迭代时知道数组的大小


数据

此解决方案适用于二维数组或指向数组的指针数组,稍作修改,无需使用像 numpy 这样的库。

我将使用 int 作为类型而不是 double,我们将复制源代码,它被定义为

N = 10;
M = 15;
int ** source = (int **) malloc(sizeof(int*) * N);
for(int i=0; i<N; ++i)
{
    source[i] = (int *) malloc(sizeof(int) * M);
    for(int j=0; j<M; ++j)
    {
        source[i][j] = i*N + j;
    }
}

1) 分配数组指针

Python分配

dest = ((ctypes.c_int * M) * N) ()
int_P = ctypes.POINTER(ctypes.c_int)
temp = (int_P * N) ()
for i in range(N):
    temp[i] = dest[i]
lib.copy(temp)
del temp
# temp gets collected by GC, but the data was stored into the memory allocated by dest

# You can now access dest as if it was a list of lists
for row in dest:
    for item in row:
        print(item)

C复制函数

void copy(int** dest)
{
    for(int i=0; i<N; ++i)
    {
        memcpy(dest[i], source[i], sizeof(int) * M);
    }
}

说明

我们首先分配一个二维数组。 2D array[N][M] 被分配为 1D array[N*M]2d_array[n][m] == 1d_array[n*M + m]。 由于我们的代码需要 int**,但我们的二维数组分配为 int *,我们创建一个临时数组来提供预期的结构。

我们分配 temp[N][M],然后分配我们之前分配的内存地址 temp[n] = 2d_array[n] = &1d_array[n*M](第二个等号表示我们分配的实际内存发生了什么)。

如果您更改复制代码使其复制多于 M,比方说 M+1,您将看到它不会发生段错误,但它会覆盖下一行的内存,因为它们是连续的(如果你更改复制代码,记得将python中分配的dest的大小增加1,否则在最后一行的最后一项之后写入会出现段错误)


2) 切片指针

Python分配

int_P = ctypes.POINTER(ctypes.c_int)
inner_array = (ctypes.c_int * M)
dest = (int_P * N) ()
for i in range(N):
    dest[i] = inner_array()
lib.copy(dest)

for row in dest:
    # Python knows the length of dest, so everything works fine here
    for item in row:
        # Python doesn't know that row is an array, so it will continue to read memory without ever stopping (actually, a segfault will stop it)
        print(item)

dest = [internal[:M] for internal in dest]

for row in dest:
    for item in row:
        # No more segfaulting, as now python know that internal is M item long
        print(item)

C复制函数

Same as for solution 1

说明

这次我们分配一个实际的数组指针数组,就像分配源一样。

由于最外面的数组 ( dest ) 是一个指针数组,python 不知道指向的数组的长度(它甚至不知道那是一个数组,它可能是一个指向单个 int 的指针)。

如果您遍历该指针,python 将不会进行绑定检查,它会开始读取您的所有内存,从而导致段错误。

因此,我们将指针切片为前 M 个元素(实际上是数组中的所有元素)。现在 python 知道它应该只迭代前 M 个元素,并且不会再出现段错误。

我相信 python 使用此方法复制指向新列表的内容 (see sources)


2.1) 切片指针,继续

Eryksun 在评论中加入并提出了一个解决方案,可以避免复制新列表中的所有元素。

Python分配

int_P = ctypes.POINTER(ctypes.c_int)
inner_array = (ctypes.c_int * M)
inner_array_P = ctypes.POINTER(inner_array)
dest = (int_P * N) ()
for i in range(N):
    dest[i] = inner_array()
lib.copy(dest)

dest_arrays = [inner_array_p.from_buffer(x)[0] for x in dest]

for row in dest_arrays:
    for item in row:
        print(item)

C复制代码

Same as for solution 1

3) 连续内存

只有在C端可以更改复制代码的情况下才可以使用此方法。 source不需要更改。

Python分配

dest = ((ctypes.c_int * M) * N) ()
lib.copy(dest)

for row in dest:
    for item in row:
        print(item)

C复制函数

void copy(int * dest) {
    for(int i=0; i < N; ++i)
    {
        memcpy(&dest[i * M], source[i], sizeof(int) * M);
    }
}

说明

这一次,就像 1) 的情况一样,我们正在分配一个连续的二维数组。但由于我们可以更改 C 代码,我们不需要创建不同的数组并复制指针,因为我们将向 C 提供预期的类型。

在复制函数中,我们传递每行第一项的地址,复制该行的M个元素,然后转到下一行。

复制模式与 1) 中的完全相同,但是这次我们没有在 python 中编写接口,以便 C 代码按预期方式接收数据,而是更改了 C期望数据采用这种精确格式的代码。

如果保留此 C 代码,您也可以使用 numpy 数组,因为它们是二维行主数组。


感谢@eryksun 在原始问题下方的精彩(简洁)评论,所有这些答案都是可能的。