From 5f218b3f2c7de3666defecbe355c96e2a4fa011b Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Sun, 17 Jan 2016 22:36:39 +0100 Subject: [PATCH] keep_alive call policy (analogous to Boost.Python's with_custodian_and_ward, fixes #62) --- CMakeLists.txt | 3 +- docs/advanced.rst | 27 ++++++++++++++ example/example.cpp | 2 ++ example/example13.cpp | 37 +++++++++++++++++++ example/example13.py | 46 ++++++++++++++++++++++++ example/example13.ref | 25 +++++++++++++ include/pybind11/common.h | 1 + include/pybind11/pybind11.h | 71 +++++++++++++++++++++++++++++++++++-- 8 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 example/example13.cpp create mode 100644 example/example13.py create mode 100644 example/example13.ref diff --git a/CMakeLists.txt b/CMakeLists.txt index b1fcb9bb8..ebb4ea67f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ add_library(example SHARED example/example10.cpp example/example11.cpp example/example12.cpp + example/example13.cpp ) # Don't add a 'lib' prefix to the shared library @@ -179,7 +180,7 @@ endif() enable_testing() set(RUN_TEST ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/example/run_test.py) -foreach(i RANGE 1 12) +foreach(i RANGE 1 13) add_test(NAME example${i} COMMAND ${RUN_TEST} example${i}) endforeach() diff --git a/docs/advanced.rst b/docs/advanced.rst index 43a3b1a97..84ca73cc6 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -446,6 +446,33 @@ See below for an example that uses the return m.ptr(); } + +Additional call policies +======================== + +In addition to the above return value policies, further `call policies` can be +specified to indicate dependencies between parameters. There is currently just +one policy named ``keep_alive``, 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 +indices start at one, while zero refers to the return value. Arbitrarily many +call policies can be specified. + +For instance, binding code for a a list append operation that ties the lifetime +of the newly added element to the underlying container might be declared as +follows: + +.. code-block:: cpp + + py::class_(m, "List") + .def("append", &List::append, py::keep_alive<1, 2>()); + +.. note:: + + ``keep_alive`` is analogous to the ``with_custodian_and_ward`` (if Nurse, + Patient != 0) and ``with_custodian_and_ward_postcall`` (if Nurse/Patient == + 0) policies from Boost.Python. + Implicit type conversions ========================= diff --git a/example/example.cpp b/example/example.cpp index 0b97359b0..d130551a2 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -21,6 +21,7 @@ void init_ex9(py::module &); void init_ex10(py::module &); void init_ex11(py::module &); void init_ex12(py::module &); +void init_ex13(py::module &); PYBIND11_PLUGIN(example) { py::module m("example", "pybind example plugin"); @@ -37,6 +38,7 @@ PYBIND11_PLUGIN(example) { init_ex10(m); init_ex11(m); init_ex12(m); + init_ex13(m); return m.ptr(); } diff --git a/example/example13.cpp b/example/example13.cpp new file mode 100644 index 000000000..782db3109 --- /dev/null +++ b/example/example13.cpp @@ -0,0 +1,37 @@ +/* + example/example13.cpp -- keep_alive modifier (pybind11's version + of Boost.Python's with_custodian_and_ward / with_custodian_and_ward_postcall) + + Copyright (c) 2015 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "example.h" + +class Child { +public: + Child() { std::cout << "Allocating child." << std::endl; } + ~Child() { std::cout << "Releasing child." << std::endl; } +}; + +class Parent { +public: + Parent() { std::cout << "Allocating parent." << std::endl; } + ~Parent() { std::cout << "Releasing parent." << std::endl; } + void addChild(Child *) { } + Child *returnChild() { return new Child(); } +}; + +void init_ex13(py::module &m) { + py::class_(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>()); + + py::class_(m, "Child") + .def(py::init<>()); +} diff --git a/example/example13.py b/example/example13.py new file mode 100644 index 000000000..ad0176ed4 --- /dev/null +++ b/example/example13.py @@ -0,0 +1,46 @@ +from __future__ import print_function +import sys +import gc +sys.path.append('.') + +from example import Parent, Child + +if True: + p = Parent() + p.addChild(Child()) + gc.collect() + print(p) + p = None + +gc.collect() +print("") + +if True: + p = Parent() + p.returnChild() + gc.collect() + print(p) + p = None + +gc.collect() +print("") + +if True: + p = Parent() + p.addChildKeepAlive(Child()) + gc.collect() + print(p) + p = None +gc.collect() +print("") + +if True: + p = Parent() + p.returnChildKeepAlive() + gc.collect() + print(p) + p = None + +gc.collect() +print("") +print("Terminating..") diff --git a/example/example13.ref b/example/example13.ref new file mode 100644 index 000000000..7eb02c51f --- /dev/null +++ b/example/example13.ref @@ -0,0 +1,25 @@ +Allocating parent. +Allocating child. +Releasing child. + +Releasing parent. + +Allocating parent. +Allocating child. +Releasing child. + +Releasing parent. + +Allocating parent. +Allocating child. + +Releasing parent. +Releasing child. + +Allocating parent. +Allocating child. + +Releasing parent. +Releasing child. + +Terminating.. diff --git a/include/pybind11/common.h b/include/pybind11/common.h index 8b34e4e89..a2a44989e 100644 --- a/include/pybind11/common.h +++ b/include/pybind11/common.h @@ -167,6 +167,7 @@ template > struct in PyObject_HEAD type *value; PyObject *parent; + PyObject *weakrefs; bool owned : 1; bool constructed : 1; holder_type holder; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 19a465582..e8b385cb0 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -58,6 +58,28 @@ struct name { const char *value; name(const char *value) : value(value) { } }; /// Annotation for function siblings struct sibling { PyObject *value; sibling(handle value) : value(value.ptr()) { } }; +/// Keep patient alive while nurse lives +template struct keep_alive { }; + +NAMESPACE_BEGIN(detail) +template struct process_dynamic; +template struct process_dynamic { + static void precall(PyObject *) { } + static void postcall(PyObject *, PyObject *) { } +}; +template <> struct process_dynamic<> : public process_dynamic { }; +template struct process_dynamic { + static void precall(PyObject *arg) { + process_dynamic::precall(arg); + process_dynamic::precall(arg); + } + static void postcall(PyObject *arg, PyObject *ret) { + process_dynamic::postcall(arg, ret); + process_dynamic::postcall(arg, ret); + } +}; +NAMESPACE_END(detail) + /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object class cpp_function : public function { private: @@ -110,6 +132,8 @@ private: (void) unused; } + template + static void process_extra(const keep_alive &, function_entry *) { } static void process_extra(const char *doc, function_entry *entry) { entry->doc = (char *) doc; } static void process_extra(const pybind11::doc &d, function_entry *entry) { entry->doc = (char *) d.value; } static void process_extra(const pybind11::name &n, function_entry *entry) { entry->name = (char *) n.value; } @@ -154,7 +178,10 @@ public: cast_in args; if (!args.load(pyArgs, true)) return (PyObject *) 1; /* Special return code: try next overload */ - return cast_out::cast(args.template call((Return (*)(Args...)) entry->data), entry->policy, parent); + detail::process_dynamic::precall(pyArgs); + PyObject *result = cast_out::cast(args.template call((Return (*)(Args...)) entry->data), entry->policy, parent); + detail::process_dynamic::postcall(pyArgs, result); + return result; }; process_extras(std::make_tuple(std::forward(extra)...), m_entry); @@ -210,7 +237,10 @@ private: cast_in args; if (!args.load(pyArgs, true)) return (PyObject *) 1; /* Special return code: try next overload */ - return cast_out::cast(args.template call(((capture *) entry->data)->f), entry->policy, parent); + detail::process_dynamic::precall(pyArgs); + PyObject *result = cast_out::cast(args.template call(((capture *) entry->data)->f), entry->policy, parent); + detail::process_dynamic::postcall(pyArgs, result); + return result; }; process_extras(std::make_tuple(std::forward(extra)...), m_entry); @@ -568,8 +598,10 @@ public: type->ht_type.tp_as_sequence = &type->as_sequence; type->ht_type.tp_as_mapping = &type->as_mapping; type->ht_type.tp_base = (PyTypeObject *) parent; + type->ht_type.tp_weaklistoffset = offsetof(instance, weakrefs); + if (doc) { - size_t size = strlen(doc)+1; + size_t size = strlen(doc) + 1; type->ht_type.tp_doc = (char *) PyObject_MALLOC(size); memcpy((void *) type->ht_type.tp_doc, doc, size); } @@ -654,6 +686,8 @@ protected: registered_instances.erase(it); } Py_XDECREF(self->parent); + if (self->weakrefs) + PyObject_ClearWeakRefs((PyObject *) self); } Py_TYPE(self)->tp_free((PyObject*) self); } @@ -927,6 +961,37 @@ template struct init { class_.def("__init__", [](Base *instance, Args... args) { new (instance) Base(args...); }, std::forward(extra)...); } }; + +PYBIND11_NOINLINE inline void keep_alive_impl(int Nurse, int Patient, PyObject *arg, PyObject *ret) { + /* Clever approach based on weak references taken from Boost.Python */ + PyObject *nurse = Nurse > 0 ? PyTuple_GetItem(arg, Nurse - 1) : ret; + PyObject *patient = Patient > 0 ? PyTuple_GetItem(arg, Patient - 1) : ret; + + if (nurse == nullptr || patient == nullptr) + throw std::runtime_error("Could not activate keep_alive"); + + cpp_function disable_lifesupport( + [patient](handle weakref) { Py_DECREF(patient); weakref.dec_ref(); } + ); + + PyObject *weakref = PyWeakref_NewRef(nurse, disable_lifesupport.ptr()); + if (weakref == nullptr) + throw std::runtime_error("Could not allocate weak reference!"); + + Py_INCREF(patient); /* reference patient and leak the weak reference */ +} + +template struct process_dynamic> : public process_dynamic { + template ::type = 0> + static void precall(PyObject *arg) { keep_alive_impl(Nurse, Patient, arg, nullptr); } + template ::type = 0> + static void postcall(PyObject *, PyObject *) { } + template ::type = 0> + static void precall(PyObject *) { } + template ::type = 0> + static void postcall(PyObject *arg, PyObject *ret) { keep_alive_impl(Nurse, Patient, arg, ret); } +}; + NAMESPACE_END(detail) template detail::init init() { return detail::init(); };