Итак, я придумал, как это сделать, но он включает в себя 1) взлом самого кода pybind11 и 2) внесение некоторой неэффективности в размер связанных типов python. С моей точки зрения, размер проблемы не имеет значения. Да, было бы лучше, чтобы все было идеально по размеру, но я возьму дополнительные байты памяти для удобства использования. Однако, учитывая эту неэффективность, я не отправляю это в качестве PR проекту pybind11. Хотя я думаю, что компромисс того стоит, я сомневаюсь, что сделать это по умолчанию для большинства людей было бы желательно. Было бы возможно, я думаю, скрыть эту функциональность за #define
в c ++, но похоже, что это будет очень грязно в долгосрочной перспективе. Вероятно, есть лучший долгосрочный ответ, который включал бы определенную степень метапрограммирования шаблонов (параметризацию типа контейнера python для class_
), к которому я просто не дошел.
Здесь я представляю свои изменения в виде различий с текущей master
веткой в git, когда это было написано (ha sh a54eab92d265337996b8e4b4149d9176c2d428a6).
Базовый подход c было
- Измените pybind11, чтобы разрешить спецификацию базового класса исключения для экземпляра
class_
. - Измените внутренний контейнер pybind11, чтобы иметь дополнительные поля, необходимые для исключения python type
- Напишите небольшой объем пользовательского кода привязки для правильной установки ошибки в python.
Для первой части я добавил новый атрибут в type_record, чтобы указать, если класс является исключением и добавил связанный с ним вызов process_attribute для его анализа.
diff --git a/src/pybind11/include/pybind11/attr.h b/src/pybind11/include/pybind11/attr.h
index 58390239..b5535558 100644
--- a/src/pybind11/include/pybind11/attr.h
+++ b/src/pybind11/include/pybind11/attr.h
@@ -73,6 +73,9 @@ struct module_local { const bool value; constexpr module_local(bool v = true) :
/// Annotation to mark enums as an arithmetic type
struct arithmetic { };
+// Annotation that marks a class as needing an exception base type.
+struct is_except {};
+
/** \rst
A call policy which places one or more guard variables (``Ts...``) around the function call.
@@ -211,7 +214,8 @@ struct function_record {
struct type_record {
PYBIND11_NOINLINE type_record()
: multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false),
- default_holder(true), module_local(false), is_final(false) { }
- default_holder(true), module_local(false), is_final(false),
- is_except(false) { }
/// Handle to the parent scope
handle scope;
@@ -267,6 +271,9 @@ struct type_record {
/// Is the class inheritable from python classes?
bool is_final : 1;
- // Does the class need an exception base type?
- bool is_except : 1;
- PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *)) {
auto base_info = detail::get_type_info(base, false);
if (!base_info) {
@@ -451,6 +458,11 @@ struct process_attribute<is_final> : process_attribute_default<is_final> {
static void init(const is_final &, type_record *r) { r->is_final = true; }
};
+template <>
+struct process_attribute<is_except> : process_attribute_default<is_except> {
- static void init(const is_except &, type_record *r) { r->is_except = true; }
+};
Я изменил файл internals.h, чтобы добавить отдельный базовый класс для типов исключений. Я также добавил дополнительный аргумент bool в make_object_base_type.
diff --git a/src/pybind11/include/pybind11/detail/internals.h b/src/pybind11/include/pybind11/detail/internals.h
index 6224dfb2..d84df4f5 100644
--- a/src/pybind11/include/pybind11/detail/internals.h
+++ b/src/pybind11/include/pybind11/detail/internals.h
@@ -16,7 +16,7 @@ NAMESPACE_BEGIN(detail)
// Forward declarations
inline PyTypeObject *make_static_property_type();
inline PyTypeObject *make_default_metaclass();
-inline PyObject *make_object_base_type(PyTypeObject *metaclass);
+inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except);
// The old Python Thread Local Storage (TLS) API is deprecated in Python 3.7 in favor of the new
// Thread Specific Storage (TSS) API.
@@ -107,6 +107,7 @@ struct internals {
PyTypeObject *static_property_type;
PyTypeObject *default_metaclass;
PyObject *instance_base;
+ PyObject *exception_base;
#if defined(WITH_THREAD)
PYBIND11_TLS_KEY_INIT(tstate);
PyInterpreterState *istate = nullptr;
@@ -292,7 +293,8 @@ PYBIND11_NOINLINE inline internals &get_internals() {
internals_ptr->registered_exception_translators.push_front(&translate_exception);
internals_ptr->static_property_type = make_static_property_type();
internals_ptr->default_metaclass = make_default_metaclass();
- internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass);
+ internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass, false);
+ internals_ptr->exception_base = make_object_base_type(internals_ptr->default_metaclass, true);
А затем в class.h
я добавил необходимый код для генерации базового типа исключения. Первый нюанс здесь. Поскольку PyExc_Exception является типом со сборкой мусора, мне пришлось охватить вызов assert
, который проверял флаг G C на типе. В настоящее время я не замечал плохого поведения в результате этого изменения, но это определенно аннулирует гарантию прямо здесь. Я очень, очень рекомендую всегда передавать флаг py:dynamic_attr()
любым классам, которые вы используете py:except
, поскольку это включает все необходимые навороты для правильной обработки G C (я думаю). Лучшим решением может быть включение всех этих вещей в make_object_base_type
без необходимости вызывать py::dynamic_attr
.
diff --git a/src/pybind11/include/pybind11/detail/class.h b/src/pybind11/include/pybind11/detail/class.h
index a05edeb4..bbb9e772 100644
--- a/src/pybind11/include/pybind11/detail/class.h
+++ b/src/pybind11/include/pybind11/detail/class.h
@@ -368,7 +368,7 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) {
/** Create the type which can be used as a common base for all classes. This is
needed in order to satisfy Python's requirements for multiple inheritance.
Return value: New reference. */
-inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
+inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except=false) {
constexpr auto *name = "pybind11_object";
auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));
@@ -387,7 +387,12 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
auto type = &heap_type->ht_type;
type->tp_name = name;
- type->tp_base = type_incref(&PyBaseObject_Type);
+ if (is_except) {
+ type->tp_base = type_incref(reinterpret_cast<PyTypeObject*>(PyExc_Exception));
+ }
+ else {
+ type->tp_base = type_incref(&PyBaseObject_Type);
+ }
type->tp_basicsize = static_cast<ssize_t>(sizeof(instance));
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
@@ -404,7 +409,9 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);
- assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
+ if (!is_except) {
+ assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
+ }
return (PyObject *) heap_type;
}
@@ -565,7 +572,8 @@ inline PyObject* make_new_python_type(const type_record &rec) {
auto &internals = get_internals();
auto bases = tuple(rec.bases);
- auto base = (bases.size() == 0) ? internals.instance_base
+ auto base = (bases.size() == 0) ? (rec.is_except ? internals.exception_base
+ : internals.instance_base)
И последнее изменение, которое является частью неэффективности. В Python все является PyObject, но на самом деле это только два поля (настраиваются с помощью макроса PyObject_HEAD), а фактическая структура объекта может иметь много дополнительных полей. И наличие очень точного макета важно, потому что python использует offsetof
, чтобы иногда искать эти вещи. Из исходного кода Python 2.7 (Include / pyerrord.h) вы можете увидеть структуру, которая используется для базовых исключений.
typedef struct {
PyObject_HEAD
PyObject *dict;
PyObject *args;
PyObject *message;
} PyBaseExceptionObject;
Любой тип pybind11, расширяющий PyExc_Exception
, должен иметь структуру экземпляра, которая содержит тот же исходный макет. И в настоящее время в pybind11 структура экземпляра имеет только PyObject_HEAD
. Это означает, что если вы не измените структуру instance
, все это будет скомпилировано, но когда python ищет этот объект, это будет происходить с предположением, что существуют дополнительные поля, а затем он будет искать сразу в конце жизнеспособная память, и вы получите всевозможные забавные ошибки. Таким образом, это изменение добавляет эти дополнительные поля к каждому class_
в pybind11. Кажется, что наличие этих дополнительных полей не мешает нормальным классам, и определенно кажется, что исключения работают правильно. Если мы нарушили гарантию раньше, мы просто разорвали ее и подожгли.
diff --git a/src/pybind11/include/pybind11/detail/common.h b/src/pybind11/include/pybind11/detail/common.h
index dd626793..b32e0c70 100644
--- a/src/pybind11/include/pybind11/detail/common.h
+++ b/src/pybind11/include/pybind11/detail/common.h
@@ -392,6 +392,10 @@ struct nonsimple_values_and_holders {
/// The 'instance' type which needs to be standard layout (need to be able to use 'offsetof')
struct instance {
PyObject_HEAD
+ // Necessary to support exceptions.
+ PyObject *dict;
+ PyObject *args;
+ PyObject *message;
/// Storage for pointers and holder; see simple_layout, below, for a description
Однако, как только все эти изменения будут внесены, вот что вы можете сделать. Привязать к классу
auto PyBadData = py::class_< ::example::data::BadData>(module, "BadData", py::is_except(), py::dynamic_attr())
.def(py::init<>())
.def(py::init< std::string, std::string >())
.def("__str__", &::example::data::BadData::toString)
.def("getStack", &::example::data::BadData::getStack)
.def_property("message", &::example::data::BadData::getMsg, &::example::data::BadData::setMsg)
.def("getMsg", &::example::data::BadData::getMsg);
И взять функцию на С ++, которая генерирует исключение
void raiseMe()
{
throw ::example::data::BadData("this is an error", "");
}
и привязать ее тоже
module.def("raiseMe", &raiseMe, "A function throws");
Добавить транслятор исключений чтобы поместить весь тип python в исключение
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) {
std::rethrow_exception(p);
}
} catch (const ::example::data::BadData &e) {
auto err = py::cast(e);
auto errType = err.get_type().ptr();
PyErr_SetObject(errType, err.ptr());
}
});
И тогда вы получите все, что захотите!
>>> import example
>>> example.raiseMe()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
example.BadData: BadData(msg=this is an error, stack=)
Вы, конечно, также можете создать экземпляр и вызвать исключение из python, а также
>>> import example
>>> raise example.BadData("this is my error", "no stack")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
example.BadData: BadData(msg=this is my error, stack=no stack)