Cython против CFFI

У меня есть уморительно недостроенный модуль Python под названием sanpera, над которым я работаю время от времени. Это библиотека изображений для Python, с тщетной надеждой, что это может когда-нибудь заменить PIL. Но речь не о sanpera.

Случилось так, что sanpera питается от ImageMagick. Я различаю что это не "обертка ImageMagick ", так как оно явно не имеет ничего, напоминающего API ImageMagick, потому что судя по API это безумие. Но речь также не о ImageMagick.

Использование ImageMagick требует привязки Python к C, и это то, о чем идет речь. Есть несколько способов, чтобы использовать C-библиотеки из Python:

·            Написание модуля расширения означает, что API Python определяется в С, поэтому библиотека используется точно так, как было задумано: с C кодом. К сожалению, это требует написания много C, а также много тщательного подсчета ссылок Python. Mой C проходим, но я сделал гораздо больше читая его, чем потратил времени на написание, так что это не привлекательный вариант.

·            ctypes это библиотечный стандартный модуль, который может загружать разделяемые библиотеки и функции вызовов из них без использования компилятора C или любого нового кода C. Удобный, особенно если вы ненавидите зависимости (в этом случае, почему вы связываетесь с C?), но все ctypes-питаемые коды, которые я прочитал, были утомительными, неудобными и некрасивыми.

  • Cython, духовный порт / эволюция / ответвление / что-то от старого Pyrex, является языком подобным Python, что приводит к С, а затем компилирует в модуль расширения. Cython код может определять классы и функции Python, но также вызывать функции C и выполнять другие операции C непосредственно. Код с C семантикой переводится довольно непосредственно к С; коды, которые с семантикой Python транслируются в правильное использование API CPython; и Cython заполняет все биты для перевода между двумя.

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

Вот как выглядит Cython:

 1

 2

 3

 4

 5

 6

 7

 8

 9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

@classmethod

def read(type cls, bytes filename not None):

    cdef libc.stdio.FILE* fh = libc.stdio.fopen(<char*>filename, "rb")

    if fh == NULL:

        cpython.exc.PyErr_SetFromErrnoWithFilename(IOError, filename)

 

    cdef c_api.ImageInfo* image_info = c_api.CloneImageInfo(NULL)

    cdef MagickException exc = MagickException()

    cdef int ret

 

    cdef Image self = cls()

 

    try:

        # Force reading from this file descriptor

        image_info.file = fh

 

        self._stack = c_api.ReadImage(image_info, exc.ptr)

        exc.check()

 

        # Blank out the filename so IM doesn't try to write to it later

        self._stack.filename[0] = <char>0

    finally:

        c_api.DestroyImageInfo(image_info)

 

        ret = libc.stdio.fclose(fh)

        if ret != 0:

            cpython.exc.PyErr_SetFromErrnoWithFilename(IOError, filename)

 

    self._post_init()

    return self

(Ничего себе, оно даже выделено. Впечатляет.)

Очевидно, Python вдохновил, с C битами, витающими вокруг. cdef для статически типизированной переменной декларации, статические типы по аргументам функции, в сравнении с NULL, Краткое погружение в Python C API, когда это полезно. Конечно проще, чем писать простой C, особенно при работе с индивидуальными особенностями ImageMagick. Но я не могу легко использовать некоторые функции, такие как with, и мой код усеян указателями освобождения finally и ручным преобразованием исключений ImageMagick в Python.

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

Потом PyPy начал набирать популярность. PyPy изначально НЕ имеет C API, не будучи написанным в C вообще. Более новые последние версии имеют слой эмуляции C API, и у Cython даже есть поддержка для ориентации на него, но оно полно предостережений что не наполняют меня с уверенностью. Мне нравится PyPy, и не поддерживать его было бы позором, но навигация причуд двух разных моделей памяти именно та вещь, которой я пытался избежать с помощью Cython в первую очередь.

К счастью для меня, несколько PyPy фанатов настоятельно рекомендовали, чтоб я создал порт для CFFI. Это еще один способ для привязки к библиотекам C, с интерфейсом, заимствованным у Lua, который уверен, что звучит внушительно для меня. Он основан на той же базовой библиотеке (libffi) что и ctypes, но более склонен к PyPy по причинам за пределами моего понимания, и получает свои определения интерфейса С путем разбора заголовочных файлов, поэтому мне не придется переводить их всех очень неуклюжим Python.

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

 1

 2

 3

 4

 5

 6

 7

 8

 9

10

11

12

13

@classmethod

def read(cls, filename):

    with open(filename, "rb") as fh:

        image_info = blank_image_info()

        image_info.file = ffi.cast("FILE *", fh)

 

        with magick_try() as exc:

            ptr = ffi.gc(

                lib.ReadImage(image_info, exc.ptr),

                lib.DestroyImageList)

            exc.check(ptr == ffi.NULL)

 

    return cls(ptr)

Что подводит меня к тому, о чем этот пост на самом деле: о моем опыте портирования скромного количества Cython в CFFI.

Вы можете на глаз дифференцировать реализации Cython и CFFI выше, чтобы получить представление о том, что было вовлечено. По большей части это было делом нудного и механического удаления cdef и типовых аннотаций, копирование определения функций С из оригинальных заголовков в файл для CFFI для анализа и замены ссылки на c_api с новым lib объектом, который CFFI выплевывает.

Я на самом деле сделал этот один модуль за раз, с умеренным успехом; мой код Cython главным образом взаимодействовал, проходя через интерфейсы Python, которые продолжали работать нормально после того, как эти интерфейсы Python были перенесены на CFFI. И, конечно, все это с помощью той же библиотеки в том же процессе, так что библиотека не знает или не заботится о моем гибридном чудовище, сидящим над ним.

Были несколько мест, которые отняли больше усилий, и именно здесь CFFI начал сиять. Обратите внимание, что try/finally полностью исчез в коде выше; он был заменен ffi.gc, который добавляет немного оболочки Python-производной к указателю C и дает ему деструктор. Когда оболочка разрушается, указатель освобождается. Конечно, в не подсчитанном по ссылкам поле PyPy, в котором я на самом деле пытаюсь создал порт, это совершенно произвольно, когда это происходит на самом деле - но большинство моих указателей хранятся внутри объектов Python в любом случае, так они, скорее всего, исчезнут в то же время.

Writing Python code meant actually works, too, and I ported my crappy exception wrapper into a tiny contextmanager. ImageMagick handles errors by passing pointers to  structs around, and writing into them when something goes wrong; the object returned from  is just a thin container for such a pointer. When the  block ends, if the struct contains an error, it’s converted to a Python error and raised. Between  and , a lot of the memory-managing cruft went away, and I’m left with something that actually looks like Python code. Написание Python кода означало, что with на самом деле работает тоже, и я портировал мою исключительную дурацкую обертку в крошечный contextmanager. ImageMagick обрабатывает ошибки, передавая указатели на структуры ExceptionInfo вокруг, и путем написания в них, когда что-то идет не так; объект, возвращенный из magick_try это всего лишь тонкий контейнер для такого указателя. Когда блок with заканчивается, если структура содержит ошибку, он преобразуется в ошибку Python и поднимается. Между with и ffi.gc, ушло много хлама, управляемого памятью, и я оставил что-то, что на самом деле выглядит как Python код

Версия CFFI также передает указатель непосредственно конструктору, вместо создания объекта вручную (или с помощью неловко названной функции фэктори, как я сделал в другом месте). Это было ограничение Cython: ни одна функция, подвергшаяся Python, не может принять C-конкретные типы в качестве аргументов и __init__ обязательно подвергается Python. Каждый раз когда я нуждался в объекте, первоначально заполненном С полями, я должен был создать его пустым с Class.__new__(Class), а затем назначить атрибуты вручную. Но в CFFI указатели просто объекты Python, как и что-нибудь еще, так что я могу передать их дальше свободно. Это упростило мою жизнь в разы, особенно с общим кодом после установки, как в self._post_init() выше, которую было легко забыть вызвать.

ImageMagick использует typedef для большинства своих числовых типов, а фактический конкретный тип зависит от того, как ImageMagick был настроен. CFFI не может справиться с этим, потому что он должен знать размеры типов, взаимодействующих с конвенциями С вызывающими. Указатели на структуры неизвестного размера хороши, потому что они, в конечном счете поинтеры, а те в свою очередь всегда известного размера, но если я не знаю, есть ли у меня float или double, то у меня есть небольшая проблема.

Основной правонарушитель это Quantum, который содержит значение одного канала в пикселе; в зависимости от комплекции это может быть unsigned char, unsigned long, или long long double, который колеблется от 0 до 1 (?!). К счастью Quantum действительно существует только как поле в структуре пикселей, так что не было места для взлома: я написал крошечные обертки в C, что преобразовали пиксель в массив 0-1 double-ов и назад, и оставили пиксельную структуру как непрозрачный тип. CFFI был более чем счастлив, чтобы собрать их для меня, и подвергнуть их Python коду так же, как в остальной части библиотеки

Указатели в CFFI могут быть разыменовываны путем индексации, как и C (и ctypes): ptr[0] является духовным эквивалентом *ptr.

Но часть API ImageMagick требует указательного приращения. В Cython это должно было быть сделано с помощью специальной функции inc(ptr), которая будет подготовлена на  ptr++. В ctypes, ну ладно ... вы всегда можете разыменовать с увеличением индекса, но реальный прирост оказывается ... громоздким.

Я собирался написать еще C обертку только, чтобы вернуть ptr + 1, и я думал, попробовать очевидную вещь: ptr += 1.

Которая работает. Да. Аккуратно.

Cython порождает много C. Как ... очень много. sanpera.image является 30K из Cython, и она становится 607K от С. Это займет некоторое время на построение. Это всего лишь около 9 секунд, чтобы построить всю sanpera с нуля на моей машине, и есть частичное построение, как вы могли бы ожидать, но это небольшой бугор на дороге, когда я пытаюсь бегать быстро между написанием кода и ходовых испытаний.

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

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

Один недостаток: CFFI утверждает ошибку разбора, когда вы используете тип C, что не было объявлено еще, и возлагает вину на следующий токен без упоминания имени, которое я не узнал. Это не было очевидно на первый взгляд.

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

·       CPython 2.7 + Cython: 2.0s

·       CPython 2.7 + CFFI: 2.7s

·       PyPy 2.1 + CFFI: 4.3s

Это время, которое требуется, от теплого старта, для запуска тестов. (Который весело разбит на данный момент, но он в равной степени разбит во всех трех случаях.)

Я предполагаю, что это не слишком ужасное падение, учитывая, что я запустил составленный C код и раньше, но я по-прежнему люблю работать на улучшение его. (Только 49 тестов реально работают, с ума сойти.) И в то время как производительность PyPy является довольно паршивой, прежде он не работал и вовсе, так что это резкое улучшение. :)

Я откладывал это слишком долго, но это закончилась тем, что оно гораздо проще, чем я ожидал, и гораздо меньше уродливое, чем любой код ctypes, о котором я читал. Использование библиотеки, завернутой в CFFI, это наслаждение до сих пор, и я ожидаю, это поможет гораздо больше меня мотивировать, чтобы работать на sanpera в будущем, по крайней мере, если Wand не побьет меня в MindShare.

Я берегу это напоследок, потому что это на самом деле просто "колики" о ImageMagick и они не имеют ничего общего с CFFI.

ImageMagick чертовское безумие, давайтеэто проясним. Вы, наверное, знаете, что он претендует супер-родовые CLInames как convert и identify, но это только верхушка айсберга. Большие куски основного API, в том числе такого, как основные функции ReadImage, сами являются реализацией частей CLIutilities-вы на самом деле не можете попросить ImageMagick читать изображения из файла, не пытаясь разобрать весь хлам png:foobar.jpg[20x20]. Ох, и это написано, как 1991 C, имена функций и типов не привязаны к пространству имен CamelCaps, ни один из типов не документирован, большинство числовых типов на самом деле определения типов, которые изменяются в зависимости от того, как он был составлен, и много интересного материала существует только в макросах.

Но то, что преследовало мое первое вторжение в CFFI было просто нахождением библиотеки и ее заголовка. Заголовки ImageMagick не живут в стандартных местах; они, как правило, установлены в подкаталоге /usr/include (который варьируется в зависимости от платформы и конфигурации, конечно), и внутри, что является magick/ каталог, который на самом деле содержит заголовки. Библиотека используется, чтобы ее можно было довольно легко найти, но теперь она имеет некоторую комбинацию до трех типов суффиксов в названии, опять же в зависимости от того, как он был составлен.

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

Самый простой способ, чтобы получить все необходимые пути является pkg-config, но я могу только принять это как должное на Linux и, возможно, других не-Mac Unix вариантах, поэтому в интересах быть мультикультурным я отправился, чтобы найти надежный запасной вариант. (Подход Cython просто полагается на pkg-config и выплюнул сообщение о бесполезной ошибке, если не было доступа;! Хромой)

Первая попытка: счастливый день! ImageMagick загружается и устанавливает программу под названием Magick-config, которая, как pkg-config, но характерна для ImageMagick. Вот так, это было легко

Тогда я взглянул, и выясняется, Magick-config просто сценарий оболочки одной страницы, что вызывает pkg-config. Почему это вообще существует, в таком случае? Кто знает, черт возьми.

Вторая попытка: ImageMagick также устанавливает некоторые конфигурационные файлы, которые обещали содержать некоторое время компиляции информации. Отлично; я просто найду их. За исключением того, что ImageMagick ищет их в каждом из полдюжины мест, которые отличаются на каждой платформе, и, скорее всего, дом содержит версию ImageMagick, которой я не знаю, пока я не нашел конфигурационные файлы.

Хорошо, хорошо. К счастью, программа convert имеет плохой интерфейс к этим файлам конфигурации; convert -list configure выплюнет все пикантные подробности. Так что я попробовалэто, и оно сработало хорошо. Вроде.

Оказывается, этот файл только подскажет мне, как сам ImageMagick был построен, а не как привязаться к нему. Это содержит путь включения, который происходит, чтобы соответствовать другому на моей машине, но я почему-то подозреваю, что это не обязательно предназначено для этого. И, конечно, имя самой библиотеки не появляется в любом месте.

Потому я сдался и попробую позже. Я знаю, что могу игнорировать заголовки и просто поискать библиотеку на OS X, что естественно проще. Сообщенная библиотека Wand, которая тоже ищет оболочку для ImageMagick, на данный момент просто дико вращается до того, как найдет библиотеку, которая фактически существует (о, и у них есть эти очень особенные инструкции для установки ImageMagick на Windows которые включают настройку специальной среды, которая настраивается так, чтоб ImageMagick мог найти даже свои собственные файлы).

Оригинал статьи