Allow python builtins to be used as callbacks (#1413)

* Allow python builtins to be used as callbacks

* Try to fix pypy segfault

* Add expected fail for PyPy

* Fix typo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add more info to xfail

* Add env

* Try returning false

* Try removing the move for pypy

* Fix bugs

* Try removing move

* Just keep ignoring for PyPy

* Add back xfail

* Fix ctors

* Revert change of std::move

* Change to skip

* Fix bug and edit comments

* Remove clang-tidy bugprone fix skip bug

Co-authored-by: Aaron Gokaslan <skylion.aaron@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
David Hewitt 2021-07-27 19:16:28 +01:00 committed by GitHub
parent a0f862d428
commit a0b975965f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 37 additions and 14 deletions

View File

@ -43,27 +43,33 @@ public:
captured variables), in which case the roundtrip can be avoided. captured variables), in which case the roundtrip can be avoided.
*/ */
if (auto cfunc = func.cpp_function()) { if (auto cfunc = func.cpp_function()) {
auto c = reinterpret_borrow<capsule>(PyCFunction_GET_SELF(cfunc.ptr())); auto cfunc_self = PyCFunction_GET_SELF(cfunc.ptr());
auto rec = (function_record *) c; if (isinstance<capsule>(cfunc_self)) {
auto c = reinterpret_borrow<capsule>(cfunc_self);
auto rec = (function_record *) c;
while (rec != nullptr) { while (rec != nullptr) {
if (rec->is_stateless if (rec->is_stateless
&& same_type(typeid(function_type), && same_type(typeid(function_type),
*reinterpret_cast<const std::type_info *>(rec->data[1]))) { *reinterpret_cast<const std::type_info *>(rec->data[1]))) {
struct capture { struct capture {
function_type f; function_type f;
}; };
value = ((capture *) &rec->data)->f; value = ((capture *) &rec->data)->f;
return true; return true;
}
rec = rec->next;
} }
rec = rec->next;
} }
// PYPY segfaults here when passing builtin function like sum.
// Raising an fail exception here works to prevent the segfault, but only on gcc.
// See PR #1413 for full details
} }
// ensure GIL is held during functor destruction // ensure GIL is held during functor destruction
struct func_handle { struct func_handle {
function f; function f;
func_handle(function&& f_) : f(std::move(f_)) {} func_handle(function &&f_) noexcept : f(std::move(f_)) {}
func_handle(const func_handle& f_) { func_handle(const func_handle& f_) {
gil_scoped_acquire acq; gil_scoped_acquire acq;
f = f_.f; f = f_.f;
@ -77,7 +83,7 @@ public:
// to emulate 'move initialization capture' in C++11 // to emulate 'move initialization capture' in C++11
struct func_wrapper { struct func_wrapper {
func_handle hfunc; func_handle hfunc;
func_wrapper(func_handle&& hf): hfunc(std::move(hf)) {} func_wrapper(func_handle &&hf) noexcept : hfunc(std::move(hf)) {}
Return operator()(Args... args) const { Return operator()(Args... args) const {
gil_scoped_acquire acq; gil_scoped_acquire acq;
object retval(hfunc.f(std::forward<Args>(args)...)); object retval(hfunc.f(std::forward<Args>(args)...));

View File

@ -156,6 +156,12 @@ TEST_SUBMODULE(callbacks, m) {
.def(py::init<>()) .def(py::init<>())
.def("triple", [](CppBoundMethodTest &, int val) { return 3 * val; }); .def("triple", [](CppBoundMethodTest &, int val) { return 3 * val; });
// This checks that builtin functions can be passed as callbacks
// rather than throwing RuntimeError due to trying to extract as capsule
m.def("test_sum_builtin", [](const std::function<double(py::iterable)> &sum_builtin, const py::iterable &i) {
return sum_builtin(i);
});
// test async Python callbacks // test async Python callbacks
using callback_f = std::function<void(int)>; using callback_f = std::function<void(int)>;
m.def("test_async_callback", [](const callback_f &f, const py::list &work) { m.def("test_async_callback", [](const callback_f &f, const py::list &work) {

View File

@ -3,6 +3,7 @@ import pytest
from pybind11_tests import callbacks as m from pybind11_tests import callbacks as m
from threading import Thread from threading import Thread
import time import time
import env # NOQA: F401
def test_callbacks(): def test_callbacks():
@ -124,6 +125,16 @@ def test_movable_object():
assert m.callback_with_movable(lambda _: None) is True assert m.callback_with_movable(lambda _: None) is True
@pytest.mark.skipif(
"env.PYPY",
reason="PyPy segfaults on here. See discussion on #1413.",
)
def test_python_builtins():
"""Test if python builtins like sum() can be used as callbacks"""
assert m.test_sum_builtin(sum, [1, 2, 3]) == 6
assert m.test_sum_builtin(sum, []) == 0
def test_async_callbacks(): def test_async_callbacks():
# serves as state for async callback # serves as state for async callback
class Item: class Item: