-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Feature/local exception translator #2650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/local exception translator #2650
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is an important feature and the implementation looks reasonable to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I general, I'm happy with this (but I was also involved in the Gitter discussion, so I'm not entirely well-placed to judge this).
This is also one of the things @wjakob should probably have a look at? Does it make sense to always check module-local exception translators before the global, shared ones?
We're still lacking changes to the actual documentation, by the way. A note about the existence of module-local exception/exception translators, and stating the order in which they are resolved is important.
Also, what about the auxiliary function register_exception
? Should it get an extra argument, or somehow use the py::module_local()
tag, or an extra template argument, or a sibling function register_module_local_exception
, or ... ?
include/pybind11/detail/internals.h
Outdated
/// Works like `internals.registered_types_cpp`, but for module-local registered types: | ||
inline type_map<type_info *> ®istered_local_types_cpp() { | ||
static type_map<type_info *> locals{}; | ||
return locals; | ||
return get_module_internals().registered_local_types_cpp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how other parts of pybind11 do it, but you do have a duplicate use of registered_local_types_cpp
here (field as well as the function name). Can you check what conventions other parts of internals.h
follow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I overcame my laziness and looked up myself. Can we go for get_registered_local_types_cpp
as function name, for example? I don't think this should break ABI things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you prefer me to rename this function or just replace usages of it with get_module_internals().registered_local_types_cpp;
so it works more like get_internals?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or would that break ABI? I don't think this function should be ever used in user code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matching get_internals()
closer might be nice, indeed, yes, if you ask me :-)
And no, this shouldn't break user code (since it's in the detail
namespace), and ... yeah, no, the whole point is that this isn't shared between modules, so there's no shared ABI :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have updated this to be called get_local_internals()
, deleted get_registered_local_types_cpp()
completely, and changed the usage to be more like get_internals()
. Do we want to leave the double local
in the names or match the member names to the internals
member names?
currently:
get_local_internals().registered_local_types_cpp;
proposed change to remove the local in the member name
get_local_internals().registered_types_cpp;
include/pybind11/detail/internals.h
Outdated
@@ -311,12 +311,23 @@ PYBIND11_NOINLINE inline internals &get_internals() { | |||
return **internals_pp; | |||
} | |||
|
|||
|
|||
struct module_internals { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can/should we make this module_local_internals
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same for get_module_local_internals
, then, if we do)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd do either local_internals
or module_internals
to contrast with the normal internals
. I don't see a reason to give it all the names. but if that is what people want, I'll do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of "module local" being a concept, but it's true it also wasn't done for registered_local_types_cpp
. Maybe matching local
is more important than module
, though?
Let's see what others say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently went with local_internals
and get_local_internals
. obviously easy to change depending on what everyone wants
Documentation has also been updated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. If this gets one or two more reviews, I think it can be merged :-)
include/pybind11/cast.h
Outdated
@@ -171,7 +171,7 @@ PYBIND11_NOINLINE inline detail::type_info* get_type_info(PyTypeObject *type) { | |||
} | |||
|
|||
inline detail::type_info *get_local_type_info(const std::type_index &tp) { | |||
auto &locals = registered_local_types_cpp(); | |||
auto &locals = get_local_internals().registered_local_types_cpp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with this, given it's 1) readable, and 2) matching the non-module-local internals. The suggested rename of registered_local_types_cpp
to registered_types_cpp
would even make sense to me, to avoid having two times local
.
Let's check with other reviewers, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay. My preference is to remove the second local too. Hopefully other people agree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rwgk any thoughts on the naming in the local_internals
struct?
struct local_internals {
type_map<type_info *> registered_local_types_cpp;
std::forward_list<void (*) (std::exception_ptr)> registered_local_exception_translators;
};
vs
struct local_internals {
type_map<type_info *> registered_types_cpp;
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
};
include/pybind11/pybind11.h
Outdated
auto & registered_local_exception_translators = get_local_internals().registered_local_exception_translators; | ||
for (auto& translator : registered_local_exception_translators) { | ||
try { | ||
translator(last_exception); | ||
} catch (...) { | ||
last_exception = std::current_exception(); | ||
continue; | ||
} | ||
return nullptr; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is slightly repetitive, but fixing it might be more annoying than keeping the code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sort of thing that chain
from python is perfect for but doesn't seem to be std lib version of it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is too much duplication, and I assume it's easy to move this to a helper function. Could you please try that? (Even if the number of lines of code is similar in the end, it's worth it IMO. Duplication easily leads to accidents when working on maintenance changes.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this duplication has been removed and made into a helper. Let me know if you don't like something about my implementation.
* exception object and translator directly. | ||
*/ | ||
template <typename CppException> | ||
exception<CppException> ®ister_local_exception(handle scope, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other reviewers: should this be merged with register_exception
? Answer: probably.
How do we best do this? An auxiliary helper with an extra runtime or template argument, and keep the current API with 2 functions? Or do we also want to merge these two functions in the API (in a backwards-compatible way, with a default argument at the end of the argument list or template arguments) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely, this is too much duplication.
I think having two functions (register_exception
, register_local_exception
) is fine, but supported by detail::register_exception_impl
or similar.
I'd try hard if necessary. This kind of duplication isn't healthy.
5498fe8
to
eec4e1b
Compare
eec4e1b
to
8e4d62d
Compare
8e4d62d
to
9f07a85
Compare
59b35a7
to
c0cfc66
Compare
Thanks @Skylion007 for pointing out this PR, I missed it before. I didn't look at all details, but I see others have already. My thinking: @jesse-sony if you rebase and resolve merge conflicts, I'll run the refreshed version through the Google-global testing (includes testing a large number of open source packages) asap, if that checks out I'll merge. |
Since we now have two things that are going to be module local, it felt correct to add a struct to manage them.
These are added via the register_local_exception_translator function and are then applied before the global translators
Rename it to get_registered_local_types_cpp() to disambiguate from the new member of module_internals
c0cfc66
to
2f2bd18
Compare
@rwgk Branch has been rebased and pushed. Should be in a good state to merge |
Thanks! My testing will take a day or so. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Google-global testing came back green, feature-wise this PR looks great to me, but the code duplication is a concern. I think it's important to go the extra mile for best practices straightaway. Usually such things don't get fixed later but rather tend to get duplicated even more (slippery slope).
docs/advanced/exceptions.rst
Outdated
Local vs Global Exception Translators | ||
===================================== | ||
|
||
Similar to how the ``py::module_local`` flag allows uesrs to limit a bound class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This first sentence seems misplaced and the grammar incomplete. I think it reads better if you simply remove it here. The section title provides sufficient context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
docs/advanced/exceptions.rst
Outdated
consistent error handling behavior is needed, then local translators should be | ||
used. | ||
|
||
Changing the previous example to use register_local_exception_translator would |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double backquotes (code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
docs/advanced/exceptions.rst
Outdated
mean that when invalid_argument is thrown in the module2 code, the module2 | ||
translator will always handle it, while in module1, the module1 translator will | ||
do the same. | ||
>>>>>>> 854aa95a (Update documentation for new local exception feature) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accident resolving merge conflict?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops
|
||
/// Works like `get_internals`, but for things which are locally registered. | ||
inline local_internals &get_local_internals() { | ||
static local_internals locals; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you think through the consequences of mixing extensions built with e.g. pybind11 2.7.0 and 2.7.1 (assuming it includes this change)?
At first sight it seems fine to me, but this is the kind of thing with devils in the details. Ideally, could you please add the rationale to the PR description (bottom) why it is safe to not bump the PYBIND11_INTERNALS_VERSION
even though the locals
type here changes, to document that we thought about it carefully?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thought process on this (and I'm definitely not an expert in all the intricacies of pybind11 ABI compatibility) is that the internals struct in pybind11 is shared between all the modules. Any changes to that definitely requires a lot of careful thought about whether backwards compatibility is being broken. The local_internals
that I added here should, by design, be segregated to a single module. Whether a module has local internals or not should not impact any other modules, because the only things accessing the local internals is the module that contains them.
If this makes sense to you as well, I'll happily add a comment to the end of this PR explaining that thought process
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make total sense to me, but I don't consider myself an authority on this, too. Adding this to the PR description is to expose it, in case someone is able to poke a hole into the rationale, we want to know asap.
include/pybind11/pybind11.h
Outdated
auto & registered_local_exception_translators = get_local_internals().registered_local_exception_translators; | ||
for (auto& translator : registered_local_exception_translators) { | ||
try { | ||
translator(last_exception); | ||
} catch (...) { | ||
last_exception = std::current_exception(); | ||
continue; | ||
} | ||
return nullptr; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is too much duplication, and I assume it's easy to move this to a helper function. Could you please try that? (Even if the number of lines of code is similar in the end, it's worth it IMO. Duplication easily leads to accidents when working on maintenance changes.)
include/pybind11/pybind11.h
Outdated
/** | ||
* Add a new module-local exception translator. Locally registered functions | ||
* will be tried before any globally registered exception translators, which | ||
* will only be invoked if the moduke-local handlers do not deal with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo (strange that codespell didn't get this one!?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
include/pybind11/pybind11.h
Outdated
* the exception. | ||
*/ | ||
template <typename ExceptionTranslator> | ||
void register_local_exception_translator(ExceptionTranslator&& translator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe, unfortunately this duplicates something that's not great in the existing code: there is zero wiggle room for what ExceptionTranslator
can be: it has to be void (*) (std::exception_ptr)
exactly. I actually tried it out recently (https://github.com/pybind/pybind11/pull/3095/files) but didn't apply it to master. However, now that we get into it here, I think it's best to fold in the simplification, and to change the existing function and this new function to be simple (not template) inline
functions with void (*translator) (std::exception_ptr)
as the argument. Could you please give that a try? But please don't hesitate to push back if you run into trouble or if you think I'm overlooking something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I did this. I did not change any of the type information, because I'm pretty sure that would be a PYBIND11_INTERNALS_VERSION ABI breaking change. internals
still has
std::forward_list<ExceptionTranslator> registered_exception_translators;
not
std::forward_list<const ExceptionTranslator> registered_exception_translators;
as it did in your PR.
But I removed all the templating from the register functions. I also turned ExceptionTranslator
into a using
alias, which makes the typing a little bit nicer to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, perfect, thanks. I did not mean to suggest adding the const. My PR was just to record that I thought about it carefully and then discarded the idea because I think it's too disruptive for too little gain.
include/pybind11/pybind11.h
Outdated
@@ -2085,6 +2114,32 @@ exception<CppException> ®ister_exception(handle scope, | |||
return ex; | |||
} | |||
|
|||
/** | |||
* Registers a Python exception in `m` of the given `name` and installs an exception translator to | |||
* translate the C++ exception to the created Python exception using the exceptions what() method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete exceptions
(to not repeat that word too much in one sentence)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
* exception object and translator directly. | ||
*/ | ||
template <typename CppException> | ||
exception<CppException> ®ister_local_exception(handle scope, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely, this is too much duplication.
I think having two functions (register_exception
, register_local_exception
) is fine, but supported by detail::register_exception_impl
or similar.
I'd try hard if necessary. This kind of duplication isn't healthy.
Separated out the exception processing into a standalone function in the details namespace. Clean-up some comments as per PR notes as well
But I added a using declaration to alias the type.
Hi @jesse-sony, today I only got a chance to glance through your comments really quickly, not even the code. The comments I saw look good, thanks!. I'll come back here asap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome, thanks!
I'm still struggling a bit navigating this review system. If I overlooked a question you asked, please tag me again. (I tried to tie all my responses to the Review.)
I'm holding off merging until you make your final decision about the local_internals naming. Just let me know.
|
||
/// Works like `get_internals`, but for things which are locally registered. | ||
inline local_internals &get_local_internals() { | ||
static local_internals locals; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make total sense to me, but I don't consider myself an authority on this, too. Adding this to the PR description is to expose it, in case someone is able to poke a hole into the rationale, we want to know asap.
include/pybind11/pybind11.h
Outdated
* the exception. | ||
*/ | ||
template <typename ExceptionTranslator> | ||
void register_local_exception_translator(ExceptionTranslator&& translator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, perfect, thanks. I did not mean to suggest adding the const. My PR was just to record that I thought about it carefully and then discarded the idea because I think it's too disruptive for too little gain.
include/pybind11/detail/internals.h
Outdated
return locals; | ||
|
||
struct local_internals { | ||
type_map<type_info *> registered_local_types_cpp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answering the question you asked in a different comment:
I'd prefer the 2nd alternative you pointed out there, with local
not repeated inside the struct.
But I'd approve either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Names have been changed, since everyone seems to prefer them without the local
@rwgk Cleaned up the naming, updated the PR description with a note about why it does not break ABI compatability with the internals struct, and also add some notes in the internals.h file about the same ABI stuff. Happy to make other changes if there is more to be done, but I think I've hit everything mentioned so far. |
The one failing CI job is a flake:
Safe to ignore. |
Thanks @jesse-sony for the change, @rhaschke, @YannickJadoul for the reviews, and @Skylion007 for pointing out this PR as one we should look at again. I'll merge this now. |
Description
Allow exception translators to be optionally registered local to a module instead of applying globally across all pybind11 modules.
This is useful in a situation where there are more than one compiled modules (e.g., clients for different micro-services) that share a lot of the same core code on the c++ side, including exceptions. Shared classes can be handled via the py::module_local() tag on the classes, but there is currently no way to deal with exception remapping.
So in code like:
The exception translator is global, so which module's exception you get back is determined by import order.
As an example, if you did something like
you get back an exception from the tempo module.
However, if you were to do
you'd get back an exception from the rhapsody module. Since those are different types in python (but the same type in c++), it means catch statements which are expecting the exception to come from the same module as the call are failing.
Suggested changelog entry:
Why does this not require an Internals Version bump?
The internals struct in pybind11 is shared between all the modules. Any changes to that definitely requires a lot of careful thought about whether backwards compatibility is being broken. The local_internals that are added here should, by design, be segregated to a single module. Whether a module has local internals or not should not impact any other modules, because the only things accessing the local internals is the module that contains them.