Оборачивание библиотеки C в Python: C, Cython или ctypes? - PullRequest
260 голосов
/ 21 декабря 2009

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

  1. Создайте действительный модуль расширения на C. Возможно, это излишне, и я также хотел бы избежать накладных расходов при обучении написанию расширений.
  2. Используйте Cython , чтобы открыть соответствующие части из библиотеки C для Python.
  3. Сделайте все это в Python, используя ctypes для связи с внешней библиотекой.

Я не уверен, что 2) или 3) - лучший выбор. Преимущество 3) состоит в том, что ctypes является частью стандартной библиотеки, и полученный код будет представлять собой чистый Python & ndash; хотя я не уверен, насколько велико это преимущество на самом деле.

Есть ли еще преимущества / недостатки при любом выборе? Какой подход вы рекомендуете?


Редактировать: Спасибо за все ваши ответы, они предоставляют хороший ресурс для тех, кто хочет сделать что-то подобное. Решение, конечно же, еще предстоит принять для отдельного случая - нет единственного ответа «Это правильная вещь». Для моего собственного случая я, вероятно, пойду с ctypes, но я также с нетерпением жду возможности попробовать Cython в каком-то другом проекте.

Поскольку единого истинного ответа не существует, принять его несколько произвольно; Я выбрал ответ FogleBird, так как он дает хорошее представление о ctypes, и в настоящее время он также является самым популярным. Тем не менее, я предлагаю прочитать все ответы, чтобы получить хороший обзор.

Еще раз спасибо.

Ответы [ 11 ]

141 голосов
/ 16 апреля 2011

Предупреждение: мнение разработчика ядра Cython впереди.

Я почти всегда рекомендую Cython поверх ctypes. Причина в том, что у него гораздо более плавный путь обновления. Если вы используете ctypes, поначалу многие вещи будут простыми, и, безусловно, здорово написать свой код FFI на простом Python, без компиляции, сборок зависимостей и всего такого. Тем не менее, в какой-то момент вы почти наверняка обнаружите, что вам приходится часто заходить в свою библиотеку C, либо в цикле, либо в более длинных сериях взаимозависимых вызовов, и вы хотели бы ускорить это. В этот момент вы заметите, что вы не можете сделать это с помощью ctypes. Или, когда вам нужны функции обратного вызова и вы обнаружите, что ваш код обратного вызова Python становится узким местом, вы хотели бы ускорить его и / или перенести его также в C. Опять же, вы не можете сделать это с ctypes. Таким образом, вы должны переключить языки в этот момент и начать переписывать части своего кода, что может привести к обратному преобразованию кода Python / ctypes в обычный C, тем самым испортив все преимущества написания кода на простом Python.

С помощью Cython, OTOH, вы можете совершенно свободно делать код переноса и вызова таким тонким или толстым, как вам нужно. Вы можете начать с простых вызовов в ваш C-код из обычного Python-кода, и Cython переведет их в нативные C-вызовы без каких-либо дополнительных затрат на вызовы и с чрезвычайно низкими издержками преобразования для параметров Python. Когда вы заметите, что вам нужно еще больше производительности в какой-то момент, когда вы делаете слишком много дорогостоящих вызовов в вашу библиотеку C, вы можете начать аннотировать окружающий код Python статическими типами и позволить Cython оптимизировать его прямо на C для вас. Или вы можете начать переписывать части вашего C-кода на Cython, чтобы избежать вызовов, а также специализировать и алгоритмически затягивать ваши циклы. А если вам нужен быстрый обратный вызов, просто напишите функцию с соответствующей сигнатурой и передайте ее непосредственно в реестр обратных вызовов C. Опять же, никаких накладных расходов, и это дает вам простую производительность C-вызовов. И в гораздо менее вероятном случае, когда вы действительно не можете получить свой код достаточно быстро в Cython, вы все равно можете рассмотреть возможность переписать действительно важные его части в C (или C ++ или Fortran) и вызывать его из своего кода Cython естественным и естественным образом. Но тогда это действительно станет последним средством вместо единственного варианта.

Итак, ctypes хорош для простых вещей и быстрого запуска. Однако, как только ситуация начнет расти, вы, скорее всего, придете к тому, что заметите, что лучше использовать Cython с самого начала.

107 голосов
/ 21 декабря 2009

ctypes - ваш лучший выбор для быстрого выполнения работы, с которым приятно работать, так как вы все еще пишете Python!

Я недавно обернул драйвер FTDI для связи с USB-чипом с использованием ctypes, и это было здорово. Я сделал все это и работал менее чем за один рабочий день. (Я реализовал только те функции, которые нам нужны, около 15 функций).

Ранее мы использовали сторонний модуль PyUSB для той же цели. PyUSB - это модуль расширения C / Python. Но PyUSB не выпускал GIL при блокировке чтения / записи, что вызывало у нас проблемы. Поэтому я написал наш собственный модуль с использованием ctypes, который освобождает GIL при вызове нативных функций.

Следует отметить, что ctypes не будет знать о #define константах и ​​содержимом используемой вами библиотеки, только о функциях, поэтому вам придется переопределить эти константы в своем собственном коде.

Вот пример того, как код в итоге выглядел (множество вырвано, просто пытаюсь показать вам суть):

from ctypes import *

d2xx = WinDLL('ftd2xx')

OK = 0
INVALID_HANDLE = 1
DEVICE_NOT_FOUND = 2
DEVICE_NOT_OPENED = 3

...

def openEx(serial):
    serial = create_string_buffer(serial)
    handle = c_int()
    if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK:
        return Handle(handle.value)
    raise D2XXException

class Handle(object):
    def __init__(self, handle):
        self.handle = handle
    ...
    def read(self, bytes):
        buffer = create_string_buffer(bytes)
        count = c_int()
        if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK:
            return buffer.raw[:count.value]
        raise D2XXException
    def write(self, data):
        buffer = create_string_buffer(data)
        count = c_int()
        bytes = len(data)
        if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK:
            return count.value
        raise D2XXException

Кто-то сделал несколько тестов по различным вариантам.

Я мог бы быть более нерешительным, если бы мне пришлось обернуть библиотеку C ++ большим количеством классов / шаблонов / и т. Д. Но ctypes хорошо работает со структурами и может даже callback в Python.

97 голосов
/ 21 декабря 2009

Cython - довольно интересный инструмент, который стоит изучить, и он удивительно близок к синтаксису Python. Если вы выполняете какие-либо научные вычисления с Numpy, то Cython - это то, что нужно, потому что он интегрируется с Numpy для быстрых операций с матрицами.

Cython - это расширенная версия языка Python. Вы можете выбросить в него любой допустимый файл Python, и он выплюнет действительную программу на Си. В этом случае Cython просто сопоставит вызовы Python с базовым API CPython. Это может привести к ускорению на 50%, поскольку ваш код больше не интерпретируется.

Чтобы получить некоторые оптимизации, вы должны начать рассказывать Cython дополнительные факты о вашем коде, такие как объявления типов. Если вы скажете это достаточно, он может свести код до чистого C. То есть цикл for в Python становится циклом for в C. Здесь вы увидите значительное увеличение скорости. Вы также можете ссылаться на внешние программы на С здесь.

Использование кода Cython также невероятно просто. Я думал, что руководство делает это звучит сложно. Вы буквально просто делаете:

$ cython mymodule.pyx
$ gcc [some arguments here] mymodule.c -o mymodule.so

и тогда вы можете import mymodule в своем коде Python и полностью забыть, что он компилируется до C.

В любом случае, поскольку Cython очень прост в настройке и использовании, я советую попробовать, чтобы он соответствовал вашим потребностям. Это не будет пустой тратой, если окажется, что это не тот инструмент, который вы ищете.

40 голосов
/ 18 февраля 2013

Для вызова библиотеки C из приложения Python есть также cffi , которая является новой альтернативой для ctypes . Это приносит свежий взгляд на FFI:

  • это решает проблему захватывающим, чистым способом (в отличие от ctypes )
  • не требуется писать код не на Python (как в SWIG, Cython , ...)
21 голосов
/ 21 декабря 2009

Я скину еще одну: SWIG

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

Если вы используете SWIG, вы создаете новый модуль расширения Python, но SWIG делает большую часть тяжелой работы за вас.

18 голосов
/ 21 декабря 2009

Лично я написал бы модуль расширения на C. Не пугайтесь расширений Python C - их совсем нетрудно писать. Документация очень понятная и полезная. Когда я впервые написал расширение C на Python, я подумал, что мне понадобилось около часа, чтобы понять, как его написать - совсем немного времени.

10 голосов
/ 27 декабря 2009

ctypes замечательно, когда у вас уже есть скомпилированный большой двоичный объект библиотеки (например, библиотеки ОС). Затраты на вызовы являются серьезными, однако, если вы будете делать много вызовов в библиотеку, и вы все равно будете писать код C (или, по крайней мере, его компилировать), я бы сказал, чтобы Cython . Это не намного больше работы, и будет намного быстрее и более питонно использовать полученный pyd-файл.

Лично я склонен использовать cython для быстрого ускорения кода на python (циклы и целочисленные сравнения - это две области, в которых особенно ярко проявляется cython), и когда будет задействован еще какой-то код / ​​перенос других библиотек, я перейду к Boost.Python . Boost.Python может быть сложен в настройке, но как только он заработает, он упрощает перенос кода C / C ++.

cython также хорош для упаковки numpy (чему я научился в ходе SciPy 2009 ), но я не использовал numpy, поэтому я не могу это комментировать.

9 голосов
/ 21 декабря 2009

Если у вас уже есть библиотека с определенным API, я думаю, ctypes - лучший вариант, так как вам нужно только немного инициализировать, а затем более или менее вызвать библиотеку так, как вы привыкли.

Я думаю, что Cython или создание модуля расширения в C (что не очень сложно) более полезны, когда вам нужен новый код, например, вызов этой библиотеки и выполнение некоторых сложных, трудоемких задач, а затем передача результата в Python.

Другой подход для простых программ - это напрямую выполнить другой процесс (скомпилированный извне), выводить результат в стандартный вывод и вызывать его с помощью модуля подпроцесса. Иногда это самый простой подход.

Например, если вы создаете консольную программу C, которая работает более или менее таким образом

$miCcode 10
Result: 12345678

Вы можете позвонить с Python

>>> import subprocess
>>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE)
>>> std_out, std_err = p.communicate()
>>> print std_out
Result: 12345678

С небольшим форматированием строки вы можете получить результат любым способом. Вы также можете зафиксировать стандартный вывод ошибок, так что он довольно гибкий.

6 голосов
/ 12 июня 2013

Есть одна проблема, которая заставила меня использовать ctypes, а не cython, и которая не упоминается в других ответах.

При использовании ctypes результат не зависит от используемого вами компилятора. Вы можете написать библиотеку, используя более или менее любой язык, который может быть скомпилирован в нативную общую библиотеку. Неважно, какая система, какой язык и какой компилятор. Однако Cython ограничен инфраструктурой. Например, если вы хотите использовать компилятор Intel для Windows, гораздо сложнее заставить Cython работать: вы должны «объяснить» компилятор на Cython, перекомпилировать что-либо с помощью этого точного компилятора и т. Д. Это значительно ограничивает переносимость.

3 голосов
/ 23 февраля 2014

Если вы ориентируетесь на Windows и решили обернуть некоторые проприетарные библиотеки C ++, то вскоре вы обнаружите, что разные версии msvcrt***.dll (среда выполнения Visual C ++) немного несовместимы.

Это означает, что вы не сможете использовать Cython, поскольку полученный wrapper.pyd связан с msvcr90.dll (Python 2.7) или msvcr100.dll (Python 3.x) . Если библиотека, которую вы упаковываете, связана с другой версией среды выполнения, то вам не повезло.

Затем, чтобы все заработало, вам нужно создать оболочки C для библиотек C ++, связать эту оболочку dll с той же версией msvcrt***.dll, что и вашей библиотекой C ++. А затем используйте ctypes для динамической загрузки dll-оболочки, упакованной вручную, во время выполнения.

Итак, есть много мелких деталей, которые подробно описаны в следующей статье:

"Красивые родные библиотеки (в Python) ": http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

...