|
Boost : |
From: David Abrahams (abrahams_at_[hidden])
Date: 2001-03-24 19:55:30
The previous posting came out with annoying line-wraps (sorry!)
I hope this one looks better
----------
A number of things have come at once which argue for a rework of part of the
Boost.Python library. Since these changes would affect the existing library
interface, I'd like to discuss possible approaches here before doing
anything.
A Missing Feature [Ullrich and Ralf are already familiar with this part]
=================
One issue is the need to be able to return references and pointers to the
internals of classes exposed to Python. For example, if the following
classes are exported to Python:
struct inner {
void set_x(int x) { this->x = x; }
int get_x() const { return x; }
int x;
};
struct outer {
inner& get_inner() { return b; }
inner b;
};
We should be able to do the following:
>>> o = outer()
>>> o.get_inner().set_x(1)
>>> assert o.get_inner().get_x() == 1
[This ought to work identically if outer::get_inner() returned an inner*
instead of an inner&]
It seems rather obvious at first that this should work, but getting it to
work safely is problematic. The problem occurs in wrapping
outer::get_inner(): we need to return an object which acts just like a inner
instance created from Python via
>>> i = inner()
but which contains only a reference to the inner embedded in outer instead
of a new inner instance. Although we have the technology to do this, it is
problematic because the lifetime of the inner instance is tied to the
lifetime of the outer instance:
>>> o = outer()
>>> i = o.get_inner()
>>> del o # its inner is gone, now, too
>>> i.get_x() # crash!!
The obvious fix for this case is to have i manage a reference-count on o. In
other words, the reference-count on o is incremented when get_inner()
returns, and it is decremented when the return value is finally destroyed.
It is tempting to think we should automatically manage a reference-count on
the "self" object whenever a function returning a reference is wrapped, but
this approach doesn't generalize well:
1. A member function may return a reference into one of its arguments:
// unlikely but plausible example
struct outer {
inner& get_other_inner(outer& other)
{ return other.b; }
};
2. Functions at namespace scope can also return references:
inner& get_inner2(outer& x) { return x.get_inner(); }
So we need a way to tell Boost.Python how a function wrapper should manage
its return value. I suggest the following interface which I have already
discussed a bit with Ullrich and Ralf:
outer_class_builder.def(boost::python::owned_by_arg(0),
&outer::get_inner, "get_inner");
Since the "self" parameter is implicitly passed as argument zero, this will
cause a reference count on the self parameter to be managed by the resulting
object.
This usage suggests a generalization wherein an option initial parameter to
def() could be used to indicate that a pointer return value should be
completely managed by Python, allowing us to conveniently wrap factory
functions as recently requested by Karl Bellve:
inner* make_inner() { return new inner; }
...
my_module_builder.def(boost::python::factory_function,
make_inner, "make_inner");
An Existing Bug
===============
How should we convert the return values of wrapped functions to python?
* For types that have an immutable analogue in Python (int, std::string) the
answer is simple: values and const references should be converted to the
corresponding Python type, but an attempt to wrap a function returning a
nonconst reference should not compile (presumably, the intention of such a
function is to eventually allow the caller to modify the referent).
* For class types with an accessible copy constructor that have been exposed
to Python with a class_builder, we can convert returned values by copying
them into a new extension class instance. The existing Boost.Python
strategy is to convert const references the same way as values. This
strategy has two problems:
1. The referent is copied unneccessarily.
2. It is not exactly equivalent to returning a value; even though the
returned reference is const, another function is free to modify the
value of the referent:
struct inner2 {
inner2() : x(0) {}
void get_x() { return this->x; }
int x;
};
struct outer2 {
const inner2& get_inner() { return this->y; }
void set_x(int x) { this->y.x = x; }
inner y;
};
>>> o = outer2()
>>> i = o.get_inner() # copies o.y into a new extension instance
>>> o.set_x(1) # affects o.y, but not i
>>> i.get_x() # expecting to see 1 here!
0
With the approach outlined above under "A Missing Feature", both of the
above problems could be avoided. The downside is that existing code
wrapping functions that return references would have to change to use the
owned_by_arg(n) syntax. My opinion is that it's worth it, but I would like
to hear if there are any objections.
* Boost.Python was intended to prevent the wrapping of functions returning
non-const references to class types, for reasons already discussed. This
is where the bug occurs.
The mechanism that converts return values of wrapped functions to python
is simple: the function's return value is passed to the overloaded
to_python function, and the resulting PyObject* is passed on to
Python. The problem with this strategy is that if you allow the conversion
of values or const references to python, you also allow the conversion of
non-const references. This occurs due to the ordinary C++ type conversion
rules. In other words:
PyObject* to_python(inner);
PyObject* to_python(const outer&);
void f(inner& i, outer& o)
{
PyObject* p1 = to_python(i); // if inner is copyable, this compiles!
PyObject* p2 = to_python(o); // compiles no matter what!
}
As Charlie Barrows discovered in
http://groups.yahoo.com/group/boost/message/10024, the result is that a
returned non-const reference is silently treated just like a returned
value or const reference (the referent is copied into a new object).
Possible Fixes
==============
The most conservative approach I can think of (short of not fixing the bug!)
goes like this:
1. Change return-value handling so that to_python is passed an additional
parameter of type boost::type<R>, where R is the return type of the
wrapped function. By carefully generating only to_python overloads for
the types we want to implicitly convert to python (i.e. without using
owned_by_arg(n)), we can control which types get converted. This works
because boost::type<R&> is not implicitly converted to boost::type<R>
or boost::type<R const&>. We would then have a nice symmetry between
the functions used for conversion: from_python(PyObject*, type<T>)
to_python(const T&, type<T>)
from_python(PyObject*, type<const T&>)
to_python(const T&, type<const T&>)
2. Retain the single-argument version of to_python() as a template
function. Why? Because the availability of the single-argument
to_python has already been exposed to users, who may be using it for a
variety of things. It would look something like this:
template <class T>
PyObject* to_python(const T& x)
{
return to_python(x, boost::type<T>());
}
[Lots of gory detail omitted here, since we need additional
metaprogramming to deal with noncopyable types as described in the
declaration of python_extension_class_converters in
boost/python/detail/extension_class.hpp]
This fix breaks any user code that supplies customized single-argument
to_python() functions, since the function return mechanism will now be
searching for a two-argument version.
A more radical possibility would be to completely change the conversion
mechanism to use converter objects instead of functions:
Ullrich proposed this approach months ago, and even coded up a sample
implementation (though as I recall it requires partial specialization). It
would allow us to stop using C++ exception-handling to deal with argument
type errors and overload resolution. There are two disadvantages to using
EH for this purpose:
1. Using EH for overload resolution causes exceptions to be handled when
there's really no error, potentially slowing down function
dispatching.
2. Some compilers (e.g. GCC 2.95.2) still have EH bugs which can cause
leaks or, presumably, worse problems.
On the other hand, I see several potential downsides:
1. Complexity: Each converter object would have to contain
suitably-aligned uninitialized storage for an instance of the type
being converted from_python in addition to a pointer.
2. Code size: It looks to me like it would generate a lot more inline
code than the current approach, since we would have do deal with
explicit conversion error checking in addition to any exceptions
thrown by the wrapped function which we'd have to handle anyway.
3. Incompatibility: as a more radical change, it would probably break
more user code - customized from_python functions would no longer
work either.
I'd like to get some feedback from the user and developer communities about
these approaches. If there are alternatives, I'd love to hear them.
Regards,
Dave
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk