Archives pour la catégorie Boost

Logo officiel de Boost

Boost::Python, dates et conversions

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 :

[crayon lang="c++"]
/**
* \file module.cpp
* \author Julien Kauffmann
* \brief The Python module file.
*/

#include

BOOST_PYTHON_MODULE(module)
{
}
[/crayon]

Rien de fou donc, pour l’instant.

Ajoutons une fonction qui prend en paramètre et retourne un boost::posix_time::ptime :
[crayon lang="c++"]/**
* \file module.cpp
* \author Julien Kauffmann
* \brief The Python module file.
*/

#include
#include

#include

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 »);
}[/crayon]
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 :
[crayon lang="python"]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)[/crayon]
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 :
[crayon lang="c++"]/**
* \file module.cpp
* \author Julien Kauffmann
* \brief The Python module file.
*/

#include
#include
#include

#include

#include

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(value.date().year()),
static_cast(value.date().month()),
static_cast(value.date().day()),
static_cast(value.time_of_day().hours()),
static_cast(value.time_of_day().minutes()),
static_cast(value.time_of_day().seconds()),
static_cast(value.time_of_day().total_microseconds() – value.time_of_day().total_seconds() * 1000000L)
);
}
};

BOOST_PYTHON_MODULE(module)
{
boost::python::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 »);
}[/crayon]

Analyse

Regardons ligne par ligne les changements apportés.
[crayon lang="c++"]#include
#include [/crayon]
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.
[crayon lang="c++"]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(value.date().year()),
static_cast(value.date().month()),
static_cast(value.date().day()),
static_cast(value.time_of_day().hours()),
static_cast(value.time_of_day().minutes()),
static_cast(value.time_of_day().seconds()),
static_cast(value.time_of_day().total_microseconds() – value.time_of_day().total_seconds() * 1000000L)
);
}
};[/crayon]
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.
[crayon lang="c++"] boost::python::to_python_converter();[/crayon]
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 :
[crayon lang="c++"]/**
* \file module.cpp
* \author Julien Kauffmann
* \brief The Python module file.
*/

#include
#include
#include

#include

#include

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(value.date().year()),
static_cast(value.date().month()),
static_cast(value.date().day()),
static_cast(value.time_of_day().hours()),
static_cast(value.time_of_day().minutes()),
static_cast(value.time_of_day().seconds()),
static_cast(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*>(data)->storage.bytes;

if (obj_ptr == Py_None)
{
new (storage) boost::posix_time::ptime();
} else
{
PyDateTime_IMPORT;
PyDateTime_DateTime* dt_ptr = reinterpret_cast(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::python::converter::registry::push_back(&date_from_python_converter::is_convertible, &date_from_python_converter::convert, boost::python::type_id());

boost::python::def(« add_five_seconds », &add_five_seconds, boost::python::args(« ptime »), « Add five seconds to a datetime then return the result »);
}[/crayon]

Analyse

Regardons encore une fois, ligne par ligne les modifications apportées :
[crayon lang="c++"]struct date_from_python_converter[/crayon]
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.
[crayon lang="c++"]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;
}[/crayon]
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.
[crayon lang="c++"]static void convert(PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data)
{
assert(obj_ptr);

void* const storage = reinterpret_cast*>(data)->storage.bytes;

if (obj_ptr == Py_None)
{
new (storage) boost::posix_time::ptime();
} else
{
PyDateTime_IMPORT;
PyDateTime_DateTime* dt_ptr = reinterpret_cast(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;
}[/crayon]
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.
[crayon lang="c++"] boost::python::converter::registry::push_back(&date_from_python_converter::is_convertible, &date_from_python_converter::convert, boost::python::type_id());[/crayon]
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 :
[crayon lang="python"]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[/crayon]
Ç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 :
[crayon lang="python"]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'])[/crayon]

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).