[C++-sig] high level overview of boost.python implementation

Martin Casado casado2 at llnl.gov
Mon Jul 1 18:14:42 CEST 2002


On Saturday 29 June 2002 10:58, you wrote:
> David Abrahams wrote:
> >Sounds great. I will try to throw you all a clue now and again also.
> >

I've found the most conceptually challenging aspects of Boost deal with 
handeling the conversion between PyObjects and C++ arguments.  In light
of this, I sat down and wrote a really rough overview of how things
seemed to work.  I don't vouch for the correctness of this (it is most likely
wrong), but at the very least it might provide a basis for discussion.  I am
really keen on getting a good set of overview documentation so... any 
feedback,
additions, etc. would be greatly appreciated....

            ~~m


File:   HandlingArgumentsInBpl.txt
===============================================================================
 
Suggested Background Readings
 
boost::function documentation: http://www.boost.org/libs/function/index.html
boost::bind documentation:     http://www.boost.org/libs/bind/bind.html
 
===============================================================================
                    How Arguments From Python Are Handled
===============================================================================
 
It is perhaps easiest to start the discussion by describing the mechanism
used by BPL to convert python objects used as arguments from within python to
C++ objects for passing into wrapped functions.
 
  The main mechanism to handle the conversions is the struct
 
  template <typename T> struct arg_from_python
 
  arg_from_python is a class template which is used to convert
  a PyObject* to a type T.  arg_from_python has the following
  members.
 
      arg_from_python(PyObject*) // constructor
      bool convertible();
      T operator()(PyObject*) const;
 
   convertible(..) returns true if BPL knows how to convert T
   operator() handles the mechanics of the conversion.  You could
   use arg_from_python as follows to attempt to convert an arbitrary
   PyObject into an object of type std::vector<int>
 
   std::vector<int> foo(PyObject* obj)
   {
     arg_from_python<std::vector<int> > from_py(obj);
     if(from_py.convertible())
     {
        return from_py(obj);
     }
   }


   This is simple enough, right? Unfortunately the implementation
   of arg_from_python is not exactly straight-forward (for us
   C++ weenies that is).  Taking a peak at the declaration..
   found in boost/python/arg_from_python.hpp
 
        template <class T>
        struct arg_from_python
            : converter::select_arg_from_python<T>::type
        {
            typedef typename converter::select_arg_from_python<T>::type base;
            arg_from_python(PyObject*);
        };
 
   we see that the actual implementations of convertible() and operator() are
   dependant on some wiz-bang static mechanism for determining the correct
   type via the class select_arg_from_python.  The selection mechanism and
   all implementations can be found in 
boost/python/converter/arg_from_python.hpp.
 
   *** Note: discussion of registry here ?!?!
       (I have a notion that the non specialized arg_from_python uses the 
registry
        to determine how to do the conversion using the registry)
 
===============================================================================
             How functions are called from python
             (warning this is an incomplete and most
             likely inaccurate description of the process)
===============================================================================
 
   The last section discussed how, given an argument parameter such as
std::vector<int>, BPL handles the conversion, namely from creating an object 
of
type arg_from_python<std::vector<int> > and using a specialized conversion
mechanism or using the default. (which probably uses the registry)  However,
there has been no mention of how BPL actually gets access to the arguments of
the wrapped C++ method.  The best start for a foray into this complex world is
to take a peak at boost/python/make_function.hpp.  BPL functions are at the
highest level encapsulated in objects::function
(boost/python/objects/function.hpp) which handles overloading and ensuring
argument count is correct (and other things I don't know about).
 
   The creation of a default objects::function object as shown in
make_function.hpp is as follows:

  return new objects::function(
           objects::py_function(
                       ::boost::bind<PyObject*>(detail::caller(), f, _1, _2,
                       default_call_policies())),
                       detail::arg_tuple_size<F>::value);
 
   A function is created by a py_function object  (which is really just a
   typedef of boost:function2<PyObject*,PyObject*,PyObject*>) and an unisgned
   int representing the minimum number of arguments accepted into the 
function.
   The minimum number of arguments is discovered through some static mechanism
   I'm not even going to begin to pretend to understand.
 
   The py_function object is simply boost::function created from a bound
   functor (detail::caller), which when called is passed f (the function
   we are wrapping), the first two default arguments and a call policy
   (in the case of the example it is passed the default call policy).  Next
   obvious next question is, "what does detail::caller do when called?"
 
   detail::caller's () operator actually invokes a static method in 
detail::returning
   (boost/python/detail/returning.hpp) as you can see in the following code
   from boost/python/detail/caller.hpp.
 
    PyObject* operator()(
        BOOST_PYTHON_FN(*f,0,args_)
      , PyObject* args, PyObject* keywords
      , P const& policies
      ) const
    {
        return returning<R>::call(f, args, keywords,&policies);
    }


    returning<>::call does all sorts of fancy whizbang  preprocessing and
    metaprogramming tricks (feel free to browse the code to get a feel for 
what I'm
    talking about), which in essence boils down to the following.. (I think)
 
    - For the 0th item in the tuple (self) check to see if it is convertible
      to the correct type the method is being called on.
 
        /* check that each of the arguments is convertible */
        /* self argument is special */
        arg_from_python<A0&> c0(PyTuple_GET_ITEM(args_, 0));
        if (!c0.convertible()) return 0;
 
    - For each item in the tuple (not including the self argument) check to 
see
      if that item is convertible to its respective argument in the C++
      argument list.
 
       /* Unroll a loop for the rest of them */
       BOOST_PP_REPEAT_FROM_TO(1,args,BOOST_PYTHON_ARG_CONVERTIBLE,nil)
 
     Where, BOOST_PYTHON_ARG_CONVERTIBLE is defined as ..
 
    # define BOOST_PYTHON_ARG_CONVERTIBLE(index,ignored)
        arg_from_python<BOOST_PP_CAT(A,index)>
        BOOST_PP_CAT(c,index)(PyTuple_GET_ITEM(args_, index));
        if (!BOOST_PP_CAT(c,index).convertible()) return 0;

    As the source code demonstrates, the mechanism for checking the 
conversions
    is instantiating an arg_from_python<arg> where arg is the argument type
    and calling convertible() on that object.
 
    After the arguments are checked, the return value is checked to see if it
    can be converted to a python object (a process not discussed in this text)
    via the lines...
 
        /* find the result converter */
        typedef typename P::result_converter result_converter;
        typename mpl::apply1<result_converter,R>::type cr;
        if (!cr.convertible()) return 0;
 
    The call policy's precall() method is then called, the method is called 
with the
    converted arguments and finally the call policy's postcall method  is 
called and
    the result is returned.
 
      if (!policies->precall(args_)) return 0;
 
       PyObject* result = cr(
            ((BOOST_PYTHON_GET_ARG(0,nil)).*pmf)(
                BOOST_PP_ENUM_SHIFTED(args,BOOST_PYTHON_GET_ARG,nil))
            );
 
      return policies->postcall(args_, result);
 
   Of course for completeness it is worth discussing the actuall call to the 
method.

  Aka: ((BOOST_PYTHON_GET_ARG(0,nil)).*pmf)(
                BOOST_PP_ENUM_SHIFTED(args,BOOST_PYTHON_GET_ARG,nil))
 
   If the function pointer in question (aka pmf) is a pointer to a member 
function, it is
   bound to the first argument converted to the correct c++ type via 
BOOST_PYTHON_GET_ARG.
   This function is then called with the correct arguments which are 
converted also via
   BOOST_PYTHON_GET_ARG.  What does BOOST_PYTHON_GET_ARG do?  It calls the () 
operator
   on the arg_from_python type passing in, as an argument, python object from 
the argument
   tuple that is to be converted.
 
    # define BOOST_PYTHON_GET_ARG(index,ignored)
            BOOST_PP_CAT(c,index)(PyTuple_GET_ITEM(args_, index))
 
   Note, that there have been a number of things left out in this discussion.
   In reality pointers to member functions and pointers to non member 
functions
   are handled differently.  In the latter case, the object is passed in as 
the
   first argument to the function.  Also the content on the actual mechanics 
of
   the meta-programming has been virtually nil, mainly because I don't
   understand how it works :-P





More information about the Cplusplus-sig mailing list