Ceux qui ont déjà joué avec Boost::Python le savent : cette API est très complète mais malheureusement trop peu documentée. Si elle permet de rendre accessible du code C++ depuis un environnement Python avec parfois une déconcertante simplicité, certaines tâches triviales sont en revanche plus difficiles à réaliser qu’on aurait pu l’imaginer.
Si vous êtes sur cette page, c’est très probablement parce que vous aussi devez jouer avec des dates et des durées, de C++ vers Python, inversement ou bien les deux et que vous vous êtes vous aussi heurté à la complexité de Boost::Python.
L’article qui suit est le résultat de mes recherches en la matière, et la synthèse de la solution à laquelle je suis parvenu.
Le code de base
Pour l’ensemble de l’article, nous partirons du principe que vous utilisez la classe boost::posix_time::ptime
côté C++, et datetime.datetime
côté Python.
Remarque : Si jamais vous utilisiez une autre classe pour vos dates (quelle soit “maison” ou issue d’une autre bibliothèque), vous devriez pouvoir adapter le principe sans trop de problèmes.
Le code minimal pour un module Python avec Boost::Python dont nous partirons est le suivant :
1 2 3 4 5 6 7 8 9 10 11 |
/** * \file module.cpp * \author Julien Kauffmann * \brief The Python module file. */ #include <boost/python/module.hpp> BOOST_PYTHON_MODULE(module) { } |
Rien de fou donc, pour l’instant.
Ajoutons une fonction qui prend en paramètre et retourne un boost::posix_time::ptime
:
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 |
/** * \file module.cpp * \author Julien Kauffmann * \brief The Python module file. */ #include <boost/python/module.hpp> #include <boost/python/def.hpp> #include <boost/date_time/posix_time/posix_time.hpp> boost::posix_time::ptime add_five_seconds(boost::posix_time::ptime ptime) { if (!ptime.is_special()) { return ptime + boost::posix_time::seconds(5); } return ptime; } BOOST_PYTHON_MODULE(module) { boost::python::def("add_five_seconds", &add_five_seconds, boost::python::args("ptime"), "Add five seconds to a datetime then return the result"); } |
Cette fonction ajoute bêtement 5 secondes à toute date qui lui est passée, sauf si celle-ci n’est pas une date valide.
Si le code actuel compile et donne une bibliothèque Python valide, l’appel de add_five_seconds()
depuis l’interpréteur Python provoque la levée d’une exception :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Python 2.4.3 (#1, Apr 14 2011, 20:41:59) Type "copyright", "credits" or "license" for more information. In [1]: import datetime, module In [2]: module.add_five_seconds(datetime.datetime.today()) --------------------------------------------------------------------------- ArgumentError Traceback (most recent call last) /home/ereon/workbench/pythondate/ ArgumentError: Python argument types in module.add_five_seconds(datetime.datetime) did not match C++ signature: add_five_seconds(boost::posix_time::ptime ptime) |
En effet : à aucun moment nous n’avons indiqué à Boost::Python comment convertir une date Python en date C++, ni même que cette conversion était possible.
Pour que ceci fonctionne, il faut ajouter deux “converters” : un de C++ vers Python, et l’autre de Python vers C++.
Conversion de C++ vers Python
Si Boost::Python ne prend pas nativement en charge la conversion de date, il fournit néanmoins des outils puissants pour nous permettre d’y arriver.
Ajoutons le code de conversion à notre exemple précédent :
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/** * \file module.cpp * \author Julien Kauffmann <julien.kauffmann@freelan.org> * \brief The Python module file. */ #include <boost/python/module.hpp> #include <boost/python/def.hpp> #include <boost/python/class.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <datetime.h> boost::posix_time::ptime add_five_seconds(boost::posix_time::ptime ptime) { if (!ptime.is_special()) { return ptime + boost::posix_time::seconds(5); } return ptime; } struct date_to_python_converter { static PyObject* convert(boost::posix_time::ptime value) { if (value.is_not_a_date_time()) return Py_None; PyDateTime_IMPORT; return PyDateTime_FromDateAndTime( static_cast<int>(value.date().year()), static_cast<int>(value.date().month()), static_cast<int>(value.date().day()), static_cast<int>(value.time_of_day().hours()), static_cast<int>(value.time_of_day().minutes()), static_cast<int>(value.time_of_day().seconds()), static_cast<int>(value.time_of_day().total_microseconds() - value.time_of_day().total_seconds() * 1000000L) ); } }; BOOST_PYTHON_MODULE(module) { boost::python::to_python_converter<boost::posix_time::ptime, date_to_python_converter>(); boost::python::def("add_five_seconds", &add_five_seconds, boost::python::args("ptime"), "Add five seconds to a datetime then return the result"); } |
Analyse
Regardons ligne par ligne les changements apportés.
1 2 |
#include <boost/python/class.hpp> #include <datetime.h> |
Le premier include est nécessaire pour utiliser boost::python::to_python_converter
; le second pour rendre disponibles les types Python natifs, tels que PyObject
ou PyDateTime
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct date_to_python_converter { static PyObject* convert(boost::posix_time::ptime value) { if (value.is_not_a_date_time()) return Py_None; PyDateTime_IMPORT; return PyDateTime_FromDateAndTime( static_cast<int>(value.date().year()), static_cast<int>(value.date().month()), static_cast<int>(value.date().day()), static_cast<int>(value.time_of_day().hours()), static_cast<int>(value.time_of_day().minutes()), static_cast<int>(value.time_of_day().seconds()), static_cast<int>(value.time_of_day().total_microseconds() - value.time_of_day().total_seconds() * 1000000L) ); } }; |
Ici nous déclarons un “converter”. Au sens de Boost::Python, un “converter” est une simple structure ou classe qui contient une méthode statique nommée convert()
qui prend un paramètre le type natif C++ à convertir, et qui retourne un PyObject*
.
Dans le cas où notre date n’est pas une date valide, nous choisissons ici de renvoyer None
. Libre à vous de modifier ce comportement pour satisfaire vos propres besoins.
Remarque : La valeur retournée doit avoir un compteur de référence strictement positif.
1 |
boost::python::to_python_converter<boost::posix_time::ptime, date_to_python_converter>(); |
Enfin nous déclarons notre “converter” en spécifiant le type natif et la structure/classe à utiliser pour la conversion.
À partir de ce moment là, notre module Python sera capable de convertir implicitement tout boost::posix_time::ptime
en datetime.datetime
Python.
Conversion de Python vers C++
La conversion dans l’autre sens demande un peu plus de travail :
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
/** * \file module.cpp * \author Julien Kauffmann <julien.kauffmann@freelan.org> * \brief The Python module file. */ #include <boost/python/module.hpp> #include <boost/python/def.hpp> #include <boost/python/class.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <datetime.h> boost::posix_time::ptime add_five_seconds(boost::posix_time::ptime ptime) { if (!ptime.is_special()) { return ptime + boost::posix_time::seconds(5); } return ptime; } struct date_to_python_converter { static PyObject* convert(boost::posix_time::ptime value) { if (value.is_not_a_date_time()) return Py_None; PyDateTime_IMPORT; return PyDateTime_FromDateAndTime( static_cast<int>(value.date().year()), static_cast<int>(value.date().month()), static_cast<int>(value.date().day()), static_cast<int>(value.time_of_day().hours()), static_cast<int>(value.time_of_day().minutes()), static_cast<int>(value.time_of_day().seconds()), static_cast<int>(value.time_of_day().total_microseconds() - value.time_of_day().total_seconds() * 1000000L) ); } }; struct date_from_python_converter { static void* is_convertible(PyObject* obj_ptr) { assert(obj_ptr); if (obj_ptr == Py_None) return obj_ptr; PyDateTime_IMPORT; if (PyDateTime_Check(obj_ptr)) return obj_ptr; return NULL; } static void convert(PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data) { assert(obj_ptr); void* const storage = reinterpret_cast<boost::python::converter::rvalue_from_python_storage<boost::posix_time::ptime>*>(data)->storage.bytes; if (obj_ptr == Py_None) { new (storage) boost::posix_time::ptime(); } else { PyDateTime_IMPORT; PyDateTime_DateTime* dt_ptr = reinterpret_cast<PyDateTime_DateTime*>(obj_ptr); const int year = PyDateTime_GET_YEAR(dt_ptr); const int month = PyDateTime_GET_MONTH(dt_ptr); const int day = PyDateTime_GET_DAY(dt_ptr); const int hour = PyDateTime_DATE_GET_HOUR(dt_ptr); const int minute = PyDateTime_DATE_GET_MINUTE(dt_ptr); const int second = PyDateTime_DATE_GET_SECOND(dt_ptr); const int microsecond = PyDateTime_DATE_GET_MICROSECOND(dt_ptr); new (storage) boost::posix_time::ptime(boost::gregorian::date(year, month, day), boost::posix_time::time_duration(hour, minute, second, 0) + boost::posix_time::microseconds(microsecond)); } data->convertible = storage; } }; BOOST_PYTHON_MODULE(module) { boost::python::to_python_converter<boost::posix_time::ptime, date_to_python_converter>(); boost::python::converter::registry::push_back(&date_from_python_converter::is_convertible, &date_from_python_converter::convert, boost::python::type_id<boost::posix_time::ptime>()); boost::python::def("add_five_seconds", &add_five_seconds, boost::python::args("ptime"), "Add five seconds to a datetime then return the result"); } |
Analyse
Regardons encore une fois, ligne par ligne les modifications apportées :
1 |
struct date_from_python_converter |
Nous ajoutons une structure qui va contenir les routines de conversions. Contrairement à tout à l’heure, et comme nous le verrons plus tard, ceci n’est pas indispensable. Pour effectuer des conversions de Python vers C++, Boost::Python a juste besoin de deux fonctions. Que celles-ci soient statiques au sein d’une classe ou libres n’a aucune incidence.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static void* is_convertible(PyObject* obj_ptr) { assert(obj_ptr); if (obj_ptr == Py_None) return obj_ptr; PyDateTime_IMPORT; if (PyDateTime_Check(obj_ptr)) return obj_ptr; return NULL; } |
La fonction is_convertible()
sera utilisée par Boost::Python pour déterminer si l’instance Python à convertir peut l’être.
Dans notre cas nous acceptons tout d’abord None
comme valeur “valide” pour respecter la symétrie, puis nous testons si l’instance est de type datetime.datetime
grâce à la fonction PyDateTime_Check()
.
En cas de succès, nous renvoyons la valeur passée en paramètre telle quelle. En cas d’erreur, nous renvoyons NULL
.
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 |
static void convert(PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data) { assert(obj_ptr); void* const storage = reinterpret_cast<boost::python::converter::rvalue_from_python_storage<boost::posix_time::ptime>*>(data)->storage.bytes; if (obj_ptr == Py_None) { new (storage) boost::posix_time::ptime(); } else { PyDateTime_IMPORT; PyDateTime_DateTime* dt_ptr = reinterpret_cast<PyDateTime_DateTime*>(obj_ptr); const int year = PyDateTime_GET_YEAR(dt_ptr); const int month = PyDateTime_GET_MONTH(dt_ptr); const int day = PyDateTime_GET_DAY(dt_ptr); const int hour = PyDateTime_DATE_GET_HOUR(dt_ptr); const int minute = PyDateTime_DATE_GET_MINUTE(dt_ptr); const int second = PyDateTime_DATE_GET_SECOND(dt_ptr); const int microsecond = PyDateTime_DATE_GET_MICROSECOND(dt_ptr); new (storage) boost::posix_time::ptime(boost::gregorian::date(year, month, day), boost::posix_time::time_duration(hour, minute, second, 0) + boost::posix_time::microseconds(microsecond)); } data->convertible = storage; } |
C’est ici que se passe le gros du travail : lorsque cette fonction est appelée, cela signifie que l’instance obj_ptr
a passé l’appel à is_convertible()
et est prête à être convertie.
Le paramètre data
lui contient entre autres l’adresse de la zone mémoire où nous devons instancier notre résultat de conversion. Notez que pour ce faire, nous utilisons le “placement new” qui permet de construire une instance à un emplacement mémoire donné. Le delete
correspondant sera automatiquement appelé par Boost::Python au besoin.
Pour finir, nous renseignons le champ convertible
du paramètre data
pour y indiquer où nous avons alloué notre résultat.
1 |
boost::python::converter::registry::push_back(&date_from_python_converter::is_convertible, &date_from_python_converter::convert, boost::python::type_id<boost::posix_time::ptime>()); |
Comme auparavant, la dernière étape consiste à enregistrer le “converter” pour le faire connaître de Boost::python.
On remarque ici que comme énoncé précédemment, l’appel prend en paramètre deux fonctions qui n’ont pas nécessairement besoin de faire partie d’une classe ou d’une structure.
Résultat
Compilez le code ci-dessus (voir le script SConstruct en annexe), puis chargez votre module au sein de l’interpreteur Python :
1 2 3 4 5 6 7 8 9 10 11 12 |
Python 2.4.3 (#1, Apr 14 2011, 20:41:59) Type "copyright", "credits" or "license" for more information. In [1]: import datetime, module In [2]: d = datetime.datetime.now() In [3]: print d 2011-08-09 11:30:43.239460 In [4]: print module.add_five_seconds(d) 2011-08-09 11:30:48.239460 |
Ça y est ! La conversion boost::posix_time::ptime
<=> datetime.datetime
fonctionne parfaitement.
Conclusion
Boost::Python est définitivement une bibliothèque très puissante. On peut certes regretter la qualité de sa documentation, mais fort heureusement les ressources à son sujet sur l’Internet ne manquent pas. Son extensibilité la rend utilisable dans toutes les situations et facilite grandement la vie du développeur.
J’espère que cet article vous aura aidé et/ou donné envie de découvrir/utiliser Boost::Python. Comme toujours, n’hésitez pas à me signaler toute coquille, erreur ou optimisation qui m’aurait échappé.
Annexe
Voici le script SConstruct que j’ai utilisé pour la compilation :
1 2 3 4 5 6 |
import os env = Environment(ENV = os.environ.copy()) env['CPPPATH'] = ['/usr/include/python2.4'] module = env.SharedLibrary(source = Glob('*.cpp'), target = 'python/module.so', SHLIBPREFIX='', LIBS = ['boost_python', 'python2.4']) |
Sources
Ces pages m’ont été très utiles lors de la rédaction de cet article :
- L’API datetime sur python.org (en Anglais);
- l’API Boost::Posix Time sur boost.org (en Anglais);
- l’API de Boost::Python sur boost.org (en Anglais);
- cet article de misspent (en Anglais).
Article très intéressant, moi qui brûle d’envie de me mettre au Python, si en plus je peux le lier au C++ d’une manière légère comme celle-ci cela ne peut que m’attirer encore plus.
En ce qui concerne les conversions je pense qu’il y a moyen de générer ce code là (en faisant ses propres outils), j’essaierai de voir ce qu’il y a à faire dans cette voie là quand je me mettrai au Python.
Merci beaucoup.
En fait, je ne pense même pas que ça soit nécessaire : je montre ici comment ajouter un type non supporté de base par Boost::Python donc c’est un peu verbeux, mais la plupart des types usuels (std::string, types ordinaux, chaînes C, etc.) sont déjà implicitement convertis, et dans les deux sens.
Et si tu comptes te mettre au Python, je te recommande chaudement http://diveintopython.org/, qui est un bouquin excellent !
Merci pour le lien, je vais me l’acheter pour apprendre un langage rien de mieux qu’un livre imprimé.
Je pense que le python peut beaucoup m’apporter en tant que langage pour la réalisation de scripts d’automatisation des builds, déploiements etc.
Ton exemple est très bon notamment pour nos propres types. Si seulement un pont de cette qualité là existait entre C++ et Java, parce que je trouve cela toujours un peu bricolage les JNI.
Pour les build, je te le confirme. Jette un oeil du côté de SConstruct, que je trouve très bien foutu. Surtout en comparaison avec Make…
C’est fait en Python, c’est extensible et franchement la gestion des dépendances est un vrai plaisir.
Tout à fait d’accord avec ta remarque pour JNI : j’ai justement eu à réaliser une série de wrappers Python et JNI à partir de la même bibliothèque en C++ : je te laisse deviner laquelle j’ai pu écrire en une journée et laquelle en a pris quatre.