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

284

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

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

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

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


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

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

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

balpha
источник
3
В некоторой степени конкретное приложение (то, что делает библиотека) может повлиять на выбор подхода. Мы довольно успешно использовали ctypes для общения с поставляемыми вендором DLL для различных частей аппаратного обеспечения (например, осциллографов), но я бы не стал сначала выбирать ctypes для общения с библиотекой числовой обработки из-за дополнительных издержек по сравнению с Cython или SWIG.
Питер Хансен
1
Теперь у вас есть то, что вы искали. Четыре разных ответа. (Кто-то также нашел SWIG). Это означает, что теперь у вас есть 4 варианта вместо 3.
Лука Ран
@ralu Я тоже об этом думаю :-) А если серьезно, я не ожидал (или не хотел) таблицы «за» и «против» или одного единственного ответа: «Вот что вам нужно сделать». На любой вопрос о принятии решения лучше всего ответить «поклонникам» каждого возможного выбора, указав свои причины. Голосование сообщества тогда делает свое дело, как и моя собственная работа (просмотр аргументов, применение их к моему делу, чтение предоставленных источников и т. Д.). Короче говоря: здесь есть несколько хороших ответов.
Балфа
Итак, какой подход вы собираетесь использовать? :)
FogleBird
1
Насколько я знаю (пожалуйста, поправьте меня, если я ошибаюсь), Cython - это форк Pyrex с большим развитием, что делает Pyrex в значительной степени устаревшим.
Балфа

Ответы:

115

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 хорошо работает со структурами и может даже вызвать обратный вызов в Python.

FogleBird
источник
5
Присоединяясь к похвалам за ctypes, но обратите внимание на одну (недокументированную) проблему: ctypes не поддерживает разветвление. Если вы выполняете ветвление из процесса с использованием ctypes, а родительские и дочерние процессы продолжают использовать ctypes, вы наткнетесь на неприятную ошибку, связанную с ctypes, использующими разделяемую память.
Орен Шемеш
1
@OrenShemesh Можете ли вы мне еще что-нибудь рассказать по этому вопросу? Я думаю, что я могу быть в безопасности с проектом, над которым я сейчас работаю, так как я верю, что только родительский процесс использует ctypes(для pyinotify), но я хотел бы понять проблему более тщательно.
зигг
Этот отрывок мне очень помогает. One thing to note is that ctypes won't know about #define constants and stuff in the library you're using, only the functions, so you'll have to redefine those constants in your own code.Итак, я должен определить константы, которые есть в winioctl.h....
swdev
как насчет производительности? ctypesнамного медленнее, чем c-extension, поскольку узким местом является интерфейс от Python к C
TomSawyer
154

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

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

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

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

Стефан Бенель
источник
4
+1 это хорошие моменты, большое спасибо! Хотя мне интересно, если на Cython переносить только узкие места, это действительно слишком много. Но я согласен, если вы ожидаете каких-либо проблем с производительностью, вы также можете использовать Cython с самого начала.
Балфа
Это относится и к программистам, имеющим опыт работы с C и Python? В этом случае можно утверждать, что Python / ctypes - лучший выбор, поскольку векторизация циклов C (SIMD) иногда более проста. Но, кроме этого, я не могу думать о каких-либо недостатках Cython.
Алекс ван Хаутен
Спасибо за ответ! Одна вещь, с которой у меня возникли проблемы с Cython - это правильно настроить процесс сборки (но это также связано с тем, что я никогда раньше не писал модуль Python) - должен ли я скомпилировать его раньше или включить исходные файлы Cython в sdist и подобные вопросы. Я написал в блоге об этом на тот случай, если у кого-то
Martinsos
Спасибо за ответ! Один недостаток, когда я использую Cython, заключается в том, что перегрузка оператора не полностью реализована (например __radd__). Это особенно раздражает, когда вы планируете, чтобы ваш класс взаимодействовал со встроенными типами (например, intи float). Кроме того, магические методы в Cython просто немного глючат в целом.
Монолит
100

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

Cython - это расширенная версия языка Python. Вы можете добавить в него любой допустимый файл Python, и он выплюнет действительную программу на Си. В этом случае 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 очень прост в настройке и использовании, я советую попробовать, чтобы он соответствовал вашим потребностям. Это не будет пустой тратой, если окажется, что это не тот инструмент, который вы ищете.

деревенщина
источник
1
Нет проблем. Приятно, что в Cython вы можете узнать только то, что вам нужно. Если вам нужно только небольшое улучшение, все, что вам нужно сделать, это скомпилировать ваши файлы Python, и все готово.
Карл
18
«Вы можете выбросить в него любой допустимый файл Python, и он выплюнет действительную C-программу». <- Не совсем, есть некоторые ограничения: docs.cython.org/src/userguide/limitations.html Скорее всего, не проблема для большинства случаев использования, но просто хотелось быть завершенным.
Рэнди Сиринг,
7
С каждым выпуском проблем становится меньше, и на этой странице написано, что «большинство проблем было решено в 0.15».
Генри Гомерсалл
3
Чтобы добавить, есть ДАЖЕ более простой способ импорта кода Cython: напишите свой код Cython как mymod.pyxмодуль, а затем сделайте, import pyximport; pyximport.install(); import mymodи компиляция происходит за кулисами.
Кашик Гхосе
3
@kaushik Еще проще - pypi.python.org/pypi/runcython . Просто используйте runcython mymodule.pyx. И в отличие от pyximport вы можете использовать его для более сложных задач связывания. Единственное предостережение в том, что я тот, кто написал для него 20 строк bash и может быть предвзятым.
RussellStewart
42

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

  • это решает проблему захватывающим, чистым способом (в отличие от ctypes )
  • не требует написания кода не на Python (как в SWIG, Cython , ...)
Роберт Заремба
источник
определенно путь для упаковки , как хотел ОП. Cython отлично подходит для написания горячих циклов, но для интерфейсов cffi - это прямое обновление ctypes.
летающие овцы
21

Я добавлю еще один: SWIG

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

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

Крис Аргуин
источник
18

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

mipadi
источник
Упаковка библиотеки C Вы можете найти код здесь: github.com/mdippery/lehmer
mipadi
1
@forivall: Код был не так уж и полезен, и есть лучшие генераторы случайных чисел. У меня есть только резервная копия на моем компьютере.
Мипади
2
Согласовано. C-API Python не так страшен, как кажется (если вы знаете C). Однако, в отличие от python и его библиотеки библиотек, ресурсов и разработчиков, при написании расширений на C вы в основном сами по себе. Вероятно, его единственный недостаток (кроме тех, которые обычно идут с написанием на C).
Нуб Сайбот
1
@mipadi: хорошо, но они отличаются между Python 2.x и 3.x, поэтому удобнее использовать Cython для написания своего расширения, заставить Cython выяснить все детали и затем скомпилировать сгенерированный код C для Python 2.x или 3.x по мере необходимости.
0xC0000022L
2
@mipadi кажется, что ссылка на github мертва и она не доступна на archive.org. У вас есть резервная копия?
JRH
11

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

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

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

Райан Джинстрем
источник
11

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

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

Khelben
источник
Хотя в этом ответе нет ничего неправильного, люди должны быть осторожны, если код должен быть открыт для доступа других, так как вызов подпроцесса shell=Trueможет легко привести к некоторой уязвимости, когда пользователь действительно получает оболочку. Хорошо, когда разработчик является единственным пользователем, но в мире есть целая куча раздражающих уколов, просто ожидающих чего-то подобного.
Бен
7

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

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

Миша
источник
4

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

Это означает, что вы, возможно, не сможете использовать, 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/

iljau
источник
Эта статья не имеет никакого отношения к проблемам, которые вы поднимаете с совместимостью компиляторов Microsoft. Заставить расширения Cython работать в Windows на самом деле не очень сложно. Я смог использовать MinGW практически для всего. Хороший дистрибутив Python помогает, хотя.
Янв
2
+1 за упоминание о возможной проблеме с окнами (которая у меня сейчас тоже есть ...). @IanH В общем, дело не в окнах, а в беспорядке, если вы застряли с какой-либо сторонней библиотекой, которая не соответствует вашему дистрибутиву Python.
Себастьян
2

Я знаю, что это старый вопрос, но этот вопрос возникает в Google, когда вы ищете что-то подобное ctypes vs cython, и большинство ответов здесь написаны теми, кто уже владеет знаниями, cythonили cкоторые могут не отражать фактического времени, которое вам нужно было потратить, чтобы изучить эти вопросы. реализовать ваше решение. Я полный новичок в обоих. Я никогда не трогал cythonраньше, и у меня очень мало опыта c/c++.

В течение последних двух дней я искал способ делегировать часть моего кода, требующую высокой производительности, на более низкий уровень, чем python. Я реализовал свой код в ctypesи Cython, который состоял в основном из двух простых функций.

У меня был огромный список строк, которые нужно обработать. Обратите внимание listи string. Оба типа не полностью соответствуют типам in c, потому что строки python по умолчанию являются юникодом, а cстроки - нет. Списки в python просто НЕ являются массивами c.

Вот мой вердикт. Использование cython. Он более свободно интегрируется с python и с ним легче работать. Если что-то идет не так, ctypesпросто выдает вам segfault, по крайней мере cython, когда это возможно, вы получите предупреждения о компиляции с трассировкой стека, и вы можете легко вернуть действительный объект python с помощью cython.

Вот подробный отчет о том, сколько времени мне нужно было потратить на то и другое, чтобы реализовать одну и ту же функцию. Кстати, я очень мало занимался программированием на C / C ++:

  • Ctypes:

    • Около 2 часов на изучение того, как преобразовать мой список строк Unicode в AC совместимый тип.
    • Около часа, как правильно вернуть строку из функции ac. Здесь я фактически предоставил свое собственное решение для SO, как только я написал функции.
    • Около получаса, чтобы написать код на c, скомпилировать его в динамическую библиотеку.
    • 10 минут, чтобы написать тестовый код на python, чтобы проверить, cработает ли код.
    • Около часа выполнения некоторых тестов и реорганизации cкода.
    • Затем я вставил cкод в реальную базу кода и увидел, что ctypesон плохо работает с multiprocessingмодулем, так как его обработчик по умолчанию не выбирается.
    • Около 20 минут я переставил свой код, чтобы не использовать multiprocessingмодуль, и повторил попытку.
    • Затем вторая функция в моем cкоде генерировала ошибки в моей кодовой базе, хотя и прошла тестовый код. Ну, это, наверное, моя вина, что я плохо проверил крайние случаи, я искал быстрое решение.
    • Примерно 40 минут я пытался определить возможные причины этих ошибок.
    • Я разделил свои функции на две библиотеки и попробовал снова. Все еще были segfaults для моей второй функции.
    • Я решил отпустить вторую функцию и использовать только первую функцию cкода, и на второй или третьей итерации цикла python, который ее использует, у меня была UnicodeErrorмысль не декодировать байт в некоторой позиции, хотя я кодировал и декодировал все явно.

На данный момент, я решил искать альтернативу и решил изучить cython:

  • Cython
    • 10 минут чтения Cython Hello World .
    • 15 минут проверки SO о том, как использовать Cython setuptoolsвместо distutils.
    • 10 минут чтения на типах и типах. Я узнал, что могу использовать большинство встроенных типов Python для статической типизации.
    • 15 минут для повторного добавления кода Python с типами Cython.
    • 10 минут модификации setup.pyдля использования скомпилированного модуля в моей кодовой базе.
    • Подключил модуль непосредственно к multiprocessingверсии кодовой базы. Оно работает.

Для справки, я, конечно, не измерил точные сроки моих инвестиций. Вполне возможно, что мое восприятие времени было немного внимательным из-за умственных усилий, которые требовались, когда я имел дело с ctypes. Но это должно передать чувство борьбы cythonиctypes

Каан Э.
источник