Interfaçage de code C++ pour Ruby et Python avec SWIG

Magazine
Marque
GNU/Linux Magazine
Numéro
226
Mois de parution
mai 2019
Spécialité(s)


Résumé

Dans cet article nous allons, en étudiant pas à pas un exemple simple, voir comment on peut, grâce au projet SWIG, créer facilement des extensions binaires pour Python et Ruby à partir d’un code C++.


Body

 

Tous les utilisateurs de langages de script utilisent (parfois sans le savoir) des extensions leur donnant accès à des librairies écrites en C ou C++. À chaque require en Ruby ou import en Python, c’est parfois du code script qui est chargé, mais souvent un module binaire en .so qui fait le lien avec une librairie existante, comme la librairie openssl. Cela permet depuis le langage de script d’utiliser des portions de code existantes, sans avoir à les ré-écrire. C’est ce qui fait d’ailleurs la force de ces langages de script qui par ce biais donnent accès à une quantité impressionnante de fonctionnalités, NumPy et SciPy en sont de bons exemples pour Python.

Alors voilà, on a écrit une superbe bibliothèque C++ aux fonctionnalités révolutionnaires, mais quand on va la proposer aux utilisateurs potentiels : « ah oui c’est super! Mais, heu, je peux m’en servir avec Python ? »... Dont acte, si on veut augmenter le potentiel de notre librairie, il va falloir y passer : il nous faut créer des extensions !

Ruby [1]comme Python [2]propose une manière de créer ces extensions. C’est plus ou moins facile et cela demande un certain investissement en temps. Si de plus, vous envisagez de créer des extensions pour plusieurs langages, ça accroît encore l’investissement !

C’est là que le projet SWIG [3]vient à notre secours ! SWIG est l’acronyme de Simplified Wrapper and Interface Generator, SWIG est donc un outil qui se charge de faire le lien entre une bibliothèque et le langage de votre choix. Dans cet article, nous nous cantonnerons à Ruby et Python, mais SWIG supporte de nombreux langages, qui ne sont d’ailleurs pas tous des langages de script ; citons parmi ceux-ci : JavaScript, Perl, PHP, Tcl, C#, Go, Java, Lua, Octave, Scilab, et R.

SWIG est un projet très bien documenté [4], néanmoins quand on se lance en pratique dans son utilisation et qu’on s’éloigne un tant soit peu d’un exemple trivial, on tombe rapidement sur des écueils qui, bien que trouvant effectivement leur solution dans la documentation, nécessitent pas mal de temps de recherche dans celle-ci.

Dans cet article, nous allons prendre un exemple simple que nous allons complexifier petit à petit et comprendre, au fil des problèmes rencontrés, comment on va pouvoir utiliser SWIG pour générer nos extensions Ruby et Python (dans toute la suite, on considère qu’on utilise Python 3.X).

Fichiers d’exemple de cet article

Les fichiers d’exemple de cet article sont disponibles dans plusieurs branches d’un dépôt sur GitHub. Si vous souhaitez vous en servir :

$ git clone https://github.com/wdaniau/Nombre.git

$ cd Nombre

Il sera indiqué au cours de l’article sur quelle branche basculer pour avoir les fichiers correspondant à un état donné. La branche par défaut initial contient les fichiers correspondant à la fin de la section 1.2.5.

1. Nombre, une classe C++ simple

1.1 La classe

Dans un premier temps, prenons cette classe C++ très simple (fichier nombre.h) :

#ifndef NOMBRE_H

#define NOMBRE_H

class Nombre

{

public:

    Nombre();

    Nombre(double v);

    void setV(double v);

    double getV();

    double carre();

private:

    double valeur;

};

#endif // NOMBRE_H

Notre objet Nombre a un seul attribut qui est un nombre flottant, deux méthodes d’accès à cet attribut getV() et setV(double v), une méthode retournant le carré de la valeur et deux constructeurs.

Puis, la classe elle-même dans nombre.cpp :

#include "nombre.h"

Nombre::Nombre() {

    valeur=0.0;

}

Nombre::Nombre(double v) {

    valeur=v;

}

void Nombre::setV(double v) {

    valeur=v;

}

double Nombre::getV() {

    return valeur;

}

double Nombre::carre() {

    return valeur*valeur;

}

On a ici l’implémentation de notre classe et nous y ajoutons un main.cpp pour la tester qui, après avoir créé une instance de l’objet, utilise ces méthodes :

#include <iostream>

#include "nombre.h"

using namespace std;

int main()

{

    cout << "Test classe Nombre" << endl;

    Nombre* a= new Nombre();

    a->setV(2.5);

    cout << a->getV()  << endl;

    cout << a->carre() << endl;

    return 0;

}

On compile et on lance le test de notre classe :

$ g++ -fPIC -c *.cpp

$ g++ -fPIC *.o -o testNombre

$ ./testNombre

Test classe Nombre

2.5

6.25

On prend ici un peu d’avance sur la suite avec l’utilisation de l’option -fPIC « Position Independant Code ». En effet, nos modules d’extension ne sont rien d’autre que des librairies partagées et cette option est nécessaire pour que les fichiers objets générés puissent être intégrés à une telle librairie (en fait, sur la seconde commande l’option n’est pas nécessaire, car on ne fait que linker, elle est ici juste ignorée, mais je préfère la mettre, car si on a une commande qui à la fois compile et linke, cela permet de ne pas l’oublier).

Nous sommes prêts !

1.2 Let’s SWIG!

Le fonctionnement de SWIG se passe de la manière suivante :

  1. on lance le programme swig avec comme argument un fichier spécifique d’interfaçage. Le programme génère alors un fichier C++ de wrap ;
  2. on compile ce fichier wrap généré par swig et on le linke avec les fichiers objets des classes qu’on interface pour créer une librairie dynamique, qui constituera notre module.

1.2.1 Le fichier d’interface de SWIG

Voici le fichier d’interface de SWIG le plus simple qui soit (myext.i) :

%module myext

%{

#include "nombre.h"

%}

%include "nombre.h"

Ce fichier comporte en fait trois instructions.

La première, %module myext, indique le nom de l’extension, en l’occurrence ici myext. C’est le nom qui sera utilisé avec Python ou Ruby pour le import ou le require.

La seconde instruction est constituée par le bloc encadré par les balises %{ et %}. Tout ce qui se trouve entre ces balises sera reporté directement en en-tête du fichier wrap généré. Comme notre fichier est censé faire l’interface entre notre langage de script et notre classe, il paraît logique d’y intégrer le header de notre classe Nombre.

Enfin, la dernière instruction %include "nombre.h" indique d’inclure à cette place le header nombre.h. Cela a en fait le même effet que de recopier le contenu du fichier à cet endroit. Cette partie est la partie qu’on demande à SWIG d’interfacer.

1.2.2 Génération du wrap

Pour cet exemple, on va tout d’abord construire le module pour Ruby. On exécute donc swig sur notre fichier myext.i avec la commande suivante :

$ swig -c++ -ruby myext.i

Dans cette commande, le premier flag -c++ indique à swig que l’on travaille en C++ plutôt qu’en C qui est le défaut. Le second argument indique le langage ciblé, en l’occurrence ici Ruby.

Cette commande nous génère le fichier myext_wrap.cxx.

1.2.3 Compilation du module

Il ne nous reste plus qu’à compiler le module :

$ g++ -fPIC -shared nombre.o myext_wrap.cxx -o myext.so

myext_wrap.cxx:879:10: fatal error: ruby.h: Aucun fichier ou dossier de ce type

 #include <ruby.h>

Mais cela ne marche pas ! Bien évidemment, on a besoin d’avoir accès aux headers de Ruby ! Comment connaître le chemin adéquat ? Pour cela, le plus simple est d’utiliser pkg-config --cflags package. Il faut tout d’abord vérifier qu’on a installé les paquets de développement pour Ruby et Python. Ensuite pkg-config --cflags ruby (sur certaines distributions, le nom du package n’est pas ruby, mais ruby-2.x) et pkg-config --cflags python3 nous indiquerons le chemin des headers. Par exemple sur une Ubuntu 18.04 :

$ pkg-config --cflags ruby

-I/usr/include/x86_64-linux-gnu/ruby-2.5.0 -I/usr/include/ruby-2.5.0

$ pkg-config --cflags python3

-I/usr/include/python3.6m -I/usr/include/x86_64-linux-gnu/python3.6m

Ce qui nous amène à notre nouvelle commande de compilation du module (les guillemets entourant la commande pkg-config sont des guillemets inversés (<AltGr> + <7>) :

$ g++ -fPIC -shared `pkg-config --cflags ruby` nombre.o myext_wrap.cxx -o myext.so

Il ne nous reste plus qu’à tester notre module myext.so dans une session interactive Ruby (le répertoire courant n’est pas par défaut dans le chemin de recherche de Ruby, d’où le ./ dans le require) :

$ irb

irb(main):001:0> require './myrbext'

=> true

irb(main):002:0> a=Myrbext::Nombre.new

=> #<Myrbext::Nombre:0x00005640fc17ae10 @__swigtype__="_p_Nombre">

irb(main):003:0> a.setV(2.5)

=> nil

irb(main):004:0> a.getV

=> 2.5

irb(main):005:0> a.carre

=> 6.25

Ça maaaaaaaaaaaaaarche!

Avec un minimum d’efforts, nous avons interfacé notre classe C++ pour la rendre utilisable depuis Ruby !

1.2.4 Nommage des modules

Avant de continuer, il faut noter que l’on n’a pas une totale liberté de nommage des modules. Supposons que l’on ait défini %module myext dans le fichier d’interface de swig :

  • nommage Ruby : le nom du fichier d’extension binaire devra obligatoirement être myext.so. Une fois dans l’interpréteur Ruby, le nom du module chargé sera Myext, le nom défini dans l’instruction %module, mais avec une capitale sur la première lettre (en Ruby, les constantes, les classes et les modules commencent toujours par une capitale) ;
  • nommage Python : lorsque swig va processer son fichier d’interface, il va générer deux fichiers ; le fichier wrap comme précédemment, mais également un fichier myext.py. Le fichier d’extension binaire devra quant à lui obligatoirement être nommé _myext.so.

Avec Python, l’extension est en fait composée de ces deux fichiers myext.py et _myext.so. Une fois dans l’interpréteur Python, le nom du module sera bien myext, c’est en fait la partie Python de l’extension qui chargera la partie purement binaire.

1.2.5 On peaufine

Avant de passer à la suite, nous allons peaufiner notre fichier d’interface en utilisant le fait que celui-ci peut contenir des balises #ifdefet #ifndef, que l’on trouve classiquement dans les fichiers C et C++. SWIG définit un certain nombre de variables, comme SWIGPYTHON et SWIGRUBY, dont le nom est assez explicite et que nous allons utiliser ici pour définir un nom de module différent, suivant le langage (fichier myext.i) :

// Nom du module en Python

#ifdef SWIGPYTHON

%module mypyext

#endif

// Nom du module en Ruby

#ifdef SWIGRUBY

%module myrbext

#endif

%{

#include "nombre.h"

%}

%include "nombre.h"

On peut maintenant tout reconstruire avec la séquence suivante :

$ rm *.cxx *.so

$ swig -c++ -ruby -o myext_ruby_wrap.cxx myext.i

$ g++ -fPIC -shared `pkg-config --cflags ruby` \

> myext_ruby_wrap.cxx nombre.o -o myrbext.so ;

$ swig -c++ -python -o myext_python_wrap.cxx myext.i

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> myext_python_wrap.cxx nombre.o -o _mypyext.so ;

On notera ici l’utilisation de l’option -o de swig permettant de spécifier le nom du fichier wrap généré. Cela est nécessaire, car on utilise le même fichier d’interface et sans cette option, le nom du fichier wrap généré serait le même pour Python et Ruby.

Nous pouvons maintenant tester l’extension Python :

$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)

[GCC 8.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import mypyext

>>> a=mypyext.Nombre()

>>> a.setV(2.5)

>>> a.getV()

2.5

>>> a.carre()

6.25

Ça marche (oui, l’excitation de la première fois s’estompe...).

1.2.6 Visibilité des méthodes

Pour obtenir les fichiers correspondant à cette partie :

$ git checkout visibilite

Tel qu’on a fait les choses ici, toutes les méthodes de notre classe sont exposées au langage ciblé. Il y a des fois où on souhaitera que certaines méthodes ne soient pas accessibles depuis le langage de script. Mettons par exemple qu’on ne souhaite pas que l’extension donne accès à la méthode carre. On va pouvoir procéder de deux façons.

Modification du fichier d’interface

Ici, on va remplacer l’instruction %include "nombre.h" par une version tronquée de la classe (toujours dans le fichier myext.i) :

// Nom du module en Python

#ifdef SWIGPYTHON

%module mypyext

#endif

// Nom du module en Ruby

#ifdef SWIGRUBY

%module myrbext

#endif

%{

#include "nombre.h"

%}

class Nombre

{

public:

    Nombre();

    Nombre(double v);

    void setV(double v);

    double getV();

};

Ainsi swig n’interfacera que les méthodes qu’on lui aura indiquées.

Modification du header

On peut aussi ne pas toucher au fichier d’interface et ajouter des balises conditionnelles dans le header nombre.h :

#ifndef NOMBRE_H

#define NOMBRE_H

class Nombre

{

public:

    Nombre();

    Nombre(double v);

    void setV(double v);

    double getV();

#ifndef SWIG

    double carre();

#endif

private:

    double valeur;

};

#endif // NOMBRE_H

De cette façon, lors de la compilation “normale” du fichier, la variable SWIG n’étant pas définie, il n’y aura pas de changement, par contre lorsque swig va processer l’instruction %include "nombre.h" du fichier d’interface, la variable SWIG étant définie, la méthode carre sera ignorée.

2. Classe Qt avec QString

Parmi les tâches effectuées par SWIG, il y a tout un travail bidirectionnel de conversion de types entre le C++ et les langages ciblés. SWIG connaît parfaitement les types standard, mais que se passe-t-il s’il y a des types non reconnus par SWIG ?

Pour obtenir les fichiers correspondant à cette partie :

$ git checkout qstring

2.1 La classe Nombre avec des QString

Pour tester cela, quitte à nous compliquer la vie, soyons fous, nous allons transformer notre classe en une classe utilisant des QString, la classe gérant les chaînes de caractères dans la librairie Qt. Voici donc nos fichiers, en commençant par nombre.h :

#ifndef NOMBRE_H

#define NOMBRE_H

#include<QString>

class Nombre

{

public:

    Nombre();

    Nombre(double v);

    Nombre(double v, QString n);

    void setV(double v);

    double getV();

    double carre();

    void setNom(QString n);

    QString getNom();

private:

    double valeur;

    QString nom;

};

#endif // NOMBRE_H

Notre objet Nombre a maintenant comme attributs une valeur et un nom. On ajoute les méthodes d’accès au nouvel attribut setNom(QString n) et getNom(), ainsi qu’une nouvelle surcharge du constructeur prenant deux arguments Nombre(double v, QString n). Voici le fichier nombre.cpp :

#include "nombre.h"

Nombre::Nombre() {

    valeur=0.0;

    nom="";

}

Nombre::Nombre(double v) {

    valeur=v;

    nom="";

}

Nombre::Nombre(double v, QString n) {

    valeur=v;

    nom=n;

}

void Nombre::setV(double v) {

    valeur=v;

}

double Nombre::getV() {

    return valeur;

}

double Nombre::carre() {

    return valeur*valeur;

}

void Nombre::setNom(QString n) {

    nom = n;

}

QString Nombre::getNom() {

    return nom;

}

On retrouve ici l’implémentation très simple de nos nouvelles méthodes.

Et enfin, un nouveau main.cpp pour tester notre classe.

#include <iostream>

#include "nombre.h"

#include <QCoreApplication>

using namespace std;

int main(int argc, char *argv[])

{

    QCoreApplication q(argc, argv);

    

    cout << "Test classe Nombre" << endl;

    Nombre* a= new Nombre();

    a->setV(2.5);

    cout << a->getV()  << endl;

    cout << a->carre() << endl;

    

    a->setNom("Truc");

    cout << a->getNom().toLocal8Bit().constData() << endl;

    

    Nombre* b=new Nombre(3.14,"pi");

    cout << b->getNom().toLocal8Bit().constData() << " " << b->getV() << endl;

    

    q.quit();

}

Pour ceux qui ne sont pas habitués à travailler avec Qt, il s’agit ici d’un squelette classique d’une application Qt : on crée un objet QCoreApplication auquel on passe les arguments du main, puis on fait éventuellement quelques opérations, avant de lancer la boucle événementielle Qt par la méthode q.exec(). En l’occurrence ici, il ne s’agit pas d’une « vraie » application, on teste juste la classe directement dans le main et au lieu de lancer la boucle événementielle, on quitte avec q.quit().

Quand on travaille dans un projet Qt, il est très vite assez délicat de compiler « à la main » (notamment quand on utilise des QObject, une des spécificités de Qt), c’est pourquoi Qt fournit un outil appelé qmake permettant, à partir d’un fichier projet, de générer un fichier Makefile adéquat.

Voici donc un fichier projet Qt : nombre.pro qui nous permettra de compiler notre classe et le programme de test. Je n’entre pas dans les détails de ce fichier projet, car c’est un tout autre sujet, on notera juste l’ajout de QMAKE_CXXFLAGS_RELEASE += -fPIC pour indiquer à qmake d’ajouter cette option qui, nous l’avons vu, nous est nécessaire :

QT -= gui

TEMPLATE = app

CONFIG += console c++11

CONFIG -= app_bundle

QMAKE_CXXFLAGS_RELEASE += -fPIC

TARGET = testNombre

SOURCES += main.cpp \

    nombre.cpp

HEADERS += \

    nombre.h  

Ce projet fonctionnera aussi bien en Qt4 qu’en Qt5, même si on va choisir de travailler avec Qt5 dans la suite. Pour pouvoir compiler et tester cette classe, il va falloir que les paquets de développement Qt5 ainsi que qmake soient installés. Sur Ubuntu 18.04, avec les paquets suivants, ça devrait être bon : qt5-default, qtbase5-dev, qtbase5-dev-tools, qt5-qmake-bin.

On construit le projet et on lance le programme de test, avant de passer à l’étape SWIG.

$ qmake nombre.pro

$ make

$ ./testNombre

Test classe Nombre

2.5

6.25

Truc

pi 3.14

Notre classe fonctionne ! Nous pouvons passer à la suite.

2.2 SWIG again

On s’attend au pire, mais on reprend simplement notre fichier myext.i, défini en 1.2.5.

2.2.1 Construction du module

$ swig -c++ -python -o myext_python_wrap.cxx myext.i

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> myext_python_wrap.cxx nombre.o -o _mypyext.so ;

In file included from myext_python_wrap.cxx:3114:0:

nombre.h:4:9: fatal error: QString: Aucun fichier ou dossier de ce type

 #include<QString>

         ^~~~~~~~~

compilation terminated.

Bon ben ça, c’est normal, il va falloir ajouter les headers de Qt, qu’on obtiendra avec pkg-config :

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> `pkg-config --cflags Qt5Core` \

> myext_python_wrap.cxx nombre.o -o _mypyext.so ;

C’est passé! Maintenant, testons dans une session Python :

$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)

[GCC 8.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import mypyext

...

_mypyext.so: undefined symbol: _ZN10QArrayData11shared_nullE

Je n’ai pas relevé toutes les insultes renvoyées par Python, mais l’une d’elles est particulièrement importante : ce undefined symbol pointant sur une classe Qt ! Bien évidemment, notre extension doit être linkée avec Qt, encore une fois pkg-config est notre ami, mais cette fois-ci avec le flag --libs. Recompilons donc notre extension :

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> `pkg-config --cflags Qt5Core` \

> myext_python_wrap.cxx nombre.o \

> `pkg-config --libs Qt5Core` -o _mypyext.so ;

Et testons de nouveau avec Python :

$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)

[GCC 8.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import mypyext

>>>

Pas d’insultes ! On continue.

>>> a=mypyext.Nombre()

>>> a.setV(2.5)

>>> a.getV()

2.5

>>> a.carre()

6.25

>>> a.setNom("Truc")

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "(...)/mypyext.py", line 122, in setNom

    return _mypyext.Nombre_setNom(self, n)

TypeError: in method 'Nombre_setNom', argument 2 of type 'QString'

>>> a.getNom()

<Swig Object of type 'QString *' at 0x7fb79cb7af60>

Les méthodes utilisant des QString ne fonctionnent pas. Le wrapper ne savait tout simplement pas comment les gérer, il va donc falloir expliquer à swig que faire de ces QString, cela va se faire en introduisant des directives supplémentaires dans le fichier d’interface, avec le mot-clé %typemap.

2.2.2 Les typemaps

L’écriture de ces %typemap n’est pas triviale, il s’agit en fait d’écrire un morceau de code C, qui va utiliser les fonctions de conversion de l’API C de Python [5] ou du langage sur lequel on travaille, bien sûr. Pour chaque langage, la plupart de ces fonctions sont répertoriées dans la documentation de SWIG, sous la rubrique « Useful functions » de la section associée au langage. Malheureusement, dans cette documentation, en ce qui concerne Python, les fonctions répertoriées pour les chaînes de caractères sont celles de Python 2, qui ne sont pas les mêmes que celles de Python 3 (PyString en Python 2 est en gros PyUnicode en Python 3), il faut donc rechercher dans l’API les équivalents.

Voici donc le fichier d’interface modifié (myext.i) :

// Nom du module en Python

#ifdef SWIGPYTHON

%module mypyext

// C++ to Python

%typemap(out) QString {

  $result = PyUnicode_FromString($1.toUtf8().constData());

}

// Python to C++

%typemap(in) QString {

  $1 = QString(PyUnicode_AsUTF8($input));  

}

#endif

// Nom du module en Ruby

#ifdef SWIGRUBY

%module myrbext

#endif

%{

#include "nombre.h"

%}

%include "nombre.h"

Examinons tout d’abord la première partie ajoutée, le %typemap(out) :

// C++ to Python

%typemap(out) QString {

  $result = PyUnicode_FromString($1.toUtf8().constData());

}

Cette partie gère le cas où on a un QString qui vient du C++ et qu’on veut transmettre sous forme d’une chaîne de caractères Python. Dans cette macro %typemap, il y a deux variables définies par SWIG, qui sont $result et $1. $1 correspond à la variable C++ et $result à la variable Python. Le petit bout de code de conversion devient alors limpide. En effet, toUtf8().constData() appliqué à un objet QString retourne une variable de type char* [6], et PyUnicode_FromString retourne un objet PyUnicode à partir d’une variable d’entrée de type char*!

Voyons maintenant la seconde partie ajoutée, le %typemap(in) :

// Python to C++

%typemap(in) QString {

  $1 = QString(PyUnicode_AsUTF8($input));  

}

Cette partie gère le cas où on a une chaîne de caractère venant de Python (donc un PyUnicode) qu’on veut transmettre au C++ sous forme d’un QString. Cette fois-ci, les variables définies par SWIG sont $input et $1. Ici aussi, $1 correspond à la variable côté C++ et $input correspond à la variable côté Python. PyUnicode_AsUTF8 retourne une variable de type char* à partir de la variable d’entrée de type PyUnicode, et on peut construire un QString à partir d’une variable de type char*.

On va maintenant reconstruire le module et le tester :

$ swig -c++ -python -o myext_python_wrap.cxx myext.i

nombre.h:11: Warning 472: Overloaded method Nombre::Nombre(double,QString)

with no explicit typecheck typemap for arg 1 of type 'QString'

Argh, un warning ! Visiblement la surcharge avec le QString va poser problème. Bon, on continue quand même, un problème à la fois :

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> `pkg-config --cflags Qt5Core` \

> myext_python_wrap.cxx nombre.o \

> `pkg-config --libs Qt5Core` -o _mypyext.so ;

Python 3.6.7 (default, Oct 22 2018, 11:32:17)

[GCC 8.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import mypyext

>>> a=mypyext.Nombre(2.5)

>>> a.setNom("Truc")

>>> a.getNom()

'Truc'

>>> b=mypyext.Nombre(3.14,"pi")

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "(...)/mypyext.py", line 106, in __init__

    this = _mypyext.new_Nombre(*args)

NotImplementedError: Wrong number or type of arguments for

overloaded function 'new_Nombre'.

  Possible C/C++ prototypes are:

    Nombre::Nombre()

    Nombre::Nombre(double)

    Nombre::Nombre(double,QString)

Bonne nouvelle : nos %typemap fonctionnent bien dans les deux sens, comme le montre l’utilisation des méthodes setNom et getNom. Par contre, comme l’indiquait le warning apparu lors de l’exécution de la commande swig, le constructeur surchargé avec un QString ne fonctionne pas. La raison en est que SWIG n’a en fait aucune idée de ce qu’est un QString, malgré nos %typemap qui, en fait, ne donnent que des recettes de cuisine pour passer des QString de C++ à Python et inversement. Quand côté Python, on invoque le constructeur Nombre(3.14,"pi"), en fait on invoque Nombre avec un PyFloat et un PyUnicode et ça, ce n’est pas défini.

Il va nous falloir ajouter un nouveau type de %typemap, un %typemap(typecheck) qui va faire le lien entre le type C++ QString et le type chaîne de caractères.

On ajoute donc à notre fichier d’interface le bloc suivant :

// typecheck mandatory for overloaded constructor

%typemap(typecheck,precedence=SWIG_TYPECHECK_STRING) QString {

 $1 = PyUnicode_Check($input)? 1 : 0;

}

Il y a deux parties à noter dans cette définition : d’une part l’en-tête qui indique à SWIG qu’il doit considérer que QString est une chaîne de caractères (variable prédéfinie SWIG_TYPECHECK_STRING), d’autre part le bloc interne qui donne une méthode pour vérifier qu’il s’agit bien d’un objet que l’on saura convertir ensuite en QString. On teste donc ici le fait que l’objet Python en entrée est bien un PyUnicode.

Nous pouvons maintenant reconstruire et tester notre module :

$ swig -c++ -python -o myext_python_wrap.cxx myext.i

$ g++ -fPIC -shared `pkg-config --cflags python3` \

> `pkg-config --cflags Qt5Core` \

> myext_python_wrap.cxx nombre.o \

> `pkg-config --libs Qt5Core` -o _mypyext.so ;

$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)

[GCC 8.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import mypyext

>>> b=mypyext.Nombre(3.14,"pi")

>>> b.getV()

3.14

>>> b.getNom()

'pi'

>>>

Pour finir, voici donc notre fichier d’interface final avec, en prime, les versions Ruby des %typemap (myext.i) :

// Nom du module en Python

#ifdef SWIGPYTHON

%module mypyext

// C++ to Python

%typemap(out) QString {

  $result = PyUnicode_FromString($1.toUtf8().constData());

}

// Python to C++

%typemap(in) QString {

  $1 = QString(PyUnicode_AsUTF8($input));  

}

// typecheck mandatory for overloaded constructor

%typemap(typecheck,precedence=SWIG_TYPECHECK_STRING) QString {

 $1 = PyUnicode_Check($input)? 1 : 0;

}

#endif

// Nom du module en Ruby

#ifdef SWIGRUBY

%module myrbext

// C++ to Ruby

%typemap(out) QString {

  $result = rb_str_new2($1.toUtf8().constData());

}

// Ruby to C++

%typemap(in) QString {

  $1 = QString(StringValuePtr($input));

}

// typecheck mandatory for overloaded constructor

%typemap(typecheck,precedence=SWIG_TYPECHECK_STRING) QString {

 $1 = RB_TYPE_P($input, T_STRING)? 1 : 0;

}

#endif

%{

#include "nombre.h"

%}

%include "nombre.h"

Ah et allez hop c’est pour moi c’est cadeau, voici la version des %typemap pour Python2 :

// C++ to Python

%typemap(out) QString {

  $result = PyString_FromString($1.toUtf8().constData());

}

// Python to C++

%typemap(in) QString {

  $1 = QString(PyString_AsString($input));  

}

// typecheck mandatory for overloaded constructor

%typemap(typecheck,precedence=SWIG_TYPECHECK_STRING) QString {

 $1 = PyString_Check($input)? 1 : 0;

}

2.3 Et si on avait simplement mis des string C?

Pour obtenir les fichiers correspondant à cette partie :

$ git checkout cstring

Si, au lieu d’utiliser les QString de Qt, on avait simplement mis des string C, c’est-à-dire des char*? Comme il s’agit d’un type standard C, c’est parfaitement géré par SWIG et on n’a pas besoin d’écrire de %typemap.

2.4 Et si on avait simplement mis des string C++?

Pour obtenir les fichiers correspondant à cette partie :

$ git checkout stdstring

Si, au lieu d’utiliser les QString de Qt, on avait mis des string C++, c’est-à-dire des std::string? Il s’agit d’un type standard C++ et c’est parfaitement géré par SWIG, mais il faut lui dire de le faire en ajoutant dans le fichier d’interface la ligne suivante :

%include <std_string.i>

2.5 Classe Qt héritant de QObject

Pour obtenir les fichiers correspondant à cette partie :

$ git checkout qstring_qobject

En complément, il est assez fréquent d’avoir des classes Qt qui héritent de Qobject ; cela va avoir plusieurs conséquences avec SWIG. D’une part, la définition de la classe va contenir la macro Q_OBJECT, par exemple notre classe d’exemple héritant de QObject donnerait ceci :

#ifndef NOMBRE_H

#define NOMBRE_H

#include<QString>

#include<QObject>

class Nombre : public QObject

{

    Q_OBJECT

public:

    Nombre(QObject* parent=0);

    Nombre(double v,QObject* parent=0);

    Nombre(double v, QString n,QObject* parent=0);

    void setV(double v);

    double getV();

    double carre();

    void setNom(QString n);

    QString getNom();

private:

    double valeur;

    QString nom;

};

#endif // NOMBRE_H

La présence de cette macro à cette place posera problème à swig lors de l’exécution du %include "nombre.h". Il faudra utiliser la même stratégie que décrite dans la section 1.2.6, c’est-à-dire soit recopier la classe « nettoyée » directement dans le fichier d’interface ou alors ajouter des #ifndef SWIG dans le fichier nombre.h.

D’autre part, lors de la compilation du projet Qt, les classes dérivant de QObject génèrent un fichier objet supplémentaire préfixé par moc_ ; par exemple dans notre cas, on aurait nombre.o et moc_nombre.o. Lors de la construction de l’extension, il faut impérativement y ajouter ces fichiers moc_.

Conclusion

Nous arrivons au terme de cet article de « travaux pratiques » qui, je l’espère, vous permettra de vous aider à démarrer dans l’utilisation de SWIG, afin de permettre à vos langages de script préférés d’utiliser vos librairies!

Références

[1] Création d’extensions Ruby : https://docs.ruby-lang.org/en/2.5.0/extension_rdoc.html

[2] Création d’extensions Python : https://docs.python.org/3/extending/building.html

[3] Page d’accueil de SWIG : http://www.swig.org

[4] Documentation de SWIG : http://www.swig.org/Doc3.0/index.html

[5] API C de Python : https://docs.python.org/3/c-api/index.html

[6] Documentation de QString : https://doc.qt.io/qt-5.6/qstring.html

 



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous