2025, Nov 15 00:08
Как передать C/C++ буфер в Python через ctypes и NumPy
Как превратить uint8_t* и длину из C/C++ в bytes или NumPy через ctypes: без медленных циклов, с контролем времени жизни памяти и верным освобождением буфера.
Передача сырого байтового буфера из C/C++ в Python через ctypes кажется простой, пока не сталкиваешься с длиной и производительностью. Функция возвращает uint8_t* и длину через выходной параметр, нулевого терминатора нет, а вам нужен объект bytes или массив NumPy без медленного цикла на Python. Ключевой вопрос — как безопасно и эффективно превратить этот указатель в пригодный для работы тип Python.
Постановка задачи
Ниже — минимальная схема взаимодействия. Буфер — это числовые данные произвольной длины; скрытого терминатора нет, поэтому одного указателя недостаточно.
// C/C++
uint8_t* fetch_buf(uint32_t* out_len); // возвращает числовой буфер произвольной длины
# код на Python
import ctypes as ct
lib = ct.CDLL("libshared.so")
lib.fetch_buf.argtypes = [ct.POINTER(ct.c_uint32)]
lib.fetch_buf.restype = ct.POINTER(ct.c_uint8)
size_out = ct.c_uint32(0)
ptr = lib.fetch_buf(size_out)
# что дальше?
Мне нужен тип, который можно преобразовать в массив NumPy.
Что на самом деле происходит
Сырой указатель C не содержит информации о размере. Конструктор bytes в Python тоже не выводит длину из указателя, поэтому необходимо явно связать указатель с длиной, полученной через выходной параметр. Далее есть три практичных варианта, которые покрывают типичные задачи: сделать копию в bytes, получить на стороне Python список int с помощью среза или обернуть буфер в массив NumPy, который разделяет исходную память.
Вызов ct.string_at(ptr, length) возвращает объект bytes, копируя область памяти указанного размера. Срез вида ptr[:length] формирует новый список целых чисел, соответствующих байтам. А np.ctypeslib.as_array(ptr, shape=(length,)) оборачивает уже существующий буфер без копирования; это возвращает numpy.ndarray, который разделяет исходную память, — предпочтительный путь, когда важна производительность.
Если буфер выделяется на стороне C, не забудьте освободить его. После копирования (bytes или список) можно освобождать сразу. Если же память разделяется с NumPy, не освобождайте её, пока не закончите работать с массивом.
Практическое решение
Если нужен формат, сразу пригодный для NumPy, переходите прямо к массиву NumPy через совместно используемую память. Если удобнее bytes-объект или список Python, используйте методы с копированием. Ниже — рабочий пример, демонстрирующий все три подхода.
Рабочий пример (Windows)
Исходник, который выделяет буфер из пяти байт, заполняет его значениями 0..4, возвращает указатель и размер, а также предоставляет функцию освобождения.
// файл: demo.c
#include <stdint.h>
#include <stdlib.h>
__declspec(dllexport)
uint8_t* fetch_buf(uint32_t* out_len) {
uint8_t* buf = malloc(5);
for(uint8_t i = 0; i < 5; ++i)
buf[i] = i;
*out_len = 5;
return buf;
}
__declspec(dllexport)
void release_buf(uint8_t* p) {
free(p);
}
# файл: demo.py
import ctypes as ct
import numpy as np
mod = ct.CDLL('./demo')
mod.fetch_buf.argtypes = (ct.POINTER(ct.c_uint32),)
mod.fetch_buf.restype = ct.POINTER(ct.c_uint8)
mod.release_buf.argtypes = (ct.POINTER(ct.c_uint8),)
mod.release_buf.restype = None
count = ct.c_uint32(0)
ptr = mod.fetch_buf(count)
# 1) Копия в bytes через ctypes.string_at
as_bytes = ct.string_at(ptr, count.value)
print(as_bytes) # объект bytes
# 2) Копия в список Python срезом указателя
as_list = ptr[:count.value]
print(as_list) # список int
# 3) Разделить буфер с NumPy (без копирования)
as_np = np.ctypeslib.as_array(ptr, shape=(count.value,))
print(as_np) # массив numpy
as_np[0] = 7 # меняем через numpy...
print(as_np) # массив numpy
print(ptr[0]) # ...меняет и исходный буфер
mod.release_buf(ptr)
b'\x00\x01\x02\x03\x04'
[0, 1, 2, 3, 4]
[0 1 2 3 4]
[7 1 2 3 4]
7
Почему это важно
Производительность и безопасность памяти при межъязыковом взаимодействии зависят от этих деталей. Выбор между копией и совместно используемым буфером влияет и на скорость, и на управление временем жизни. Если нужен высокий пропускной поток и обработка данных на месте, подход с общим буфером NumPy избегает лишних выделений и копирования. Если требуется неизменяемый снимок, bytes — аккуратная автономная копия. В обоих случаях связка «указатель + размер» и своевременное освобождение памяти предотвращают утечки и use-after-free.
Главное
Всегда передавайте явную длину вместе с указателем; одного указателя недостаточно. Для прямого сценария с NumPy оборачивайте буфер через np.ctypeslib.as_array, указывайте shape и откладывайте освобождение памяти до завершения работы с массивом. Для автономных данных используйте ctypes.string_at, чтобы получить bytes, или возьмите срез для списка Python и освобождайте память сразу после копирования. Следуя этим схемам, вы избегаете медленных циклов на Python и сохраняете контроль над владением памятью и производительностью.