所以我想出了一种方法来实际做到这一点,但它涉及 1) 对 pybind11 代码本身进行一些黑客攻击和 2) 为绑定的 python 类型引入一些大小效率低下。在我看来,尺寸问题是相当无关紧要的。是的,最好让所有东西都大小合适,但为了便于使用,我会占用一些额外的内存字节。但是,鉴于这种低效率,我不会将此作为 PR 提交给 pybind11 项目。虽然我认为权衡是值得的,但我怀疑将其作为大多数人的默认设置是否合乎需要。有可能,我想将此功能隐藏在#define
在 c++ 中,但这似乎是长期的超级混乱。可能有一个更好的长期答案,这将涉及class_
我无法达到的一定程度的模板元编程(在 python 容器类型上参数化)。
我在这里提供我的更改作为与master
git 中当前分支的差异(哈希 a54eab92d265337996b8e4b4149d9176c2d428a6)。
基本方法是
class_
修改 pybind11 以允许为实例指定异常基类。
- 修改 pybind11 的内部容器,使其具有 python 异常类型所需的额外字段
- 编写少量自定义绑定代码来处理在 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 文件,为异常类型添加了一个单独的基类。我还为 make_object_base_type 添加了一个额外的 bool 参数。
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
确定检查该类型上 GC 标志的调用的范围。我目前没有看到此更改有任何不良行为,但这肯定会使保修失效。我强烈建议您始终将py:dynamic_attr()
标志传递给您正在使用的任何类py:except
,因为这会打开所有必要的花里胡哨来正确处理 GC(我认为)。更好的解决方案可能是打开所有这些东西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 宏设置),实际的对象结构可能有很多额外的字段。并且有一个非常精确的布局很重要,因为 pythonoffsetof
有时会使用这些东西来寻找这些东西。从 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);
并在 C++ 中使用一个引发异常的函数
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)