[C++-sig] [Implementation] Calling wrapped functions, converters, policies

David Abrahams dave at boost-consulting.com
Tue Sep 16 21:15:03 CEST 2003


I was recently trying to implement
http://boost-consulting.com/boost/libs/python/todo.html#injected-constructors
and ran into some issues with the way Boost.Python calls wrapped
functions.  As I began to explore boost/python/detail/caller.hpp and
boost/python/detail/invoke.hpp, I realized that the things I wanted to
do were very closely tied to some work Lijun Qin has been doing
(http://aspn.activestate.com/ASPN/Mail/Message/C++-sig/1771145), and
with the desired ability to allow converters to execute post-call
processing actions.

Since the impact of these change has the potential to be quite
sweeping, I thought we should discuss the broad requirements here
before I start implementing anything.

Here are some things I think we need:

1. Per-call state.  

   Example 1: Lijun Qin's patches decode incoming Python unicode
   object into ordinary Python strings in the Policies' precall
   function, and rely on the Policies object producing a modified
   version of the incoming Python arg tuple.

   Example 2: In implementing injected constructors I want to strip
   the incoming "self" argument before calling the inner factory
   function, then install the result in the self object.

   In both the examples above, we need to maintain a reference to the
   newly-constructed Python tuple so that we can decrement its
   reference count when the call completes.  This state is
   "per-call", since there is a new tuple created each time the
   wrapped function is called.

   Right now we can maintain per-wrapped-function state in either of
   two objects: the function itself (when it's a function object with
   an operator()), or in the Policies object.  For all practical
   purposes, though, that state has to be immutable.  Consider a
   recursive wrapped function call; the inner call will wipe out any
   information stored in the Policies object during an outer call.  

   Lijun Qin's patch gets around this by storing a std::stack of
   newly-constructed tuples in the Policies object, but not only is
   that cumbersome for the implementor, it doesn't interact well with
   multithreading.  If the wrapped function call releases the
   interpreter lock and another thread returns, the top of the
   std::stack will contain the wrong argument tuple.

   What's needed is a way to get the per-call state onto the program
   stack.  I can think of two main approaches:

   a. A copy of the Policies object is made on the stack and used
      during the function call; the copy can maintain its own state.
      An advantage of this approach is simplicity.  A disadvantage is
      that you may for things you don't use: storage for per-call
      state in the wrapped function itself, and cycles for copying
      per-call state to the stack.

   b. The policies object has a init_state() function which
      produces a new state object of a possibly-different type.  This
      doesn't have the disadvantages above, but costs complexity in
      several ways:

      i. The requirements on the Policies class become more
         complicated.  It probably needs to have a nested ::state
         type.  It's possible to get around the need for this typedef
         by passing the results of init_state() directly to a function
         template parameter (e.g. into invoke(...)), but that may
         force us to have a function call boundary at an undesirable
         place.

      ii. Policies composition may become much more complicated.  How
          do you come up with the state object corresponding to
          several composed policies?  You could use tuples... hmm,
          maybe this is a job for mpl::inherit_linearly.

   I'm leaning towards a. but I'm really not sure which one is best
   and would appreciate comments.

2. The ability to select a specialized from-Python conversion method
   for each argument.  Right now, we can only select the to-Python
   conversion method for the result, but Luabind has shown the wisdom
   of being able to do the converse, and in fact Mike Rovner was just
   asking how we could implement Luabind's "adopt" policy for
   stealing ownership of C++ objects held by auto_ptr.

   Right now, an MPL argument signature sequence gets passed to
   caller_arity<N>::impl, and the elements of that (except the first,
   which is the return type) fully determine the _static_ type (see
   below) of the from-Python argument converters.  We need a way to
   customize the type of the argument converters which are actually
   used for each argument.  I think Luabind has a solution for this.
   I can imagine several similar approaches, so I don't think this is
   hard.

3. Dynamic converter per-call state and postcall actions.  

      [Refresher: The standard from-Python converter has a static type
       that provides per-call storage for an implementation which is
       dynamically-selected based on the source Python object.  If the
       target argument type is a value or a const reference, the
       per-call storage is enough to construct an object of the target
       type, and either rvalue or lvalue converters can be used.
       Otherwise, a matching lvalue converter is required and the
       static converter type contains storage for a pointer to the
       target]

   It is sometimes desirable to allow a particular
   dynamically-selected from-python converter to use additional state.

   For example, people have asked that they be able to pass a Python
   list where a std::vector<T>& argument is expected, modify the
   vector, and have the Python list automatically updated when the
   function call returns.  Because this converter requires a
   std::vector<T> lvalue, it doesn't contain storage for the vector
   whose lifetime needs to span the length of the call.  Furthermore,
   if the call completes successfully, the source list must be updated
   to reflect the new contents of the vector.

   I propose that whichever state model is chosen in item 1 above, the
   state contains a chain of dynamically-allocated polymorphic
   postcall objects, and that dynamic converter implementations are
   passed a reference to that chain so that they can register new
   postcall actions.  The postcall actions are invoked when the call
   completes successfully, and are unconditionally deleted at the end
   of the call.  One thing I'm still not clear about is whether a
   converter's convertible() function need access to that chain.

4. The ability to release the Python interpreter lock during the
   wrapped call based on the choice of Policies.  If releasing the
   lock becomes the default behavior, it's important that it be
   automatically disabled when the wrapped function is handling
   python::object or any of its derived classes.  The release must
   happen inside the invoke(...)  function, or at least, if the
   implementation changes, after all converters have completed their
   work, since the lock must be held while any reference counts are
   changed.

   This suggests that there is an inner layer of action needed, and it
   should probably be generalized.  So the flow looks something like:

     caller<F,Policies,Sig>::operator()(
          PyObject* args, PyObject* kw)

         Create per-call state on stack

         args,kw = state.precall(args,kw)

         For each argument
             Create converter
             If not convertible, fail overload resolution

         For each converter c
             c.convert(state)  // optionally register postcall actions

         inner action (release interpreter lock)

         PyObject* result = invoke wrapped F with converted arguments

         inner "un-action" (acquire interpreter lock)

         if any postcall actions stored, execute them

         return state.postcall(result, args, kw)

    I'm not sure how this interacts with Daniel's recursive "best
    overload" resolution.


Comments?
       
-- 
Dave Abrahams
Boost Consulting
www.boost-consulting.com





More information about the Cplusplus-sig mailing list