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 << "spam::action()" << std::endl;
}
int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};
// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}
int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}
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 <iostream>
#include <boost/function.hpp>
#include <boost/function_types/components.hpp>
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 <typename Signature>
inline typename boost::function_types::components<Signature>::type
get_signature(boost::function<Signature>&, void* = 0)
{
return typename boost::function_types::components<Signature>::type();
}
} // namespace detail
} // namespace python
} // namespace boost
#include <boost/python.hpp>
/// @brief Mockup class with member functions.
class spam
{
public:
void action()
{
std::cout << "spam::action()" << std::endl;
}
int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};
// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}
int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
// Expose class and member-function.
python::class_<spam>("Spam")
.def("action", &spam::action)
.def("times_two", boost::function<int(spam&, int)>(
&spam::times_two))
;
// Expose non-member function.
python::def("action", &action);
python::def("times_two", boost::function<int()>(
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<int(spam&, int)>(
&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<int()>(
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 <typename Signature,
typename Guard>
class guarded_function
{
public:
typedef typename boost::function_types::result_type<Signature>::type
result_type;
template <typename Fn>
guarded_function(Fn fn)
: fn_(fn)
{}
result_type operator()()
{
Guard g;
return fn_();
}
// ... overloads for operator()
private:
boost::function<Signature> 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 <typename Guard,
typename Fn>
boost::python::object
with(Fn fn)
{
return boost::python::make_function(
guarded_function<Guard, Fn>(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<no_gil>(&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 <iostream>
#include <boost/function.hpp>
#include <boost/function_types/components.hpp>
#include <boost/function_types/function_type.hpp>
#include <boost/function_types/result_type.hpp>
#include <boost/python.hpp>
#include <boost/tuple/tuple.hpp>
namespace detail {
/// @brief Functor that will invoke a function while holding a guard.
/// Upon returning from the function, the guard is released.
template <typename Signature,
typename Guard>
class guarded_function
{
public:
typedef typename boost::function_types::result_type<Signature>::type
result_type;
template <typename Fn>
guarded_function(Fn fn)
: fn_(fn)
{}
result_type operator()()
{
Guard g;
return fn_();
}
template <typename A1>
result_type operator()(A1 a1)
{
Guard g;
return fn_(a1);
}
template <typename A1, typename A2>
result_type operator()(A1 a1, A2 a2)
{
Guard g;
return fn_(a1, a2);
}
private:
boost::function<Signature> fn_;
};
/// @brief Provides signature type.
template <typename Signature>
struct mpl_signature
{
typedef typename boost::function_types::components<Signature>::type type;
};
// Support boost::function.
template <typename Signature>
struct mpl_signature<boost::function<Signature> >:
public mpl_signature<Signature>
{};
/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn,
typename Policy>
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<Fn>::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<
mpl_signature_type>::type signature_type;
// Create a callable boost::python::object that delegates to the
// guarded_function.
return boost::python::make_function(
guarded_function<signature_type, Guard>(fn),
policy, mpl_signature_type());
}
} // namespace detail
/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn,
typename Policy>
boost::python::object with(const Fn& fn, const Policy& policy)
{
return detail::with_aux<Guard>(fn, policy);
}
/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn>
boost::python::object with(const Fn& fn)
{
return with<Guard>(fn, boost::python::default_call_policies());
}
/// @brief Mockup class with member functions.
class spam
{
public:
void action()
{
std::cout << "spam::action()" << std::endl;
}
int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};
// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}
int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}
/// @brief Guard that will unlock the GIL upon construction, and
/// reacquire it upon destruction.
struct no_gil
{
public:
no_gil() { state_ = PyEval_SaveThread();
std::cout << "no_gil()" << std::endl; }
~no_gil() { std::cout << "~no_gil()" << std::endl;
PyEval_RestoreThread(state_); }
private:
PyThreadState* state_;
};
/// @brief Guard that prints to std::cout.
struct echo_guard
{
echo_guard() { std::cout << "echo_guard()" << std::endl; }
~echo_guard() { std::cout << "~echo_guard()" << std::endl; }
};
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
// Expose class and member-function.
python::class_<spam>("Spam")
.def("action", &spam::action)
.def("times_two", with<no_gil>(&spam::times_two))
;
// Expose non-member function.
python::def("action", &action);
python::def("times_two", with<boost::tuple<no_gil, 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<boost::tuple<no_gil, 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 <boost/type_traits/add_pointer.hpp>
# include <boost/mpl/placeholders.hpp>
template <typename Signature> struct mpl_signature
{
typedef typename boost::function_types::components<Signature,
boost::add_pointer<boost::mpl::placeholders::_> >::type type;
};
template <typename Signature> struct mpl_signature<boost::function<Signature> >
{
typedef typename boost::function_types::components<Signature>::type type;
};
And now everything works like a charm.
If someone can confirm this is indeed the right fix, I'd be interested :)