Библиотека ctypes: Python и C вместе
Библиотека ctypes: вызов C функций из Python
Как загрузить динамическую библиотеку и вызвать простейшую C функцию?
Основной механизм ctypes основан на загрузке разделяемой библиотеки (dll, so, dylib) через ctypes.CDLL (или WinDLL, OleDLL под Windows) и последующем вызове её функций. Рассмотрим вызов функции strlen из libc.
import ctypes
# Загрузка библиотеки libc
# На Linux имя libc.so.6, на macOS libc.dylib, на Windows msvcrt.dll
libc = ctypes.CDLL("libc.so.6")
# Получаем атрибут функции
strlen_func = libc.strlen
# Указываем типы аргументов и возвращаемого значения (не обязательно, но желательно)
strlen_func.argtypes = [ctypes.c_char_p]
strlen_func.restype = ctypes.c_size_t
# Вызов
result = strlen_func(b"Hello, World!")
print(result) # 13
Use python in c (использование python в коде c (встраивание))
13
Python list in c (использование списков python в c)
Если типы не указывать, ctypes пытается угадать их по умолчанию (int для аргументов, int для результата) – это часто приводит к ошибкам. Рекомендуется всегда задавать argtypes и restype.
Типичные ошибки:
- AttributeError – функция не найдена в библиотеке (проверьте имя и регистр).
- OSError – библиотека не загружена (неверный путь, отсутствие файла).
- Неверный результат из‑за неправильных типов (например, передача Python str вместо bytes).
Как определить собственные типы и работать со структурами?
ctypes позволяет создавать классы, наследующие ctypes.Structure, для описания C структур. Поля задаются через атрибут _fields_.
import ctypes
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_int),
("y", ctypes.c_int)]
# Создание экземпляра
p = Point(10, 20)
print(p.x, p.y) # 10 20
# Передача в C функцию, ожидающую указатель на структуру
# Например, функция set_point(Point* p, int x, int y)
# Можно передавать ctypes.byref(p) для указателя
Python load c lib (загрузка c библиотеки в python)
Для работы с указателями на структуры используется ctypes.POINTER(Point).
Проблемы:
- Несовпадение выравнивания полей – ctypes автоматически подстраивает выравнивание, но при необходимости можно задавать _pack_.
- Битовые поля не поддерживаются напрямую; требуется ручное представление через целые числа.
Как передать в C функцию массив чисел?
Массивы создаются умножением типа на длину: (ctypes.c_int * 10)(). Это создаёт массив из 10 нулей.
import ctypes
# Создание массива из 5 целых чисел
arr = (ctypes.c_int * 5)(1, 2, 3, 4, 5)
print(arr[0], arr[4]) # 1 5
# Передача в функцию, ожидающую int* (указатель на первый элемент)
# Можно использовать ctypes.cast(arr, ctypes.POINTER(ctypes.c_int))
# или просто передать arr – ctypes автоматически приводит к указателю
Python c types (библиотека ctypes в python)
Для многомерных массивов создаются вложенные типы: (ctypes.c_int * 3) * 4 – матрица 4x3.
- Важно помнить, что массивы ctypes – это изменяемые объекты, занимающие непрерывную память.
- При передаче в функцию не нужно брать адрес – массив сам конвертируется в указатель.
Как реализовать обратный вызов (callback) из C в Python?
Для создания указателя на Python функцию используется ctypes.CFUNCTYPE с указанием типов возвращаемого значения и аргументов.
import ctypes
# Определяем тип функции обратного вызова: принимает два int, возвращает int
CMPFUNC = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
# Python функция сравнения
def py_cmp(a, b):
return a - b
# Создаём указатель
cmp_ptr = CMPFUNC(py_cmp)
# Используем, например, в C функции qsort
# Предполагаем, что у нас есть libc.qsort
libc = ctypes.CDLL("libc.so.6")
libc.qsort.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t, CMPFUNC]
libc.qsort.restype = None
arr = (ctypes.c_int * 5)(5, 3, 1, 4, 2)
libc.qsort(arr, len(arr), ctypes.sizeof(ctypes.c_int), cmp_ptr)
print(list(arr)) # [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
- Callback не должен быть сборщиком мусора – надо сохранять ссылку на него (как сделано выше).
- Исключения внутри callback могут привести к краху интерпретатора; лучше обрабатывать их в Python.
Как обрабатывать строки с учётом кодировки?
ctypes ожидает байтовые строки (тип c_char_p). Для преобразования Python str в bytes используется .encode(), для обратного – .decode(). Если C функция возвращает char*, ctypes по умолчанию возвращает bytes, но можно установить restype = ctypes.c_char_p и затем вызвать .decode().
import ctypes
libc = ctypes.CDLL("libc.so.6")
# Функция strdup – копирует строку и возвращает указатель
libc.strdup.argtypes = [ctypes.c_char_p]
libc.strdup.restype = ctypes.c_char_p
# Передаём байтовую строку
result_ptr = libc.strdup(b"Привет, мир!")
# Получаем bytes
result_bytes = ctypes.string_at(result_ptr)
print(result_bytes.decode("utf-8")) # "Привет, мир!"
# Освобождаем память (если нужно)
libc.free(result_ptr)
- Не использовать Python str напрямую – это вызовет TypeError.
- Возвращаемый указатель на строку должен быть освобождён, если это требуется (например, для strdup, malloc).
Как получить errno после вызова C функции?
ctypes не выставляет errno автоматически. Для его получения нужно использовать ctypes.get_errno() (доступно с Python 3.3). Перед вызовом можно установить собственный обработчик.
import ctypes
import os
libc = ctypes.CDLL("libc.so.6")
# Вызов open, который может установить errno
libc.open.argtypes = [ctypes.c_char_p, ctypes.c_int]
libc.open.restype = ctypes.c_int
fd = libc.open(b"nonexistent.txt", 0) # O_RDONLY = 0
if fd == -1:
err = ctypes.get_errno()
print(f"Ошибка {err}: {os.strerror(err)}")
Ошибка 2: No such file or directory
Важно:
- Вызов get_errno работает только на платформах, поддерживающих errno (Unix). На Windows используйте ctypes.get_last_error().
- Не все C библиотеки корректно возвращают errno.
Как использовать ctypes для работы с WinAPI (Windows)?
На Windows функции обычно имеют тип __stdcall, поэтому загружайте библиотеки через ctypes.WinDLL. Типы HANDLE, DWORD, LPCSTR определены в ctypes.wintypes.
import ctypes
from ctypes import wintypes
user32 = ctypes.WinDLL("user32")
# MessageBoxW (широкие символы)
user32.MessageBoxW.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.UINT]
user32.MessageBoxW.restype = wintypes.INT
# Вызов
result = user32.MessageBoxW(None, "Привет из Python!", "Заголовок", 0)
print(f"Выбрано: {result}")
- Для строк используйте ctypes.create_unicode_buffer.
- Различие в соглашении о вызовах – всегда указывайте WinDLL для stdcall.
Расширенные примеры и нестандартные случаи использования ctypes
Создание и использование битовых полей через ручное маскирование
C структуры с битовыми полями не поддерживаются ctypes напрямую. Однако можно эмулировать их с помощью обычных целых и масок. Пример для структуры с полями a (3 бита), b (5 бит).
import ctypes
class BitField(ctypes.Structure):
_fields_ = [("data", ctypes.c_uint32)]
@property
def a(self):
return (self.data >> 0) & 0x7 # 3 бита
@a.setter
def a(self, val):
self.data = (self.data & ~0x7) | (val & 0x7)
@property
def b(self):
return (self.data >> 3) & 0x1F # 5 бит
@b.setter
def b(self, val):
self.data = (self.data & ~(0x1F << 3)) | ((val & 0x1F) << 3)
b = BitField()
b.a = 5
b.b = 17
print(f"data = {b.data:#010x}") # двоичный вид
print(f"a={b.a}, b={b.b}")
data = 0x0000008d a=5, b=17
Работа с макросами offsetof и sizeof
ctypes предоставляет ctypes.sizeof() для получения размера типа, но offsetof нет. Реализуем его через вычисление смещения.
import ctypes
class Test(ctypes.Structure):
_fields_ = [
("c", ctypes.c_char),
("i", ctypes.c_int),
("d", ctypes.c_double)
]
def offsetof(cls, field_name):
# Создаём временный экземпляр и определяем адрес поля
temp = cls()
addr_struct = ctypes.addressof(temp)
addr_field = ctypes.addressof(getattr(temp, field_name))
return addr_field - addr_struct
print("Размер Test:", ctypes.sizeof(Test))
print("Смещение 'c':", offsetof(Test, 'c'))
print("Смещение 'i':", offsetof(Test, 'i'))
print("Смещение 'd':", offsetof(Test, 'd'))
Размер Test: 16 Смещение 'c': 0 Смещение 'i': 4 Смещение 'd': 8
Использование ctypes с расширенными числовыми типами (long long, size_t)
Для 64‑битных целых и указателей есть c_longlong, c_uint64, c_size_t. Пример вызова strtol с указателем на long.
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.strtol.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.c_int]
libc.strtol.restype = ctypes.c_long
endptr = ctypes.c_char_p()
result = libc.strtol(b"12345abc", ctypes.byref(endptr), 10)
print("Число:", result)
print("Оставшаяся строка:", endptr.value.decode())
Число: 12345 Оставшаяся строка: abc
Создание собственных указателей и работа с c_void_p
Иногда нужно передать в C функцию произвольный указатель. c_void_p подходит для любого адреса. Пример с функцией memset.
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.memset.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t]
libc.memset.restype = ctypes.c_void_p
# Буфер из 10 байт
buf = (ctypes.c_byte * 10)()
libc.memset(buf, 0xFF, len(buf))
print(list(buf)) # [255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
Динамическое создание структур с неизвестным размером (гибкие массивы)
C позволяет структуры с последним полем‑массивом без указания размера. ctypes может эмулировать это, используя _length_ или динамическое создание класса.
import ctypes
class Flexible(ctypes.Structure):
_fields_ = [("len", ctypes.c_int), ("data", ctypes.c_char * 0)] # нулевой массив
# Создаём экземпляр с дополнительной памятью
# Используем ctypes.create_string_buffer с подходящим размером
buf_size = ctypes.sizeof(Flexible) + 10 # 10 дополнительных байт
buf = ctypes.create_string_buffer(buf_size)
flex = ctypes.cast(buf, ctypes.POINTER(Flexible))[0]
flex.len = 10
# Заполняем data через ctypes.memmove
ctypes.memmove(flex.data, b"HelloWorld", 10)
print(ctypes.string_at(flex.data, 10)) # b'HelloWorld'
b'HelloWorld'
Работа с функциями переменного числа аргументов (vararg)
ctypes не поддерживает vararg напрямую, но можно использовать низкоуровневые механизмы, например, ctypes.CFUNCTYPE с ... (только в Python 3.12+). Ниже показан обходной путь через явную передачу аргументов упакованными в массив.
import ctypes
libc = ctypes.CDLL("libc.so.6")
# printf с одним известным аргументом
libc.printf.argtypes = [ctypes.c_char_p]
libc.printf.restype = ctypes.c_int
# Нормальный вызов с одним аргументом
libc.printf(b"Hello\n")
# Для нескольких аргументов придётся использовать ctypes.cast и ручное управление стеком (сложно – не рекомендуется).
# Более простой способ – использовать модуль syscall или написать C обёртку.
Использование ctypes с потоками и глобальной блокировкой интерпретатора (GIL)
При вызове C функций, которые могут блокироваться (например, сокетные операции), интерпретатор отпускает GIL. ctypes делает это автоматически. Однако при ручном создании потоков с C функциями может потребоваться явное освобождение GIL с помощью ctypes.pythonapi.PyGILState_Ensure и аналогичных – это продвинутая техника.
import ctypes
import threading
libc = ctypes.CDLL("libc.so.6")
libc.usleep.argtypes = [ctypes.c_uint32]
libc.usleep.restype = ctypes.c_int
def sleep_work(seconds):
libc.usleep(int(seconds * 1_000_000))
print(f"Вызов завершён через {seconds} сек")
t = threading.Thread(target=sleep_work, args=(1.5,))
t.start()
print("Главный поток продолжает работу")
t.join()
Главный поток продолжает работу Вызов завершён через 1.5 сек
Динамическая загрузка библиотек по имени без указания пути
Иногда нужно загрузить библиотеку из стандартного пути. ctypes ищет библиотеку по стандартным правилам ОС. На Linux можно использовать ctypes.util.find_library.
import ctypes
import ctypes.util
lib_path = ctypes.util.find_library("c")
if lib_path:
libc = ctypes.CDLL(lib_path)
print(f"Загружена: {lib_path}")
else:
print("Библиотека не найдена")
Загружена: libc.so.6