Объединяя C++ и Python. Тонкости Boost.Python. Часть перваяПрофилирование уже запущенных программИсточник: habrahabr Qualab
Boost.Python во всех отношениях замечательная библиотека, выполняющая своё предназначение на 5+, хотите ли вы сделать модуль на С++ для Python либо хотите построить скриптовую обвязку на Python для нативного приложения написанного на С++. ВведениеИсходим из того, что у вас уже установлен удобный инструментарий для сборки динамически-линкуемой библиотеки на C++, а также установлен интерпретатор Python. Также понадобится скачать библиотеку Boost, после чего собрать её, следуя инструкции для своей ОСWindows или Linux. В двух словах в Windows все действия сводятся к двум строкам в командной строке. Распакуйте скачанный архив Boost в любое место на диске, перейдите туда в командной строке и наберите последовательно две команды:
Для сборки x64 нужно добавить аргумент address-model=64 Если у вас уже есть библиотека Boost, но вы не устанавливали Python, либо вы скачали и установили свежий интерпретатор Python и хотите собрать только Boost.Python, это делается дополнительным ключом--with-python То есть вся строка для сборки только Boost.Python с 64-разрядной адресацией выглядит так:
Стоит заметить, что x64 сборку следует заказывать, если у вас установлен Python x64. Также и модули для него нужно будет собирать с 64-разрядной адресацией. Ключ --with-python серьёзно сэкономит вам время, если вам из библиотеки Boost кроме функционала Boost.Python ничего не нужно. Если у вас установлено несколько интерпретаторов, крайне рекомендую прочитать подробную документацию по сборке Boost.Python После сборки у вас появятся в папке Boost\stage\lib собранные библиотеки Boost.Python, они нам очень скоро понадобятся.
Настраиваем проект на C++Создаём проект для создания динамически-линкуемой библиотеки на C++, предлагаю назвать его example. После создания проекта, требуется указать дополнительные INCLUDE каталоги Python\include и корень Boost , а также каталоги для поиска библиотек Python\libs и Boost\stage\lib Под Windows также следует в настройках Post-build events задать переименование $(TargetPath) в модуль с расширением example.pyd в корне проекта. Также возможно стоит скопировать собранные библиотеки Boost.Python в каталог с собираемым модулем. Подключение модуля после запуска интерпретатора в том же каталоге сведётся к одной команде:
Не забываем также про сборку под x64 если вы собираете для 64-разрядного Python.
Обычный класс с простыми полямиИтак, давайте заведём нашем новом проекте сразу три файла: some.h some.cpp wrap.cpp В файлах some.h и some.cpp опишем некий замечательный класс Some, который обернём для Python в модуле example в файле wrap.cpp - для этого в файле wrap.cpp следует подключить <boost/python.hpp> и использовать макрос BOOST_PYTHON_MODULE( example ) {… }, также для лаконичности будет совсем не лишним использовать using namespace boost::python. В целом наш будущий модуль будет выглядеть вот так:
В файле some.h нам следует наваять объявление нашего чудо-класса. Для объяснения большинства базовых механизмов нам достаточно всего два поля:
Допустим класс содержит описание чего-то, что имеет имя и целочисленный идентификатор. Как ни странно этот несложный класс вызовет кучу сложностей, благодаря в основном стандартному классу string, перегрузкам методов, константной ссылке и статическому свойству NOT_AN_IDENTIFIER, которое мы конечно же тоже введём:
Разумеется эта константа нужна как идентификатор для объекта созданного конструктором по умолчанию, опишем также и другой конструктор, задающий оба поля:
В файле some.cpp опишем реализацию данных конструкторов, в дальнейшем реализацию описывать я не буду, но давайте конструкторы напишем вместе:
Одновременно с появлением класса Some будет появляться обёртка класса для Python в файле wrap.cpp:
Здесь используется бессовестный обман зрения и шаблон boost::python::class_, который создаёт описание класса для Python в указанном модуле с помощью Python C-API, жутко сложного и непонятно при описании методов, а потому полностью скрытого за объявлением простого метода def() на каждой строчке. Конструктор по умолчанию и конструктор копирования создаются для объекта по умолчанию, если не указано обратное, но мы этого ещё коснёмся чуть ниже. Уже сейчас можно собрать модуль, импортировать его из интерпретатора Python и даже создать экземпляр класса, но ни прочитать его свойства, ни вызывать методы мы у него пока не можем, пока они физически отсутствуют. Давайте это исправим, создадим "богатейшее" API нашего чудо класса. Вот полный код нашего заголовочного файла some.h:
Раз реализация методов получилась также довольно короткой, давайте приведу и код some.cpp:
Что ж, самое время описать обёртку в файле wrap.cpp: Первый метод Some::ID() оборачивается без каких-либо проблем:
Зато второй с результатом в виде константной ссылки на строку уже показывает, что всё не так просто:
Как видим, можно указать как Python должен интерпретировать возвращаемое значение, если метод в C++ возвращает указатель или ссылку. Дело в том, что зверский Garbage Collector (GC) очень любит удалять всё бесхозное, поэтому просто так объявить метод возвращающий указатель или ссылку, вам никто не даст, всё печально закончится на этапе компиляции, поскольку GC должен знать что ему делать с возвращаемым значением, для разработчика будет весьма печально, если он начнёт удалять содержимое объекта в C++. Всего есть несколько вариантов return_value_policy для разных случаев, самые важные из них следующие:
Понимание того, как работает тот или иной return_value_policy в деталях приходит со временем, эксперементируйте, пробуйте, читайте документацию и набивайте руку. Для стандартного string ссылка в зависимости от константности при возвращении почти всегда copy_const_reference либоcopy_non_const_reference, просто запомните, т.к. string по значению преобразуется на уровне Python в объект встроенного класса str, а по ссылке нужно явно указывать return_value_policy. Метод Some::ResetID я намерено перегрузил, чтобы усложнить задачу с передачей указателя на метод в .def():
Как видите, можно указать, с каким именем в Python будет создан аргумент метода. Как известно имя аргумента в Python куда важнее чем в C++. Рекомендую указывать имена аргументов для каждой обёртки метода, принимающего параметры:
Осталось описать статическим свойством константу NOT_AN_IDENTIFIER:
Здесь используется специальная функция boost::python::make_getter, которая по свойству класса генерирует get-функцию. Вот так примерно выглядит наша обёртка:
Если написать несложный тестовый скрипт вроде этого (Python 3.x):
Мы увидим вывод:
Питонизируем обёртку классаИтак, класс со всеми методами обёрнут, но счастья не наступило. При попытке из командной строки Python выполнив Some(123,'asd') мы не увидим описания полей и вообще объекта, поскольку мы не обзавелись методом __repr__, так же как и преобразование к строке, тот же print( Some(123,'asd') ) будет ужасно неинформативен, так как мы не обзавелись методом __str__. Очевидно также, что работать со свойствами через методы на C++ на Python не имеет смысла, это в C++ мы не имеем возможности заводить property, в Python их можно и нужно завести. Однако как же мы навесим методы на готовый класс C++ предназначенные для Python? Очень просто: вспоминаем, что в Python методы не отличаются от функций, принимающих первым параметром ссылку на self - экземпляр класса. Заводим в C++ такие функции прямо во wrap.cpp и описываем их как методы в обёртке:
Сами функции можно описать например вот так:
Со свойствами идентификатора и имени ещё проще, так как методы set и get для них уже описаны в классе:
При описании свойств однако было два тонких момента: Выполняем print( Some(123,'asd') ) и просто Some(123,'asd') из командной строки после from example import * и видим что подозрительно похожее на встроенный питоновский dict: { ID: 123, Name: 'asd' }
Здесь использован класс boost::python::dict, для доступа на уровне C++ к стандартному dict Python. Также есть классы для доступа к str, list, tuple, называются они соответственно. Ведут себя классы в C++ так же как и в Python в плане операторов, вот только возвращают по большей части boost::python::object, из которого требуется ещё извлечь значение через функцию boost::python::extract.
В заключение первой частиВ первой части был рассмотрен вполне стандартный класс с конструктором по умолчанию и дефолтным конструктором копирования. Несмотря на некоторые тонкости с работой со строками, и перегрузкой методов, класс вполне стандартный. Работать с Boost.Python довольно просто, обёртка любой функции сводится обычно к одной строке, которая выглядит как аналогичное объявление метода в Python. В следующей части мы научимся оборачивать классы, которые создаются не так тревиально, создадим класс на основе структуры, обернём enum, познакомимся на практике с другим важным return_value_policy<reference_existing_object>. В третьей части рассмотрим конвертеры типов в стандартные типы Python напрямую без обёртки на примере массива байт. Научимся пробрасывать исключения определённого типа из C++ в Python и обратно. Тема довольно обширная.
Ссылка на проектПроект первой части для Windows выложен здесь. Проект MSVS v11 настроен на сборку с Python 3.3 x64. Собранные .dll Boost.Python соответствующей версии прилагаются. Но ничего не мешает собрать файлы some.h, some.cpp, wrap.cpp любым другим сборочным аппаратом с привязкой к любой другой версии Python. |