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

от автора

Данная статья является продолжением первой части.
Продолжаем мучить Boost.Python. В этот раз настала очередь класса, который нельзя ни создать, ни скопировать.
Обернём обычные обычную сишную структуру с необычным конструктором.
И поработаем с возвращением ссылки на поле объекта C++, так чтобы сборщик мусора Python его не удалил ненароком. Ну и наоборот, сделаем альтернативный вариант, чтобы Python прибрал мусор после удаления того, что ему отдали на хранение.
Поехали…

Подготавливаем проект

Нам для наших целей вполне хватит дополнить проект example оставшийся с предыдущей части.
Давайте добавим в него ещё пару файлов для работы с классом синглтона:
single.h
single.cpp
И вынесем объявления вспомогательных функций для обёртки в Python в отдельный файл:
wrap.h

От прежнего проекта должен был остаться файл, который мы активно будем менять:
wrap.cpp
И замечательные файлы с чудо-классом, который так нам помог в первой части, они останутся как есть:
some.h
some.cpp

Оборачиваем простую структуру

Начнём с того, что заведём в single.h небольшую C-style структуру, просто с описанием полей.
Давайте для интереса это будет не просто структура, а некий загадочный тип описания конфигурации:

struct Config {     double coef;     string path;     int max_size;      Config( double def_coef, string const& def_path, int def_max_size ); }; 

Сделать обёртку для такой структуры не составит никакого труда, нужно лишь специально описать конструктор с параметрами с помощью шаблона конструктора boost::python::init<…>(…) параметра шаблона обёртки boost::python::class_:

    class_<Config>( "Config", init<double,string,int>( args( "coef", "path", "max_size" ) )         .add_property( "coef", make_getter( &Config::coef ), make_setter( &Config::coef ) )         .add_property( "path", make_getter( &Config::path ), make_setter( &Config::path ) )         .add_property( "max_size", make_getter( &Config::max_size ), make_setter( &Config::max_size ) )     ; 

Как видите, здесь даже не приходится применять return_value_policy<copy_const_reference> для строкового поля. Просто потому, что поле здесь берётся по сути по значению, а стало быть автоматически преобразуется в стандартную строку str языка Python.
Функции make_setter делают ещё очень полезную работу по проверке типа входящего значения, попробуйте например присвоить в питоне полю coef значение строкового типа или задать max_size значением типа float, получите исключение.
Поля сишной структуры Config по сути превращаются в свойства объекта полноценного питоновского класса Config. Ну почти полноценного… Давайте по аналогии с классом Some из прошлой главы добавим в обёртку методы __str__ и __repr__, а заодно добавим свойство as_dict для преобразования полей структуры в стандартный dict питона и обратно.
Объявление новых функций, так же как и старых, перенесём в наш новый файл wrap.h:

#pragma once  #include <boost/python.hpp> #include "some.h" #include "single.h" #include "protocol.h"  using namespace boost::python;  string Some_Str( Some const& ); string Some_Repr( Some const& ); dict Some_ToDict( Some const& ); void Some_FromDict( Some&, dict const& );  string Config_Str( Config const& ); string Config_Repr( Config const& ); dict Config_ToDict( Config const& ); void Config_FromDict( Config&, dict const& ); 

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

string Config_Str( Config const& config ) {     stringstream output;     output << "{ coef: " << config.coef << ", path: '" << config.path << "', max_size: " << config.max_size << " }";     return output.str(); }  string Config_Repr( Config const& config ) {     return "Config: " + Config_Str( config ); }  dict Config_ToDict( Config const& config ) {     dict res;     res["coef"] = config.coef;     res["path"] = config.path;     res["max_size"] = config.max_size;     return res; }  void Config_FromDict( Config& config, dict const& src ) {     if( src.has_key( "coef" ) ) config.coef = extract<double>( src["coef"] );     if( src.has_key( "path" ) ) config.path = extract<string>( src["path"] );     if( src.has_key( "max_size" ) ) config.max_size = extract<int>( src["max_size"] ); } 

Это я конечно уже с жиру бешусь, но назовём это повторением пройденного.
В обёртку структуры разумеется добавляем новые объявления:

    class_<Config>( "Config", init<double,string,int>( args( "coef", "path", "max_size" ) ) )         .add_property( "coef", make_getter( &Config::coef ), make_setter( &Config::coef ) )         .add_property( "path", make_getter( &Config::path ), make_setter( &Config::path ) )         .add_property( "max_size", make_getter( &Config::max_size ), make_setter( &Config::max_size ) )         .def( "__str__", Config_Str )         .def( "__repr__", Config_Repr )         .add_property( "as_dict", Config_ToDict, Config_FromDict )     ; 

Со структурой ничего сложного, из неё получился замечательный питоновский класс, зеркально повторяющий свойства структуры Config в C++ и при этом класс вполне питонист. Единственная проблема с данным классом будет в том, что в конструктор при создании надо будет что-нибудь указать.
Для заполнения параметров конфигурации и доступа к ним давайте заведём синглтон, заодно снабдим его «полезным» счётчиком.

Обёртка класса без возможности создания и копирования

Итак синглтон. Пусть в нём будут содержаться вышеупомянутые параметры конфигурации текущего приложения и некий волшебный счётчик для получения текущего идентификатора.

class Single { public:     static int CurrentID();     static Config& AppConfig();     static void AppConfig( Config const& );  private:     int mCurrentID;     Config mAppConfig;      Single();     Single( Single const& );      static Single& Instance();      int ThisCurrentID();     Config& ThisAppConfig();     void ThisAppConfig( Config const& ); }; 

Как вы наверное успели заметить, я не очень-то люблю вытаскивать в секцию public бесполезный метод Instance() и предпочитаю работать с функционалом синглтона как с набором статических методов. От этого синглтон не перестаёт быть синглтоном, а пользователь класса скажет вам спасибо, за то, что спрятали вызов Instance() в реализацию.
Вот собственно и она, реализация в файле single.cpp:

#include "single.h"  #include <boost/thread.hpp>  using boost::mutex; using boost::unique_lock;  const double CONFIG_DEFAULT_COEF     = 2.5; const int    CONFIG_DEFAULT_MAX_SIZE = 0x1000; const string CONFIG_DEFAULT_PATH     = ".";  int Single::CurrentID() {     return Instance().ThisCurrentID(); }  Config& Single::AppConfig() {     return Instance().ThisAppConfig(); }  void Single::AppConfig( Config const& config ) {     Instance().ThisAppConfig( config ); }  Single::Single()     : mCurrentID( 0 ) {     mAppConfig.coef     = CONFIG_DEFAULT_COEF;     mAppConfig.max_size = CONFIG_DEFAULT_MAX_SIZE;     mAppConfig.path     = CONFIG_DEFAULT_PATH; }  Single& Single::Instance() {     static mutex single_mutex;     unique_lock<mutex> single_lock( single_mutex );     static Single instance;     return instance; }  int Single::ThisCurrentID() {     static mutex id_mutex;     unique_lock<mutex> id_lock( id_mutex );     return ++mCurrentID; }  Config& Single::ThisAppConfig() {     return mAppConfig; }  void Single::ThisAppConfig( Config const& config ) {     mAppConfig = config; } 

Всего три статичных метода, обёртка не должна получиться сложной, если не учитывать одно но… хотя нет, не совсем одно:
1. Нельзя создавать экземпляр класса
2. Нельзя копировать экземпляр класса
3. Мы ещё не оборачивали статические методы

    class_<Single, noncopyable>( "Single", no_init )         .def( "CurrentID", &Single::CurrentID )         .staticmethod( "CurrentID" )         .def( "AppConfig", static_cast< Config& (*)() >( &Single::AppConfig ), return_value_policy<reference_existing_object>() )         .def( "AppConfig", static_cast< void (*)( Config const& ) >( &Single::AppConfig ) )         .staticmethod( "AppConfig" )     ; 

Как видите все сложности связанные с 1-м и 2-м пунктам сводятся к указанию параметра шаблона boost::noncopyable и передаче параметра boost::python::no_init конструктору шаблонного класса boost::python::class_.
Если требуется, чтобы класс поддерживал копирование или содержал конструктор по умолчанию, можете стереть соответствующий глушитель генератора свойств класса-обёртки.
Вообще говоря конструктор по умолчанию можно объявить ниже в .def( init<>() ), некоторые так и делают, для единообразия с другими конструкторами с параметрами, описываемыми отдельно, также передавая no_init в конструктор шаблона обёртки. Есть также вариант с заменой конструктора по-умолчанию на конструктор с параметрами прямо при объявлении класса-обёртки, как это мы уже сделали для структуры Config.
С третьим пунктом вообще всё просто, объявлением того, что метод статичный занимается .staticmethod() после объявления через .def() обёртки всех перегрузок данного метода.
В общем-то остальное уже не вызывает вопросов и знакомо нам по первой части, кроме одной забавной мелочи — политики возвращаемого по ссылке значения return_value_policy<reference_existing_object>, о ней далее.

Политика «не бейте меня, я — переводчик»

Самую большую сложность в обёртке методов нашего синглтона вызвало возвращение ссылки на объект из метода

    static Config& AppConfig(); 

Просто для того, чтобы Garbage Collector (GC) интерпретатора Python не удалил содержимое поля класса возвращённого по ссылке из метода, мы используем return_value_policy<reference_existing_object>.
Магия Boost.Python настолько сурова, что при выполнении кода на Python, изменение полей результата AppConfig() приведёт к изменениям в поле синглтона так, как будто это происходит в C++! Выполнив в командной строке Python следующий код:

from example import * Single.AppConfig().coef = 123.45 Single.AppConfig() 

Мы получим вывод:

Config: { coef: 123.45, path: '.', max_size: 4096 } 

Добавляем свойство с политикой для get-метода

Наверное уже все успели заметить, что я люблю перегрузить метод-другой в примере, чтобы объявление получилось как можно более зубодробительным. Сейчас мы для удобства использования класса Single в питоне добавим свойства для чтения счётчика и для получения и задания параметров конфигурации, благо все методы для этого уже есть.

        .add_static_property( "current_id", &Single::CurrentID )         .add_static_property( "app_config", make_function( static_cast< Config& (*)() >( &Single::AppConfig ), return_value_policy<reference_existing_object>() ), static_cast< void (*)( Config const& ) >( &Single::AppConfig ) ) 

Метод Single::CurrentID обернут свойством current_id на раз-два, зато смотрите какая «красивая» обёртка для двух перегрузок Single::AppConfig, соответственно get- и set-методов свойства app_config. Причём обратите внимание, для get-метода нам пришлось использовать специальную функцию make_function для того, чтобы навесить политику возвращаемого значения return_value_policy<reference_existing_object>.
Будьте очень внимательны, вы не можете использовать функцию make_getter для методов, она используется только для полей классов C++, для методов нужно использовать функции как есть. Если вам требуется задать в одном из методов политику возвращаемого значения для одного из методов свойства, нужно использовать make_function. Вспомогательного дополнительного аргумента для return_value_policy как в .def у вас уже нет, поэтому приходится передавать одним аргументом сразу же и функцию, и политику возвращаемого значения.

Политика «вот новый объект — удали его»

Итак, мы уже разобрались, как не давать GC питона удалять возвращаемый по ссылке объект. Однако иногда требуется передать в питон на хранение новый объект. GC корректно удалит объект как только умрёт в мучениях последняя переменная, ссылающаяся на ваш результат. Для этого есть политика return_value_policy<manage_new_object>.
Давайте заведём метод, клонирующий параметры конфигурации в новый объект. Добавим во wrap.h объявление:

Config* Single_CloneAppConfig(); 

И во wrap.cpp добавляем её реализацию:

Config* Single_CloneAppConfig() {     return new Config( Single::AppConfig() ); } 

В обёртке класса Single соответственно появится новый метод с политикой manage_new_object:

        .def( "CloneAppConfig", Single_CloneAppConfig, return_value_policy<manage_new_object>() ) 

Для проверки того, что Config действительно удаляется когда надо, объявим деструктор в совсем уже не C-style структуре Config. В деструкторе просто выводим в STDOUT через std::cout поля удаляемого экземпляра Config:

Config::~Config() {     cout << "Config destructor of Config: { coef: " << coef           << ", path: '" << path << "', max_size: " << max_size << " }" << endl; } 

Пробуем!
В тестовом скрипте на Python 3.x клонируем конфиги, поизменяем их по-всякому и сбросим все ссылки на созданный через CloneAppConfig() объект:

from example import * c = Single.CloneAppConfig() c.coef = 11.11; c.path = 'cloned'; c.max_size = 111111 print( "c.coef = 12.34; c.path = 'cloned'; c.max_size = 100500" ) print( "c:", c ); print( "Single.AppConfig():", Single.AppConfig() ) print( "c = Single.CloneAppConfig()" ); c = Single.CloneAppConfig() c.coef = 22.22; c.path = 'another'; c.max_size = 222222 print( "c.coef = 22.22; c.path = 'another'; c.max_size = 222222" ) print( "c:", c ); print( "Single.app_config:", Single.app_config ) print( "c = None" ); c = None print( "Single.app_config:", Single.app_config ) 

Деструкторы вызываются ровно тогда когда это и ожидается, когда на объект пропадает последняя ссылка.
Вот что должно вывестись на экран:

c.coef = 12.34; c.path = 'cloned'; c.max_size = 100500 c: { coef: 11.11, path: 'cloned', max_size: 111111 } Single.AppConfig(): { coef: 2.5, path: '.', max_size: 4096 } c = Single.CloneAppConfig() Config::~Config() destructor of object: { coef: 11.11, path: 'cloned', max_size: 111111 } c.coef = 22.22; c.path = 'another'; c.max_size = 222222 c: { coef: 22.22, path: 'another', max_size: 222222 } Single.app_config: { coef: 2.5, path: '.', max_size: 4096 } c = None Config::~Config() destructor of object: { coef: 22.22, path: 'another', max_size: 222222 } Single.app_config: { coef: 2.5, path: '.', max_size: 4096 } Config::~Config() destructor of object: { coef: 2.5, path: '.', max_size: 4096 } 

В качестве домашнего задания, попробуйте ещё добавить в обёртку Config метод __del__ — аналог деструктора в Python, увидите насколько непохоже ведут себя обёртки и объекты на которые они ссылаются.

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

Итак, мы познакомились на практике с двумя новыми политиками возвращаемого значения по ссылке: reference_existing_object и manage_new_object. То есть научились использовать объект-обёртку возвращаемого значения как ссылку на существующий C++ объект, а также передавать на попечение GC Python новые создаваемые в C++ объекты.
Разобрались вкратце как действовать в случае если на дефолтные конструкторы класса в C++ наложены ограничения. Это актуально не только в случае синглтона или абстрактного класса, но также и для множества специфичных классов, примеры которых наверняка сейчас у вас перед глазами.
В третьей части нас ждёт несложная обёртка enum, мы напишем свой конвертер для массива байт из C++ в Python и обратно, а также научимся использовать наследование C++ классов на уровне их обёрток.
Далее нас ждёт волшебный мир конвертации исключений из C++ в Python и обратно.
Что будет после пока загадывать не буду, тема раскручивается как клубок: такой маленький и компактный, пока его разматывать не начинаешь…
Ссылку на проект 2-й части можно найти здесь. Проект MSVS v11, настроен на сборку с Python 3.3 x64.

Полезные ссылки

Документация Boost.Python
Конструктор обёртки класса boost::python::class_
Политики возвращаемых по ссылке значений в Boost.Python
Начало работы с Boost для Windows
Начало работы с Boost для *nix
Тонкости сборки Boost.Python

ссылка на оригинал статьи http://habrahabr.ru/post/168233/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *