-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add a scope guard call policy #740
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
Conversation
Nice approach, I like it! |
Cool, I like it as well! |
OK, the documentation is in place. Feel free to review. The only thing I couldn't find out is how to check the GIL state with PyPy (it doesn't seem to be supported via the C API) so that test is skipped there, but the generic guard test covers the same functionality. |
This looks great, the documentation is very clear. One potential area for improvement: I think it's only a matter of time until people will try to mix multiple call guards for a single bound function (just like one can specify multiple |
(This could be something like a filter over |
That's a good point about multiple guards. I forgot to document how that would work with the current implementation. Unfortunately, I don't think that a A solution with well-defined order would be for users to create a simple composite guard: struct CompositeGuard {
T1 guard1;
T2 guard2;
}; So perhaps just add that to the documentation + a |
Thinking about it a bit more, (ab)using Return call_impl(Func &&f, index_sequence<Is...>, std::initializer_list<int>); call_impl<Return>(std::forward<Func>(f), indices{}, {((void) Guard{}, 0)...}); |
I have some trouble seeing why that would guarantee the guards aren't destroyed until the call is finished; wouldn't it be valid for a compiler to destroy them after evaluating the initializer list? |
Something like: template <typename Guard, typename...> struct call_guardian { Guard guard; };
template <typename Guard1, typename Guard2, typename... Guards>
struct call_guardian<Guard1, Guard2, Guards...> {
Guard1 guard;
call_guardian<Guard2, Guards...> next;
}; ought to be enough to guarantee FILO guard construction/destruction order. |
include/pybind11/common.h
Outdated
/// Check if T is an instantiation of the template `Class`. For example: | ||
/// `is_<std::shared_ptr, T>` is true if `T == std::shared_ptr<U>` where U can be anything. | ||
template <template<typename...> class Class, typename T> struct is_ : std::false_type { }; | ||
template <template<typename...> class C, typename... Us> struct is_<C, C<Us...>> : std::true_type { }; |
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.
Perhaps rename this to is_specialization
? is_
isn't immediately obvious.
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.
True, renamed. I kind of liked the naming as a prefix of some kind, e.g. is_call_guard = is_<call_guard, T>
, but a proper name is more useful.
Temporary objects are cleaned up at the end of the full expression in which they appear -- essentially at That being said, the init list is still ugly and I don't like. Your The only question I have now: |
Both are fine with me. It sounds like the first variant is easier to implement and faster to compile, so I'd lean towards that one. |
I thought I commented on that, but apparently not: I like the first variant more as well, unless there's some compelling alternative use you have in mind for extra parameters. |
OK, I've implemented the |
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, aside from a couple minor comment updates.
@@ -143,8 +143,11 @@ class cpp_function : public function { | |||
/* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ | |||
const auto policy = detail::return_value_policy_override<Return>::policy(call.func.policy); | |||
|
|||
/* Function scope guard -- defaults to the compile-to-nothing `void_type` */ |
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 `void_type`
should be `call_guard<>`
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.
Scratch that, I see now: it's going to be call_guard<>::type
, which is the void_type
.
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.
Yeah, the 0 and 1 argument versions just return the type directly since those are the most common cases. The additional wrappers start with 2 arguments and above.
tests/test_call_policies.cpp
Outdated
@@ -0,0 +1,92 @@ | |||
/* | |||
tests/test_keep_alive.cpp -- keep_alive modifier (pybind11's version | |||
of Boost.Python's with_custodian_and_ward / with_custodian_and_ward_postcall) |
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 comment filename and description are obsolete now. Perhaps:
tests/test_call_policies.cpp -- keep_alive (pybind11's version of Boost.Python's
with_custodian_and_ward / with_custodian_and_ward_postcall) and call_guard
examples and tests.
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, thanks.
```c++ m.def("foo", foo, py::call_guard<T>()); ``` is equivalent to: ```c++ m.def("foo", [](args...) { T scope_guard; return foo(args...); // forwarded arguments }); ```
Fixed the comment and squashed the commits. If there's nothing else, I'll merge this after the CI tests are done. |
This looks great, please go ahead! |
This is a proposed solution to #625 + a bit of generalization to allow any scope guard. The basic idea is that the following:
m.def("foo", foo, py::call_guard<T>());
is equivalent to:
To release the GIL, one would pass
py::call_guard<py::gil_scoped_release>()
(could also be aliased to something shorter). In general, any default-constructible RAII class could be used.Implementation
It compiles to nothing when the guard isn't used so there's no binary size or runtime overhead. It mainly uses the type system and integrates with the existing wrapper created for all functions.
An alternative would be to create a
cpp_function
-like wrapper as proposed in #625 and there are definitely pros and cons to both approaches. The main reason for submitting this PR is that the implementation uses minimal code compared to actual function wrapping and argument forwarding.