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 и сохраняете контроль над владением памятью и производительностью.