diff --git a/docs/advanced.rst b/docs/advanced.rst index 2a0e26870..43f520051 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -185,21 +185,32 @@ The following interactive session shows how to call them from Python. >>> plus_1(number=43) 44L -.. note:: - - This functionality is very useful when generating bindings for callbacks in - C++ libraries (e.g. a graphical user interface library). - - The file :file:`example/example5.cpp` contains a complete example that - demonstrates how to work with callbacks and anonymous functions in more detail. - .. warning:: Keep in mind that passing a function from C++ to Python (or vice versa) will instantiate a piece of wrapper code that translates function - invocations between the two languages. Copying the same function back and - forth between Python and C++ many times in a row will cause these wrappers - to accumulate, which can decrease performance. + invocations between the two languages. Naturally, this translation + increases the computational cost of each function call somewhat. A + problematic situation can arise when a function is copied back and forth + between Python and C++ many times in a row, in which case the underlying + wrappers will accumulate correspondingly. The resulting long sequence of + C++ -> Python -> C++ -> ... roundtrips can significantly decrease + performance. + + There is one exception: pybind11 detects case where a stateless function + (i.e. a function pointer or a lambda function without captured variables) + is passed as an argument to another C++ function exposed in Python. In this + case, there is no overhead. Pybind11 will extract the underlying C++ + function pointer from the wrapped function to sidestep a potential C++ -> + Python -> C++ roundtrip. This is demonstrated in Example 5. + +.. note:: + + This functionality is very useful when generating bindings for callbacks in + C++ libraries (e.g. GUI libraries, asynchronous networking libraries, etc.). + + The file :file:`example/example5.cpp` contains a complete example that + demonstrates how to work with callbacks and anonymous functions in more detail. Overriding virtual functions in Python ====================================== diff --git a/example/example5.cpp b/example/example5.cpp index 0e1d2cd13..3c144f220 100644 --- a/example/example5.cpp +++ b/example/example5.cpp @@ -65,6 +65,29 @@ py::cpp_function test_callback5() { py::arg("number")); } +int dummy_function(int i) { return i + 1; } +int dummy_function2(int i, int j) { return i + j; } +std::function roundtrip(std::function f) { + std::cout << "roundtrip.." << std::endl; + return f; +} + +void test_dummy_function(const std::function &f) { + using fn_type = int (*)(int); + auto result = f.target(); + if (!result) { + std::cout << "could not convert to a function pointer." << std::endl; + auto r = f(1); + std::cout << "eval(1) = " << r << std::endl; + } else if (*result == dummy_function) { + std::cout << "argument matches dummy_function" << std::endl; + auto r = (*result)(1); + std::cout << "eval(1) = " << r << std::endl; + } else { + std::cout << "argument does NOT match dummy_function. This should never happen!" << std::endl; + } +} + void init_ex5(py::module &m) { py::class_ pet_class(m, "Pet"); pet_class @@ -113,4 +136,10 @@ void init_ex5(py::module &m) { /* p should be cleaned up when the returned function is garbage collected */ }; }); + + /* Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer */ + m.def("dummy_function", &dummy_function); + m.def("dummy_function2", &dummy_function2); + m.def("roundtrip", &roundtrip); + m.def("test_dummy_function", &test_dummy_function); } diff --git a/example/example5.py b/example/example5.py index 1361c7519..2526042b6 100755 --- a/example/example5.py +++ b/example/example5.py @@ -54,3 +54,30 @@ f = test_callback5() print("func(number=43) = %i" % f(number=43)) test_cleanup() + +from example import dummy_function +from example import dummy_function2 +from example import test_dummy_function +from example import roundtrip + +test_dummy_function(dummy_function) +test_dummy_function(roundtrip(dummy_function)) +test_dummy_function(lambda x: x + 2) + +try: + test_dummy_function(dummy_function2) + print("Problem!") +except Exception as e: + if 'Incompatible function arguments' in str(e): + print("All OK!") + else: + print("Problem!") + +try: + test_dummy_function(lambda x, y: x + y) + print("Problem!") +except Exception as e: + if 'missing 1 required positional argument' in str(e): + print("All OK!") + else: + print("Problem!") diff --git a/example/example5.ref b/example/example5.ref index f32ba3af1..c2e8eef5d 100644 --- a/example/example5.ref +++ b/example/example5.ref @@ -1,20 +1,13 @@ Rabbit is a parrot +Rabbit is a parrot Polly is a parrot +Polly is a parrot +Molly is a dog Molly is a dog Woof! -func(43) = 44 -Payload constructor -Payload copy constructor -Payload move constructor -Payload destructor -Payload destructor -Payload destructor -Rabbit is a parrot -Polly is a parrot -Molly is a dog The following error is expected: Incompatible function arguments. The following argument types are supported: 1. (example.Dog) -> NoneType - Invoked with: + Invoked with: Callback function 1 called! False Callback function 2 called : Hello, x, True, 5 @@ -24,4 +17,22 @@ False Callback function 3 called : Partial object with one argument False func(43) = 44 +func(43) = 44 func(number=43) = 44 +Payload constructor +Payload copy constructor +Payload move constructor +Payload destructor +Payload destructor +Payload destructor +argument matches dummy_function +eval(1) = 2 +roundtrip.. +argument matches dummy_function +eval(1) = 2 +could not convert to a function pointer. +eval(1) = 3 +could not convert to a function pointer. +All OK! +could not convert to a function pointer. +All OK! diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 1e063a5f9..7925b8e29 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -113,6 +113,9 @@ struct function_record { /// True if name == '__init__' bool is_constructor : 1; + /// True if this is a stateless function pointer + bool is_stateless : 1; + /// True if the function has a '*args' argument bool has_args : 1; diff --git a/include/pybind11/functional.h b/include/pybind11/functional.h index f74a9bdf9..d289f6185 100644 --- a/include/pybind11/functional.h +++ b/include/pybind11/functional.h @@ -23,6 +23,29 @@ public: src_ = detail::get_function(src_); if (!src_ || !PyCallable_Check(src_.ptr())) return false; + + { + /* + When passing a C++ function as an argument to another C++ + function via Python, every function call would normally involve + a full C++ -> Python -> C++ roundtrip, which can be prohibitive. + Here, we try to at least detect the case where the function is + stateless (i.e. function pointer or lambda function without + captured variables), in which case the roundtrip can be avoided. + */ + if (PyCFunction_Check(src_.ptr())) { + capsule c(PyCFunction_GetSelf(src_.ptr()), true); + auto rec = (function_record *) c; + using FunctionType = Return (*) (Args...); + + if (rec && rec->is_stateless && rec->data[1] == &typeid(FunctionType)) { + struct capture { FunctionType f; }; + value = ((capture *) &rec->data)->f; + return true; + } + } + } + object src(src_, true); value = [src](Args... args) -> Return { gil_scoped_acquire acq; @@ -35,7 +58,11 @@ public: template static handle cast(Func &&f_, return_value_policy policy, handle /* parent */) { - return cpp_function(std::forward(f_), policy).release(); + auto result = f_.template target(); + if (result) + return cpp_function(*result, policy).release(); + else + return cpp_function(std::forward(f_), policy).release(); } PYBIND11_TYPE_CASTER(type, _("function<") + diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 3c9e6910d..96a97824b 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -82,6 +82,9 @@ protected: /* Store the capture object directly in the function record if there is enough space */ if (sizeof(capture) <= sizeof(rec->data)) { + /* Without these pragmas, GCC warns that there might not be + enough space to use the placement new operator. However, the + 'if' statement above ensures that this is the case. */ #if defined(__GNUG__) && !defined(__clang__) && __GNUC__ >= 6 # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wplacement-new" @@ -118,7 +121,7 @@ protected: capture *cap = (capture *) (sizeof(capture) <= sizeof(rec->data) ? &rec->data : rec->data[0]); - /* Perform the functioncall */ + /* Perform the function call */ handle result = cast_out::cast(args_converter.template call(cap->f), rec->policy, parent); @@ -140,6 +143,16 @@ protected: if (cast_in::has_args) rec->has_args = true; if (cast_in::has_kwargs) rec->has_kwargs = true; + + /* Stash some additional information used by an important optimization in 'functional.h' */ + using FunctionType = Return (*)(Args...); + constexpr bool is_function_ptr = + std::is_convertible::value && + sizeof(capture) == sizeof(void *); + if (is_function_ptr) { + rec->is_stateless = true; + rec->data[1] = (void *) &typeid(FunctionType); + } } /// Register a function call with Python (generic non-templated code goes here) @@ -157,6 +170,7 @@ protected: else if (a.value) a.descr = strdup(((std::string) ((object) handle(a.value).attr("__repr__"))().str()).c_str()); } + auto const ®istered_types = detail::get_internals().registered_types_cpp; /* Generate a proper function signature */ @@ -215,10 +229,10 @@ protected: rec->name = strdup("__nonzero__"); } #endif - rec->signature = strdup(signature.c_str()); rec->args.shrink_to_fit(); rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__"); + rec->is_stateless = false; rec->has_args = false; rec->has_kwargs = false; rec->nargs = (uint16_t) args;