coriolis/documentation/content/pages/python-cpp-new/Implementation.rst

653 lines
26 KiB
ReStructuredText

.. -*- Mode: rst -*-
2. Implementation
=================
We do not try to provides an iterface as sleek as ``pybind11`` that completely
hides the Python/C API. Instead we keep mostly visible the classic structure of
the Python/C API but we provides templates to automate as much as possible the
boring tasks (and code duplication). This way, if we need a very specific
feature at some point, we can still revert back to the pure Python/C API.
The wrapper basically provides support for three kind of operations:
1. Encapsulate C++ object *in* Python ones, done by ``cToPy<>()`` template.
Template specializations are defined for the ``POD`` and basic ``STL``
types like ``std::string``.
To add more easily new specializations, they resides in the top level
scope (**not** inside ``Isobar``).
2. Decapsulate a C++ object *from* a Python one, done by ``pyToC()``.
It's implementation is slightly different from the one of ``cToPy<>()``
in the sense that it is a mix of normal C++ functions and template
functions. I was having trouble inside the ``callMethod()`` to access
to templates specialization defined *after* that point, so function be
it.
There are two mutually exclusives versions of the ``pyToC<>()`` for
objects managed through the type manager. One is for value types and
the other for pointer types.
3. Wrap C/C++ functions & methods inside C-linkage ``PyCFunction``, that is
``PyOject* (*)(PyObject*, PyObject*)``. This is done respectively through
``callFunction<>()`` and ``callMethod<>()``. ``callMethod<>()`` having
two variants, one for directly calling the function member, if there is
no overload and one for calling one given flavor of the overloaded
function member.
In addition we provides special variants for Python unary, binary and
operator functions.
In addition to those user exposed features, we have:
* The ``PyTypeManager`` and it's derived classes to store and share informations
about all our newly defined ``PyTypeObjects``.
* Various wrapper *classes* to wrap functions & methods. They are not directly
exposed because the template class intanciation needs the template parameters
to be explicitely given, wich is tedious. Instead we create them *through*
a function template call, which will perform for us the template type
deduction.
We creates only two kind of ``PyObject`` (but many ``PyTypeObject``):
* ``PyOneVoid`` which encapsulate one void pointer to the C++ associated
object.
.. code-block:: Python
extern "C" {
typedef struct PyOneVoid {
PyObject_HEAD
void* _object1;
};
}
* ``PyTwoVoid`` which encapsulate one void pointer to the C++ associated
object (an iterator) and one another to the ``PyObject`` of the container.
.. code-block:: Python
extern "C" {
typedef struct PyTwoVoid {
PyObject_HEAD
void* _object1; // C++ iterator.
PyObject* _object2; // Python wrapped container.
};
}
.. note:: A ``PyTwoVoid`` can be casted/accessed as a ``PyOneVoid``.
2.1 PyTypeManager
~~~~~~~~~~~~~~~~~
``PyTypeManager`` has two usage:
* Act as a registry of all the created ``PyTypeObject``, and serve as a
dispatcher for the ``PyTypeObject`` *tp* like methods.
* Provide a non-template abstract base class for all the derived ``PyTypeObject``.
As said, it is not a template class but it supplies function member
template. Derived classes are provided to manage different kind of C++
classes.
* :cb:`PyTypeManagerVTrunk<CppT>`
Is an intermediate between the non-template base class and all the
templatized others (do **not** use it directly).
* :cb:`PyTypeManagerNonDBo<CppT>`
Template for standalone C++ classes that are not derived from ``DBo``.
For example ``Box`` or ``Parameter``.
* :cb:`PyTypeManagerDBo<CppT>`
Template for C++ classes that *are* not derived from ``DBo``.
For example ``Cell`` or ``Instance``.
* :cb:`PyTypeManagerDerivedDBo<CppT,BaseT>`
Template for a ``CppT`` class derived derived from a ``BaseT`` class.
``CppT`` doesn'y need to be a direct derived of ``BaseT``. ``BaseT``
needs to derive from ``DBo``
* :cb:`PyTypeManagerVector<CppT>`, template for C++ ``std::vector<CppT*>``.
* :cb:`PyTypeManagerVectorIterator<CppT>`
Template for C++ ``std::vector<CppT*>::iterator``, automatically created
from the vector registration.
* :cb:`PyTypeManagerMap<CppK,CppT>`, template for C++ ``std::map<CppK*,CppT*>``.
* :cb:`PyTypeManagerMapIterator<CppK,CppT>`
Template for C++ ``std::vector<CppK*,CppT*>::iterator``, automatically created
from the map registration.
* :cb:`PyTypeManagerCollection<,CppT>`, template for C++ ``Hurricane::Collection<CppT*>``.
* :cb:`PyTypeManagerCollectionIterator<,CppT>`
Template for C++ ``Hurricane::Locator<CppT*>``, automatically created from
the collection registration.
2.2 Highjacking the *tp* methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Functions of a ``PyTypeObject`` like the *tp* methods (``tp_alloc``, ``tp_print``,
``tp_hash``, ...) must have a C-linkage. So we create *one* function per slot that
we want to use, set that *same* function for all the created ``PyTypeObject``, and
perform a dispacth in it. The drawback is that for each access we have to perform
a map lookup. Hope it is fast.
Excerpt from the code:
.. code-block:: C++
namespace Isobar3 {
extern "C" {
// Here we have C-linkage.
extern long _tpHash ( PyObject* self )
{
// Dispatch towards the relevant class, based on ob_type pointer.
return PyTypeManager::get( Py_TYPE(self)->_getTpHash( self );
}
}
class PyTypeManager {
public:
void PyTypeManager::_setupPyType ()
// Derived classes must implement it as they see fit.
virtual long _getTpHash ( PyObject* ) = 0;
template<typename CppT>
static PyTypeManager* _get();
private:
PyTypeObject _typeObject;
};
void PyTypeManager::_setupPyType ()
{
PyTypeObject* ob_type = _getTypeObject();
ob_type->tp_name = _getPyTypeName().c_str();
ob_type->tp_dealloc = (destructor)&::Isobar3::_tpDeAlloc;
ob_type->tp_str = (reprfunc) &::Isobar3::_tpStr;
// All Python Type will call the same _tpHash().
ob_type->tp_hash = (hashfunc) &::Isobar3::_tpHash;
ob_type->tp_compare = (cmpfunc) &::Isobar3::_getTpCompare;
ob_type->tp_methods = _getMethods();
ob_type->tp_getset = _getGetsets();
}
} // Isobar3 namespace.
2.3 Going From Python to C++
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To convert a C++ object (pointer) into a Python object, a mix of
:cb:`pyToC<>()` templates functions and real functions are supplieds.
Templates functions are used for all types/classes managed through
the ``PyTypeManger``. They come in two flavors:
2. **Value as pointer version** (C++ argment type is ``T*``):
The encapsulated C++ object is still a pointer,
but to a *stand-alone* one which has been created for the sole
purpose of this ``PyObject``. Typical example is the ``Box``.
Then, we *copy by value* the contents of the pointed object into
the contents of the pointer argument that we where given.
3. **Pointer version** (C++ argument type is ``T**``):
The encapsulated C++ object is truly a pointer
to an element of the data-structure, then we just extract the
C++ pointer value.
Normal function overload are used for ``POD`` types (``bool``, ``int``,
``long``, ``double``, ...) and basic ``STL`` types (``std::string``, ...).
Specialization for all POD type that can be directly translated into
Python types must be provideds (``bool``, ``int``, ``long``, ``double``,
``std::string``, ...).
Those templates/functions are the ones the ``Isobar::parse_objects()`` recursive
template function call in turn for each ``PyObject*`` argument.
.. note:: ``Hurricane::Name`` are *not* exposed to the Python interface, they
must be treated as ``std::string``.
.. code-block:: C++
// Template/Pointer to a value flavor.
template< typename T
, typename std::enable_if< !std::is_pointer<T>::value, bool >::type = true >
inline bool pyToC ( PyObject* pyArg, T* arg )
{
typedef typename std::remove_cv<T>::type NonConstT;
Isobar3::PyTypeManager* manager = Isobar3::PyTypeManager::_get<T>();
if (not manager) {
std::cerr << "Isobar3::pyToC<>(const T*): Unsupported type." << std::endl;
return false;
}
if (Py_TYPE(pyArg) != manager->_getTypeObject()) return false;
*(const_cast< NonConstT* >(arg)) = *(( T* )( Isobar3::object1( pyArg )));
return true;
}
// Template/Pointer to a pointer flavor.
template<typename T>
inline bool pyToC ( PyObject* pyArg, T** arg )
{
Isobar3::PyTypeManager* manager = Isobar3::PyTypeManager::_get<T>();
if (not manager) {
std::cerr << "Isobar3::pyToC<T>(T*&): Unsupported type \"" << typeid(T).name() << "\"" << std::endl;
return false;
}
*arg = (T*)( Isobar3::object1( pyArg ));
return true;
}
// True function overload for std::string.
inline bool pyToC ( PyObject* pyArg, std::string* arg )
{
if (not PyUnicode_Check(pyArg)) return false;
PyObject* pyBytes = PyUnicode_AsASCIIString( pyArg );
*arg = PyBytes_AsString( pyBytes );
Py_DECREF( pyBytes );
return true;
}
// True function overload for bool.
inline bool pyToC ( PyObject* pyArg, bool* arg )
{
if (not PyBool_Check(pyArg)) return false;
*arg = (pyArg == Py_True);
return true;
}
// True function overload for int.
inline bool pyToC ( PyObject* pyArg, int* arg )
{
if (PyLong_Check(pyArg)) { *arg = PyLong_AsLong( pyArg ); }
else return false;
return true;
}
2.4 Going From C++ to Python
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To convert a Python object into a C++ object, a set of
:cb:`cToPy<>` templates functions are supplieds.
We completely disable the partially specialized templates for
objects that are non-POD as the compiler seems to be unable to
choose the fully specialized template in this case (or I still
misunderstood the template resolution mechanism).
In the case of object registered in ``PyTypeManager``, we delegate
the ``PyObject`` creation to the ``PyTypeManager::link()`` template
function, which in turn, can call the right ``PyTypeManagerVTrunk<CppT>::_link()``
method.
.. note:: The ``PyTypeManagerVTrunk<CppT>::_link()`` method is the reason
**why** we need the intermediate ``PyTypeManagerVTrunk<CppT>``
template class.
.. note:: **Different C++ templates.** You may notice that the two following templates
may look like specializations of the same one:
* ``template<typename CppT> PyObject* cToPy ( CppT object )``
* ``template<typename CppT> PyObject* cToPy ( CppT* object )``
Which would be illegal (function templates are not allowed to have *partial*
specialization), but they are *not*. The two pairs
``(template parameter,function parameter)``, that is ``(CppT,CppT)`` and
``(CppT,CppT*)`` cannot be made to be a specialization of each other.
.. code-block:: C++
// Generic template for values.
template< typename CppT >
inline PyObject* cToPy ( CppT object )
{
if (not Isobar3::PyTypeManager::hasType<CppT>()) {
std::string message = "Overload for Isobar3::cToPy< "
+ Hurricane::demangle(typeid(CppT).name()) + " >() Type not registered in the manager.";
PyErr_SetString( Isobar3::HurricaneError, message.c_str() );
return NULL;
}
return Isobar3::PyTypeManager::link<CppT>( new CppT (object) );
}
// Disabled for POD & STL types, pointer flavor.
template< typename CppT
, typename std::enable_if< !std::is_same<CppT,bool>::value
&& !std::is_same<CppT,int >::value
&& !std::is_same<CppT,std::string>::value
&& !std::is_same<CppT,const std::string>::value,bool>::type = true >
inline PyObject* cToPy ( CppT* object )
{ return Isobar3::PyTypeManager::link<CppT>( object ); }
// Disabled for POD & STL types, const pointer flavor.
template< typename CppT
, typename std::enable_if< !std::is_same<CppT,bool>::value
&& !std::is_same<CppT,int >::value
&& !std::is_same<CppT,std::string>::value
&& !std::is_same<CppT,const std::string>::value,bool>::type = true >
inline PyObject* cToPy ( const CppT* object )
{ return Isobar3::PyTypeManager::link<CppT>( const_cast<CppT*>( object )); }
// Specialization for booleans.
template<>
inline PyObject* cToPy<bool> ( bool b )
{ if (b) Py_RETURN_TRUE; Py_RETURN_FALSE; }
// Specialization for STL std::string.
template<> inline PyObject* cToPy< std::string> ( std::string s ) { return PyUnicode_FromString( s.c_str() ); }
template<> inline PyObject* cToPy<const std::string > ( const std::string s ) { return PyUnicode_FromString( s.c_str() ); }
template<> inline PyObject* cToPy<const std::string*> ( const std::string* s ) { return PyUnicode_FromString( s->c_str() ); }
// Specialization for POD int.
template<> inline PyObject* cToPy< int > ( int i ) { return PyLong_FromLong( i ); }
template<> inline PyObject* cToPy<const int > ( const int i ) { return PyLong_FromLong( i ); }
template<> inline PyObject* cToPy<const int*> ( const int* i ) { return PyLong_FromLong( *i ); }
2.5 Object Methods Wrappers
~~~~~~~~~~~~~~~~~~~~~~~~~~~
One of the more tedious task in exporting a C++ interface towards Python is
to have wrap the C++ functions/methods into C-linkage functions that can be
put into the ``PyMethodDef`` table.
Basically, we want to fit:
* A C++ function or method with a variable number of arguments, each argument
having it's own type.
.. code-block:: C++
class Parameter {
// ...
public:
void addValue ( std::string s, int v );
// ...
};
* Into a ``PyCFunction`` prototype.
.. code-block:: C++
extern "C" {
typedef PyObject* ( *PyCFunction )( PyObject* self, PyObject* args );
}
Here, the C++ object is provided through the first argument and the
functions arguments through a *tuple* in second argument. In Python
wrappers, the tuple doesn't have any complex structure, it boils down
to a sequence of ``PyObject*`` (that must match the number of arguments
of it's C++ function conterpart).
So, the problem is to change a Python tuple which size is only kown at
*runtime* into a list of C/C++ parameters known at *compile time*.
I am not such an expert in template programming so I can find a *generic*
solution able to handle any number of parameters. Instead I did write
a set of templates managing the translation from zero to ten parameters.
I did delay that translation as much as possible so it happens very close
to the C++ function call and the duplicated code needed for each template
is kept to a minimum.
To translate the Python tuple into an ordered list (vector like) of C++
object *of different types*, the obvious choice should have been ``std::tuple<>``,
but I did encouter problems when the functions signature did contains
references. So to manage that I did implement:
* A ``BaseArg`` class and it's template derived ``Arg<T>`` to hold
one value of a type (more or less like ``std::any<>``).
The type of the value attribute of ``Arg<T>`` is ``T`` *stripped*
from reference and constness. This internal type is accessible
through ``Arg<T>::ValueT``.
* A template list of arguments ``Args<typename... Ts>`` analogous to
``std::tuple<>`` which holds a table of ``BaseArg`` to convert all the
arguments.
* A recursive template converter function ``parse_pyobjects<>``, which is
called through the ``Args<>::parse()`` function.
Another challenge is the return type. I distinguish three flavor of
return type:
* Function returning nothing (``void``).
* Function returning a value.
* Function returning a reference to a value.
* Function returning a pointer.
To uniformize the return type we create four templates ``_callMethodReturn<>()``
that takes whatever the C++ return type is, and turn it into a ``PyObject*``.
Except for the functions returning ``void``, we call ``cToPy<>()`` to
wrap the value. Given the return type of the method, only one template
will match. But as functions template do not allow partial specialization,
only one must be defined for that method (the one *matching* it's
return type), so we make the template mutually exclusives based on
the ``TR`` type (with the ``std::enable_if<>`` clause).
.. note:: In the various ``_callMethodReturn<>`` we have *two* sets for the
method parameters types : ``TArgsF...`` and ``TArgsW...``. This is to
allow a wider range of matching in the template as the type of the
arguments of the method (``TArgsF...``) may not *exactly* matches the
one passed by the wrapper (``TArgsW...``), typically the method has
a ``const`` parameter which is non-``const`` in the wrapper.
Here is an excerpt of the code:
.. code-block:: C++
// Flavor for "return by value" (seems to match std::is_object<>)
template< typename TC, typename TR, typename... TArgsF, typename... TArgsW
, typename std::enable_if< !std::is_reference<TR>::value
&& !std::is_pointer <TR>::value
&& !std::is_void <TR>::value,bool>::type = true >
inline PyObject* _callMethodReturn ( TR(TC::* method)(TArgsF...), TC* cppObject, TArgsW... args )
{
TR value = (cppObject->*method)( args... );
return cToPy( value );
}
// Flavor for "return by reference"
template< typename TC, typename TR, typename... TArgsF, typename... TArgsW
, typename std::enable_if< std::is_reference<TR>::value
&& !std::is_pointer <TR>::value
&& !std::is_void <TR>::value,bool>::type = true >
inline PyObject* _callMethodReturn ( TR(TC::* method)(TArgsF...), TC* cppObject, TArgsW... args )
{
TR rvalue = (cppObject->*method)( args... );
return cToPy( rvalue );
}
// Flavor for "return by pointer".
template< typename TC, typename TR, typename... TArgsF, typename... TArgsW
, typename std::enable_if<std::is_pointer<TR>::value,bool>::type = true >
inline PyObject* _callMethodReturn ( TR(TC::* method)(TArgsF...), TC* cppObject, TArgsW... args )
{
TR pvalue = (cppObject->*method)( args... );
return cToPy( pvalue );
}
// Flavor for "return void".
template< typename TC, typename TR, typename... TArgsF, typename... TArgsW
, typename std::enable_if<std::is_void<TR>::value,bool>::type = true >
inline PyObject* _callMethodReturn ( TR(TC::* method)(TArgsF...), TC* cppObject, TArgsW... args )
{
(cppObject->*method)( args... );
Py_RETURN_NONE;
}
// Make the translation call for a method without arguments.
template< typename TC, typename TR >
inline PyObject* _callMethod ( TR(TC::* method)(), TC* cppObject, Args<>& )
{ return _callMethodReturn<TC,TR>( method, cppObject ); }
// Make the translation call for a method one argument.
template< typename TC, typename TR, typename TA0 >
inline PyObject* _callMethod ( TR(TC::* method)(TA0), TC* cppObject, Args<TA0>& args )
{ return _callMethodReturn( method, cppObject, as<TA0>( args[0] ) ); }
// Make the translation call for a method two argument.
template< typename TC, typename TR, typename TA0, typename TA1 >
PyObject* _callMethod ( TR(TC::* method)(TA0,TA1), TC* cppObject, Args<TA0,TA1>& args )
{ return _callMethodReturn( method, cppObject, as<TA0>( args[0] ), as<TA1>( args[1] ) ); }
The complete work of translating the Python tuple into a ``Args<>`` is done inside
a dedicated template class ``PyWrapper`` and it's ``call()`` method.
C++ exceptions are catched and translated into Python ones.
* ``PyWrapper`` the base class wich handle both C++ and Python exceptions.
Provides the ``call()`` function which in turn wraps the ``_call()`` that
must be overloaded in derived classes.
* ``PyFunctionWrapper<>``, template derived class for C/C++ normal functions.
* ``PyMethodWrapper<>``, template derived class for C++ class methods.
Two flavors are supported, the real method and a function build upon a
method (first argument beaing the object itself). The later is used when
we need to desambiguate overloaded functions, we must create one *function*
per overload.
As a class template cannot guess the template parameters, we wrap them into a
function template which can perform the guess. The ``callMethod<>`` template function.
In the end, what the user can write is simply:
.. code-block:: C++
static PyObject* PyParameter_addValue ( PyObject* self, PyObject* args )
{ return callMethod("Parameter.addValue",&Parameter::addValue,self,args); }
PyMethodDef PyParameter_Methods[] =
{ { "isFile" , (PyCFunction)PyParameter_isFile , METH_NOARGS
, "Tells if this parameter (string) holds a file name." }
, { "addValue", (PyCFunction)PyParameter_addValue, METH_VARARGS
, "Add a new value to parameter of enumerated type." }
// ...
, {NULL, NULL, 0, NULL} /* sentinel */
};
2.6 Case of C++ overloaded functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This apply to both overloaded functions and functions with default arguments values.
In that case, the only solution is to create a set of different functions
with differents arguments to expose all the various signature of the function.
We then create a function wrapper that calls them in decreasing number of
parameters order.
.. note::
If something goes wrong in a ``callMethod()``, it returns ``NULL`` and
sets an error exception. If, say, the ``setString3()`` variant fails,
but ``setString2()`` succeed, it will clear the error and sets ``rvalue``
to something non-``NULL``.
You may also notice that the signature of an un-overloaded function is that
of a normal function, not a class method, with the object (aka C++ ``this``
passed as the first argument). So ``callMethod()`` and ``PyMethodWrapper``
support both case (through different constructors).
.. code-block:: C++
static bool setString1 ( Parameter* self, std::string value )
{ return self->setString(value); }
static bool setString2 ( Parameter* self, std::string value, unsigned int flags )
{ return self->setString(value,Configuration::getDefaultPriority(),flags); }
static bool setString3 ( Parameter* self
, std::string value
, unsigned int flags
, Parameter::Priority pri )
{ return self->setString(value,pri,flags); }
static PyObject* PyParameter_setString ( PyObject* self, PyObject* args )
{
PyObject* rvalue = callMethod("Parameter.setString",&setString3,self,args);
if (not rvalue) rvalue = callMethod("Parameter.setString",&setString2,self,args);
if (not rvalue) rvalue = callMethod("Parameter.setString",&setString1,self,args);
return rvalue;
}
PyMethodDef PyParameter_Methods[] =
{ { "isFile" , (PyCFunction)PyParameter_isFile , METH_NOARGS
, "Tells if this parameter (string) holds a file name." }
, { "setString", (PyCFunction)PyParameter_setString, METH_VARARGS
, "Set the parameter value as a string." }
// ...
, {NULL, NULL, 0, NULL} /* sentinel */
};
2.7 Wrapper for ordinary functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The same mechanic as for the object methods has been built for ordinary
functions. The top level wrapper beeing ``callFunction<>()`` ...
.. code-block:: C++
static PyObject* PyCfg_hasParameter ( PyObject* module, PyObject* args )
{ return callFunction("hasParameter",&hasParameter,args); }
static PyMethodDef PyCfg_Methods[] =
{ { "hasParameter", (PyCFunction)PyCfg_hasParameter, METH_VARARGS
, "Tells if a parameter exists already in the DB." }
// ...
, {NULL, NULL, 0, NULL} /* sentinel */
};
2.8 Object post-create hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~
By defining specialization of the ``pyTypePostModuleinit<>()`` function template,
you can add any post-treatment to a Python type object. Like adding sub-classes
or constants values.
In the following code, we add ``Priority`` as a sub-object of ``Parameter`` then
set some constant values in ``Priority``. This was we emulate the behavior of
the ``Priority`` ``enum``.
.. code-block:: C++
template<>
inline void pyTypePostInit<Cfg::Parameter> ( PyTypeObject* typeObject )
{
PyTypeManagerNonDBo<Cfg::Parameter::Priority>::create( (PyObject*)typeObject
, Cfg::PyParameterPriority_Methods
, NULL
, PyTypeManager::NoCppDelete );
}
template<>
inline void pyTypePostInit<Cfg::Parameter::Priority> ( PyTypeObject* typeObject )
{
// Parameter::Priority enum.
addConstant( typeObject, "UseDefault" , Cfg::Parameter::UseDefault );
addConstant( typeObject, "ApplicationBuiltin", Cfg::Parameter::ApplicationBuiltin );
addConstant( typeObject, "ConfigurationFile" , Cfg::Parameter::ConfigurationFile );
addConstant( typeObject, "UserFile" , Cfg::Parameter::UserFile );
addConstant( typeObject, "CommandLine" , Cfg::Parameter::CommandLine );
addConstant( typeObject, "Interactive" , Cfg::Parameter::Interactive );
}