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
});
```
This commit is contained in:
Dean Moldovan 2017-03-16 11:22:26 +01:00
parent 83a8a977a7
commit 1ac19036d6
10 changed files with 217 additions and 59 deletions

View File

@ -162,14 +162,16 @@ Additional call policies
======================== ========================
In addition to the above return value policies, further *call policies* can be In addition to the above return value policies, further *call policies* can be
specified to indicate dependencies between parameters. In general, call policies specified to indicate dependencies between parameters or ensure a certain state
are required when the C++ object is any kind of container and another object is being for the function call.
added to the container.
There is currently just Keep alive
one policy named ``keep_alive<Nurse, Patient>``, which indicates that the ----------
argument with index ``Patient`` should be kept alive at least until the
argument with index ``Nurse`` is freed by the garbage collector. Argument In general, this policy is required when the C++ object is any kind of container
and another object is being added to the container. ``keep_alive<Nurse, Patient>``
indicates that the argument with index ``Patient`` should be kept alive at least
until the argument with index ``Nurse`` is freed by the garbage collector. Argument
indices start at one, while zero refers to the return value. For methods, index indices start at one, while zero refers to the return value. For methods, index
``1`` refers to the implicit ``this`` pointer, while regular arguments begin at ``1`` refers to the implicit ``this`` pointer, while regular arguments begin at
index ``2``. Arbitrarily many call policies can be specified. When a ``Nurse`` index ``2``. Arbitrarily many call policies can be specified. When a ``Nurse``
@ -194,10 +196,36 @@ container:
Patient != 0) and ``with_custodian_and_ward_postcall`` (if Nurse/Patient == Patient != 0) and ``with_custodian_and_ward_postcall`` (if Nurse/Patient ==
0) policies from Boost.Python. 0) policies from Boost.Python.
Call guard
----------
The ``call_guard<T>`` policy allows any scope guard type ``T`` to be placed
around the function call. For example, this definition:
.. code-block:: cpp
m.def("foo", foo, py::call_guard<T>());
is equivalent to the following pseudocode:
.. code-block:: cpp
m.def("foo", [](args...) {
T scope_guard;
return foo(args...); // forwarded arguments
});
The only requirement is that ``T`` is default-constructible, but otherwise any
scope guard will work. This is very useful in combination with `gil_scoped_release`.
See :ref:`gil`.
Multiple guards can also be specified as ``py::call_guard<T1, T2, T3...>``. The
constructor order is left to right and destruction happens in reverse.
.. seealso:: .. seealso::
The file :file:`tests/test_keep_alive.cpp` contains a complete example The file :file:`tests/test_call_policies.cpp` contains a complete example
that demonstrates using :class:`keep_alive` in more detail. that demonstrates using `keep_alive` and `call_guard` in more detail.
.. _python_objects_as_args: .. _python_objects_as_args:

View File

@ -15,6 +15,7 @@ T2>, myFunc)``. In this case, the preprocessor assumes that the comma indicates
the beginning of the next parameter. Use a ``typedef`` to bind the template to the beginning of the next parameter. Use a ``typedef`` to bind the template to
another name and use it in the macro to avoid this problem. another name and use it in the macro to avoid this problem.
.. _gil:
Global Interpreter Lock (GIL) Global Interpreter Lock (GIL)
============================= =============================
@ -68,6 +69,13 @@ could be realized as follows (important changes highlighted):
return m.ptr(); return m.ptr();
} }
The ``call_go`` wrapper can also be simplified using the `call_guard` policy
(see :ref:`call_policies`) which yields the same result:
.. code-block:: cpp
m.def("call_go", &call_go, py::call_guard<py::gil_scoped_release>());
Binding sequence data types, iterators, the slicing protocol, etc. Binding sequence data types, iterators, the slicing protocol, etc.
================================================================== ==================================================================

View File

@ -67,6 +67,44 @@ struct metaclass {
/// Annotation to mark enums as an arithmetic type /// Annotation to mark enums as an arithmetic type
struct arithmetic { }; struct arithmetic { };
/** \rst
A call policy which places one or more guard variables (``Ts...``) around the function call.
For example, this definition:
.. code-block:: cpp
m.def("foo", foo, py::call_guard<T>());
is equivalent to the following pseudocode:
.. code-block:: cpp
m.def("foo", [](args...) {
T scope_guard;
return foo(args...); // forwarded arguments
});
\endrst */
template <typename... Ts> struct call_guard;
template <> struct call_guard<> { using type = detail::void_type; };
template <typename T>
struct call_guard<T> {
static_assert(std::is_default_constructible<T>::value,
"The guard type must be default constructible");
using type = T;
};
template <typename T, typename... Ts>
struct call_guard<T, Ts...> {
struct type {
T guard{}; // Compose multiple guard types with left-to-right default-constructor order
typename call_guard<Ts...>::type next{};
};
};
/// @} annotations /// @} annotations
NAMESPACE_BEGIN(detail) NAMESPACE_BEGIN(detail)
@ -374,6 +412,9 @@ struct process_attribute<metaclass> : process_attribute_default<metaclass> {
template <> template <>
struct process_attribute<arithmetic> : process_attribute_default<arithmetic> {}; struct process_attribute<arithmetic> : process_attribute_default<arithmetic> {};
template <typename... Ts>
struct process_attribute<call_guard<Ts...>> : process_attribute_default<call_guard<Ts...>> { };
/*** /***
* Process a keep_alive call policy -- invokes keep_alive_impl during the * Process a keep_alive call policy -- invokes keep_alive_impl during the
* pre-call handler if both Nurse, Patient != 0 and use the post-call handler * pre-call handler if both Nurse, Patient != 0 and use the post-call handler
@ -410,6 +451,13 @@ template <typename... Args> struct process_attributes {
} }
}; };
template <typename T>
using is_call_guard = is_instantiation<call_guard, T>;
/// Extract the ``type`` from the first `call_guard` in `Extras...` (or `void_type` if none found)
template <typename... Extra>
using extract_guard_t = typename first_of_t<is_call_guard, call_guard<>, Extra...>::type;
/// Check the number of named arguments at compile time /// Check the number of named arguments at compile time
template <typename... Extra, template <typename... Extra,
size_t named = constexpr_sum(std::is_base_of<arg, Extra>::value...), size_t named = constexpr_sum(std::is_base_of<arg, Extra>::value...),

View File

@ -1383,14 +1383,14 @@ public:
return load_impl_sequence(call, indices{}); return load_impl_sequence(call, indices{});
} }
template <typename Return, typename Func> template <typename Return, typename Guard, typename Func>
enable_if_t<!std::is_void<Return>::value, Return> call(Func &&f) { enable_if_t<!std::is_void<Return>::value, Return> call(Func &&f) {
return call_impl<Return>(std::forward<Func>(f), indices{}); return call_impl<Return>(std::forward<Func>(f), indices{}, Guard{});
} }
template <typename Return, typename Func> template <typename Return, typename Guard, typename Func>
enable_if_t<std::is_void<Return>::value, void_type> call(Func &&f) { enable_if_t<std::is_void<Return>::value, void_type> call(Func &&f) {
call_impl<Return>(std::forward<Func>(f), indices{}); call_impl<Return>(std::forward<Func>(f), indices{}, Guard{});
return void_type(); return void_type();
} }
@ -1406,8 +1406,8 @@ private:
return true; return true;
} }
template <typename Return, typename Func, size_t... Is> template <typename Return, typename Func, size_t... Is, typename Guard>
Return call_impl(Func &&f, index_sequence<Is...>) { Return call_impl(Func &&f, index_sequence<Is...>, Guard &&) {
return std::forward<Func>(f)(cast_op<Args>(std::get<Is>(value))...); return std::forward<Func>(f)(cast_op<Args>(std::get<Is>(value))...);
} }

View File

@ -536,9 +536,15 @@ using is_template_base_of = decltype(is_template_base_of_impl<Base>::check((remo
struct is_template_base_of : decltype(is_template_base_of_impl<Base>::check((remove_cv_t<T>*)nullptr)) { }; struct is_template_base_of : decltype(is_template_base_of_impl<Base>::check((remove_cv_t<T>*)nullptr)) { };
#endif #endif
/// Check if T is an instantiation of the template `Class`. For example:
/// `is_instantiation<shared_ptr, T>` is true if `T == shared_ptr<U>` where U can be anything.
template <template<typename...> class Class, typename T>
struct is_instantiation : std::false_type { };
template <template<typename...> class Class, typename... Us>
struct is_instantiation<Class, Class<Us...>> : std::true_type { };
/// Check if T is std::shared_ptr<U> where U can be anything /// Check if T is std::shared_ptr<U> where U can be anything
template <typename T> struct is_shared_ptr : std::false_type { }; template <typename T> using is_shared_ptr = is_instantiation<std::shared_ptr, T>;
template <typename U> struct is_shared_ptr<std::shared_ptr<U>> : std::true_type { };
/// Ignore that a variable is unused in compiler warnings /// Ignore that a variable is unused in compiler warnings
inline void ignore_unused(const int *) { } inline void ignore_unused(const int *) { }

View File

@ -143,8 +143,11 @@ protected:
/* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ /* 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); const auto policy = detail::return_value_policy_override<Return>::policy(call.func.policy);
/* Function scope guard -- defaults to the compile-to-nothing `void_type` */
using Guard = detail::extract_guard_t<Extra...>;
/* Perform the function call */ /* Perform the function call */
handle result = cast_out::cast(args_converter.template call<Return>(cap->f), handle result = cast_out::cast(args_converter.template call<Return, Guard>(cap->f),
policy, call.parent); policy, call.parent);
/* Invoke call policy post-call hook */ /* Invoke call policy post-call hook */

View File

@ -28,6 +28,7 @@ endif()
set(PYBIND11_TEST_FILES set(PYBIND11_TEST_FILES
test_alias_initialization.cpp test_alias_initialization.cpp
test_buffers.cpp test_buffers.cpp
test_call_policies.cpp
test_callbacks.cpp test_callbacks.cpp
test_chrono.cpp test_chrono.cpp
test_class_args.cpp test_class_args.cpp
@ -40,7 +41,6 @@ set(PYBIND11_TEST_FILES
test_exceptions.cpp test_exceptions.cpp
test_inheritance.cpp test_inheritance.cpp
test_issues.cpp test_issues.cpp
test_keep_alive.cpp
test_kwargs_and_defaults.cpp test_kwargs_and_defaults.cpp
test_methods_and_attributes.cpp test_methods_and_attributes.cpp
test_modules.cpp test_modules.cpp

View File

@ -0,0 +1,91 @@
/*
tests/test_call_policies.cpp -- keep_alive and call_guard
Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include "pybind11_tests.h"
class Child {
public:
Child() { py::print("Allocating child."); }
~Child() { py::print("Releasing child."); }
};
class Parent {
public:
Parent() { py::print("Allocating parent."); }
~Parent() { py::print("Releasing parent."); }
void addChild(Child *) { }
Child *returnChild() { return new Child(); }
Child *returnNullChild() { return nullptr; }
};
test_initializer keep_alive([](py::module &m) {
py::class_<Parent>(m, "Parent")
.def(py::init<>())
.def("addChild", &Parent::addChild)
.def("addChildKeepAlive", &Parent::addChild, py::keep_alive<1, 2>())
.def("returnChild", &Parent::returnChild)
.def("returnChildKeepAlive", &Parent::returnChild, py::keep_alive<1, 0>())
.def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
.def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());
py::class_<Child>(m, "Child")
.def(py::init<>());
});
struct CustomGuard {
static bool enabled;
CustomGuard() { enabled = true; }
~CustomGuard() { enabled = false; }
static const char *report_status() { return enabled ? "guarded" : "unguarded"; }
};
bool CustomGuard::enabled = false;
struct DependentGuard {
static bool enabled;
DependentGuard() { enabled = CustomGuard::enabled; }
~DependentGuard() { enabled = false; }
static const char *report_status() { return enabled ? "guarded" : "unguarded"; }
};
bool DependentGuard::enabled = false;
test_initializer call_guard([](py::module &pm) {
auto m = pm.def_submodule("call_policies");
m.def("unguarded_call", &CustomGuard::report_status);
m.def("guarded_call", &CustomGuard::report_status, py::call_guard<CustomGuard>());
m.def("multiple_guards_correct_order", []() {
return CustomGuard::report_status() + std::string(" & ") + DependentGuard::report_status();
}, py::call_guard<CustomGuard, DependentGuard>());
m.def("multiple_guards_wrong_order", []() {
return DependentGuard::report_status() + std::string(" & ") + CustomGuard::report_status();
}, py::call_guard<DependentGuard, CustomGuard>());
#if defined(WITH_THREAD) && !defined(PYPY_VERSION)
// `py::call_guard<py::gil_scoped_release>()` should work in PyPy as well,
// but it's unclear how to test it without `PyGILState_GetThisThreadState`.
auto report_gil_status = []() {
auto is_gil_held = false;
if (auto tstate = py::detail::get_thread_state_unchecked())
is_gil_held = (tstate == PyGILState_GetThisThreadState());
return is_gil_held ? "GIL held" : "GIL released";
};
m.def("with_gil", report_gil_status);
m.def("without_gil", report_gil_status, py::call_guard<py::gil_scoped_release>());
#endif
});

View File

@ -95,3 +95,17 @@ def test_return_none(capture):
del p del p
pytest.gc_collect() pytest.gc_collect()
assert capture == "Releasing parent." assert capture == "Releasing parent."
def test_call_guard():
from pybind11_tests import call_policies
assert call_policies.unguarded_call() == "unguarded"
assert call_policies.guarded_call() == "guarded"
assert call_policies.multiple_guards_correct_order() == "guarded & guarded"
assert call_policies.multiple_guards_wrong_order() == "unguarded & guarded"
if hasattr(call_policies, "with_gil"):
assert call_policies.with_gil() == "GIL held"
assert call_policies.without_gil() == "GIL released"

View File

@ -1,40 +0,0 @@
/*
tests/test_keep_alive.cpp -- keep_alive modifier (pybind11's version
of Boost.Python's with_custodian_and_ward / with_custodian_and_ward_postcall)
Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include "pybind11_tests.h"
class Child {
public:
Child() { py::print("Allocating child."); }
~Child() { py::print("Releasing child."); }
};
class Parent {
public:
Parent() { py::print("Allocating parent."); }
~Parent() { py::print("Releasing parent."); }
void addChild(Child *) { }
Child *returnChild() { return new Child(); }
Child *returnNullChild() { return nullptr; }
};
test_initializer keep_alive([](py::module &m) {
py::class_<Parent>(m, "Parent")
.def(py::init<>())
.def("addChild", &Parent::addChild)
.def("addChildKeepAlive", &Parent::addChild, py::keep_alive<1, 2>())
.def("returnChild", &Parent::returnChild)
.def("returnChildKeepAlive", &Parent::returnChild, py::keep_alive<1, 0>())
.def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
.def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());
py::class_<Child>(m, "Child")
.def(py::init<>());
});