Building boost with python

Объединяя C++ и Python. Тонкости Boost.Python. Часть первая

Boost.Python во всех отношениях замечательная библиотека, выполняющая своё предназначение на 5+, хотите ли вы сделать модуль на С++ для Python либо хотите построить скриптовую обвязку на Python для нативного приложения написанного на С++.
Самое сложное в Boost.Python — это обилие тонкостей, поскольку и C++ и Python — два языка изобилующие возможностями, и потому на стыке их приходится учитывать все нюансы: передать объект по ссылке или по значению, отдать в Python копию объекта или существующий класс, преобразовать во внутренний тип Python или в обёртку написанного на C++, как передать конструктор объекта, перегрузить операторы, навесить несуществующие в C++, но нужные в Python методы.
Не обещаю, что в своих примерах опишу все тонкости взаимодействия этих фундаментальных языков, но постараюсь сразу охватить как можно больше частоиспользуемых примеров, чтобы вы не лазили за каждой мелочью в документацию, а увидели все необходимые основы здесь, или хотя бы получили о них базовое представление.

Оглавление

Введение

Исходим из того, что у вас уже установлен удобный инструментарий для сборки динамически-линкуемой библиотеки на C++, а также установлен интерпретатор Python.
Также понадобится скачать библиотеку Boost, после чего собрать её, следуя инструкции для своей ОС Windows или Linux.
В двух словах в Windows все действия сводятся к двум строкам в командной строке. Распакуйте скачанный архив Boost в любое место на диске, перейдите туда в командной строке и наберите последовательно две команды:

bootstrap b2 --build-type=complete stage 

Для сборки x64 нужно добавить аргумент address-model=64
Если у вас уже есть библиотека Boost, но вы не устанавливали Python, либо вы скачали и установили свежий интерпретатор Python и хотите собрать только Boost.Python, это делается дополнительным ключом —with-python
То есть вся строка для сборки только Boost.Python с 64-разрядной адресацией выглядит так:

b2 --build-type=complete address-model=64 --with-python stage 

Стоит заметить, что x64 сборку следует заказывать, если у вас установлен Python x64. Также и модули для него нужно будет собирать с 64-разрядной адресацией.
Ключ —with-python серьёзно сэкономит вам время, если вам из библиотеки Boost кроме функционала Boost.Python ничего не нужно.
Если у вас установлено несколько интерпретаторов, крайне рекомендую прочитать подробную документацию по сборке Boost.Python
После сборки у вас появятся в папке Boost\stage\lib собранные библиотеки Boost.Python, они нам очень скоро понадобятся.

Читайте также:  Java search change to

Настраиваем проект на 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_MODULE( example ) , также для лаконичности будет совсем не лишним использовать using namespace boost::python. В целом наш будущий модуль будет выглядеть вот так:

#include . using namespace boost::python; . BOOST_PYTHON_MODULE( example ) < . >. 

В файле some.h нам следует наваять объявление нашего чудо-класса. Для объяснения большинства базовых механизмов нам достаточно всего два поля:

private: int mID; string mName; 

Допустим класс содержит описание чего-то, что имеет имя и целочисленный идентификатор. Как ни странно этот несложный класс вызовет кучу сложностей, благодаря в основном стандартному классу string, перегрузкам методов, константной ссылке и статическому свойству NOT_AN_IDENTIFIER, которое мы конечно же тоже введём:

public: static const int NOT_AN_IDENTIFIER = -1; 

Разумеется эта константа нужна как идентификатор для объекта созданного конструктором по умолчанию, опишем также и другой конструктор, задающий оба поля:

 Some(); Some( int some_id, string const& name ); 

В файле some.cpp опишем реализацию данных конструкторов, в дальнейшем реализацию описывать я не буду, но давайте конструкторы напишем вместе:

Some::Some() : mID(NOT_AN_IDENTIFIER) < >Some::Some( int some_id, string const& name ) : mID(some_id), mName(name)
BOOST_PYTHON_MODULE( example ) < class_( "Some" ) .def( init( args( "some_id", "name" ) ) ) ; > 

Здесь используется бессовестный обман зрения и шаблон boost::python::class_, который создаёт описание класса для Python в указанном модуле с помощью Python C-API, жутко сложного и непонятно при описании методов, а потому полностью скрытого за объявлением простого метода def() на каждой строчке.
Конструктор по умолчанию и конструктор копирования создаются для объекта по умолчанию, если не указано обратное, но мы этого ещё коснёмся чуть ниже.
Уже сейчас можно собрать модуль, импортировать его из интерпретатора Python и даже создать экземпляр класса, но ни прочитать его свойства, ни вызывать методы мы у него пока не можем, пока они физически отсутствуют.
Давайте это исправим, создадим «богатейшее» API нашего чудо класса. Вот полный код нашего заголовочного файла some.h:

#pragma once #include using std::string; class Some < public: static const int NOT_AN_IDENTIFIER = -1; Some(); Some( int some_id, string const& name ); int ID() const; string const& Name() const; void ResetID(); void ResetID( int some_id ); void ChangeName( string const& name ); void SomeChanges( int some_id, string const& name ); private: int mID; string mName; >; 

Раз реализация методов получилась также довольно короткой, давайте приведу и код some.cpp:

#include "some.h" Some::Some() : mID(NOT_AN_IDENTIFIER) < >Some::Some( int some_id, string const& name ) : mID(some_id), mName(name) < >int Some::ID() const < return mID; >string const& Some::Name() const < return mName; >void Some::ResetID() < mID = NOT_AN_IDENTIFIER; >void Some::ResetID( int some_id ) < mID = some_id; >void Some::ChangeName( string const& name ) < mName = name; >void Some::SomeChanges( int some_id, string const& name )

Что ж, самое время описать обёртку в файле wrap.cpp:
Первый метод Some::ID() оборачивается без каких-либо проблем:

 .def( "Name", &Some::Name, return_value_policy() ) 
  • copy_non_const_reference — создаёт новый объект в Python, который содержит неконстантную ссылку на объект в C++, не требует обёртки для класса из C++ (пример: string не имеет обёртки, только конвертер в питоновский str)
  • copy_const_reference — создаёт новый объект в Python, который содержит константную ссылку на объект в C++, не требует обёртки для класса из C++ (пример: тот же string)
  • manage_new_object — создаёт новый объект в Python, используя обёртку класса из C++, по завершении содержимое удаляется
  • reference_existing_object — создаёт новый объект в Python, используя обёртку класса из C++, по завершении содержимое остаётся

Метод Some::ResetID я намерено перегрузил, чтобы усложнить задачу с передачей указателя на метод в .def():

 .def( "ResetID", static_cast< void (Some::*)() >( &Some::ResetID ) ) .def( "ResetID", static_cast< void (Some::*)(int) >( &Some::ResetID ), args( "some_id" ) ) 

Как видите, можно указать, с каким именем в Python будет создан аргумент метода. Как известно имя аргумента в Python куда важнее чем в C++. Рекомендую указывать имена аргументов для каждой обёртки метода, принимающего параметры:

 .def( "ChangeName", &Some::ChangeName, args( "name" ) ) .def( "SomeChanges", &Some::SomeChanges, args( "some_id", "name" ) ) 

Осталось описать статическим свойством константу NOT_AN_IDENTIFIER:

 .add_static_property( "NOT_AN_IDENTIFIER", make_getter( &Some::NOT_AN_IDENTIFIER ) ) 

Здесь используется специальная функция boost::python::make_getter, которая по свойству класса генерирует get-функцию.
Вот так примерно выглядит наша обёртка:

#include #include "some.h" using namespace boost::python; BOOST_PYTHON_MODULE( example ) < class_( "Some" ) .def( init( args( "some_id", "name" ) ) ) .def( "ID", &Some::ID ) .def( "Name", &Some::Name, return_value_policy() ) .def( "ResetID", static_cast< void (Some::*)() >( &Some::ResetID ) ) .def( "ResetID", static_cast< void (Some::*)(int) >( &Some::ResetID ), args( "some_id" ) ) .def( "ChangeName", &Some::ChangeName, args( "name" ) ) .def( "SomeChanges", &Some::SomeChanges, args( "some_id", "name" ) ) .add_static_property( "NOT_AN_IDENTIFIER", make_getter( &Some::NOT_AN_IDENTIFIER ) ) ; > 
from example import * s = Some() print( "s = Some(); ID: , Name: ".format(ID=s.ID(),Name=s.Name()) ) s = Some(123,'asd') print( "s = Some(123,'asd'); ID: , Name: ".format(ID=s.ID(),Name=s.Name()) ) s.ResetID(234); print("s.ResetID(234); ID:",s.ID()) s.ResetID(); print("s.ResetID(); ID:",s.ID()) s.ChangeName('qwe'); print("s.ChangeName('qwe'); Name:'%s'" % s.Name()) s.SomeChanges(345,'zxc') print( "s.SomeChanges(345,'zxc'); ID: , Name: ".format(ID=s.ID(),Name=s.Name()) ) 
s = Some(); ID: -1, Name: '' s = Some(123,'asd'); ID: 123, Name: 'asd' s.ResetID(234); ID: 234 s.ResetID(); ID: -1 s.ChangeName('qwe'); Name:'qwe' s.SomeChanges(345,'zxc'); ID: 345, Name: 'zxc' 

Питонизируем обёртку класса

Итак, класс со всеми методами обёрнут, но счастья не наступило. При попытке из командной строки Python выполнив Some(123,’asd’) мы не увидим описания полей и вообще объекта, поскольку мы не обзавелись методом __repr__, так же как и преобразование к строке, тот же print( Some(123,’asd’) ) будет ужасно неинформативен, так как мы не обзавелись методом __str__. Очевидно также, что работать со свойствами через методы на C++ на Python не имеет смысла, это в C++ мы не имеем возможности заводить property, в Python их можно и нужно завести. Однако как же мы навесим методы на готовый класс C++ предназначенные для Python?
Очень просто: вспоминаем, что в Python методы не отличаются от функций, принимающих первым параметром ссылку на self — экземпляр класса. Заводим в C++ такие функции прямо во wrap.cpp и описываем их как методы в обёртке:

. string Some_Str( Some const& ); string Some_Repr( Some const& ); . BOOST_PYTHON_MODULE( example ) < class_( "Some" ) . .def( "__str__", Some_Str ) .def( "__repr__", Some_Repr ) . 
string Some_Str( Some const& some ) < stringstream output; output "; return output.str(); > string Some_Repr( Some const& some )

Со свойствами идентификатора и имени ещё проще, так как методы set и get для них уже описаны в классе:

 .add_property( "some_id", &Some::ID, static_cast< void (Some::*)(int) >( &Some::ResetID ) ) .add_property( "name", make_function( static_cast< string const& (Some::*)() const >( &Some::Name ), return_value_policy() ), &Some::ChangeName ) 

При описании свойств однако было два тонких момента:
1. Для set-метода свойства some_id было явное приведение к типу метода, принимающего int, т.к. есть ещё одна перегрузка метода.
2. Для get-метода свойства name была использована конструкция boost::python::make_function, которая позволила повесить return_value_policy на результат метода возвращающего константную ссылку на string.

Выполняем print( Some(123,'asd') ) и просто Some(123,'asd') из командной строки после from example import * и видим что подозрительно похожее на встроенный питоновский dict: < ID: 123, Name: 'asd' >
Почему бы не завести свойство инициализирующее экземпляр Some от стандартного dict и обратно?
Заведём ещё пару питонистических функций и заведём свойство as_dict:

. dict Some_ToDict( Some const& ); void Some_FromDict( Some&, dict const& ); . BOOST_PYTHON_MODULE( example ) < class_( "Some" ) . .add_property( "as_dict", Some_ToDict, Some_FromDict ) ; . > . dict Some_ToDict( Some const& some ) < dict res; res["ID"] = some.ID(); res["Name"] = some.Name(); return res; >void Some_FromDict( Some& some, dict const& src ) < src.has_key( "ID" ) ? some.ResetID( extract( src["ID"] ) ) : some.ResetID(); some.ChangeName( src.has_key( "Name" ) ? extract( src["Name"] ) : string() ); > 

Здесь использован класс boost::python::dict, для доступа на уровне C++ к стандартному dict Python.
Также есть классы для доступа к str, list, tuple, называются они соответственно. Ведут себя классы в C++ так же как и в Python в плане операторов, вот только возвращают по большей части boost::python::object, из которого требуется ещё извлечь значение через функцию boost::python::extract.

В заключение первой части

В первой части был рассмотрен вполне стандартный класс с конструктором по умолчанию и дефолтным конструктором копирования. Несмотря на некоторые тонкости с работой со строками, и перегрузкой методов, класс вполне стандартный.
Работать с Boost.Python довольно просто, обёртка любой функции сводится обычно к одной строке, которая выглядит как аналогичное объявление метода в Python.
В следующей части мы научимся оборачивать классы, которые создаются не так тревиально, создадим класс на основе структуры, обернём enum, познакомимся на практике с другим важным return_value_policy.
В третьей части рассмотрим конвертеры типов в стандартные типы Python напрямую без обёртки на примере массива байт. Научимся пробрасывать исключения определённого типа из C++ в Python и обратно.
Тема довольно обширная.

Ссылка на проект

Проект первой части для Windows выложен здесь.
Проект MSVS v11 настроен на сборку с Python 3.3 x64. Собранные .dll Boost.Python соответствующей версии прилагаются.
Но ничего не мешает собрать файлы some.h, some.cpp, wrap.cpp любым другим сборочным аппаратом с привязкой к любой другой версии Python.

Источник

Оцените статью