Friday, August 22, 2008

Использование Boost Python

“...one of the most highly regarded and expertly designed C++ library projects in the world.”
— Herb Sutter and Andrei Alexandrescu, C++ Coding Standards
Действительно, библиотека Boost - это живой пример того, насколько С++ может быть крут. И Boost.Python - не исключение.
Итак, Boost.Python - это библиотека, которая позволяет писать на С++ модули-расширения для Python, а также использовать возможности Python из С++. Зачем это может быть нужно? Например,
  • Переписывание bottlenecks на С/C++
  • Сокрытие части реализации в компилируемом модуле(весьма важно для распространениея коммерческих приложений)
  • Использование существующих библиотек(как на С/С++, так и на Python)
Конечно, можно использовать для решения этих задач Python C API, но, с одной стороны, С++ куда удобней для всего цикла разработки, а с другой - Boost.Python делает разработку на С++ настолько простой, что отказаться от нее невозможно.
Этим постом я не хочу повторять документацию по Boost.Python. Я ставлю целью скорее показать мощность Boost.Python и то, как он преобразует все вокруг :) И так как код - лучшая демонстрация, то я буду использовать фрагменты кода с пояснениями.
Итак, простейший модуль-расширение на С++:
#include <boost/python.hpp>
using namespace boost::python;
struct World
{
World(std::string msg_): msg(msg_) {}
std::string greet()
{
return msg;
}
std::string msg;
};
BOOST_PYTHON_MODULE(имя_модуля)
{
class_<World>("World", init<std::string>())
.def("greet", &World::greet)
;
}
Этот пример должен быть знаком тем, кто заглядывал в документацию. В нем ничего сложного нет - наш модуль включает в себя класс с конструктором и одной функцией. Предлагаю его взять в качестве основы и добавлять в него фрагменты ниже. Но сначала - как собирать этот пример. Предлагаю сохранить этот файл как имя_файла.cpp, после этого сборка может производиться при помощи gcc так:
gcc -shared -Wl,-soname,имя_модуля.so -o имя_модуля.so имя_файла.cpp -I /usr/include/python2.5/ -lboost_python
Обратите внимание, что имя_модуля должно совпадать с именем, указанным в файле. Также я использую python2.5. Тем, кто использует другую версию, нужно также поменять номер версии в пути для include.
Естественно, модуль не обязан состоять из одного файла, однако для сборки больших проектов рекомендую использовать distutils. Документация по нему достаточно подробная, чтобы собрать любой модуль. Самое главное - не забыть в описание Extension включить libraries=['boost_python'], а также указать то же имя Extension, что и внутри файлов используется.
После того, как простейший модуль собран и опробован,
In [1]: from имя_модуля import World
In [2]: w = World("Hello!")
In [3]: w.greet()
Out[3]: 'Hello!'
, можно приступать к самому интересному - к полезным обрывкам кода:
/*добавим две функции к World, "экспорт" их описывать не буду, можно по аналогии*/
void set(std::string msg) { this->msg = msg; }
void anotherWorldSet(World * world, std::string msg)
{
world->set(msg);
}
Проверяем:
In [1]: from hello_ext import World
In [2]: w1 = World("w1")
In [3]: w2 = World("w2")
In [4]: w1.anotherWorldSet(w2,"w2_new")
In [5]: w2.greet()
Out[5]: 'w2_new'
Т.е., Boost.Python прекрасно понимает, когда вы хотите получить объект по значению, а когда по ссылке. Более того, можно использовать и ссылки (void anotherWorldSet(World& world, std::string msg);) - разницы нет. Дальше перейдем к самому интересному - к boost::python::object. Это класс, который эмулирует работу Python объекта. Вот так, например:
void anotherObjectSet(object obj, std::string msg)
{
obj.attr("set")(msg);
}
И это работает, также как и предыдущий вариант. Обратите внимание на метод attr(). Его можно использовать как для вызова методов(см. выше), так и для получения значений:
std::string get_version()
{
/*Так импортятся Python модули. В качестве имени можно использовать и разделенные точкой названия*/
object sys = import("sys");
return extract<std::string>(sys.attr("version"));/*так преобразуются Python объекты в С++ объекты*/
}
std::string get_some(object myObj,std::string str)/*еще один пример*/
{
return extract<std::string>(myObj.attr(str.c_str()));
}
Раз уж зашла речь о Python объектах, то стоит рассказать о Python str и dict объектах:
dict foo_dict()
{
dict result;
result["1"]="2";
result["2"]="3";
return result;
}
object foo_string()
{
char buf[12]={0};
sprintf(buf,"%08XHELP",4242);
str s(buf,12);/*12 - длина строки*/
std::vector m;
m.push_back('1');m.push_back('2');m.push_back('3');m.push_back('4');
s += str(&m[0],m.size());
s += str("super test 2",12);
return s;
}
Т.е., работать с dict и str - одно удовольствие. Идем дальше, одна из пречудеснейших возможностей:
object get_func()
{
return make_function(&World::func);/*make_function - это чистая магия :)*/
}
std::string func()
{
return "I am func!";
}
Использование предыдущих фрагментов я не демонстрировал из Python, так как они очень просты. Представленный выше фрагмент ничуть не сложнее, но я все же покажу, как его использовать:
In [1]: from hello_ext import World
In [2]: w = World("Hello")
In [3]: f = w.get_func()
In [4]: f(w)
Out[4]: 'I am func!'
Обратите внимание на вызов f(w), здесь w - это self.
В общем, основные моменты я осветил. Многое еще можно найти в документации, во многом можно положиться на Boost.Python - он работает именно так, как вы от него ожидаете(например, C++ исключения транслируются в Python и т.д.).
Отдельно хочется упомянуть о нескольких важных вещах, которые могут привести к ошибкам:
  • В документации прекрасно описано наследование в Python от С++ классов. Однако, если вы переопределяете конструктор - обязательно вызывайте конструктор базового класса, даже если в базовом С++ классе он тривиален. Пренебрежение этим может вызвать непонятные ошибки
  • Если вы передаете Python объект по ссылке в С++ модуль, а потом сохраняете его там для дальнейшего использования - помните, что ссылка может стать невалидной - Python GC может и удалить его, если ссылка останется лишь в С++ части
Возможно, я буду обновлять этот список интересных моментов.
Ну, и на десерт. Если вы используете совместно С++ и Python модули (например, С++ модули используются для сокрытия функционала) и если вы используете логгирование, то скорее всего, вам захочется, чтобы логгирование всех модулей велось однотипно. Очень удобно было бы для этого использовать стандартный Python модуль logging. Чтобы было легче использовать этот модуль из С++, я написал небольшой класс:
/*----------------------------------------*/
/*logging.hpp*/
/*----------------------------------------*/
#pragma once
#include <boost/python.hpp>
#include <map>
#include <string>

class Logging
{
public:
static boost::python::object getLogger(const std::string& name);
static boost::python::object debug(const std::string& name);
static boost::python::object info(const std::string& name);
static boost::python::object warning(const std::string& name);
static boost::python::object error(const std::string& name);
static boost::python::object critical(const std::string& name);
static boost::python::object log(const std::string& name);

private:
typedef std::map<std::string,boost::python::object> LoggersMap;
static LoggersMap m_LoggersMap;
};
/*----------------------------------------*/
/*logging.cpp*/
/*----------------------------------------*/
#include "logging.hpp"

Logging::LoggersMap Logging::m_LoggersMap;

boost::python::object Logging::getLogger(const std::string& name)
{
static boost::python::object logging = boost::python::import("logging");
static boost::python::object settings = boost::python::import("conf.settings");/*Допустим, здесь хранятся настройки, в том числе и настройки логгирования*/

LoggersMap::iterator it = m_LoggersMap.find(name);
if(it != m_LoggersMap.end())
{
return (*it).second;
}
else
{
boost::python::object logger = logging.attr("getLogger")(name);
boost::python::object level = settings.attr("LOG_MODULES")[name];/*LOG_MODULES - dict, который ставит имя в соответствие с logging level*/
logger.attr("setLevel")(level);
m_LoggersMap[name] = logger;
return logger;
}
}

boost::python::object Logging::debug(const std::string& name)
{
return getLogger(name).attr("debug");
}

boost::python::object Logging::info(const std::string& name)
{
return getLogger(name).attr("info");
}

boost::python::object Logging::warning(const std::string& name)
{
return getLogger(name).attr("warning");
}

boost::python::object Logging::error(const std::string& name)
{
return getLogger(name).attr("error");
}

boost::python::object Logging::critical(const std::string& name)
{
return getLogger(name).attr("critical");
}

boost::python::object Logging::log(const std::string& name)
{
return getLogger(name).attr("log");
}
Использовать этот код очень просто:
Logging::getLogger("SomeLogger").attr("debug")("1.Logging stuff %s %s\n","1","2");
Logging::info("SomeLogger")("2.Logging stuff %s %s\n","1","2");
Logging::debug("SomeLogger2")("3.Logging stuff %s %s\n","1","2");
Logging::warning("SomeLogger")("4.Logging stuff %s %s\n","1","2");
Logging::error("SomeLogger2")("5.Logging stuff %s %s\n","1","2");
Logging::critical("SomeLogger2")("6.Logging stuff %s %s\n","1","2");

Спасибо за внимание! Буду рад любым исправлениям, корректировкам и дополнениям.

7 comments:

  1. Использование С++ из Python вообще-то особых проблем не вызывает. :)

    ReplyDelete
  2. Замечательная статья! Спасибо! Мучался с проблемой, конвертации из std::vector в питоновский list. Вы открыли мне глаза! :) буду сразу возвращать данные в питоновских объектах.

    ReplyDelete
  3. @gorban
    Рад, что статья пригодилась :)

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Скажите, а использование питона в качестве встраиваемого языка в прогу на с ++ (с ограничение права вызываемых модулей) возможно?, есть ли библиотеки для этого?

    ReplyDelete
  6. @Mirage
    Ограничения, вероятно, доведётся вручную делать, а так - подойдёт любая, тот же boost::python.

    ReplyDelete