I have some C++ code that defines two classes, A and B. B takes an instance of A during construction. I have wrapped A with Boost.Python so that Python can create instances of A, as well as subclasses. I want to do the same with B.
class A {
public:
A(long n, long x, long y) : _n(n), _x(x), _y(y) {};
long get_n() { return _n; }
long get_x() { return _x; }
long get_y() { return _y; }
private:
long _n, _x, _y;
};
class B {
public:
B(A a) : _a(a) {};
doSomething() { ... };
private:
A _a;
};
While wrapping B, I needed to work out how to pass an instance of A to B’s constructor. I did some digging and the solution I found was to write a “converter” class:
struct A_from_python_A {
static void * convertible(PyObject* obj_ptr) {
// assume it is, for now...
return obj_ptr;
}
// Convert obj_ptr into an A instance
static void construct(PyObject* obj_ptr,
boost::python::converter::rvalue_from_python_stage1_data* data) {
// extract 'n':
PyObject * n_ptr = PyObject_CallMethod(obj_ptr, (char*)"get_n", (char*)"()");
long n_val = 0;
if (n_ptr == NULL) {
cout << "... an exception occurred (get_n) ..." << endl;
} else {
n_val = PyInt_AsLong(n_ptr);
Py_DECREF(n_ptr);
}
// [snip] - also do the same for x, y
// Grab pointer to memory into which to construct the new A
void* storage = (
(boost::python::converter::rvalue_from_python_storage<A>*)
data)->storage.bytes;
// in-place construct the new A using the data
// extracted from the python object
new (storage) A(n_val, x_val, y_val);
// Stash the memory chunk pointer for later use by boost.python
data->convertible = storage;
}
// register converter functions
A_from_python_A() {
boost::python::converter::registry::push_back(
&convertible,
&construct,
boost::python::type_id<A>());
}
};
Then I register this with:
BOOST_PYTHON_MODULE(interpolation_ext)
{
// register the from-python converter for A
A_from_python_A();
class_<A>("A", init<long, long, long>())
;
class_<B>("B", init<object>())
;
}
Convertible and construct are methods that answer the “is this convertible?” and “how to convert?” questions respectively. I have observed that the construct() method is non-trivial – it has to reach into A’s PyObject*, extract all relevant fields, then rebuild a C++ instance that it then passes to B’s constructor. Because A contains some private fields, it has to do this via public access mechanisms (whereas with a pure Python object it wouldn’t have to, right?). This seems to work.
However, is the field extraction in the ‘construct’ function really necessary? It seems laborious. If A is a compound object, it could get very complicated, and possibly require one converter to invoke another. I perhaps understand the requirement if A is a Python class, but if the A instance originated from the C++ side, is there a way to determine that this is the case, and then simply get a handle (e.g. pointer) to this ‘native’ object, as a shortcut?
Here’s the associated python code:
from my_ext import A, B
a = A(1,2,3)
b = B(a)
b.doSomething()
In short, define
B‘s wrapper as:instead of
When defining a wrapper for class in Boost.Python (at least in 1.50), the
class_template generates convert and construct functions. This allowsAto be converted to and constructed from aA‘s wrapper. ThesePyObjectconversions have strict type-checking, and require that the following be true in python:isinstance( obj, A ).Custom converters are often used to support:
std::pair< long, long >to and from aPyTupleObject.Baccept classD, which is not derived fromA, as long asDprovides a compatible interface.Constructing
Bfrom an instance ofASince
AandBare neither existing Python types nor is duck-typing required, custom converters are not necessary. ForBto take an instance ofA, it can be as simple as specifying thatinittakes anA.Here is a simplified example of
AandB, whereBcan be constructed from anA.And the wrappers would be defined as:
B‘s wrapper explicitly indicates that it will be constructed from anAobject viainit< A >(). Also,A‘s interface is not fully exposed to the Python objects, as no wrapper was defined for theA::n()function.This also works for types that are derived from
A. For example:However, duck-typing is not enabled.
Constructing
Bfrom an object that is convertible toA.To support the case where
Bcan be constructed from an object that provides a compatible interface, then custom converters are required. Although wrappers were not previously generated forA::n(), lets continue with the statement that an object can be converted toAif the object provides aget_num()method that returns anint.First, write an
A_from_pythonstruct that provides converter and constructors functions.boost::python::expect_non_nullis used to throw an exception ifNULLis returned. This helps provide the duck-typing guarantee that the python object must provide aget_nummethod. If thePyObjectis known to be an instance of given type, then it is possible to useboost::python::api::handleandboost::python::api::objectto directly extract the type, and avoid having to generically make calls through thePyObjectinterface.Next, register the converter in the module.
No changes have occurred to
A,B, or their associated wrapper definitions. The auto-conversion functions were created, and then defined/registered within the module.D::get_num()exists, and thusAis constructed from an instance ofDwhenDis passed toB‘s constructor. However,E::get_num()does not exists, and an exception is raised when trying to constructAfrom an instance ofE.An alternative conversion solution.
Implementing duck-typing via the C-API can become very complicated with larger types. An alternative solution is perform the duck-typing in python, and distribute the python file alongside the library.
example_ext.pywill import theAandBtypes, as well as monkey patchB‘s constructor:The only change required to the end-user is to import
example_extinstead ofexample:Since the patched constructor guarantees an instance of
Awill be passed toB, theA_from_python::constructwill not get invoked. Hence, the missing print statements in the output.While this approach avoids the C-API, making it easier to perform duck-typing, it does have one major trade-off in that it requires parts of the API to be specifically patched for conversions. On the other hand, no patching is required when auto-type conversion functions are available.
Also, for what it is worth, access control in both C++ and Python is intended to protect against accidental misuse. Neither protect against deliberately obtaining access to members with private visibility. It is much easier to do in Python, but it is specifically permitted in the C++ Standard through explicit template instantiations.