2025, Nov 13 03:03
Исправляем unsupported PEP 3118 в Numba cfunc: не захватывайте ctypes-указатель
Почему Numba падает с unsupported PEP 3118 при захвате ctypes-указателя в замыкании и как исправить: передавайте arr.ctypes и согласуйте intc. Пример.
Когда вы строите фабрику, возвращающую Numba cfunc для работы с массивами NumPy, может показаться удобным передать «сырой» указатель, полученный через ctypes. На практике это часто заканчивается падением с ошибкой unsupported PEP 3118 format: замыкание захватывает ctypes‑указатель, который Numba не может типизировать в режиме nopython. Исправление проще, чем кажется: при вызове скомпилированной функции передавайте напрямую атрибут .ctypes массива.
Воспроизведение проблемы
Ниже приведён минимальный пример фабрики, которая компилирует C‑функцию без аргументов и внутри вызывает другой cfunc для вычисления суммы массива NumPy. Логика сохранена; изменены лишь имена для удобства чтения.
import numpy as np
from numba import cfunc, carray
from numba.types import intc, CPointer, float64
import ctypes
class CalcBox:
def __init__(self, buf):
self.buf = buf
self.length = buf.size
# C-тип сигнатуры: double func(double* input, int n)
c_sig = float64(CPointer(float64), intc)
c_sig_void = float64()
@staticmethod
@cfunc(c_sig)
def c_sum(ptr, n):
view = carray(ptr, n)
return np.sum(view)
def make_sum(self):
arr = self.buf
size = self.length
compiled = type(self).c_sum
raw_ptr = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
@cfunc(self.c_sig_void)
def thunk():
return compiled(raw_ptr, size)
return thunk.address
vec = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
box = CalcBox(vec)
addr = box.make_sum()
C0 = ctypes.CFUNCTYPE(ctypes.c_double)
fn = C0(addr)
print("Sum:", fn())
Такой шаблон нередко падает с трассировкой, где фигурируют unsupported PEP 3118 format и untyped global name, связанный с захваченным указателем. Суть проблемы не в суммировании, а в том, как ctypes‑указатель попадает в замыкание скомпилированной функции.
Почему это ломается
Вложенная скомпилированная функция замыкает объект Python — ctypes‑указатель. Внутри cfunc это превращается в проблему типизации: Numba не распознаёт объект ctypes‑указателя как допустимое nopython‑значение и выдаёт ошибку декодирования, связанную с форматами PEP 3118. Проще говоря, указатель, захваченный замыканием, не может быть протипизирован фронтендом Numba, и имя остаётся нетипизированным.
Есть и контекст, объясняющий сопутствующие ловушки. Замыкания поддерживаются довольно неэффективным способом: захваченные переменные рассматриваются так, будто они фактически константы для сгенерированной функции. Numba может сгенерировать отдельную C‑функцию в своём LLVM‑модуле, сохранив эти значения как глобальные. Изменять захваченную переменную после компиляции — это неопределённое поведение (UB): иногда кажется, что всё работает, но может тихо сломаться, особенно когда оптимизаторы не полностью распространяют константы через указатели. Такой подход также способен увеличивать время компиляции и размер модулей, в зависимости от массива и версии инструментов. Наконец, хотя захват ints или floats часто кажется безобидным, с указателями возникают проблемы, и объект ctypes‑указателя в этой ситуации системой типов не распознаётся.
Решение
Обходной путь прямолинеен: не создавайте явно ctypes‑указатель и не захватывайте его; вместо этого передавайте атрибут .ctypes массива непосредственно в скомпилированную функцию. Так вы избегаете нетипизируемого объекта ctypes внутри замыкания.
import numpy as np
from numba import cfunc, carray
from numba.types import intc, CPointer, float64
import ctypes
class CalcBox:
def __init__(self, buf):
self.buf = buf
self.length = buf.size
c_sig = float64(CPointer(float64), intc)
c_sig_void = float64()
@staticmethod
@cfunc(c_sig)
def c_sum(ptr, n):
view = carray(ptr, n)
return np.sum(view)
def make_sum(self):
arr = self.buf
size = np.intc(self.length) # при необходимости приводим к intc
compiled = type(self).c_sum
@cfunc(self.c_sig_void)
def thunk():
return compiled(arr.ctypes, size)
return thunk.address
vec = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
box = CalcBox(vec)
addr = box.make_sum()
C0 = ctypes.CFUNCTYPE(ctypes.c_double)
fn = C0(addr)
print("Sum:", fn())
Единственное функциональное изменение — заменить явный ctypes‑указатель на arr.ctypes в месте вызова. Если сигнатура ожидает intc для размера, передавайте np.intc, чтобы соответствовать ей.
Почему это важно
Связывание NumPy, Numba и C‑интерфейсов очень мощно, но именно детали — например, как указатель попадает в скомпилированное замыкание — определяют, получите ли вы чисто вызываемую из C функцию или неясную ошибку типизации. Понимание того, что захваченные значения рассматриваются как константы, помогает избежать неопределённого поведения при их изменении в дальнейшем. Это также формирует ожидания насчёт возможных накладных расходов компиляции и роста размера модулей в отдельных сценариях.
Выводы
Если вам нужна фабрика, возвращающая Numba cfunc, который потребляет массив NumPy, не захватывайте «сырые» указатели ctypes внутри скомпилированного замыкания. Используйте arr.ctypes при вызове скомпилированной функции и следите за соответствием разрядностей целых объявленной сигнатуре — например, передавайте np.intc, когда сигнатура использует intc. Это сохраняет типизируемость в nopython‑режиме, устраняет ошибки неподдерживаемого формата PEP 3118 и снижает риск сюрпризов из‑за того, как понижены замыкания под капотом.