Ctypes: выделить double **, передать его в C, затем использовать его в Python

РЕДАКТИРОВАТЬ 3

У меня есть некоторый код C ++ (внешний вид как C), к которому я обращаюсь из Python.
Я хочу выделить double** в python передайте его в код C / C ++ для копирования содержимого внутренних данных класса, а затем используйте его в python аналогично тому, как я использовал бы список списков.

К сожалению, мне не удается указать 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 (это segfaults. Поместите его в тот же каталог, в котором находится 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, чтобы я мог рассматривать массив как список?
(Мне нужно только прочитать из него)

2

Решение

3 способа передачи массива int ** из Python в C и обратно

Чтобы Python знал размер массива при итерации


Данные

Эти решения работают с 2d-массивом или массивом указателей на массивы с небольшими изменениями, без использования таких библиотек, как 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-массив. 2D array[N][M] выделяется как 1D array[N*M], с 2d_array[n][m] == 1d_array[n*M + m],
Так как наш код ожидает int**, но наш 2D массив выделен как int *Мы создаем временный массив для предоставления ожидаемой структуры.

Мы выделяем temp[N][M], а затем мы назначаем адрес памяти, выделенной ранее temp[n] = 2d_array[n] = &1d_array[n*M] (второе равенство показывает, что происходит с реальной памятью, которую мы выделили).

Если вы измените код копирования так, чтобы он копировал больше, чем M, скажем, M + 1, вы увидите, что он не будет работать с ошибкой, но перезапишет память следующей строки, потому что они смежные (если вы измените код копирования) , не забудьте добавить увеличение на 1 размер dest, выделенного в python, иначе он будет зависеть при записи после последнего элемента последней строки)


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 не будет проверять привязку и начнет читать всю вашу память, что приведет к segfault.

Итак, мы нарезаем указатель, беря первые M элементов (которые фактически являются всеми элементами в массиве). Теперь Python знает, что он должен перебирать только первые M элементов, и он больше не будет зависать.

Я полагаю, что Python копирует содержимое, указанное в новый список, используя этот метод ( см источники )


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) мы выделяем непрерывный 2D-массив. Но так как мы можем изменить код C, нам не нужно создавать другой массив и копировать указатели, так как мы будем передавать ожидаемый тип C.

В функции копирования мы передаем адрес первого элемента каждой строки и копируем M элементов в этой строке, затем переходим к следующей строке.

Шаблон копирования точно такой же, как и в случае 1), но на этот раз вместо написания интерфейса на python, чтобы код C получал данные так, как он ожидает, мы изменили код C, чтобы ожидать данные в этом точном формате.

Если вы сохраните этот C-код, вы также сможете использовать пустые массивы, так как они являются основными массивами 2D-строк.


Весь этот ответ возможен благодаря замечательным (и кратким) комментариям @eryksun ниже исходного вопроса.

3

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]