Boost.Python: Wrap functions to release the GIL
Asked Answered
R

2

10

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.

Remissible answered 4/9, 2013 at 15:46 Comment(4)
I found an excellent answer from e.tadeu: #2135957Remissible
Although it should be safe, 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.Veradia
This is a very interesting point. I wonder what is lacking in their implementation.Remissible
I too found myself interested, so decided to dig through the code. I have included my findings in the answer below, as well as a potentially reusable solution to your problem.Veradia
V
13

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(&times_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(&times_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> >(
      &times_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> >(
      &times_two));

When invoked in Python, example.times_two(21) produces the following output:

no_gil()
echo_guard()
times_two()
~echo_guard()
~no_gil()
42
Veradia answered 6/9, 2013 at 1:14 Comment(2)
First of all, I really appreciate this perfect and clear explanation, as well as the fact that you didn't use C++11 in it (nothing against it of course). This solution is AMAZING! It is clean and generic, thank you so much! It solves my problem perfectly and I am sure I will reuse that code a couple times. Guards should be at the door, not inside :)Remissible
Hi Tanner, I encounter this wrong signature issue when using your code. I posted my solution to this problem below. Maybe I missed something in your explanation. Anyway, thanks again for your answer, it helped me immensely!Remissible
R
2

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 :)

Remissible answered 20/11, 2013 at 18:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.