Skip to content

Commit d65edfb

Browse files
authored
Feature/local exception translator (#2650)
* Create a module_internals struct Since we now have two things that are going to be module local, it felt correct to add a struct to manage them. * Add local exception translators These are added via the register_local_exception_translator function and are then applied before the global translators * Add unit tests to show the local exception translator works * Fix a bug in the unit test with the string value of KeyError * Fix a formatting issue * Rename registered_local_types_cpp() Rename it to get_registered_local_types_cpp() to disambiguate from the new member of module_internals * Add additional comments to new local exception code path * Add a register_local_exception function * Add additional unit tests for register_local_exception * Use get_local_internals like get_internals * Update documentation for new local exception feature * Add back a missing space * Clean-up some issues in the docs * Remove the code duplication when translating exceptions Separated out the exception processing into a standalone function in the details namespace. Clean-up some comments as per PR notes as well * Remove the code duplication in register_exception * Cleanup some formatting things caught by clang-format * Remove the templates from exception translators But I added a using declaration to alias the type. * Remove the extra local from local_internals variable names * Add an extra explanatory comment to local_internals * Fix a typo in the code
1 parent 6d5d4e7 commit d65edfb

10 files changed

+274
-45
lines changed

docs/advanced/exceptions.rst

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ Registering custom translators
7575

7676
If the default exception conversion policy described above is insufficient,
7777
pybind11 also provides support for registering custom exception translators.
78-
To register a simple exception conversion that translates a C++ exception into
79-
a new Python exception using the C++ exception's ``what()`` method, a helper
80-
function is available:
78+
Similar to pybind11 classes, exception translators can be local to the module
79+
they are defined in or global to the entire python session. To register a simple
80+
exception conversion that translates a C++ exception into a new Python exception
81+
using the C++ exception's ``what()`` method, a helper function is available:
8182

8283
.. code-block:: cpp
8384
@@ -87,29 +88,39 @@ This call creates a Python exception class with the name ``PyExp`` in the given
8788
module and automatically converts any encountered exceptions of type ``CppExp``
8889
into Python exceptions of type ``PyExp``.
8990

91+
A matching function is available for registering a local exception translator:
92+
93+
.. code-block:: cpp
94+
95+
py::register_local_exception<CppExp>(module, "PyExp");
96+
97+
9098
It is possible to specify base class for the exception using the third
9199
parameter, a `handle`:
92100

93101
.. code-block:: cpp
94102
95103
py::register_exception<CppExp>(module, "PyExp", PyExc_RuntimeError);
104+
py::register_local_exception<CppExp>(module, "PyExp", PyExc_RuntimeError);
96105
97106
Then `PyExp` can be caught both as `PyExp` and `RuntimeError`.
98107

99108
The class objects of the built-in Python exceptions are listed in the Python
100109
documentation on `Standard Exceptions <https://docs.python.org/3/c-api/exceptions.html#standard-exceptions>`_.
101110
The default base class is `PyExc_Exception`.
102111

103-
When more advanced exception translation is needed, the function
104-
``py::register_exception_translator(translator)`` can be used to register
112+
When more advanced exception translation is needed, the functions
113+
``py::register_exception_translator(translator)`` and
114+
``py::register_local_exception_translator(translator)`` can be used to register
105115
functions that can translate arbitrary exception types (and which may include
106-
additional logic to do so). The function takes a stateless callable (e.g. a
116+
additional logic to do so). The functions takes a stateless callable (e.g. a
107117
function pointer or a lambda function without captured variables) with the call
108118
signature ``void(std::exception_ptr)``.
109119

110120
When a C++ exception is thrown, the registered exception translators are tried
111121
in reverse order of registration (i.e. the last registered translator gets the
112-
first shot at handling the exception).
122+
first shot at handling the exception). All local translators will be tried
123+
before a global translator is tried.
113124

114125
Inside the translator, ``std::rethrow_exception`` should be used within
115126
a try block to re-throw the exception. One or more catch clauses to catch
@@ -168,6 +179,53 @@ section.
168179
with ``-fvisibility=hidden``. Therefore exceptions that are used across ABI boundaries need to be explicitly exported, as exercised in ``tests/test_exceptions.h``.
169180
See also: "Problems with C++ exceptions" under `GCC Wiki <https://gcc.gnu.org/wiki/Visibility>`_.
170181

182+
183+
Local vs Global Exception Translators
184+
=====================================
185+
186+
When a global exception translator is registered, it will be applied across all
187+
modules in the reverse order of registration. This can create behavior where the
188+
order of module import influences how exceptions are translated.
189+
190+
If module1 has the following translator:
191+
192+
.. code-block:: cpp
193+
194+
py::register_exception_translator([](std::exception_ptr p) {
195+
try {
196+
if (p) std::rethrow_exception(p);
197+
} catch (const std::invalid_argument &e) {
198+
PyErr_SetString("module1 handled this")
199+
}
200+
}
201+
202+
and module2 has the following similar translator:
203+
204+
.. code-block:: cpp
205+
206+
py::register_exception_translator([](std::exception_ptr p) {
207+
try {
208+
if (p) std::rethrow_exception(p);
209+
} catch (const std::invalid_argument &e) {
210+
PyErr_SetString("module2 handled this")
211+
}
212+
}
213+
214+
then which translator handles the invalid_argument will be determined by the
215+
order that module1 and module2 are imported. Since exception translators are
216+
applied in the reverse order of registration, which ever module was imported
217+
last will "win" and that translator will be applied.
218+
219+
If there are multiple pybind11 modules that share exception types (either
220+
standard built-in or custom) loaded into a single python instance and
221+
consistent error handling behavior is needed, then local translators should be
222+
used.
223+
224+
Changing the previous example to use ``register_local_exception_translator``
225+
would mean that when invalid_argument is thrown in the module2 code, the
226+
module2 translator will always handle it, while in module1, the module1
227+
translator will do the same.
228+
171229
.. _handling_python_exceptions_cpp:
172230

173231
Handling exceptions from Python in C++

include/pybind11/detail/class.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ extern "C" inline void pybind11_meta_dealloc(PyObject *obj) {
209209
internals.direct_conversions.erase(tindex);
210210

211211
if (tinfo->module_local)
212-
registered_local_types_cpp().erase(tindex);
212+
get_local_internals().registered_types_cpp.erase(tindex);
213213
else
214214
internals.registered_types_cpp.erase(tindex);
215215
internals.registered_types_py.erase(tinfo->type);

include/pybind11/detail/internals.h

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
#include "../pytypes.h"
1313

1414
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
15+
16+
using ExceptionTranslator = void (*)(std::exception_ptr);
17+
1518
PYBIND11_NAMESPACE_BEGIN(detail)
19+
1620
// Forward declarations
1721
inline PyTypeObject *make_static_property_type();
1822
inline PyTypeObject *make_default_metaclass();
@@ -100,7 +104,7 @@ struct internals {
100104
std::unordered_set<std::pair<const PyObject *, const char *>, override_hash> inactive_override_cache;
101105
type_map<std::vector<bool (*)(PyObject *, void *&)>> direct_conversions;
102106
std::unordered_map<const PyObject *, std::vector<PyObject *>> patients;
103-
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
107+
std::forward_list<ExceptionTranslator> registered_exception_translators;
104108
std::unordered_map<std::string, void *> shared_data; // Custom data to be shared across extensions
105109
std::vector<PyObject *> loader_patient_stack; // Used by `loader_life_support`
106110
std::forward_list<std::string> static_strings; // Stores the std::strings backing detail::c_str()
@@ -313,12 +317,25 @@ PYBIND11_NOINLINE inline internals &get_internals() {
313317
return **internals_pp;
314318
}
315319

316-
/// Works like `internals.registered_types_cpp`, but for module-local registered types:
317-
inline type_map<type_info *> &registered_local_types_cpp() {
318-
static type_map<type_info *> locals{};
319-
return locals;
320+
321+
// the internals struct (above) is shared between all the modules. local_internals are only
322+
// for a single module. Any changes made to internals may require an update to
323+
// PYBIND11_INTERNALS_VERSION, breaking backwards compatibility. local_internals is, by design,
324+
// restricted to a single module. Whether a module has local internals or not should not
325+
// impact any other modules, because the only things accessing the local internals is the
326+
// module that contains them.
327+
struct local_internals {
328+
type_map<type_info *> registered_types_cpp;
329+
std::forward_list<ExceptionTranslator> registered_exception_translators;
330+
};
331+
332+
/// Works like `get_internals`, but for things which are locally registered.
333+
inline local_internals &get_local_internals() {
334+
static local_internals locals;
335+
return locals;
320336
}
321337

338+
322339
/// Constructs a std::string with the given arguments, stores it in `internals`, and returns its
323340
/// `c_str()`. Such strings objects have a long storage duration -- the internal strings are only
324341
/// cleared when the program exits or after interpreter shutdown (when embedding), and so are

include/pybind11/detail/type_caster_base.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ PYBIND11_NOINLINE inline detail::type_info* get_type_info(PyTypeObject *type) {
160160
}
161161

162162
inline detail::type_info *get_local_type_info(const std::type_index &tp) {
163-
auto &locals = registered_local_types_cpp();
163+
auto &locals = get_local_internals().registered_types_cpp;
164164
auto it = locals.find(tp);
165165
if (it != locals.end())
166166
return it->second;

include/pybind11/pybind11.h

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@
5656

5757
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
5858

59+
PYBIND11_NAMESPACE_BEGIN(detail)
60+
61+
// Apply all the extensions translators from a list
62+
// Return true if one of the translators completed without raising an exception
63+
// itself. Return of false indicates that if there are other translators
64+
// available, they should be tried.
65+
inline bool apply_exception_translators(std::forward_list<ExceptionTranslator>& translators) {
66+
auto last_exception = std::current_exception();
67+
68+
for (auto &translator : translators) {
69+
try {
70+
translator(last_exception);
71+
return true;
72+
} catch (...) {
73+
last_exception = std::current_exception();
74+
}
75+
}
76+
return false;
77+
}
78+
79+
PYBIND11_NAMESPACE_END(detail)
80+
81+
5982
/// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object
6083
class cpp_function : public function {
6184
public:
@@ -550,6 +573,7 @@ class cpp_function : public function {
550573
}
551574
}
552575

576+
553577
/// Main dispatch logic for calls to functions bound using pybind11
554578
static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) {
555579
using namespace detail;
@@ -830,8 +854,12 @@ class cpp_function : public function {
830854
#endif
831855
} catch (...) {
832856
/* When an exception is caught, give each registered exception
833-
translator a chance to translate it to a Python exception
834-
in reverse order of registration.
857+
translator a chance to translate it to a Python exception. First
858+
all module-local translators will be tried in reverse order of
859+
registration. If none of the module-locale translators handle
860+
the exception (or there are no module-locale translators) then
861+
the global translators will be tried, also in reverse order of
862+
registration.
835863
836864
A translator may choose to do one of the following:
837865
@@ -840,17 +868,15 @@ class cpp_function : public function {
840868
- do nothing and let the exception fall through to the next translator, or
841869
- delegate translation to the next translator by throwing a new type of exception. */
842870

843-
auto last_exception = std::current_exception();
844-
auto &registered_exception_translators = get_internals().registered_exception_translators;
845-
for (auto& translator : registered_exception_translators) {
846-
try {
847-
translator(last_exception);
848-
} catch (...) {
849-
last_exception = std::current_exception();
850-
continue;
851-
}
871+
auto &local_exception_translators = get_local_internals().registered_exception_translators;
872+
if (detail::apply_exception_translators(local_exception_translators)) {
873+
return nullptr;
874+
}
875+
auto &exception_translators = get_internals().registered_exception_translators;
876+
if (detail::apply_exception_translators(exception_translators)) {
852877
return nullptr;
853878
}
879+
854880
PyErr_SetString(PyExc_SystemError, "Exception escaped from default exception translator!");
855881
return nullptr;
856882
}
@@ -951,6 +977,7 @@ class cpp_function : public function {
951977
}
952978
};
953979

980+
954981
/// Wrapper for Python extension modules
955982
class module_ : public object {
956983
public:
@@ -1124,7 +1151,7 @@ class generic_type : public object {
11241151
auto tindex = std::type_index(*rec.type);
11251152
tinfo->direct_conversions = &internals.direct_conversions[tindex];
11261153
if (rec.module_local)
1127-
registered_local_types_cpp()[tindex] = tinfo;
1154+
get_local_internals().registered_types_cpp[tindex] = tinfo;
11281155
else
11291156
internals.registered_types_cpp[tindex] = tinfo;
11301157
internals.registered_types_py[(PyTypeObject *) m_ptr] = { tinfo };
@@ -1310,7 +1337,7 @@ class class_ : public detail::generic_type {
13101337
generic_type::initialize(record);
13111338

13121339
if (has_alias) {
1313-
auto &instances = record.module_local ? registered_local_types_cpp() : get_internals().registered_types_cpp;
1340+
auto &instances = record.module_local ? get_local_internals().registered_types_cpp : get_internals().registered_types_cpp;
13141341
instances[std::type_index(typeid(type_alias))] = instances[std::type_index(typeid(type))];
13151342
}
13161343
}
@@ -2010,12 +2037,24 @@ template <typename InputType, typename OutputType> void implicitly_convertible()
20102037
pybind11_fail("implicitly_convertible: Unable to find type " + type_id<OutputType>());
20112038
}
20122039

2013-
template <typename ExceptionTranslator>
2014-
void register_exception_translator(ExceptionTranslator&& translator) {
2040+
2041+
inline void register_exception_translator(ExceptionTranslator &&translator) {
20152042
detail::get_internals().registered_exception_translators.push_front(
20162043
std::forward<ExceptionTranslator>(translator));
20172044
}
20182045

2046+
2047+
/**
2048+
* Add a new module-local exception translator. Locally registered functions
2049+
* will be tried before any globally registered exception translators, which
2050+
* will only be invoked if the module-local handlers do not deal with
2051+
* the exception.
2052+
*/
2053+
inline void register_local_exception_translator(ExceptionTranslator &&translator) {
2054+
detail::get_local_internals().registered_exception_translators.push_front(
2055+
std::forward<ExceptionTranslator>(translator));
2056+
}
2057+
20192058
/**
20202059
* Wrapper to generate a new Python exception type.
20212060
*
@@ -2049,22 +2088,20 @@ PYBIND11_NAMESPACE_BEGIN(detail)
20492088
// directly in register_exception, but that makes clang <3.5 segfault - issue #1349).
20502089
template <typename CppException>
20512090
exception<CppException> &get_exception_object() { static exception<CppException> ex; return ex; }
2052-
PYBIND11_NAMESPACE_END(detail)
20532091

2054-
/**
2055-
* Registers a Python exception in `m` of the given `name` and installs an exception translator to
2056-
* translate the C++ exception to the created Python exception using the exceptions what() method.
2057-
* This is intended for simple exception translations; for more complex translation, register the
2058-
* exception object and translator directly.
2059-
*/
2092+
// Helper function for register_exception and register_local_exception
20602093
template <typename CppException>
2061-
exception<CppException> &register_exception(handle scope,
2062-
const char *name,
2063-
handle base = PyExc_Exception) {
2094+
exception<CppException> &register_exception_impl(handle scope,
2095+
const char *name,
2096+
handle base,
2097+
bool isLocal) {
20642098
auto &ex = detail::get_exception_object<CppException>();
20652099
if (!ex) ex = exception<CppException>(scope, name, base);
20662100

2067-
register_exception_translator([](std::exception_ptr p) {
2101+
auto register_func = isLocal ? &register_local_exception_translator
2102+
: &register_exception_translator;
2103+
2104+
register_func([](std::exception_ptr p) {
20682105
if (!p) return;
20692106
try {
20702107
std::rethrow_exception(p);
@@ -2075,6 +2112,36 @@ exception<CppException> &register_exception(handle scope,
20752112
return ex;
20762113
}
20772114

2115+
PYBIND11_NAMESPACE_END(detail)
2116+
2117+
/**
2118+
* Registers a Python exception in `m` of the given `name` and installs a translator to
2119+
* translate the C++ exception to the created Python exception using the what() method.
2120+
* This is intended for simple exception translations; for more complex translation, register the
2121+
* exception object and translator directly.
2122+
*/
2123+
template <typename CppException>
2124+
exception<CppException> &register_exception(handle scope,
2125+
const char *name,
2126+
handle base = PyExc_Exception) {
2127+
return detail::register_exception_impl<CppException>(scope, name, base, false /* isLocal */);
2128+
}
2129+
2130+
/**
2131+
* Registers a Python exception in `m` of the given `name` and installs a translator to
2132+
* translate the C++ exception to the created Python exception using the what() method.
2133+
* This translator will only be used for exceptions that are thrown in this module and will be
2134+
* tried before global exception translators, including those registered with register_exception.
2135+
* This is intended for simple exception translations; for more complex translation, register the
2136+
* exception object and translator directly.
2137+
*/
2138+
template <typename CppException>
2139+
exception<CppException> &register_local_exception(handle scope,
2140+
const char *name,
2141+
handle base = PyExc_Exception) {
2142+
return detail::register_exception_impl<CppException>(scope, name, base, true /* isLocal */);
2143+
}
2144+
20782145
PYBIND11_NAMESPACE_BEGIN(detail)
20792146
PYBIND11_NOINLINE inline void print(const tuple &args, const dict &kwargs) {
20802147
auto strings = tuple(args.size());

0 commit comments

Comments
 (0)