C/C++ и Python = Boost.Python на Windows

Для многих не секрет, что Python🐍 не блещет скоростью обработки большого количества данных. При этом обладает преимуществом легкого подключения расширений на C/C++. Я писал о расширениях C в этой статье, а здесь рассмотрен альтернативный способ с помощью популярной С++ библиотеки Boost.

Рассказывает Роман Щеголихин. А расширение будет подключаться к Python 2.7🐍.

Постановка задачи

Питон работает медленно относительно бинарного кода. Критичные участки вычислений имеет смысл вынести во внешний исполняемый файл, написанный на C/C++. Но сразу возникает вопрос — как обмениваться данными, вернее, в каком формате?

Первое, что мне удалось сделать — это, используя модуль CTypes, подключить dll, написанную на C++, которая рассчитывала корреляционную матрицу. Стояла задача за приемлемое время обработать порядка 10 млн пар массивов ёмкостью около 500-600 элементов типа double. Dll писалась на Visual Studio 2015 с использованием библиотеки OpenMP. Мне удалось сократить время расчета с нескольких десятков минут до примерно 220 сек. Наверно, можно и ещё быстрее, если считать на GPU.

Узким местом оказался обмен данными между питоном и C++.

Моё решение заключалось в передаче входных и выходных данных в виде строки с разделителями. Python-скрипт “запаковывал” исходные данные в строку и передавал её в качестве параметра в dll. Внутри dll происходила “распаковка” входных данных в векторы STL и производились расчеты. Результаты помещались в строку и отправлялись обратно в Python-скрипт, где распаковывались.

Хотелось избавиться от операций со строками и передавать в питон (и принимать, конечно) его родные типы данных.  В идеале сразу в виде массивов Numpy или в виде каких-либо ещё более функциональных объектов.

Реализация

Существует несколько способов взаимодействия в связке Python ↔ С++. Ниже рассматривается использование библиотеки Boost.Python. Это одна из многих библиотек в составе Boost. Она позволяет обмениваться данными, которые будут понятны на обоих сторонах. Поддерживается обработка исключений. Классы C++ можно заворачивать в вид, “понятный” питону. Причем можно исполнять код питона внутри C++. И многое другое!

Библиотека бесплатна и реализована для всех основных платформ, включая Linux и Windows. Функционал у неё огромный! Документация исчерпывающая, но местами не хватает примеров, поэтому для себя (чтобы не забыть и не потерять) и для всех, кому это может быть полезно, ниже я привожу несколько примеров по использованию библиотеки Boost.Python.

Все примеры ниже рассматриваются для Windows и Python 2.7 разрядности 64 bit. IDE разработки MS Visual Studio 2015. При необходимости всё то же самое можно повторить и под Linux. А также под другую версию и разрядность питона. Важно только соблюдать соответствие разрядности установленного питона и разрядности используемой библиотеки Boost.

Установка Boost

На этом этапе в системе уже должен быть установлен Python. Желательно, чтобы он был добавлен в состав переменной PATH. Тогда команда python будет запускаться из любого места. Проверьте, работает у вас? Заодно посмотрите разрядность вашего питона. Я потратил много времени пытаясь на 64-разрядном питоне запустить dll, которую собирал для 32-разрядной платформы. У меня в системе сейчас такой питон:

[code]

[/code]

Первым делом установим Boost. Собственно, эта библиотека не требует какой-либо установки, т.к. является набором исходных файлов на С/С++, но некоторые её компоненты всё же требуют предварительной компиляции. К этим компонентам относится и Boost.Python.

Качаем последнюю версию Boost. У меня это была версия 1.65.1.

Скачанный архив распаковываем в любой удобный каталог на диске. Я привожу пример в каталоге:

[code]

[/code]

Заходим в каталог и запускаем скрипт [code]bootstrap.bat[/code].

Нужно запустить сборку командой [code]b2[/code]. Но по умолчанию она соберет библиотеки для 32-разрядной версии. Поэтому запускаем её так:

[code]

[/code]

Эта команда соберет ВСЕ библиотеки Boost, которые требуют компиляции (не только Boost.Python). Есть возможность указать сборку ТОЛЬКО Boost.Python ([code]—with-python[/code]), но у меня такой вариант не заработал в VS. Библиотеки компилировались с большим количеством предупреждений(warnings) и при подключении к питону сразу приводили к его обрушению. Поэтому я описываю то, что у меня точно заработало. В крайнем случае будет создано на диске несколько “лишних” библиотек и время компиляция продлиться чуть дольше.

Пробуем собрать первый пример

  1. Запустите Visual Studio и создайте новый проект (Файл→Создать→Проект).
  2. Выберите тип проекта Visual C++→Консольное приложение Win32:

  1. Ну и назовите его, например, “TestBoost”. Далее — далее… 😃 И на последнем диалоге поставьте галочку “Пустой проект”:

  1. Добавьте в проект файл исходного текста Source.cpp:

  1. Содержимое файла Source.cpp будет таким:

[code c]

[/code]

Попытка сборки закончится ошибкой ещё на моменте компиляции (это нормально):

  1. Для успешной сборки надо подключить к проекту саму библиотеку Boost (тот каталог, куда был распакован архив с Boost). Проект→Свойства→C/C++→Дополнительные каталоги включаемых файлов:

  1. Также в руководстве к Boost рекомендуется отключить использование предварительно скомпилированных заголовков:

  1. Компилируем и запускаем:

[code]

[/code]

Отлично! Заработало! Это пример использования библиотеки Boost, которая не требует предварительной компиляции (“headers only”).

Пример с использованием скомпилированной библиотеки Boost

Заменим содержимое файла Source.cpp на следующий код:

[code c]

[/code]

Сборка приводит к ошибке линковщика:

Необходимо подключить к проекту предварительно скомпилированные библиотеки Boost:

Компилируем и запускаем:

[code]

[/code]

Работает! Теперь линковщик успешно подключает предварительно скомпилированную библиотеку Boost.Regex и программа работает. Мы подошли к самому интересному.

Создание dll как модуля для Python

  1. Создадим новый проект в Visual Studio типа «консольное приложение», но укажем, что это будет dll:

  1. Добавим к проекту файл Source.cpp следующего содержания:

[code c]

[/code]

  1. Подключим к проекту дополнительные каталоги включаемых файлов:
    1. каталог с библиотеками Boost (C:\boost_1_65_1);
    2. каталог с исходными файлами (*.h) установленного в системе питона (у меня это каталог C:\Python27\include).

  1. Подключим к проекту предварительно скомпилированные библиотеки Boost (C:\boost_1_65_1\stage\lib, C:\boost_1_65_1)и библиотеки установленного в системе питона (C:\Python27\libs):

  1. Сборка проекта должна пройти без ошибок. В результате в папке проекта должен появиться файл HelloExt.dll Переименуем его в файл HelloExt.pyd. Это переименование удобно “повесить” на событие после компиляции. В результате после каждой сборки dll будет автоматически переименовываться в *.pyd:

  1. В каталоге, там где появился файл HelloExt.pyd создадим файл 1.py с таким содержимым:

[code python]

[/code]

Запускаем:

[code]

[/code]

Передаём массив из C++ в Python

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

Source.cpp:

[code c]

[/code]

Создадим файл 2.py:

[code python]

[/code]

Собираем, запускаем:

[code]

[/code]

Объект типа массив был создан внутри C++ и передан в Python. Видно, что питон его “понимает” и может использовать.

[raaAdSenceArticle]

Создадим внутри C++ массив Numpy и передадим его в Python

На этом шаге возникла проблема. Сверившись с документацией, я изменил файл Source.cpp так:

[code c]

[/code]

Однако при сборке линковщик не нашел файл boost_numpy-vc140-mt-1_65_1.lib, хотя рядом лежал файл libboost_numpy-vc140-mt-1_65_1.lib. Это было очень странно. Почитав issue на гитхабе по поводу подобных ошибок, я понял, что поддержка numpy была недавно включена в Boost.Python, а раньше это была отдельная библиотека. Поэтому в каких-то случаях у линковщика могут возникать проблемы с определением имен библиотек. Также предлагались решения, связанные с изменением некоторых файлов самой Boost, что меня крайне не устраивало. Проблема решилась пересборкой Boost с такими параметрами:

[code]

[/code]

На этот раз тоже было много предупреждений. Но в каталоге C:\boost_1_65_1\stage\lib появились оба файла — boost_numpy-vc140-mt-1_65_1.lib и libboost_numpy-vc140-mt-1_65_1.lib.

Проект успешно собрался и запустился!

Скрипт вызова такой:

[code python]

[/code]

Результат:

[code]

[/code]

Видно, что объект numpy.ndarray корректно создается. Заполняется нулями, и Python его может использовать.

При написании этой статьи использовались следующие материалы:

🏁Заключение

Данный подход будет работать на Python 3+. Для Linux шаги будут другие, но проще в сравнении с Windows. Готовы поделиться такой инструкцией? Пишите.

Роман Щеголихин
roman.shchegolikhin@bk.ru

💬В комментариях напишите вопросы по материалу или поблагодарите автора.

☝Собрались торговать акциями? Подготовьтесь🎓 у профессиональных трейдеров👍.