Dans le cadre des travaux d'interfaçage de l'application Code_TYMPAN avec du code Python, nous avons réalisé l'étude ci-dessous sur les différents moyens de générer des liaisons Python pour du code C++. Cette étude n'a pas vocation à être exhaustive et s'est concentrée sur les aspects qui nous intéressaient directement pour les travaux susmentionnés.

Solutions existantes

Une recherche des solutions existantes a été effectuée, qui a permis d'obtenir la liste suivante pour une écriture manuelle du code d'interfaçage :

  • Cython, un langage de programmation inspiré de Python, basé sur Pyrex
  • Boost.Python, une librairie C++ de la collection Boost permettant d'écrire des liaisons Python
  • PyBindGen, un outil implémenté en Python et permettant de décrire des liaisons C++ directement dans ce langage
  • Swig, un outil permettant de générer des liaisons C++ pour plusieurs langages de programmation
  • Shiboken, un générateur de code d'enrobage pour des bibliothèques C/C++ basé sur du CPython

Des solutions existent pour automatiser cette écriture. Ce sont des outils qui se basent sur des compilateurs (gcc, clang) pour faire l'analyse grammaticale du code C++ et générer le code d'interfaçage correspondant. Par exemple :

  • XDress, qui permet de générer des fichiers Cython (.pyx, .pxd) à partir de gcc-xml ou de libclang
  • PyBindGen dispose de fonctionnalités permettant de générer des liaisons python à partir de gcc
  • Ce billet explique comment utiliser libclang pour parcourir l'AST d'un code C++ et générer des liaisons Boost.Python

Aspects pris en compte

Cet article est intéressant car il aborde de façon très complète les problématiques découlant de l'écriture de liaisons C++ pour des langages de haut niveau. Il a été écrit lors des travaux de développement de Shiboken.

Dans notre cas, les critères pour le choix d'une solution finale portaient sur différents aspects :

  • Le coût de développement : prise en main de l'outil, quantité de code à écrire pour enrober une classe C++ donnée, coût de l'intégration dans le système de build, degré d'automatisation de la solution, lisibilité du code généré, etc.
  • La gestion de la mémoire : comptage de référence, gestion de la propriété des objets
  • La qualité et l'exhaustivité du support C++ : compatibilité STL, gestion des références et pointeurs, des templates, surcharges d'opérateurs, etc.
  • La pérennité de la solution : technologies mises en œuvre par l'outil, qualité de la documentation, support, taille et degré d'activité de la communauté de développeurs

Solutions envisagées

Swig n'a pas été retenu partant de l'a priori que c'était une solution plutôt lourde et davantage orientée C que C++, constat tiré lors de travaux réalisés par Logilab il y a quelques mois de cela. La solution Boost.Python n'a pas été explorée car notre souhait était de nous rapprocher davantage du Python que du C++. Shiboken semble prometteur, bien que peu documenté et mal référencé (les premières recherches tombent sur d'anciennes pages du projet, donnant l'impression que la dernière release date d'il y a plusieurs années, alors qu'en fait, non). Il a été écarté par manque de temps.

PyBindGen et Cython ont fait l'objet de tests.

La cible des tests a été l'interfaçage de smart pointers, puisque cela correspond à un de nos besoins sur le projet Code_TYMPAN.

Les essais ont été réalisés sur des classes simplifiées:

  • MyElement, classe qui représente un élément à encapsuler dans un smart pointer et hérite de IRefCount qui implémente un comptage de référence
  • SmartPtr, classe smart pointer "maison" de l'application
  • Quelques fonctions de test manipulant des smart pointers SmartPtr

Voici un extrait des en-têtes du code C++:

#ifndef MY_ELEMENT_H
#define MY_ELEMENT_H
#include <iostream>
using namespace std;
#include "SmartPtr.h"

class MyElement : public IRefCount
{
    public:
        MyElement ();
        MyElement (string);
            string Name(){ return _name; }
            virtual ~MyElement ();

    protected:
        string _name;
};
typedef SmartPtr<MyElement> SPMyElement;
#endif

#ifndef SMART_PTR_H
#define SMART_PTR_H
template <class T> class SmartPtr
{
    public:
        SmartPtr();
        SmartPtr(T*);
        const T* getRealPointer() const;

    protected:
        T* _pObj;
}
#endif

SPMyElement BuildElement();
void UseElement(SPMyElement elt);

Cython

Cet outil offre maintenant un bon support du C++ (globalement depuis la version 0.17). Son avantage est qu'il permet la manipulation d'objets à la fois C++ et Python dans des fichiers Cython.

Utilisation
  • Écriture (facultative) d'un fichier .pxd qui contient une recopie des headers à enrober (avec un lien vers les fichiers): déclarations des types, classes, fonctions...
  • Écriture d'un fichier .pyx qui contient des appels de fonctions, constructions d'objets C ou python. Les fonctions et classes de ce module sont utilisables depuis un script Python
  • Compilation du code Cython décrivant les interfaçages C++, génération et compilation du code C++ correspondant et production d'une librairie Python.

Cython offre un support pour les conteneurs de la STL, les templates, la surcharge de la plupart des opérateurs ("->" non supporté), le passage d'arguments par référence et par pointeur, etc.

Actuellement en version 0.20.1, la dernière release date du 11 février 2014. Les outils Cython sont relativement bien documentés et sa communauté de développeurs est active.

Exemple

Voici le code d'interfaçage Cython correspondant à l'exemple exposé ci-dessus:

setup.py:

from distutils.core import setup
from Cython.Build import cythonize

setup(name='smartptr',
    ext_modules=cythonize('*.pyx',
        ),
)

smartptr.pxd:

from libcpp.string cimport string

cdef extern from "src/SmartPtr.h":
    cdef cppclass SmartPtr[T]:
        SmartPtr()
        SmartPtr(T *)
        T *getRealPointer() # Pas de surcharge de ->. L'accès à l'objet ne peut être qu'explicite

cdef extern from "src/MyElement.h":
    cdef cppclass MyElement:
        MyElement()
        MyElement(string)
        string Name()

cdef extern from "src/Test.h":
    SmartPtr[MyElement] BuildSPElement()
    void UseSPElement(SmartPtr[MyElement])

smartptr.pyx:

# distutils: language = c++
# distutils: libraries = element

cimport smartptr
cimport cython

cdef class PySPMyElement:
    cdef SmartPtr[MyElement] thisptr

    def __cinit__(self, name=""):
        """ PySPMyElement constructor """
        if name == "":
            self.thisptr = SmartPtr[MyElement](new MyElement())
        else:
            self.thisptr = SmartPtr[MyElement](new MyElement(name))

    def get_name(self):
        """ Returns the name of the element """
        return self.thisptr.getRealPointer().Name()

@cython.locals(elt=PySPMyElement)
def build_sp_elt():
    """ Calls the C++ API to build an element """
    elt = PySPMyElement.__new__(PySPMyElement)
    elt.thisptr = BuildSPElement()
    return elt

@cython.locals(elt=PySPMyElement)
def use_sp_elt(elt):
    """ Lends elt to the C++ API """
    UseSPElement(elt.thisptr)

XDress

XDress est un générateur automatique de code d'interfaçage C/C++ écrit en Python, basé sur Cython.

Utilisation
  • On liste dans un fichier xdressrc.py les classes et fonctions à envelopper (il n'est pas nécessaire de mettre la signature, le nom suffit. On peut choisir d'envelopper seulement certaines classes d'un .h).
  • On exécute xdress qui génère les .pyx et .pxd correspondants

XDress permet d'envelopper des conteneurs STL via son générateur stlwrap (les conteneurs à enrober doivent être listés dans le xdressrc.py). A titre d'exemple, les vecteurs sont convertis en numpy array du type contenu.

Ce projet est récent et pas très documenté, mais il semble prometteur.

PyBindGen

Utilisation
  • Écriture d'un script Python qui décrit les classes/fonctions C++ à enrober en s'appuyant sur le module PyBindGen (1) → permet de générer un fichier .cpp
  • Compilation du code C++ généré, avec la librairie du programme à envelopper et génération d'une librairie Python.

Ce processus peut être automatisé:

  • Écriture d'un script Python qui utilise les outils PyBindGen pour lister les modules (headers) à envelopper, les lire et lancer la génération automatique des liaisons c++

ou:

  • Écriture d'un script Python qui utilise les outils PyBindGen pour lister les modules (headers) à envelopper et générer le script Python décrit en (1) (ce qui permettrait une étape intermédiaire pour personnaliser les liaisons)

PyBindGen offre un support pour la STL, l'héritage (multiple), la gestion des exceptions C++ côté Python, la surcharge d'opérateurs, le comptage de référence, la gestion de la propriété des objets. Mais il supporte mal les templates.

Actuellement en version 0.17, la dernière release date du 15 février 2014 (entre autres ajout de la compatibilité Python 3.3).

Exemple

PyBindGen, en l'état, n'offre pas la possibilité d'envelopper simplement des templates, ni des smart pointers "maison" par extension.

Une classe de ce package permet d'envelopper des shared pointers de Boost (boost::shared_ptr). Il serait à priori possible de la modifier légèrement pour enrober les smart pointers de l'application Code_TYMPAN (non testé).

Voici néanmoins à titre d'exemple le code permettant d'envelopper la classe MyElement et des fonctions manipulant non plus des smart pointers mais des 'MyElement *'

Test.h :

MyElement *BuildElement();
void UseElement(MyElement *elt);

smartptr.py :

import pybindgen
import sys
from pybindgen import retval
from pybindgen import param

mod = pybindgen.Module('smartptr')

# File includes
mod.add_include('"src/MyElement.h"')
mod.add_include('"src/Test.h"')

# Class MyElement
MyElement = mod.add_class('MyElement')
MyElement.add_constructor([])
MyElement.add_method('Name', retval('std::string'), [])


# Test functions
# transfer_ownership=False : here Python program keeps the ownership of the element it passes to the C++ API
mod.add_function('UseElement', None, [param('MyElement *', 'elt', transfer_ownership=False)])
# caller_owns_return=True : here Python program will be responsible for destructing the element returned by BuildElement
mod.add_function('BuildElement', retval('MyElement *',  caller_owns_return=True), [])

if __name__ == '__main__':
    mod.generate(sys.stdout)

Boost.Python

Les liaisons Python s'écrivent directement en C++.

C'est un outil très fiable et pérenne, avec de par sa nature un très bon support C++ : gestion de la mémoire, templates, surcharges d'opérateurs, comptage de référence, smart pointers, héritage, etc.

Inconvénient : la syntaxe (en mode templates C++) n'est pas très intuitive.

Conclusion

Les solutions Cython et PyBindGen ont été explorées autour de la problématique d'enrobage de smart pointers. Il en est ressorti que:

  • Il est possible d'enrober facilement des smart pointers Code_TYMPAN en Cython. L'approche qui a été choisie est de manipuler depuis Python les objets C++ directement au travers de smart pointers (les objets Python contenus dans le .pyx encapsulent des objets SmartPtr[T *], agissant donc comme des proxys vers les objets). De cette façon, l'utilisation depuis Python d'un objet C++ incrémente le compteur de référence côté C++ et cela garantit qu'on ne perdra pas la référence à un objet au cours de son utilisation côté Python. Un appel à getRealPointer() pour enrober des fonctions manipulant directement des T * sera toujours possible dans le code Cython au besoin.
  • PyBindGen présente l'intérêt d'offrir des moyens de gérer l'attribution de la propriété des éléments entre C++ et Python (transfer_ownership, caller_owns_return). Malheureusement, il n'offre pas la possibilité d'enrober des smart pointers sans modification de classes PyBindGen, ni d'envelopper des templates.

Par ailleurs, après utilisation de PyBindGen, il nous a semblé que bien qu'il présente des idées intéressantes, sa documentation, ses tutoriels et son support sont trop succints. Le projet est développé par une seule personne et sa viabilité est difficile à déterminer. Cython en revanche offre un meilleur support et plus de fiabilité.

Le choix final s'est donc porté sur Cython. Il a été motivé par un souci d'utiliser un outil fiable limitant les coûts de développement (élimination de PyBindGen), aussi proche de Python que possible (élimination de Boost.Python). Cet outil semble fournir un support C++ suffisant par rapport à nos besoins tels que perçus à ce stade du projet.

De plus si on cherche un moyen de générer automatiquement les liaisons Python, XDress présente l'avantage de permettre l'utilisation de libclang comme alternative à gcc-xml (PyBindGen est basé sur gcc-xml uniquement). Une possibilité serait par ailleurs d'utiliser XDress pour générer uniquement le .pxd et d'écrire le .pyx manuellement.

Une question qui n'a pas été abordée au cours de cette étude car elle ne correspondait pas à un besoin interne, mais qui est néanmoins intéressante est la suivante: est-il possible de dériver depuis Python des classes de base définies en C++ et enveloppées en Cython, et d'utiliser les objets résultants dans l'application C++ ?

blog entry of