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

François Lanusse

Traducteur

Cosinus(x)

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 ce tutoriel. 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

- Titre : Apprendre à interfacer C++ et Python avec Boost.Python

- Auteur : François Lanusse

- Traducteur : Cosinus(x)

- Parution : 15 novembre 2018

- Licence : 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 et 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.

Sommaire

- I. Interfacer Hello World

- I-A. Classe C++ simple

- I-B. Interface Boost.Python

- I-C. Compilation avec CMake

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

- II-A. Installations requises

- II-B. Écrire un script Py++

- III. Pour aller plus loin

- IV. Remerciements

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

1. #include <iostream>

2. #include <string>

4. class Bonjour

5. {

6. // Attributs privés

7. std::string m_msg;

8. public:

9. // Constructeur

10. Bonjour(std::string msg):m_msg(msg) { }

12. // Méthodes

13. void greet() { std::cout << m_msg << std::endl; }

15. // Accesseurs/Mutateurs pour les attributs

16. void set_msg(std::string msg) { this->m_msg = msg; }

17. std::string get_msg() const { return m_msg; }

18. };

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

1. #include <boost/python.hpp>

2. #include "Bonjour.hpp"

4. using namespace boost::python;

6. BOOST_PYTHON_MODULE(pylib)

7. {

8. class_< Bonjour >("Bonjour", init<std::string>())

9. .def("greet", &Bonjour::greet)

10. .add_property("msg", &Bonjour::get_msg, &Bonjour::set_msg);

11. }

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 :

$ 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

1. cmake_minimum_required(VERSION 2.8)

2. include(ExternalProject)

4. # Ajout d'un module cmake personnalisé pour générer boost

5. list(APPEND CMAKE_MODULE_PATH  "${CMAKE_SOURCE_DIR}/cmake/Modules/")

7. project (tutorial)

9. # Trouver les bibliothèques et interpréteurs python par défaut

10. find_package(PythonInterp REQUIRED)

11. find_package(PythonLibs REQUIRED)

12. include(BuildBoost) # module personnalisé

14. include_directories(${Boost_INCLUDE_DIR} ${PYTHON_INCLUDE_DIRS})

15. link_directories(${Boost_LIBRARY_DIR})

17. # Génère et lie le module pylib

18. add_library(pylib SHARED pylib.cpp)

19. target_link_libraries(pylib ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})

20. add_dependencies(mylib Boost)

22. # Ajuste le nom de la bibliothèque pour coller à ce qu'attend Python

23. set_target_properties(pylib PROPERTIES SUFFIX .so)

24. 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 :

$ 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

1. In [1]: from pylib import Bonjour

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

5. In [3]: b.greet()

6. Hello World

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

10. In [5]: b.greet()

11. Bonjour tout le monde

13. In [6]: b.msg

14. 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 :

$ sudo apt-get install castxml

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

$ pip install --user pygccxml

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

$ 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

1. #!/usr/bin/python

2. from pygccxml import parser

3. from pyplusplus import module_builder

5. # Configurations que vous pouvez avoir à changer sur votre système

6. generator_path = "/usr/bin/castxml"

7. generator_name = "castxml"

8. compiler = "gnu"

9. ompiler_path = "/usr/bin/gcc"

11. # Créé une configuration pour CastXML

12. xml_generator_config = parser.xml_generator_configuration_t(

13. xml_generator_path=generator_path,

14. xml_generator=generator_name,

15. compiler=compiler,

16. compiler_path=compiler_path)

18. # Liste de tous les fichiers d'en-tête de votre bibliothèque

19. header_collection = ["Bonjour.hpp"]

21. # Analyse les fichiers sources et créé un objet module_builder

22. builder = module_builder.module_builder_t(

23. header_collection,

24. xml_generator_path=generator_path,

25. xml_generator_config=xml_generator_config)

27. # Détecte automatiquement les propriétés et les accesseurs/mutateurs associés

28. builder.classes().add_properties(exclude_accessors=True)

30. # Définit un nom pour le module

31. builder.build_code_creator(module_name="pylib_auto")

33. # Écrit le fichier d'interface C++

34. 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 :

$ 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 :

1. # Génère et lie le module pylib_auto

2. add_library(pylib_auto SHARED pylib_auto.cpp)

3. target_link_libraries(pylib_auto ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})

4. add_dependencies(pylib_auto Boost)

6. # Ajuste le nom de la bibliothèque pour coller à ce qu'attend Python

7. set_target_properties(pylib_auto PROPERTIES SUFFIX .so)

8. set_target_properties(pylib_auto PROPERTIES PREFIX "")

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

1. $ cd build

2. $ cmake ..

3. $ make pylib_auto

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

1. In [1]: from pylib_auto import Bonjour

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

5. In [3]: b.greet()

6. Hello World

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

10. In [5]: b.greet()

11. Bonjour tout le monde

13. In [6]: b.msg

14. 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 :

- Documentation officielle de Boost.Python

- Documentation officielle de Py++

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

- Documentation Boost.Numpy (fait partie de Boost.Python depuis 1.63)

IV - Remerciements

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