Skip to content

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

Merged
merged 2 commits into from
Apr 2, 2017
Merged

Conversation

dean0x7d
Copy link
Member

@dean0x7d dean0x7d commented Mar 16, 2017

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:

m.def("foo", [](Args&&... args) {
    T guard{};
    return foo(std::forward<Args>(args)...);
});

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.

@dean0x7d dean0x7d added this to the v2.2 milestone Mar 16, 2017
@jagerman
Copy link
Member

Nice approach, I like it!

@wjakob
Copy link
Member

wjakob commented Mar 16, 2017

Cool, I like it as well!

@dean0x7d dean0x7d changed the title WIP: Add a scope guard call policy Add a scope guard call policy Mar 25, 2017
@dean0x7d
Copy link
Member Author

dean0x7d commented Mar 25, 2017

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.

@wjakob
Copy link
Member

wjakob commented Mar 25, 2017

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 keep_alive policies). I'm wondering is there is a more general implementation of the extract_call_guard_t interface that would allow this to work?

@wjakob
Copy link
Member

wjakob commented Mar 25, 2017

(This could be something like a filter over Extras that assembles a std::tuple with all of the call guards)

@dean0x7d
Copy link
Member Author

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 std::tuple or any parameter pack based implementation is viable. I had considered it originally, but the issue is that the construction and destruction order of tuple elements is not specified. Same goes for argument initialization order in a function call (if multiple guards were expanded where there is currently one). This could yield some surprises if users assume that call_guard<T1>, call_guard<T2> will follow that order but it's actually random depending on the platform (with pretty nasty results, e.g. if T2 releases the GIL, but T1 uses the CPython API).

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 static_assert to ensure only a simple call_guard is allowed.

@dean0x7d
Copy link
Member Author

Thinking about it a bit more, (ab)using std::initializer_list would work with well-defined guard order. But I think the implementation is on the ugly side:

Return call_impl(Func &&f, index_sequence<Is...>, std::initializer_list<int>);
call_impl<Return>(std::forward<Func>(f), indices{}, {((void) Guard{}, 0)...});

@jagerman
Copy link
Member

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?

@jagerman
Copy link
Member

jagerman commented Mar 25, 2017

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.

/// 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 { };
Copy link
Member

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.

Copy link
Member Author

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.

@dean0x7d
Copy link
Member Author

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?

Temporary objects are cleaned up at the end of the full expression in which they appear -- essentially at ;. This is what makes nested and chained calls work in C++ (reference lifetime extension doesn't propagate through function returns).

That being said, the init list is still ugly and I don't like. Your call_guardian idea is much better!

The only question I have now: call_guard<T1, T2> or call_guard<T1>, call_guard<T2>? The former is trivial to implement and it would make a terser syntax. The latter is more versatile since additional guards can be added at a later stage, but it does require passing Extra... through a filter metafunction. I assume the more versatile one is more desirable?

@wjakob
Copy link
Member

wjakob commented Mar 28, 2017

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.

@jagerman
Copy link
Member

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.

@dean0x7d
Copy link
Member Author

dean0x7d commented Mar 29, 2017

OK, I've implemented the call_guard<T1, T2, ...> version and replaced first_of_t with exactly_one_t (implementation in terms of the existing constexpr functions + pack_element which may be useful for other things as well).

@jagerman
Copy link
Member

jagerman commented Apr 1, 2017

I can definitely use pack_element in #762; right now it uses std::tuple_element<N, std::tuple<...>>, but since I don't actually need the tuple, that seemed rather heavy.

Copy link
Member

@jagerman jagerman left a 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` */
Copy link
Member

@jagerman jagerman Apr 1, 2017

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<>`

Copy link
Member

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.

Copy link
Member Author

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.

@@ -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)
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks.

dean0x7d added 2 commits April 2, 2017 22:48
```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
});
```
@dean0x7d
Copy link
Member Author

dean0x7d commented Apr 2, 2017

Fixed the comment and squashed the commits. If there's nothing else, I'll merge this after the CI tests are done.

@wjakob
Copy link
Member

wjakob commented Apr 2, 2017

This looks great, please go ahead!

@dean0x7d dean0x7d merged commit 82ece94 into pybind:master Apr 2, 2017
@dean0x7d dean0x7d deleted the call_guard branch April 5, 2017 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants