Apprendre à interfacer C++ et Python avec Boost.Python

De nombreuses méthodes existent pour interfacer Python et le C/C++, le choix d'une méthode particulière dépendant principalement de la taille et de la complexité des codes que vous essayez d'interfacer. Par complexité croissante, je recommanderais en premier lieu les ctypes pour un interfaçage rapide mais pas très propre avec un lot de fonctions C. Pour traiter quelques classes C++, la meilleure approche est probablement le populaire SWIG (« Simple Wrapper Interface Generator »). Maintenant, si vous voulez interfacer une bibliothèque C++ entière, une des options les plus puissantes est Boost.Python, qui est la solution que j'introduis dans cetutoriel. Vous trouverez une comparaison des avantages respectifs de SWIG et Boost.Python sur ce wiki LSST. Je devrais également citer pybind11, une alternative plus récente et activement développée de Boost.Python.

Dans ce tutoriel, nous étudierons les sujets suivants :

  • encapsuler manuellement une classe C++ simple en utilisant Boost.Python ;
  • compiler un projet Boost.Python avec Cmake ;
  • générer automatiquement un wrapper Boost.Python avec Py++.

Vous pourrez trouver les sources de tous les exemples présentés ici sur GitHub.

Commentez Donner une note  l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Interfacer Hello World

La première partie de ce tutoriel montre comment écrire un wrapper Boost.Python simple pour une classe C++, et peut-être plus important encore, comment tout avoir en main pour compiler et lier le tout convenablement.

I-A. Classe C++ simple

Pour commencer, considérons une classe C++ très basique, que nous mettrons dans Bonjour.hpp :

Bonjour.hpp
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
#include <iostream>
#include <string>

class Bonjour
{
    // Attributs privés
    std::string m_msg;
public:
    // Constructeur
    Bonjour(std::string msg):m_msg(msg) { }

    // Méthodes
    void greet() { std::cout << m_msg << std::endl; }

    // Accesseurs/Mutateurs pour les attributs
    void set_msg(std::string msg) { this->m_msg = msg; }
    std::string get_msg() const { return m_msg; }
};

I-B. Interface Boost.Python

Notre objectif sera d'utiliser Boost.Python pour inclure cette classe dans un module, que nous appellerons pylib, qui sera importable directement depuis Python. Pour cela, Boost.Python fournit une API C++ qui nous permet de déclarer les classes et les fonctions que nous souhaitons exporter vers Python. Ces déclarations sont faites dans un fichier d'interface .cpp, que nous appellerons pylib.cpp :

pylib.cpp
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#include <boost/python.hpp>
#include "Bonjour.hpp" 

using namespace boost::python;

BOOST_PYTHON_MODULE(pylib)
{
    class_< Bonjour >("Bonjour", init<std::string>())
    .def("greet", &Bonjour::greet)
    .add_property("msg", &Bonjour::get_msg, &Bonjour::set_msg);
}

Décryptons ce qui se produit ici. La macro BOOST_PYTHON_MODULE déclare un module Python qui sera nommé pylib. Nous pourrons ajouter par la suite des classes et des fonctions à ce module en ajoutant les déclarations respectives entre les parenthèses.

Pour ajouter une classe, on crée un nouvel objet class_<>. Le modèle de cet objet est la classe que nous voulons exporter ; dans notre cas, class_<Bonjour> va encapsuler notre classe C++ vers Python. Le premier argument du constructeur de class_<>() est le nom que nous voulons utiliser pour cette classe dans Python ; ici on utilisera le même qu'en C++, « Bonjour », mais ce n'est pas obligatoire. Le second argument est utilisé pour définir quel constructeur C++ utiliser. Ici nous n'avons qu'un seul constructeur C++, mais une classe C++ peut en avoir plusieurs qui diffèrent de par leur prototype. Dans le but d'identifier celui à exporter, nous utilisons l'objet init<> dont le modèle correspond au prototype du constructeur, ici un simple argument de type std::string.

Déclarer une classe de cette manière créera simplement une classe Python vide : nous devons ensuite déclarer les méthodes et les attributs que nous voulons exporter du C++. Pour ajouter une méthode, on utilise la fonction def(), qui prend en premier argument le nom de la méthode Python, et en second argument une référence vers la méthode C++ actuelle. Enfin, on ajoute une propriété à notre classe Python à l'aide de add_property(), afin de pouvoir interroger et modifier le contenu du message d'accueil. Le premier argument de cette fonction est le nom de la propriété Python, les arguments suivants sont des références à l'accesseur et au mutateur de notre implémentation C++.

Voir cette page pour plus d'informations sur la manière d'exporter des classes.

I-C. Compilation avec CMake

Maintenant que le fichier d'interface est écrit, il faut le compiler. Ce processus de compilation implique en particulier un lien vers les bibliothèques Python et Boost.Python, ce qui peut s'avérer très pénible (en particulier sous MacOS X). Afin d'avoir une compilation aussi simple que possible, je vais utiliser CMake pour repérer le compilateur C++ par défaut, l'interpréteur Python, et télécharger et générer les modules Boost requis.

Il faut en premier lieu installer CMake. Sur une machine Ubuntu cela donne :

 
Sélectionnez
$ sudo apt-get install cmake

Vous pouvez aussi installer n'importe quel autre gestionnaire de paquets disponible sur votre système. Si exécuté sur un cluster, vous pouvez avoir à l'installer localement depuis les sources, suivre ces instructions.

Maintenant, pour compiler notre module, il faut écrire un fichier CMakeLists.txt, qui spécifie les bibliothèques nécessaires à la compilation et définit les bibliothèques ou les exécutables à générer :

CMakeLists.txt
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
cmake_minimum_required(VERSION 2.8)
include(ExternalProject)

# Ajout d'un module cmake personnalisé pour générer boost
list(APPEND CMAKE_MODULE_PATH  "${CMAKE_SOURCE_DIR}/cmake/Modules/")

project (tutorial)

    # Trouver les bibliothèques et interpréteurs python par défaut
    find_package(PythonInterp REQUIRED)
    find_package(PythonLibs REQUIRED)
    include(BuildBoost) # module personnalisé

    include_directories(${Boost_INCLUDE_DIR} ${PYTHON_INCLUDE_DIRS})
    link_directories(${Boost_LIBRARY_DIR})

    # Génère et lie le module pylib
    add_library(pylib SHARED pylib.cpp)
    target_link_libraries(pylib ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})
    add_dependencies(mylib Boost)

    # Ajuste le nom de la bibliothèque pour coller à ce qu'attend Python
    set_target_properties(pylib PROPERTIES SUFFIX .so)
    set_target_properties(pylib PROPERTIES PREFIX "")

Pour que ce fichier fonctionne, vous devez copier localement mon module CMake personnalisé pour Boost, et le placer dans un dossier nommé cmake/Modules dans le répertoire courant. Ce fichier indique à CMake de compiler notre interface pylib.cpp dans une bibliothèque qui sera nommée pylib.so. Notez qu'il n'est pas nécessaire pour vous d'avoir Boost installé sur votre système, il sera téléchargé et compilé automatiquement par CMake afin de correspondre à votre configuration.

Dernière étape, la compilation :

 
Sélectionnez
$ mkdir build    # Créé un fichier pour CMake pour faire cela
$ cd build
$ cmake ..       # Lance la configuration CMake et génère un MakeFile
$ make pylib     # Génère la bibliothèque

Et c'est tout. Ceci devrait générer un fichier nommé pylib.so, qui est un module Python directement chargeable depuis l'interpréteur :

pylib.so
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
In [1]: from pylib import Bonjour

In [2]: b = Bonjour("Hello World")

In [3]: b.greet()
Hello World

In [4]: b.msg = "Bonjour tout le monde" 

In [5]: b.greet()
Bonjour tout le monde

In [6]: b.msg
Out[6]: 'Bonjour tout le monde'

Voilà l'idée générale pour écrire et interfacer avec CMake. Voir la documentation Boost.Python pour plus de détails.

II. Génération automatique d'interface avec Py++

On peut cependant remarquer à l'aide de l'exemple ci-dessus que pour encapsuler une bibliothèque C++ entière avec Boost.Python, il faut déclarer chaque méthode et propriété de chaque classe C++, ce qui peut demander beaucoup de travail. Heureusement, ce travail ne doit pas obligatoirement être réalisé par un humain. On peut en effet avoir recours à des générateurs de code automatiques, qui vont analyser vos fichiers C++ et créer automatiquement les déclarations Boost.Python correspondantes.

Le générateur d'interface officiel pour Boost.Ptyhon était auparavant Pyste : il n'est malheureusement plus maintenu et il a été supprimé des versions actuelles de Boost. Il a été remplacé par un projet différent nommé Py++.

Attention cependant : installer et utiliser Py++ en 2017 n'est pas une mince affaire, car la plupart de la documentation et des ressources que vous trouverez ne sont pas à jour. La documentation officielle fait référence à une branche stable du projet, depuis 2008. Heureusement, le développement de Py++ n'est pas totalement à l'arrêt : une version à jour est maintenue en tant que partie d'Open Motion Planning Library OMPL.

II-A. Installations requises

Py++ repose sur CastXML (anciennement GCC-XML), un outil qui analyse les fichiers d'en-tête C++ dans un arbre XML, et sur pygccxml pour générer les fichiers de sortie XML.

CastXML est disponible en tant que paquet au moins pour Ubuntu 16.04+ et MacPorts. Pour l'installer :

 
Sélectionnez
$ sudo apt-get install castxml

Pygccxml peut être installé avec PyPl en utilisant pip :

 
Sélectionnez
$ pip install --user pygccxml

Enfin, une version de Py++ peut être installée depuis l'OMPL bitbucket repository (depuis avril 2017) :

 
Sélectionnez
$ pip install --user https://bitbucket.org/ompl/pyplusplus/get/1.7.0.zip

II-B. Écrire un script Py++

Py++ est une bibliothèque que vous pouvez utiliser dans un code Python pour analyser les fichiers sources de votre projet C++ et générer l'interface adéquate. Voici un exemple d'un tel script, qui va créer automatiquement l'interface pour votre classe C++ dans un nouveau module nommé pylib_auto (pour le différentier du précédent). Nous appellerons ce script pylib_generator.py :

pylib_generator.py
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
#!/usr/bin/python
from pygccxml import parser
from pyplusplus import module_builder

# Configurations que vous pouvez avoir à changer sur votre système
generator_path = "/usr/bin/castxml"
generator_name = "castxml"
compiler = "gnu"
ompiler_path = "/usr/bin/gcc" 

# Créé une configuration pour CastXML
xml_generator_config = parser.xml_generator_configuration_t(
    xml_generator_path=generator_path,
    xml_generator=generator_name,
    compiler=compiler,
    compiler_path=compiler_path)

# Liste de tous les fichiers d'en-tête de votre bibliothèque
header_collection = ["Bonjour.hpp"]

# Analyse les fichiers sources et créé un objet module_builder
builder = module_builder.module_builder_t(
    header_collection,
    xml_generator_path=generator_path,
    xml_generator_config=xml_generator_config)

# Détecte automatiquement les propriétés et les accesseurs/mutateurs associés
builder.classes().add_properties(exclude_accessors=True) 

# Définit un nom pour le module
builder.build_code_creator(module_name="pylib_auto")

# Écrit le fichier d'interface C++
builder.write_module('pylib_auto.cpp')

Ce code est explicite par lui-même : la partie la plus importante est la création du module_builder qui prend une liste de fichiers d'en-tête C++ à analyser. Voir la documentation Py++ pour plus d'information sur la manière de personnaliser finement le processus (ce qui peut être nécessaire pour des projets plus complexes).

Nous pouvons désormais analyser notre code source en exécutant ce script :

 
Sélectionnez
$ python pylib_generator.py

Si tout ce passe comme prévu, ce script devrait créer un nouveau fichier d'interface pylib_auto.cpp. Pour compiler ce nouveau module, ajoutons simplement une nouvelle section à la fin de notre fichier CMakeLists.txt :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
# Génère et lie le module pylib_auto
add_library(pylib_auto SHARED pylib_auto.cpp)
target_link_libraries(pylib_auto ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})
add_dependencies(pylib_auto Boost)

# Ajuste le nom de la bibliothèque pour coller à ce qu'attend Python
set_target_properties(pylib_auto PROPERTIES SUFFIX .so)
set_target_properties(pylib_auto PROPERTIES PREFIX "")

Il nous faut mettre à jour la configuration de CMake et compiler :

 
Sélectionnez
1.
2.
3.
$ cd build
$ cmake ..
$ make pylib_auto

Nous pouvons désormais tester notre module nouvellement créé :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
In [1]: from pylib_auto import Bonjour

In [2]: b = Bonjour("Hello World")

In [3]: b.greet()
Hello World

In [4]: b.msg = "Bonjour tout le monde"

In [5]: b.greet()
Bonjour tout le monde

In [6]: b.msg
Out[6]: 'Bonjour tout le monde'

Et c'est tout, nous avons créé une interface pour notre code sans rien spécifier manuellement, et le comportement est le même.

III. Pour aller plus loin

L'objectif de ce tutoriel était d'illustrer les bases de Boost.Python : ce n'était qu'un survol. Malheureusement, la documentation autour de Boost.Python est particulièrement mince, mais voici quelques liens qui pourraient vous être utiles :

Oh, et ai-je mentionné que Boost.Python avait désormais un support officiel pour l’interfaçage avec Numpy, via Boost.Numpy ?

IV. Remerciements

Nous remercions Cosinus(x) pour la traduction de ce tutoriel, Pyramidev pour sa relecture technique et naute pour sa relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 François Lanusse. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.