Conversion de type avec Boost.Python

Ça y est, je me lance ! C’est mon premier article sur ce blog. Soyez indulgent s’il y a des erreurs. 🙂

L’objectif est d’expliquer comment convertir des types C++ en Python et vice versa en utilisant la librairie Boost.Python.

On suppose que vous connaissez la notion d’exposition des classes.

Mais à quoi ça sert ?

Avant de rentrer dans le vif du sujet, intéressons-nous à l’utilité de ce genre de conversion.

Imaginons que nous voulions utiliser la classe C++ suivante dans un script Python :

#include <vector>
class Useless
{
  public:
    void save(std::vector<double> v) {vec = v;}
    std::vector<double> show() {return vec;}

  private:
    std::vector<double> vec;
};

Naïvement, on pourrait penser que l’exposition de la classe C++ s’écrit de la manière suivante dans un module Python :

#include <boost/python.hpp>
namespace bp = boost::python;

BOOST_PYTHON_MODULE(stuff)
{
  bp::class_<Useless>("Useless")
    .def("save", &Useless::save)
    .def("show", &Useless::show)
  ;
}

On peut compiler ce module mais les méthodes de la classe Useless s’avèrent inutilisables en Python.

>>> import stuff
>>> f = stuff.Useless()
>>> f.show()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: No to_python (by-value) converter found for C++ type: std::vector<double, std::allocator<double> >
>>> f.save([10., 1.2])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    Useless.save(Useless, list)
did not match C++ signature:
    save(Useless {lvalue}, std::vector<double, std::allocator<double> >)

En C++, la méthode Useless::show renvoie un type std::vector<double>. Le message d’erreur est relativement clair : il faut convertir en amont le type std::vector<double> en un type Python. On pense naturellement à une liste de flottants.

En C++, la méthode Useless::save a comme argument le type std::vector<double>. En Python, nous avons essayé de lui fournir une liste de flottants mais Boost.Python signale que ce type ne correspond pas à la signature de la méthode. Il faut spécifier en amont la conversion de la liste de flottants en std::vector<double>.

L’utilisation des méthodes de la classe Useless nécessite donc deux types de conversion :

  • une conversion du C++ en Python
  • une conversion du Python en C++

Qu’est-ce qu’on trouve sur le net ?

La documentation Boost évoque cette problématique mais avec peu d’explications. Une petite recherche sur Internet m’a permis de trouver les liens suivants (le deuxième donne de très bonnes explications en anglais) :

Néanmoins, ces deux sources présentent des exemples avec des types C++ non-génériques (des types de chaîne de caractères).

Histoire de faire preuve d’un peu d’originalité, on va s’intéresser à un type générique qui sera le conteneur std::vector choisi au hasard bien sûr. 😉

Conversion en Python

On adopte définitivement la convention suivante pour l’espace de nom boost::python :

namespace bp = boost::python;

La conversion du C++ en Python est la partie la plus facile du boulot !

On définit une structure template vector_to_python_list avec une fonction membre statique. Cette fonction doit s’appeler convert, prendre en argument le conteneur à convertir std::vector<T> et renvoyer un pointeur de PyObject.

template <typename T>
struct vector_to_python_list
{
  static PyObject* convert(const std::vector<T>& vec)
  {
    bp::list pylist;
    for(int i=0; i<vec.size(); i++) pylist.append(vec[i]);
    return bp::incref(pylist.ptr());
  }
};

Dans le détail, voici ce que cette fonction convert réalise :

  1. on déclare un objet pylist de la classe boost::python::list, il s’agit d’une liste Python exposée en C++ ;
  2. on copie les éléments du conteneur std::vector<double> dans la liste pylist ;
  3. on incrémente le compteur de références de pylist avec boost::python::incref et on renvoie le pointeur de cette liste.

Qu’est-ce qu’un compteur de référence ? C’est un mécanisme propre au ramasse-miettes du langage Python qui utilise un algorithme à comptage de références.

Pourquoi incrémente-t-on ce compteur ? Si on ne l’incrémente pas, celui-ci passe à 0 lors du return et la fonction convert risque de renvoyer un pointeur libéré par le ramasse-miettes.

Pour enregistrer la conversion dans l’environnement d’exécution, on appelle le constructeur de la classe template boost::python::to_python_converter en spécifiant le type à convertir std::vector<double> et la structure utilisée pour la conversion vector_to_python_list<double>.

BOOST_PYTHON_MODULE(stuff)
{
  ... 
  bp::to_python_converter< std::vector<double>,
                           vector_to_python_list<double> >();
  ...
}

Désormais, la méthode Useless::show évoquée plus haut est fonctionnelle.

>>> import stuff
>>> f = stuff.Useless()
>>> f.show()
[]

Bien évidemment, la structure template vector_to_python_list peut être utilisée pour n’importe quel autre type de std::vector en appelant une autre fois le constructeur de la classe boost::python::to_python_converter. Par exemple :

BOOST_PYTHON_MODULE(module)
{
  ...
  bp::to_python_converter< std::vector<int>,
                           vector_to_python_list<int> >();
  ...
}

Conversion en C++

La conversion du Python au C++ est un peu plus compliquée à mettre en œuvre.

Tout d’abord, on définit une nouvelle structure template vector_from_python_list avec une première fonction membre statique convertible qui vérifie la faisabilité de la conversion. Elle prend en argument un pointeur de PyObject, renvoie ce pointeur si la conversion est possible et 0 sinon.

template <typename T>
struct vector_from_python_list
{
  static void* convertible(PyObject* obj_ptr)
  {
    if (!PyList_Check(obj_ptr)) return 0;
    return obj_ptr;
  }
  ...
};

Une seconde fonction membre statique construct réalise concrètement cette conversion. Elle prend en argument un pointeur de PyObject et un pointeur de boost::python::converter::rvalue_from_python_stage1_data. Ce dernier pointeur contient notamment l’adresse mémoire où le résultat de la conversion sera stocké.

template <typename T>
struct vector_from_python_list
{
  ...
  static void construct(
    PyObject *obj_ptr,
    bp::converter::rvalue_from_python_stage1_data* data )
  {
    // Copie les éléments de la liste python dans un conteneur vector
    std::vector<T> vec;

    for(Py_ssize_t i=0; i<PyList_Size(obj_ptr); i++)
    {
      PyObject *pyvalue = PyList_GetItem(obj_ptr, i);
      T value = typename bp::extract<T>::extract(pyvalue);
      vec.push_back(value);
    }

    // Stocke le résultat de la conversion au moyen d'un placement new
    void* storage = (
      (bp::converter::rvalue_from_python_storage< std::vector<T> >*)
      data)->storage.bytes;

    new (storage) std::vector<T>(vec);
    
    // Enregistre l'adresse mémoire dans le champ convertible
    data->convertible = storage;
  }
  ...
};

La fonction construct utilise un placement new pour instancier le résultat de la conversion dans un emplacement mémoire donné. La libération de la mémoire sera prise en charge automatiquement par Boost.Python. La documentation Boost ne donne pas beaucoup de d’information sur ce sujet mais vous pouvez toujours consulter le contenu du fichier source rvalue_from_python_data.hpp.

Pour enregistrer la conversion dans l’environnement d’exécution, on utilise la fonction boost::python::converter::registry::push_back qui prend en argument les pointeurs des fonctions convertible, construct et un objet boost::python:type_id< std::vector >. On appelle cette fonction dans le constructeur de la structure vector_from_python_list.

template <typename T>
struct vector_from_python_list
{
  ...
  vector_from_python_list()
  {
    bp::converter::registry::push_back(
      &convertible,
      &construct,
      bp::type_id< std::vector<T> >());
  }
};

Enfin, on appelle le constructeur de la structure dans le module Python.

BOOST_PYTHON_MODULE(stuff)
{
  ...
  vector_from_python_list<double>();
  ...
}

La méthode Useless::save est maintenant opérationnelle ! 😀

>>> import stuff
>>> f=stuff.Useless()
>>> f.save([10., 1.2])
>>> f.show()
[10.0, 1.2]

Notre conversion est pleinement opérationnelle maintenant.

N’hésitez pas à laisser un commentaire si vous souhaitez faire une remarque ou poser une question.

Vous trouverez le code complet ci-dessous.

Code complet

#include <vector>
#include <boost/python.hpp>

namespace bp = boost::python;

class Useless
{
  public:
    void save(std::vector<double> v) {vec = v;}
    std::vector<double> show() {return vec;}

  private:
    std::vector<double> vec;
};

template <typename T>
struct vector_to_python_list
{
  static PyObject* convert(const std::vector<T>& vec)
  {
    bp::list pylist;
    for(int i=0; i<vec.size(); i++) pylist.append(vec[i]);
    return bp::incref(pylist.ptr());
  }
};

template <typename T>
struct vector_from_python_list
{ 
  static void* convertible(PyObject *obj_ptr)
  {
    if (!PyList_Check(obj_ptr)) return 0;
    return obj_ptr;
  }

  static void construct(
    PyObject *obj_ptr,
    bp::converter::rvalue_from_python_stage1_data* data)
  {
    std::vector<T> vec;
    for(Py_ssize_t i=0; i<PyList_Size(obj_ptr); i++)
    {
      PyObject *pyvalue = PyList_GetItem(obj_ptr, i);
      T value = typename bp::extract<T>::extract(pyvalue);
      vec.push_back(value);
    }

    void* storage = (
      (bp::converter::rvalue_from_python_storage<std::vector<T> >*)
      data)->storage.bytes;

    new (storage) std::vector<T>(vec);

    data->convertible = storage;
  }

  vector_from_python_list()
  {
    bp::converter::registry::push_back(
      &convertible,
      &construct,
      bp::type_id<std::vector<T> >());
  }
};

BOOST_PYTHON_MODULE(stuff)
{
  bp::to_python_converter< std::vector<double>,
                           vector_to_python_list<double> >();
  
  vector_from_python_list<double>();
                           
  bp::class_<Useless>("Useless")
    .def("save", &Useless::save)
    .def("show", &Useless::show)
  ;
}

Partager :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *