Расширение Python при помощи библиотек С и модуля ctypes

автор

Полное руководство по расширению программ Python при помощи написанных в С библиотек и встроенного модуля ctypes.

Руководство по ctypes

Модуль ctypes – это мощный инструмент в Python, который позволяет вам использовать существующие библиотеки в других языках путем написания простых декораторов в самом Python. Однако, нужно изучить ряд хитростей, чтобы быть в состоянии это делать. Для этого в данной статье мы исследуем основы модуля ctypes. А именно:

  • Загрузка С библиотек;
  • Вызов простой С функции;
  • Передача изменяемых и неизменных строк;
  • Управление памятью

Для начала, давайте рассмотрим простую С библиотеку, которую мы будем использовать, и попробуем её создать, после этого перейдем непосредственно к загрузке библиотеки С и вызову в ней функций.

Простая С библиотека, которую можно использовать из Python

Здесь мы рассмотрим все этапы создания и тестирования примеров кода. Небольшое предисловие о самой С библиотеке, перед тем, как мы перейдем к модулю ctypes.

Код С, который мы будем использовать в этом руководстве будет настолько прост, насколько это возможно- он написан специально для раскрытия основных понятий, рассматриваемых в этом руководстве. Это скорее «пример на пальцах», и не предназначен для более широкого применения. Давайте рассмотрим функции, которые мы будем использовать:

Функция simple_function просто возвращает подсчитанные числа. Каждый раз, когда она вызывается в счетчике increments, она выдает следующее значение:

Функция add_one_to_string добавляет единицу к каждому символу в переданном массиве char. Мы используем его при рассмотрении неизменяемых строк Python и узнаем, как работать с ними при необходимости.

Эта пара функций выделяет и освобождает строку в контексте С. Это – отличная основа для обсуждения нюансов управления памятью в ctypes. Наконец, нам нужен способ, которым мы встроим исходный файл в библиотеку. Для этой цели существует множество инструментов, но я предпочитаю пользоваться make – он отлично подходит для таких проектов благодаря своей гибкости и нетребовательности. Make также доступен на всех Linux системах. Вот фрагмент из файла Makefile, который создает библиотеку C в файле .so:

Makefile в repo настроен на создание и запуска демо из scratch. Вам нужно только запустить следующую команду в вашей оболочке:

Загрузка библиотеки С при помощи модуля ctypes

Ctypes позволяет загрузить общую библиотеку (DLL в Windows), и дать доступ к методам прямо из неё. При условии, что вы введете данные надлежащим образом. Самая простая форма этого выглядит следующим образом:

Обратите внимание на то, что предполагается, что ваша общая библиотека находится в той же папке, где и ваш скрипт, и что скрипт вызывается именно из этой папке.

Существует ряд связанных с определенными операционными системами нюансов поиска путей к библиотеки, которые мы не будем рассматривать в данной статье. Но, если вы можете упаковать файл .py вместе с общей библиотекой, вы можете воспользоваться чем-то вроде этого:

Этот код позволяет вам вызывать скрипт из любой папки.

После того, как вы загрузили библиотеку, она хранится в объекте Python, с методами для каждой экспортируемой функции.

Вызов простых функций с ctypes

Самое крутое в ctypes – это то, что он делает простые вещи еще более простыми. Просто вызов функции без параметров – тривиально. После загрузки библиотеки, функция просто является методом объекта library.

Возможно вы помните, что функция С, которую мы вызываем, выдает подсчитанные числа как объекты int. И еще раз, ctypes делает простые вещи еще проще – передача int-ов проходит очень просто и все работает как часы.

Используем переменные и неизменяемые строки в качестве параметров ctypes

В то время как основные типы – int и float, как правило, тривиально сортируются модулем ctypes, работа со строками может показаться проблематичной. В Python, строки являются неизменяемыми – это означает, что их нельзя изменять (кто бы мог подумать). Это может привести к немного странным результатам передачи строк модулю ctypes. Для следующего примера мы используем функцию add_one_to_string, которую мы использовали в примере C библиотеки выше. Если мы вызовем эту функцию, передав её строке Python – она запустится, но не изменит строку, а это не тот результат, который нам нужен.

Давайте посмотрим на код:

 

Результат выдачи:

После небольшого тестирования, я выяснил, что original_string вообще недоступна в функции С, при выполнении данной операции. Строка original_string оставалась неизменной, во многом благодаря тому, что функция С модифицировала какую-то другую память, а не строку. Так что она не только отказывается делать то, что нам нужно, она еще и вносит изменения в память, чего делать не должна, приводя к потенциальным проблемам с поврежденной памятью. Если мы хотим, чтобы функция С имела доступ к нужной строке, то нам нужно лично выполнить небольшую работу, связанную с сортировкой. К счастью, ctypes также упрощает и эту задачу. Нам нужно конвертировать оригинальную строку в биты, используя str.encode, после чего передать её конструктору для ctypes.string_buffer. String_buffer-ы – изменяемые, и переданы в С в качестве символа *, как и следует ожидать.

 

Запущенный код выводит следующее:

Обратите внимание на то, что string_buffer выведен как массив байтов со стороны Python.

Определение сигнатур функций в ctypes

Перед тем, как мы перейдем к последнему примеру этой статьи, нам нужно детально рассмотреть то, как ctypes передает параметры и возвращает значения. Как мы уже знаем, мы можем указывать тип возвращаемых, если нам это нужно. Мы можем выполнить аналогичное определение параметров функции. Ctypes сам выясняет тип указателя, и создает сопоставление по умолчанию в типе Python, но это не всегда то, что нам нужно. Кстати, предоставление сигнатуры функции позволяет Python-у убедиться в том, что вы передаете ему верные параметры, вызывая функцию С, в противном случае может происходить безумие. Так как каждая функция в загруженной библиотеке является объектом Python, со своими собственными свойствами, определение возвращаемого значения – достаточно просто. Чтобы определить возвращаемый тип функции, мы берем объект function и указываем значение restype, вот так:

Аналогичным образом вы можете определять типы любых переданных аргументов функции С, установив свойство argtypes в список типов:

Я нашел несколько хитрых методов в ходе работы, согласно которым можно упростить процесс определения сигнатур функций, но, в конечном счете, все они приводят к нашему результату.

Основы управления памятью в ctypes

Одно из главных преимуществ перехода из С в Python заключается в том, что вам больше не нужно тратить время на управление памятью вручную. Золотое правило использования ctypes – или любого инструмента кросс-языковой сортировки гласит:

Язык, который выделяет память, также должен освобождать память.

В предыдущем примере это сработало хорошо, так как Python выделил буферы строк, которые мы передавали, после чего освобождал память за ненадобностью. Однако, часто возникает необходимость выделить память под С, затем передать её Python для различных манипуляций. Это работает, но для этого нужно проделать несколько шагов, чтобы убедиться в том, что вы можете передать указать памяти обратно в С, чтобы он мог её освободить по окончанию работы.

Для следующего примера я использую эти две С функции:

  • alloc_C_string
  • free_C_string

В этом примере обе функции выведут указатель памяти, который они используют, чтобы понять то, что в данный момент происходит. Как было сказано выше, нам нужно быть в состоянии хранить актуальный указатель памяти, который выделяет функция alloc_C_string, чтобы мы могли передать её обратно в free_C_string. Для этого нам нужно дать понять ctype, что alloc_C_string должна вернуть ctypes.POINTER нашему ctypes.c_char. Мы уже видели это раньше. Объекты ctypes.POINTER не слишком эффективны в использовании, но их можно конвертировать в другие, более полезные объекты. После того, как мы конвертируем строку в ctypes.c_char, мы можем получить доступ к её атрибуту значения, для получения битов в Python.

Связав все это вместе, мы получаем следующий код:

 

После того, как мы использовали данные, выделенные в С, нам нужно освободить память. Процесс очень похожий, мы определим атрибут argtypes вместо restype:

Модуль Python ctypes – подведем итоги

Встроенный модуль ctypes позволяет вам взаимодействовать с кодом, написанным в С из Python легко и непринужденно. Взяв на заметку несколько базовых правил, вы сможете определять и вызывать все нужные функции. Однако, важно работать с памятью крайне осторожно.

Вам может быть интересно