Ситуация
Я хочу создать привязку языка Python для С++ API с помощью SWIG. Некоторые из функций API могут вызывать исключения. Приложение С++ имеет иерархию самоопределяемых исключений, например, этот пример:
std::exception
-> API::Exception
-> API::NetworkException
-> API::TimeoutException
-> API::UnreachableException
-> API::InvalidAddressException
Требуемое поведение выглядит следующим образом:
-
Все типы исключений должны иметь соответствующий класс Python как обертка. Эти классы-оболочки должны быть действительными исключениями Python.
-
Когда вызов API выдает исключение С++, он должен быть пойман. соответствующее исключение Python (т.е. Класс-оболочка пойманного исключения С++) должно быть брошено.
-
Это должен быть динамический процесс: тип исключения Python определяется во время выполнения, только на основе типа среды выполнения пойманного исключения на С++. Таким образом, нет необходимости описывать полную иерархию исключений в файле интерфейса SWIG.
Проблемы и вопросы
-
Классы Wrapper не являются исключениями Python.
В то время как SWIG создает классы-оболочки для всех самоопределяемых исключений (например, для любого другого класса), эти классы не являются исключениями Python. Обертка базового исключения (
API::Exception
в примере) расширяетObject
вместоBaseException
, класс Python, из которого должны быть выведены все исключения в Python.Кроме того, не представляется возможным позволить SWIG добавлять родительский класс вручную. Обратите внимание, что это возможно при использовании SWIG с Java через использование
%typemap(javabase)
(см. документацию SWIG для деталей). -
Как может API Python C API исключить пользовательское исключение?
Самый распространенный способ выбросить исключение Python из API Python C - это вызвать
PyErr_SetString
[reference]. Это также показано в демонстрационном приложении ниже.Но это только тривиально со стандартными (встроенными) исключениями Python, потому что ссылки на них хранятся в глобальных переменных [ссылка] в Python C API.
Я знаю, что есть метод
PyErr_NewException
[reference], чтобы получить ссылки на самоопределяемые исключения, но я не получил эту работу. -
Я предполагаю, что класс Python можно искать по имени во время выполнения через часть отражения API Python C API. Это путь? И как это делается на практике?
Демо-приложение
Чтобы поэкспериментировать с этой проблемой, я создал крошечный С++ API с одной функцией, которая вычисляет факториал числа. Он имеет минимальную самоопределяемую иерархию исключений, состоящую только из одного класса TooBigException
.
Обратите внимание, что это исключение выступает в качестве основного исключения в общей проблеме, и приложение должно работать с любым подклассом. Это означает, что решение может использовать только динамический (т.е. Время выполнения) исключенный захват для повторного его преобразования в Python (см. Ниже).
Полный исходный код демонстрационного приложения выглядит следующим образом:
// File: numbers.h
namespace numbers {
int fact(int n);
}
// File: numbers.cpp
#include "TooBigException.h"
namespace numbers {
int fact(int n) {
if (n > 10) throw TooBigException("Value too big", n);
else if (n <= 1) return 1;
else return n*fact(n-1);
}
}
// File: TooBigException.h
namespace numbers {
class TooBigException: public std::exception {
public:
explicit TooBigException(const std::string & inMessage,
const int inValue);
virtual ~TooBigException() throw() {}
virtual const char* what() const throw();
const std::string & message() const;
const int value() const;
private:
std::string mMessage;
int mValue;
};
}
// File: TooBigException.cpp
#include "TooBigException.h"
namespace numbers {
TooBigException::TooBigException(const std::string & inMessage, const int inValue):
std::exception(),
mMessage(inMessage),
mValue(inValue)
{
}
const char* TooBigException::what() const throw(){
return mMessage.c_str();
}
const std::string & TooBigException::message() const {
return mMessage;
}
const int TooBigException::value() const {
return mValue;
}
}
Чтобы получить привязку Python, я использую следующий файл интерфейса SWIG:
// File: numbers.i
%module numbers
%include "stl.i"
%include "exception.i"
%{
#define SWIG_FILE_WITH_INIT
#include "TooBigException.h"
#include "numbers.h"
%}
%exception {
try {
$action
}
catch (const numbers::TooBigException & e) {
// This catches any self-defined exception in the exception hierarchy,
// because they all derive from this base class.
<TODO>
}
catch (const std::exception & e)
{
SWIG_exception(SWIG_RuntimeError, (std::string("C++ std::exception: ") + e.what()).c_str());
}
catch (...)
{
SWIG_exception(SWIG_UnknownError, "C++ anonymous exception");
}
}
%include "TooBigException.h"
%include "numbers.h"
Таким образом, каждый вызов API завершается блоком try-catch. Первые исключения нашего базового типа пойманы и обработаны. Затем все остальные исключения захватываются и обновляются с помощью библиотеки исключений SWIG.
Обратите внимание, что любой подкласс numbers::TooBigException
пойман, и обертки их динамического (т.е. времени выполнения) должны быть выброшены, а не оболочка их статического (т.е. времени компиляции), который всегда TooBigException
!
Проект можно легко создать, выполнив следующие команды на машине Linux:
$ swig -c++ -python numbers.i
$ g++ -fPIC -shared TooBigException.cpp numbers.cpp numbers_wrap.cxx \
-I/usr/include/python2.7 -o _numbers.so
Текущая реализация
Моя текущая реализация все еще (успешно) выдает фиксированное стандартное исключение Python. Приведенный выше код <TODO>
заменяется на:
PyErr_SetString(PyExc_Exception, (std::string("C++ self-defined exception ") + e.what()).c_str());
return NULL;
Что дает следующее (ожидаемое) поведение в Python:
>>> import numbers
>>> fact(11)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: C++ self-defined exception Value too big