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-разрядной платформы. У меня в системе сейчас такой питон:

C:\boost_1_65_1>python
Python 2.7.10 (default, May 23 2015, 09:44:00) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

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

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

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

C:\boost_1_65_1

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

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

C:\boost_1_65_1>b2 toolset=msvc-14.0 address-model=64

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

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

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

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

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

  1. Содержимое файла Source.cpp будет таким:
#include <string>
#include <iostream>
#include <boost/foreach.hpp>
int main()
{
  std::string str("Hello Boost!\n");
  BOOST_FOREACH(char ch, str)
  {
    std::cout << ch;
  }
  return 0;
}

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

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

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

  1. Компилируем и запускаем:
C:\...\Release>TestBoost.exe
Hello Boost!

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

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

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

#include <iostream>
#include <boost/regex.hpp>
bool ValidateCreditCard(const std::string& str)
{
        static const boost::regex creditRegEx("(\\d{4}[- ]){3}\\d{4}");
        return regex_match(str, creditRegEx);
}

int main()
{
        std::cout << (ValidateCreditCard("1234-1234-1234-1234") ? "Valid, " : "Not valid, ")
                << (ValidateCreditCard("1234-123-1234-1234") ? "Valid" : "Not valid") << std::endl;

        return 0;
}

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

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

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

C:\...\Release>TestBoost.exe
Valid, Not valid

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

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

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

  1. Добавим к проекту файл Source.cpp следующего содержания:
#define BOOST_PYTHON_STATIC_LIB
#include <boost/python.hpp>

char const* SayHello()
{
        return "Hello, from c++ dll!";
}
BOOST_PYTHON_MODULE(HelloExt)
{
        using namespace boost::python;
        def("SayHello", SayHello);
}
  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 с таким содержимым:
import HelloExt
print(HelloExt.SayHello())

Запускаем:

C:\...\Release>python 1.py
Hello, from c++ dll!

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

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

Source.cpp:

#define BOOST_PYTHON_STATIC_LIB
#include <boost/python.hpp>

namespace py = boost::python;

py::list CreateIntList(int size)
{
        py::list l;
        for (int i = 0; i < size; i++)
                l.append(0);
        return l;
}

BOOST_PYTHON_MODULE(HelloExt)
{
        py::def("CreateIntList", CreateIntList);
}

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

import HelloExt
a = HelloExt.CreateIntList(14)
print(type(a))
print(len(a))
print (a)

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

C:\...\Release>python 2.py
<type 'list'>
14
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

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

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

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

#define BOOST_PYTHON_STATIC_LIB
#include <boost/python/numpy.hpp>

namespace py = boost::python;
namespace np = boost::python::numpy;

np::ndarray CreateNumpyDoubleArray(int size)
{
        np::initialize();

        np::dtype dt = np::dtype::get_builtin<double>();
        py::tuple shape = py::make_tuple(size);
        np::ndarray example_tuple = np::zeros(shape, dt);

        return example_tuple;
}

BOOST_PYTHON_MODULE(HelloExt)
{
        py::def("CreateNumpyDoubleArray", CreateNumpyDoubleArray);
}

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

C:\boost_1_65_1>b2 --build-type=complete address-model=64 --with-python

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

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

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

import HelloExt
a = HelloExt.CreateNumpyDoubleArray(46)
print(type(a))
print(a.shape)
print(a.size)
print (a)

Результат:

C:\...\Release>python 3.py
<type 'numpy.ndarray'>
(46L,)
46
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

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

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

🏁Заключение

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

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

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

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