Обтекание библиотеки C в Python: C, Cython или ctypes?

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

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

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

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


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

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

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

Ответ 1

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

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

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

Ответ 2

Предупреждение: впереди мнение разработчиков 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 (или С++ или Fortran) и называть его из вашего кода Cython естественным и естественным образом. Но тогда это действительно последнее средство вместо единственного варианта.

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

  • он обрабатывает проблему увлекательным, чистым способом (в отличие от ctypes)
  • не требуется писать код не Python (как в SWIG, Cython,...)

Ответ 5

Я выброшу еще один: SWIG

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

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

Ответ 6

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

Ответ 7

ctypes отлично работает, когда у вас уже есть скомпилированная библиотека blob для работы (например, библиотеки ОС). Однако накладные накладные расходы серьезны, поэтому, если вы будете делать много вызовов в библиотеке, и вы все равно будете писать код C (или, по крайней мере, скомпилировать его), я бы сказал, cython. Это не намного больше работает, и будет намного быстрее и более pythonic использовать полученный файл pyd.

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

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

Ответ 8

Если у вас уже есть библиотека с определенным 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

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

Ответ 9

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

Используя ctypes, результат не зависит от используемого вами компилятора. Вы можете написать библиотеку, используя более или менее любой язык, который может быть скомпилирован в родную общую библиотеку. Не имеет большого значения, какая система, какой язык и какой компилятор. Cython, однако, ограничен инфраструктурой. Например, если вы хотите использовать Intel-компилятор на окнах, гораздо сложнее сделать работу cython: вы должны "объяснить" компилятор на cython, перекомпилировать что-то с этим точным компилятором и т.д., Что значительно ограничивает переносимость.

Ответ 10

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

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

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

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

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

Ответ 11

Существует также одна возможность использовать GObject Introspection для библиотек, которые используют GLib.