I am currently working with Boost.Python and would like some help to solve a tricky problem.
Context
When a C++ method/function is exposed to Python, it needs to release the GIL (Global Interpreter Lock) to let other threads use the interpreter. This way, when the python code calls a C++ function, the interpreter can be used by other threads. For now, each C++ function looks like this:
// module.cpp int myfunction(std::string question) { ReleaseGIL unlockGIL; return 42; }
To pass it to boost python, I do:
// python_exposure.cpp BOOST_PYTHON_MODULE(PythonModule) { def("myfunction", &myfunction); }
Problem
This scheme works fine, however it implies that module.cpp
depends on Boost.Python
for no good reason. Ideally, only python_exposure.cpp
should depend on Boost.Python
.
Solution?
My idea was to play with Boost.Function
to wrap the function calls like this:
// python_exposure.cpp BOOST_PYTHON_MODULE(PythonModule) { def("myfunction", wrap(&myfunction)); }
Here wrap
would be in charge of unlocking the GIL during the call to myfunction
. The problem with this method is that wrap
needs to have the same signature as myfunction
which would pretty much mean re-implementing Boost.Function
...
I would be very thankful if someone had any suggestion to this problem.
Exposing functors as methods is not officially supported. The supported approach would be to expose a non-member function that delegates to the member-function. However, this can result in a large amount of boilerplate code.
As best as I can tell, Boost.Python's implementation does not explicitly preclude functors, as it allows for instances of python::object
to be exposed as a method. However, Boost.Python does place some requirements on the type of object being exposed as a method:
- The functor is CopyConstructible.
- The functor is callable. I.e. instance
o
can be called o(a1, a2, a3)
. - The call signature must be available as meta-data during runtime. Boost.Python calls the
boost::python::detail::get_signature()
function to obtain this meta-data. The meta-data is used internally to setup proper invocation, as well as for dispatching from Python to C++.
The latter requirement is where it gets complex. For some reason that is not immediately clear to me, Boost.Python invokes get_signature()
through a qualified-id, preventing argument dependent lookup. Therefore, all candidates for get_signature()
must be declared before the calling template's definition context. For example, the only overloads for get_signature()
that are considered are those declared before the definition of templates that invoke it, such as class_
, def()
, and make_function()
. To account for this behavior, when enabling a functor in Boost.Python, one must provide a get_signature()
overload prior to including Boost.Python or explicitly provide a meta-sequence representing the signature to make_function()
.
Lets work through some examples of enabling functor support, as well as providing functors that support guards. I have opted to not use C++11 features. As such, there will be some boilerplate code that could be reduced with variadic templates. Additionally, all of the examples will use the same model that provides two non-member functions and a spam
class that has two member-functions:
/// @brief Mockup class with member functions. class spam { public: void action() { std::cout
Enabling boost::function
When using the preferred syntax for Boost.Function, decomposing the signature into meta-data that meets Boost.Python requirements can be done with Boost.FunctionTypes. Here is a complete example enabling boost::function
functors to be exposed as a Boost.Python method:
#include #include #include namespace boost { namespace python { namespace detail { // get_signature overloads must be declared before including // boost/python.hpp. The declaration must be visible at the // point of definition of various Boost.Python templates during // the first phase of two phase lookup. Boost.Python invokes the // get_signature function via qualified-id, thus ADL is disabled. /// @brief Get the signature of a boost::function. template inline typename boost::function_types::components::type get_signature(boost::function&, void* = 0) { return typename boost::function_types::components::type(); } } // namespace detail } // namespace python } // namespace boost #include /// @brief Mockup class with member functions. class spam { public: void action() { std::cout ("Spam") .def("action", &spam::action) .def("times_two", boost::function( &spam::times_two)) ; // Expose non-member function. python::def("action", &action); python::def("times_two", boost::function( boost::bind(×_two, 21))); }
And its usage:
>>> import example >>> spam = example.Spam() >>> spam.action() spam::action() >>> spam.times_two(5) spam::times_two() 10 >>> example.action() action() >>> example.times_two() times_two() 42
When providing a functor that will invoke a member-function, the provided signature needs to be the non-member function equivalent. In this case, int(spam::*)(int)
becomes int(spam&, int)
.
// ... .def("times_two", boost::function( &spam::times_two)) ;
Also, arguments can be bound to the functors with boost::bind
. For example, calling example.times_two()
does not have to provide an argument, as 21
is already bound to the functor.
python::def("times_two", boost::function( boost::bind(×_two, 21)));
Custom functor with guards
Expanding upon the above example, one can enable custom functor types to be used with Boost.Python. Lets create a functor, called guarded_function
, that will use RAII, only invoking the wrapped function during the RAII object's lifetime.
/// @brief Functor that will invoke a function while holding a guard. /// Upon returning from the function, the guard is released. template class guarded_function { public: typedef typename boost::function_types::result_type::type result_type; template guarded_function(Fn fn) : fn_(fn) {} result_type operator()() { Guard g; return fn_(); } // ... overloads for operator() private: boost::function fn_; };
The guarded_function
provides similar semantics to the Python with
statement. Thus, to keep with the Boost.Python API name choices, a with()
C++ function will provide a way to create functors.
/// @brief Create a callable object with guards. template boost::python::object with(Fn fn) { return boost::python::make_function( guarded_function(fn), ...); }
This allows for functions to be exposed which will run with a guard in a non-intrusive manner:
class no_gil; // Guard // ... .def("times_two", with(&spam::times_two)) ;
Additionally, the with()
function provides the ability to deduce the function signatures, allowing the meta-data signature to be explicitly provided to Boost.Python rather than having to overload boost::python::detail::get_signature()
.
Here is the complete example, using two RAII types:
no_gil
: Releases GIL in constructor, and reacquires GIL in destructor. echo_guard
: Prints in constructor and destructor.
#include #include #include #include #include #include #include namespace detail { /// @brief Functor that will invoke a function while holding a guard. /// Upon returning from the function, the guard is released. template class guarded_function { public: typedef typename boost::function_types::result_type::type result_type; template guarded_function(Fn fn) : fn_(fn) {} result_type operator()() { Guard g; return fn_(); } template result_type operator()(A1 a1) { Guard g; return fn_(a1); } template result_type operator()(A1 a1, A2 a2) { Guard g; return fn_(a1, a2); } private: boost::function fn_; }; /// @brief Provides signature type. template struct mpl_signature { typedef typename boost::function_types::components::type type; }; // Support boost::function. template struct mpl_signature<:function> >: public mpl_signature {}; /// @brief Create a callable object with guards. template boost::python::object with_aux(Fn fn, const Policy& policy) { // Obtain the components of the Fn. This will decompose non-member // and member functions into an mpl sequence. // R (*)(A1) => R, A1 // R (C::*)(A1) => R, C*, A1 typedef typename mpl_signature::type mpl_signature_type; // Synthesize the components into a function type. This process // causes member functions to require the instance argument. // This is necessary because member functions will be explicitly // provided the 'self' argument. // R, A1 => R (*)(A1) // R, C*, A1 => R (*)(C*, A1) typedef typename boost::function_types::function_type::type signature_type; // Create a callable boost::python::object that delegates to the // guarded_function. return boost::python::make_function( guarded_function(fn), policy, mpl_signature_type()); } } // namespace detail /// @brief Create a callable object with guards. template boost::python::object with(const Fn& fn, const Policy& policy) { return detail::with_aux(fn, policy); } /// @brief Create a callable object with guards. template boost::python::object with(const Fn& fn) { return with(fn, boost::python::default_call_policies()); } /// @brief Mockup class with member functions. class spam { public: void action() { std::cout ("Spam") .def("action", &spam::action) .def("times_two", with(&spam::times_two)) ; // Expose non-member function. python::def("action", &action); python::def("times_two", with<:tuple echo_guard=""> >( ×_two)); }
And its usage:
>>> import example >>> spam = example.Spam() >>> spam.action() spam::action() >>> spam.times_two(5) no_gil() spam::times_two() ~no_gil() 10 >>> example.action() action() >>> example.times_two(21) no_gil() echo_guard() times_two() ~echo_guard() ~no_gil() 42
Notice how multiple guards can be provided by using a container type, such as boost::tuple
:
python::def("times_two", with<:tuple echo_guard=""> >( ×_two));
When invoked in Python, example.times_two(21)
produces the following output:
no_gil() echo_guard() times_two() ~echo_guard() ~no_gil() 42
If someone is interested, I had a small issue with Tanner Sansbury's code when using his final working example. For some reason, I still had the problem he mentioned about having the wrong signature in the final generated boost::function
:
// example for spam::times_two: // correct signature (manual) int (spam::*, int) // wrong signature (generated in the `guarded_function` wrapper) int (spam&, int)
even when overloading boost::python::detail::get_signature()
. The responsible for this was boost::function_types::components
; it has a default template parameter ClassTranform = add_reference<_>
which creates this class reference. To fix this, I simply changed the mpl_signature
struct as follow:
// other includes # include # include template struct mpl_signature { typedef typename boost::function_types::components >::type type; }; template struct mpl_signature<:function> > { typedef typename boost::function_types::components::type type; };
And now everything works like a charm.
If someone can confirm this is indeed the right fix, I'd be interested :)