Skip to content

Commit ca5d914

Browse files
committed
Add a scope guard call policy
```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 }); ```
1 parent 5b50376 commit ca5d914

File tree

10 files changed

+218
-60
lines changed

10 files changed

+218
-60
lines changed

docs/advanced/functions.rst

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,16 @@ Additional call policies
162162
========================
163163

164164
In addition to the above return value policies, further *call policies* can be
165-
specified to indicate dependencies between parameters. In general, call policies
166-
are required when the C++ object is any kind of container and another object is being
167-
added to the container.
168-
169-
There is currently just
170-
one policy named ``keep_alive<Nurse, Patient>``, which indicates that the
171-
argument with index ``Patient`` should be kept alive at least until the
172-
argument with index ``Nurse`` is freed by the garbage collector. Argument
165+
specified to indicate dependencies between parameters or ensure a certain state
166+
for the function call.
167+
168+
Keep alive
169+
----------
170+
171+
In general, this policy is required when the C++ object is any kind of container
172+
and another object is being added to the container. ``keep_alive<Nurse, Patient>``
173+
indicates that the argument with index ``Patient`` should be kept alive at least
174+
until the argument with index ``Nurse`` is freed by the garbage collector. Argument
173175
indices start at one, while zero refers to the return value. For methods, index
174176
``1`` refers to the implicit ``this`` pointer, while regular arguments begin at
175177
index ``2``. Arbitrarily many call policies can be specified. When a ``Nurse``
@@ -194,10 +196,36 @@ container:
194196
Patient != 0) and ``with_custodian_and_ward_postcall`` (if Nurse/Patient ==
195197
0) policies from Boost.Python.
196198

199+
Call guard
200+
----------
201+
202+
The ``call_guard<T>`` policy allows any scope guard type ``T`` to be placed
203+
around the function call. For example, this definition:
204+
205+
.. code-block:: cpp
206+
207+
m.def("foo", foo, py::call_guard<T>());
208+
209+
is equivalent to the following pseudocode:
210+
211+
.. code-block:: cpp
212+
213+
m.def("foo", [](args...) {
214+
T scope_guard;
215+
return foo(args...); // forwarded arguments
216+
});
217+
218+
The only requirement is that ``T`` is default-constructible, but otherwise any
219+
scope guard will work. This is very useful in combination with `gil_scoped_release`.
220+
See :ref:`gil`.
221+
222+
Multiple guards can also be specified as ``py::call_guard<T1, T2, T3...>``. The
223+
constructor order is left to right and destruction happens in reverse.
224+
197225
.. seealso::
198226

199-
The file :file:`tests/test_keep_alive.cpp` contains a complete example
200-
that demonstrates using :class:`keep_alive` in more detail.
227+
The file :file:`tests/test_call_policies.cpp` contains a complete example
228+
that demonstrates using `keep_alive` and `call_guard` in more detail.
201229

202230
.. _python_objects_as_args:
203231

docs/advanced/misc.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ T2>, myFunc)``. In this case, the preprocessor assumes that the comma indicates
1515
the beginning of the next parameter. Use a ``typedef`` to bind the template to
1616
another name and use it in the macro to avoid this problem.
1717

18+
.. _gil:
1819

1920
Global Interpreter Lock (GIL)
2021
=============================
@@ -68,6 +69,13 @@ could be realized as follows (important changes highlighted):
6869
return m.ptr();
6970
}
7071
72+
The ``call_go`` wrapper can also be simplified using the `call_guard` policy
73+
(see :ref:`call_policies`) which yields the same result:
74+
75+
.. code-block:: cpp
76+
77+
m.def("call_go", &call_go, py::call_guard<py::gil_scoped_release>());
78+
7179
7280
Binding sequence data types, iterators, the slicing protocol, etc.
7381
==================================================================

include/pybind11/attr.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,44 @@ struct metaclass {
6767
/// Annotation to mark enums as an arithmetic type
6868
struct arithmetic { };
6969

70+
/** \rst
71+
A call policy which places one or more guard variables (``Ts...``) around the function call.
72+
73+
For example, this definition:
74+
75+
.. code-block:: cpp
76+
77+
m.def("foo", foo, py::call_guard<T>());
78+
79+
is equivalent to the following pseudocode:
80+
81+
.. code-block:: cpp
82+
83+
m.def("foo", [](args...) {
84+
T scope_guard;
85+
return foo(args...); // forwarded arguments
86+
});
87+
\endrst */
88+
template <typename... Ts> struct call_guard;
89+
90+
template <> struct call_guard<> { using type = detail::void_type; };
91+
92+
template <typename T>
93+
struct call_guard<T> {
94+
static_assert(std::is_default_constructible<T>::value,
95+
"The guard type must be default constructible");
96+
97+
using type = T;
98+
};
99+
100+
template <typename T, typename... Ts>
101+
struct call_guard<T, Ts...> {
102+
struct type {
103+
T guard{}; // Compose multiple guard types with left-to-right default-constructor order
104+
typename call_guard<Ts...>::type next{};
105+
};
106+
};
107+
70108
/// @} annotations
71109

72110
NAMESPACE_BEGIN(detail)
@@ -374,6 +412,9 @@ struct process_attribute<metaclass> : process_attribute_default<metaclass> {
374412
template <>
375413
struct process_attribute<arithmetic> : process_attribute_default<arithmetic> {};
376414

415+
template <typename... Ts>
416+
struct process_attribute<call_guard<Ts...>> : process_attribute_default<call_guard<Ts...>> { };
417+
377418
/***
378419
* Process a keep_alive call policy -- invokes keep_alive_impl during the
379420
* pre-call handler if both Nurse, Patient != 0 and use the post-call handler
@@ -410,6 +451,13 @@ template <typename... Args> struct process_attributes {
410451
}
411452
};
412453

454+
template <typename T>
455+
using is_call_guard = is_instantiation<call_guard, T>;
456+
457+
/// Extract the ``type`` from the first `call_guard` in `Extras...` (or `void_type` if none found)
458+
template <typename... Extra>
459+
using extract_guard_t = typename first_of_t<is_call_guard, call_guard<>, Extra...>::type;
460+
413461
/// Check the number of named arguments at compile time
414462
template <typename... Extra,
415463
size_t named = constexpr_sum(std::is_base_of<arg, Extra>::value...),

include/pybind11/cast.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,14 +1383,14 @@ class argument_loader {
13831383
return load_impl_sequence(call, indices{});
13841384
}
13851385

1386-
template <typename Return, typename Func>
1386+
template <typename Return, typename Guard, typename Func>
13871387
enable_if_t<!std::is_void<Return>::value, Return> call(Func &&f) {
1388-
return call_impl<Return>(std::forward<Func>(f), indices{});
1388+
return call_impl<Return>(std::forward<Func>(f), indices{}, Guard{});
13891389
}
13901390

1391-
template <typename Return, typename Func>
1391+
template <typename Return, typename Guard, typename Func>
13921392
enable_if_t<std::is_void<Return>::value, void_type> call(Func &&f) {
1393-
call_impl<Return>(std::forward<Func>(f), indices{});
1393+
call_impl<Return>(std::forward<Func>(f), indices{}, Guard{});
13941394
return void_type();
13951395
}
13961396

@@ -1406,8 +1406,8 @@ class argument_loader {
14061406
return true;
14071407
}
14081408

1409-
template <typename Return, typename Func, size_t... Is>
1410-
Return call_impl(Func &&f, index_sequence<Is...>) {
1409+
template <typename Return, typename Func, size_t... Is, typename Guard>
1410+
Return call_impl(Func &&f, index_sequence<Is...>, Guard &&) {
14111411
return std::forward<Func>(f)(cast_op<Args>(std::get<Is>(value))...);
14121412
}
14131413

include/pybind11/common.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,15 @@ using is_template_base_of = decltype(is_template_base_of_impl<Base>::check((remo
536536
struct is_template_base_of : decltype(is_template_base_of_impl<Base>::check((remove_cv_t<T>*)nullptr)) { };
537537
#endif
538538

539+
/// Check if T is an instantiation of the template `Class`. For example:
540+
/// `is_instantiation<shared_ptr, T>` is true if `T == shared_ptr<U>` where U can be anything.
541+
template <template<typename...> class Class, typename T>
542+
struct is_instantiation : std::false_type { };
543+
template <template<typename...> class Class, typename... Us>
544+
struct is_instantiation<Class, Class<Us...>> : std::true_type { };
545+
539546
/// Check if T is std::shared_ptr<U> where U can be anything
540-
template <typename T> struct is_shared_ptr : std::false_type { };
541-
template <typename U> struct is_shared_ptr<std::shared_ptr<U>> : std::true_type { };
547+
template <typename T> using is_shared_ptr = is_instantiation<std::shared_ptr, T>;
542548

543549
/// Ignore that a variable is unused in compiler warnings
544550
inline void ignore_unused(const int *) { }

include/pybind11/pybind11.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,11 @@ class cpp_function : public function {
143143
/* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */
144144
const auto policy = detail::return_value_policy_override<Return>::policy(call.func.policy);
145145

146+
/* Function scope guard -- defaults to the compile-to-nothing `void_type` */
147+
using Guard = detail::extract_guard_t<Extra...>;
148+
146149
/* Perform the function call */
147-
handle result = cast_out::cast(args_converter.template call<Return>(cap->f),
150+
handle result = cast_out::cast(args_converter.template call<Return, Guard>(cap->f),
148151
policy, call.parent);
149152

150153
/* Invoke call policy post-call hook */

tests/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ endif()
2828
set(PYBIND11_TEST_FILES
2929
test_alias_initialization.cpp
3030
test_buffers.cpp
31+
test_call_policies.cpp
3132
test_callbacks.cpp
3233
test_chrono.cpp
3334
test_class_args.cpp
@@ -40,7 +41,6 @@ set(PYBIND11_TEST_FILES
4041
test_exceptions.cpp
4142
test_inheritance.cpp
4243
test_issues.cpp
43-
test_keep_alive.cpp
4444
test_kwargs_and_defaults.cpp
4545
test_methods_and_attributes.cpp
4646
test_modules.cpp

tests/test_call_policies.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
tests/test_call_policies.cpp -- keep_alive and call_guard
3+
4+
Copyright (c) 2016 Wenzel Jakob <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#include "pybind11_tests.h"
11+
12+
class Child {
13+
public:
14+
Child() { py::print("Allocating child."); }
15+
~Child() { py::print("Releasing child."); }
16+
};
17+
18+
class Parent {
19+
public:
20+
Parent() { py::print("Allocating parent."); }
21+
~Parent() { py::print("Releasing parent."); }
22+
void addChild(Child *) { }
23+
Child *returnChild() { return new Child(); }
24+
Child *returnNullChild() { return nullptr; }
25+
};
26+
27+
test_initializer keep_alive([](py::module &m) {
28+
py::class_<Parent>(m, "Parent")
29+
.def(py::init<>())
30+
.def("addChild", &Parent::addChild)
31+
.def("addChildKeepAlive", &Parent::addChild, py::keep_alive<1, 2>())
32+
.def("returnChild", &Parent::returnChild)
33+
.def("returnChildKeepAlive", &Parent::returnChild, py::keep_alive<1, 0>())
34+
.def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
35+
.def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());
36+
37+
py::class_<Child>(m, "Child")
38+
.def(py::init<>());
39+
});
40+
41+
struct CustomGuard {
42+
static bool enabled;
43+
44+
CustomGuard() { enabled = true; }
45+
~CustomGuard() { enabled = false; }
46+
47+
static const char *report_status() { return enabled ? "guarded" : "unguarded"; }
48+
};
49+
50+
bool CustomGuard::enabled = false;
51+
52+
struct DependentGuard {
53+
static bool enabled;
54+
55+
DependentGuard() { enabled = CustomGuard::enabled; }
56+
~DependentGuard() { enabled = false; }
57+
58+
static const char *report_status() { return enabled ? "guarded" : "unguarded"; }
59+
};
60+
61+
bool DependentGuard::enabled = false;
62+
63+
test_initializer call_guard([](py::module &pm) {
64+
auto m = pm.def_submodule("call_policies");
65+
66+
m.def("unguarded_call", &CustomGuard::report_status);
67+
m.def("guarded_call", &CustomGuard::report_status, py::call_guard<CustomGuard>());
68+
69+
m.def("multiple_guards_correct_order", []() {
70+
return CustomGuard::report_status() + std::string(" & ") + DependentGuard::report_status();
71+
}, py::call_guard<CustomGuard, DependentGuard>());
72+
73+
m.def("multiple_guards_wrong_order", []() {
74+
return DependentGuard::report_status() + std::string(" & ") + CustomGuard::report_status();
75+
}, py::call_guard<DependentGuard, CustomGuard>());
76+
77+
#if defined(WITH_THREAD) && !defined(PYPY_VERSION)
78+
// `py::call_guard<py::gil_scoped_release>()` should work in PyPy as well,
79+
// but it's unclear how to test it without `PyGILState_GetThisThreadState`.
80+
auto report_gil_status = []() {
81+
auto is_gil_held = false;
82+
if (auto tstate = py::detail::get_thread_state_unchecked())
83+
is_gil_held = (tstate == PyGILState_GetThisThreadState());
84+
85+
return is_gil_held ? "GIL held" : "GIL released";
86+
};
87+
88+
m.def("with_gil", report_gil_status);
89+
m.def("without_gil", report_gil_status, py::call_guard<py::gil_scoped_release>());
90+
#endif
91+
});

tests/test_keep_alive.py renamed to tests/test_call_policies.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,17 @@ def test_return_none(capture):
9595
del p
9696
pytest.gc_collect()
9797
assert capture == "Releasing parent."
98+
99+
100+
def test_call_guard():
101+
from pybind11_tests import call_policies
102+
103+
assert call_policies.unguarded_call() == "unguarded"
104+
assert call_policies.guarded_call() == "guarded"
105+
106+
assert call_policies.multiple_guards_correct_order() == "guarded & guarded"
107+
assert call_policies.multiple_guards_wrong_order() == "unguarded & guarded"
108+
109+
if hasattr(call_policies, "with_gil"):
110+
assert call_policies.with_gil() == "GIL held"
111+
assert call_policies.without_gil() == "GIL released"

tests/test_keep_alive.cpp

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)