понедельник, 6 февраля 2012 г.

Обработка исключений в Boost.Python

Мы в нашем проекте стараемся использовать исключения для сигнализации об ошибках. Почему? Причин несколько: удобно при отладке - если что-то пошло не так, мы сразу видим где и что произошло и можем перейти непосредственно к исправлению ошибки не теряя времени на отладку; пользователю приходится быть более дисциплинированным - ведь исключение нельзя проигнорировать, в отличие от проверки возвращаемого значения; удобно при разработке - вместо прокидывания кода ошибки через все функции, мы просто кидаем исключение, которое к тому же может быть гораздо информативнее. В результате, мы верим, что весь комплекс pykd + скрипт становится надежным инструментом.

Но разберемся, как же исключения попадают из С++ кода, написанного с помощью Boost::Python в виртуальную машину Python? Будем двигаться итеративно.

Итерация 0: Генерируем python исключение

Сгенерировать исключение в ВМ python просто. Для этого служит функция PyErr_SetString. В нее нужно передать указатель на тип исключения и сторку с описанием ошибки. Например так:
PyErr_SetString( PyExc_IndexError, "Index out of range" );
Не очень то удобно использовать эту конструкцию внтури С++ кода. Хотелось бы кидать С++ исключение и чтобы оно волшебным образом превращалось в python исключение. К счастью, boost::python предоставляет эту возможность.

Итерация 1: транслируем исключение из C++ в python

В общем все довольно просто. Любое исключение, возникшее во время выполнения python программы внутри модуля, использующего boost::python отлавливается. Существует механизм, с помощью которого можно поучаствовать в обработке такого исключения. Для этого необходимо зарегистрировать функцию-транслятор с помощью вызова register_exception_translator. Вот как это делается:

// класс исключения, для использования в C++ коде
class PyException : public std::exception
{
public:

    PyException( PyObject*  pyObj, const std::string &desc ) :
        std::exception( desc.c_str() ),
        m_typeObj( pyObj )
        {}    

    static
    void
    exceptionTranslate(const PyException &e ) {
        PyErr_SetString( e.m_typeObj, e.what() );
    }

private:

    PyObject*       m_typeObj;
};

void myFunc() {
    throw PyException( PyExc_IndexError, "Index out of range" );
}


BOOST_PYTHON_MODULE( my_module )
{
    // регистрируем транслятор
    boost::python::register_exception_translator<PyException>( &PyException::exceptionTranslate );

    // функция, вызываемая из python программы
    boost::python::def( "myFunc", &myFunc );
}

Прекрасно, генерировать исключения python теперь удобно. Но я хочу использовать собственный тип исключений. Возможно ли это? В общем да...

Итерация 2: Использование собственного типа исключений.


Идея простая. Boost::python дает возможность импортировать типы в python программу. Наш план таков: импортируем тип в python, регистрируем функцию-транслятор С++ исключения и в коде функции-трансялтора используем наш тип исключения, который мы ранее импортировали ( и запомнили! ) в python.
// класс исключения, для использования в C++ коде
class MyException : public std::exception
{
public:

    MyException ( const std::string &desc ) :
        std::exception( desc.c_str() )
        {}    

    static void setTypeObject(PyObject *p) {
        exceptTypeObject = p;
        python::register_exception_translator<MyException>( &exceptionTranslate );
    }

private:

   static PyObject  *exceptTypeObject;

    static
    void
    exceptionTranslate(const MyException &e ) {
        boost::python::object  pyExcept(e);
        PyErr_SetObject( exceptTypeObject , pyExcept.ptr());
    }
};

void myFunc() {
    throw MyException( "Something's wrong" );
}


BOOST_PYTHON_MODULE( my_module )
{
    // функция, вызываемая из python программы
    boost::python::def( "myFunc", &myFunc );

    // регистрирем тип исключения и его транслятор
    MyException::setTypeObject( 
        boost::python::class_<MyException>( "MyException", "MyException error" ).ptr()
    );
}

Вот на этом мы могли бы остановиться. Могли бы, если бы мир был совершенен. На самом деле, после чуть более тщательного тестирования, мы приходим к заключению, что работают наши исключения не совсем верно. А именно:
не работает инструкция raise MyException - вместо генерации исключения обозначенного типа генерирует питоновское исключение, общий смысл которого в двух словах сводится к тому, что тип MyException не пригоден для генерации исключений. Вторая проблема заключается в том, что при попытке сделать иерархию исключений мы сталкиваемся с тем, что неверно работает фильтрация исключений, а именно: инструкция except BaseException не ловит исключения дочерних типов. Короче говоря, имплементация наших исключений весьма далека не только от идеала, но и просто от обычных python исключений. Будем разбираться.

Итерация 3: разбираемся

Изучив внимательно текст ошибки при попытке возбудить исключение raise MyExceptionException. Кроме того, тут надо сделать небольшой ( самостоятельный ;) ) экскурс в анатомию питонов и изучить вопрос о типах и метатипах. Если в двух словах, то тип в питоне - это такой же объект и ,соответственно, может быть динамически создан вызовом конструктора у некого типа. Вот этот "некий" тип и является метатипом. Покопавшись внутрях boost::python можно придти к заключению, что все типы, которые он импортирует, создаются через один и тот же метаттип. В итоге наш MyException имеет метатип такой же, как и другие типы, импортированные из boost::python. И он не имеет родственных связей с типами питоновских исключений. А это негативно влияет на обработку исключений питоном. Наш дальнейший план каким-то образом унаследовать MyException от питоновского типа Exception. Как же это сделать то????

Итерация 4: Как же это сделать то????

Делать то нечего, приходится отказаться от регистрации типов исключений через бустовый class_. Подсмотрев в его исходники, находим место, где создается новый питоновский тип и переписываем его примерно так:

using boost::python;

// Базовым типом у нас будет PyExc_Exception
handle<>   basedtype = handle<>(PyExc_Exception);

// Всякий объект в питоне имеет свой список свойств
dict       ob_dict;
ob_dict["__doc__"] = "MyException"

// Объект в питоне кстате может иметь много пап. Но у нас будет один - самый лучший! 
tuple      ob_bases = make_tuple( basedtype );

// Вот тут весь оргазм: 
// Py_TYPE(basedtype.get()) - вернет нам тип от объекта PyExc_Exception. Тип типа - это метатип, мы это выясняли
// object( boost::python::handle<>(Py_TYPE(basedtype.get()) ) ) - это будет у нас объект меаттипа
// Что такоей вызов SomeType( a, b, c) ? - это конструирование объекта этого типа
// Теперь собираем пазл во-едино: мы создаем новый тип, который имеет такой же метатип как и PyExc_Exception
// и является наследником PyExc_Exception
object     ob = object( boost::python::handle<>(Py_TYPE(basedtype.get()) ) )( "MyException", ob_bases, ob_dict );

// остается пристроить новорожденного в приличное место, а именно в скоуп модуля
scope().attr( "MyException" ) = ob;

Теперь остается привести все в божеский вид.

Итерация 5: божеский вид

Для приведения в божеский вид нам надо написать какой то шаблон, по своим функциям хоть чуть-чуть напоминающий class_. Как минимум, хочется наследования, пусть даже не множественного. В результате, родился такой вот шаблон. Он далек от изящества к сожалению, но у него доброе сердце )).

template< class TExcept >
struct exceptPyType{

     static python::handle<>     pyExceptType;

};

template< class TExcept, class TBaseExcept = python::detail::not_specified >
class exception  {


public:

    exception( const std::string& className, const std::string& classDesc ) 
    {
        python::handle<>   basedtype;      

        if ( boost::is_same<TBaseExcept, python::detail::not_specified>::value )
        {
            basedtype = python::handle<>(PyExc_Exception);
        }
        else
        {
            basedtype = exceptPyType<TBaseExcept>::pyExceptType;
        }

        python::dict       ob_dict;
       
        ob_dict["__doc__"] = classDesc;

        python::tuple      ob_bases = python::make_tuple( basedtype );

        python::object     ob = python::object( python::handle<>(Py_TYPE(basedtype.get()) ) )( className, ob_bases, ob_dict );

        python::scope().attr( className.c_str() ) = ob;

        exceptPyType<TExcept>::pyExceptType = python::handle<>( ob.ptr() );

        python::register_exception_translator<TExcept>( &exceptionTranslate );
    }

    static
    void
    exceptionTranslate(const TExcept &e ) {

        python::object      exceptObj = python::object( exceptPyType<TExcept>::pyExceptType )( e.what() );

        PyErr_SetObject( exceptPyType<TExcept>::pyExceptType.get(), exceptObj.ptr());
    }

};

Но со стороны описания модуля выглядит все не так уж и плохо:

BOOST_PYTHON_MODULE( mymodule )
{
  exception<MyException>( "MyException", "description" );
  exception<MyExceptionEx,MyException>( "MyExceptionEx", "description" );
}

Заключение

Почему же нам пришлось прибегать к таким извращениям? Очевидно, что boost::python далек от совершенства )). Чего же в нем не хватает? Было бы классно, если бы существовала форма функции class_, позволяющая задать базовый класс через указатель на питоноский тип.