Automatiser la génération de bindings Python
Mardi 4 Avril 2017 à 22:01

L’objectif

Python est un langage très sympathique, notamment pour son expressivité. Prévenons tout de suite, je ne prendrai pas parti sur la question des performances de Python, je n’ai pas l’expérience ni les tests de performance sous la main pour tenir une position non trollesque (“Python c’est lent parce que c’est interprêté” -> []).

Ce qu’on appelle communément les bindings Python, c’est la possibilité d’implémenter une librairie native (en C/C++) ou d’en prendre une existante et d’utiliser les fonctions, variables et structures depuis Python. En fait, il s’agit juste de pouvoir définir des extensions au langage. Deux cas de figure (non exhaustifs) ou ça peut être utile :

  • Profiter des librairies C/C++ déjà existantes sans avoir à tout réimplémenter en Python.
  • Implémenter des procédures de manière très efficace et optimisée (calcul scientifique par ex) et manipuler ces blocs de code à haut niveau avec Python. Par exemple, GNU radio utilise très exactement cette méthode : le coeur des fonctions de traitement du signal est fait en C++, et les blocs sont instanciés et agencés en Python, sans se soucier de l’implémentation.

Les technologies

L’idée ici, c’est de générer des bindings simplement, via un pipeline un tant soi peu automatique. On s’appuie donc sur:

  • Simplified Wrapper and Interface Generator (SWIG) : swig permet de prendre un code en C/C++ et de générer des wrappers pour plusieurs langages. Le gros avantage est que pour un code défini et un fichier d’interface (spécifique à swig), on peut créer des bindings Python, Java, Go, Ruby, etc …
  • setuptools : Setuptools est le successeur de distutils pour compiler et installer des extensions Python. Il s’agit du moyen privilégié de packager des extensions Python.
  • Make : Pour orchestrer des builds raisonnables, un outil très efficace.

Au niveau des étapes, qu’est ce que cela donne maintenant ?

  1. Implémentation d’une librairie en C/C++, définition d’une interface avec l’ensemble des fonctions, structures, méthodes à exporter.
  2. Utilisation de SWIG pour générer un wrapper en C/C++ et un fichier Python qui va exporter l’interface depuis Python.
  3. Utilisation de setuptools pour compiler les sources en une librairie partagée avec les options de compilations propres à la plate-forme et à la version de Python utilisée.
  4. Intégrer un simple Makefile pour faire tout ça.

Une étape possible dont je ne parlerai pas, c’est publier le module sur le Python Packaging Index pour qu’il soit installable avec pip.

Show me the code !

Avouons le tout de suite, on va jouer à compiler un projet jouet. L’idée étant de mettre en place une configuration toute prête, extensible selon les besoins. Avant de passer directement à l’implémentation C, on va définir une implémentation de référence en Python pour voir les fonctionnalités que l’on veut avoir :

# household.py
class Person(object):

    def __init__(self, name="", age=0):
        self.age = age
        self.name = name

    def __eq__(self, other):
        return self.age == other.age and self.name == other.name

    def say_hello(self):
        print("Hey, my name is {} and I am {} years old."
              .format(self.name, self.age))

    def clone(self):
        return Person(self.name, self.age)


class Household(object):

    def __init__(self):
        self.occupants = []

    def add_person(self, person):
        self.occupants.append(person)

    def remove_person(self, person):
        try:
            self.occupants.remove(person)
        except ValueError:
            pass  # Silent removal

    def present_household(self):
        for occupant in self.occupants:
            occupant.say_hello()


def create_household():
    h = Household()
    h.add_person(Person("Bulbasaur", 9))
    h.add_person(Person("Charmander", 5))
    h.add_person(Person("Squirtle", 7))
    return h

Et un petit script de test, afin de vérifier les quelques fonctionnalités de notre programme, et s’assurer que l’on a pas d’erreur ou d’exception impromptue :

# main.py
import household as h


def main():
    empty = h.Person()
    half = h.Person("Half")
    tux = h.Person("Tux", 26)

    clone = tux.clone()

    char_5 = h.Person("Charmander", 5)
    bulb_7 = h.Person("Bulbasaur", 7)
    bulb_9 = h.Person("Bulbasaur", 9)
    squi_9 = h.Person("Squirtle", 9)

    hh = h.create_household()

    print(clone == tux)
    print(bulb_9 == bulb_7)
    print(bulb_9 == squi_9)
    # True
    # False
    # False

    clone.name = "Crystal"
    clone.age += 1

    empty.say_hello()
    half.say_hello()
    tux.say_hello()
    clone.say_hello()
    # Hey, my name is  and I am 0 years old.
    # Hey, my name is Half and I am 0 years old.
    # Hey, my name is Tux and I am 26 years old.
    # Hey, my name is Crystal and I am 27 years old.

    hh.present_household()
    # Hey, my name is Charmander and I am 5 years old.
    # Hey, my name is Bulbasaur and I am 7 years old.
    # Hey, my name is Squirtle and I am 9 years old.

    for i in range(3):
        hh.add_person(tux)
    hh.present_household()
    # Hey, my name is Charmander and I am 5 years old.
    # Hey, my name is Bulbasaur and I am 7 years old.
    # Hey, my name is Squirtle and I am 9 years old.
    # Hey, my name is Tux and I am 26 years old.
    # Hey, my name is Tux and I am 26 years old.
    # Hey, my name is Tux and I am 26 years old.

    hh.remove_person(tux)
    hh.present_household()
    # Hey, my name is Charmander and I am 5 years old.
    # Hey, my name is Bulbasaur and I am 7 years old.
    # Hey, my name is Squirtle and I am 9 years old.
    # Hey, my name is Tux and I am 26 years old.
    # Hey, my name is Tux and I am 26 years old.

    hh.remove_person(char_5)
    hh.remove_person(bulb_7)
    hh.remove_person(bulb_9)  # Removing a non-existant person shouldn't matter

    tux.age = 10
    tux.name = "Tutux"
    # Assert only shallow copies of tux are present in the household

    hh.present_household()
    # Hey, my name is Squirtle and I am 9 years old.
    # Hey, my name is Tutux and I am 10 years old.
    # Hey, my name is Tutux and I am 10 years old.

    hh.remove_person(tux)
    hh.remove_person(tux)
    hh.remove_person(squi_9)
    hh.present_household()
    # No output


if __name__ == "__main__":
    main()

Refactorisation

Maintenant, on va faire comme si le code précédent présentait de graves problèmes de performances, et pour cette raison on a besoin d’implémenter le module household en C. Mais on aimerait quand même garder notre programme principal en Python. Par la suite, on va suivre un processus incrémental pour construire notre petit pipeline.

Etape 1 : Implémentation de la librairie et déclaration des bindings

Un mot sur le fonctionnement de Swig pour faire des bindings Python: cet outil lit un fichier .i et va générer un fichier wrapper en C. Ce fichier présente un certain nombre de fonctions générées automatiquement comme des getters et setters, selon le contenu du fichier d’interface. C’est dans ce fichier que Swig va faire la transition entre les PyObject et les types C utilisés dans la librairie. Il va vérifier les contraintes sur le type des paramètres et gérer une partie de l’allocation/libération de mémoire. Il va aussi créer le module Python qui va directement agir sur le wrapper et exporter les fonctions, méthodes et classes en Python. Dans un second temps, on optimisera cette étape et il fera les choses un peu différemment.

Ceci permet de faire une librairie purement en C, sans se soucier du langage pour lequel ces fonctionnalités seront exportées. Ensuite, la librairie ainsi que le wrapper C généré sont compilés en une librairie partagée, et le module Python généré est utilisable depuis un code Python extérieur: l’extension est complétée !

On commence par le fichier d’interface Swig, le fichier unique qui lui sert à générer des bindings pour n’importe quel langage supporté.

/* File : household.i */
%module household
%{
/* Put headers and other declarations here */
#include "household.h"
%}

/* Indicate that we will provide custom constructors for the classes */
%nodefaultctor;

/* Indicate functions (not constructors) that allocate a new object on the heap */
%newobject Person_clone;
%newobject create_household;


typedef struct {
    int age;
    char* name;
    %extend {
        Person(char* name="" , int age=0);
        /* Do not forget to free the name once its used */
        ~Person() {
            free($self->name);
        }
        void say_hello();
        Person* clone();
        /* Overload equality operator */
        #ifdef SWIGPYTHON
            %rename(__eq__) equal;
        #endif
        long equal(Person* that);
    }
} Person;

typedef struct {
    /* Household has data members, but they should not be exposed to Python */
    %extend {
        Household();
        void add_person(Person* p);
        void remove_person(Person* p);
        void present_household();
    }
} Household;

extern Household * create_household();

Ce fichier d’interface est globalement composé de deux parties : ce qui se trouvent entre les balises %{ ... %} et le reste. Tout ce qui se trouve entre ces balises va être copié verbatim dans le wrapper C généré : ici on recopie juste notre fichier header, pour pouvoir compiler le wrapper. Le reste du document sert à dire à swig quelles fonctions/structures vont être exportées et comment les exporter.

Au niveau des instructions qui tiennent sur une ligne, assez logiquement module permet de nommer notre module. La directive nodefaultctor indique à Swig de ne pas générer les constructeurs par défaut, c’est à nous de les implémenter en suivant ses conventions. Les directives newobject indique que certaines méthodes sont des constructeurs, et Swig (devrait) l’utiliser pour s’aider à gérer la mémoire. Enfin, la directive extern de la fin dit à Swig d’exporter la fonction globale create_household.

La partie peu intuitive arrive: les expressions typedef struct ne servent pas à définir des structures (c’est le .h qui s’en charge). Elles servent à dire à Swig de générer des wrappers autour de ces structures (ouioui, c’est différent). Étudions d’abord Household, dont le code est un peu plus simple.

En l’occurrence, on dit simplement à Swig de générer un wrapper autour de notre constructeur ainsi qu’autour des trois fonctions membres de Household. À noter aussi que la structure Household a des champs internes (définis dans le .h et utilisés dans l’implémentation) mais comme on ne les a pas inclus dans le fichier d’interface, Swig ne générera pas de getter ou de setter.

La structure Person est un peu plus compliqueé, mais pas tant que ça. La présence des champs de la structure indique à Swig de créer les getters et setters appropriés. Le constructeur montre qu’il est possible d’inclure des valeurs par défaut pour les arguments : Swig va faire attention aux arguments passés par Python (nombre et type). Enfin, mis à part les deux fonctions membres, on réécrit aussi le destructeur de Person avec une syntaxe qui ressemble au C++. La pseudo-variable $self représente le this de C++. En l’occurrence, l’implémentation par défaut ne libère pas la mémoire allouée à name, d’où le besoin de réécrire le destructeur de Person.

Pour l’égalité, c’est un peu différent car chaque langage a sa propre convention pour surcharger les opérateurs. Ici, on définit une fonction equal qui prend une personne en paramètre et teste pour l’égalité avec la personne courante. Cependant, on spécifie aussi que si on génère des bindings Python (via la présence de la macro SWIGPYTHON placée par Swig), alors cette méthode equal va s’appeler __eq__, qui est la manière de surcharger l’opérateur == en Python. Swig s’occupera de transférer un appel à __eq__ vers equal.

Avec ce fichier d’interface, Swig va maintenant chercher des prototypes de fonctions particuliers. Les constructeurs d’une structure <struct> doivent se nommer new_<struct> et retourner un pointeur vers la structure. Les fonctions membres d’une structure doivent avoir le prototype <struct>_<method_name> et prendre en premier paramètre un pointeur sur la structure.

On peut voir ces conventions de nommage à l’oeuvre dans le fichier household.h.

// File: household.h
#ifndef HOUSEHOLD_H
#define HOUSEHOLD_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Struct Person
typedef struct {
    int age;
    char* name;
} Person;

// Struct Household
typedef struct {
    // Those fields are not exported
    int size;
    int max_size;
    Person** occupants;
} Household;

// Person constructor;
Person* new_Person(char* name, int age);

// Person methods
void Person_say_hello(Person* p);
Person* Person_clone(Person* p);
long Person_equal(Person* p, Person* that);

// Household constructor
Household* new_Household(void);

// Household methods
void Household_add_person(Household* h, Person* p);
void Household_remove_person(Household* h, Person* p);
void Household_present_household(Household* h);

// Additional household constructor
Household* create_household(void);

#endif

Pour l’implémentation de ces fonctions, il n’y a pas beaucoup de particularités. Il faut simplement faire attention à la mémoire allouée et libérée par swig, pour ne pas avoir de fuite mémoire ou ne pas déréférencer un pointeur nul. L’un des points qui m’a posé problème est la gestion des strings: quand il reçoit une chaîne de caractères de Python, swig la copie intégralement, la passe à la fonction appelée, puis libère la mémoire allouée à la chaîne.

Au final, le code reste assez simple, et voici l’implémentation C. On se débrouille simplement pour implémenter le tableau d’occupants de Household de manière dynamique, et c’est tout.

// File household.c
#include "household.h"

// Person constructor;
Person* new_Person(char* name, int age)
{
    Person* p;
    int size;
    p = (Person*)malloc(sizeof(Person));
    if (p != NULL) {
        size = strlen(name) + 1;
        p->name = (char*)memcpy(malloc(size * sizeof(char)), name, size * sizeof(char));
        p->age = age;
    }
    return p;
}

// Person methods
void Person_say_hello(Person* p)
{
    printf("Hey my name is %s and I am %d years old.\n", p->name, p->age);
}

// Allocate a new person
Person* Person_clone(Person* p)
{
    Person* new_p;
    int size;
    new_p = (Person*)malloc(sizeof(Person));
    if (new_p != NULL) {
        size = strlen(p->name) + 1;
        new_p->name = (char*)memcpy(malloc(size * sizeof(char)), p->name, size * sizeof(char));
        new_p->age = p->age;
    }
    return new_p;
}

// Test for equality
long Person_equal(Person* this, Person* that)
{
    return this->age == that->age && !strcmp(this->name, that->name);
}

// Household constructor
Household* new_Household(void)
{
    Household* h;
    h = (Household*)calloc(1, sizeof(Household));
    if (h != NULL) {
        h->max_size = 1;
        h->occupants = (Person**)malloc(1 * sizeof(Person*));
    }
    return h;
}

//Household methods
void Household_add_person(Household* h, Person* p)
{
    if (h->size == h->max_size) {
        h->max_size *= 2;
        h->occupants = realloc(h->occupants, sizeof(Person*) * h->max_size);
    }
    h->occupants[h->size] = p;
    h->size++;
}

void Household_remove_person(Household* h, Person* p)
{
    if (h->size != 0) {
        int delete = 0;
        int i = -1;
        //Find index to delete
        while (i < h->size - 1 && !delete) {
            i++;
            delete = Person_equal(h->occupants[i], p);
        }
        // We found an element to delete, swap it with the last element
        if (delete) {
            if (i != h->size - 1) {
                h->occupants[i] = h->occupants[h->size - 1];
            }
            h->size--;

            // Resize the array if there are too few elements
            if (h->size <= h->max_size / 4) {
                h->max_size /= 2;
                h->occupants = realloc(h->occupants, sizeof(Person*) * h->max_size);
            }
        }
    }
}

// Everyone, say hello !
void Household_present_household(Household* h)
{
    int i;
    for (i = 0; i < h->size; i++) {
        Person_say_hello(h->occupants[i]);
    }
}

// Additional household constructor
Household* create_household(void)
{
    Household* h;
    Person *charmander, *bulbasaur, *squirtle;
    h = new_Household();
    charmander = new_Person("Charmander", 5);
    bulbasaur = new_Person("Bulbasaur", 7);
    squirtle = new_Person("Squirtle", 9);
    Household_add_person(h, charmander);
    Household_add_person(h, bulbasaur);
    Household_add_person(h, squirtle);
    return h;
}

On peut aussi noter que le tableau d’occupants n’est pas du tout exporté vers Python, donc on a pas à se soucier de l’ordre des éléments, ou de la manière dont Python l’utilise : Il ne peut pas l’utiliser autrement qu’avec les méthodes add_person et remove_person. Si on souhaitait passer une liste en argument à une fonction et implémenter le traitement en C, il faudrait plonger dans les entrailles de swig et regarder comment faire (je n’ai pas été jusque là personnellement, mais ça implique des typemap).

Etape 2 : Génération de code

Tous les fichiers sont prêts, on peut passer à la génération de code ! Rien de plus simple, il suffit d’appeler swig -python household.i. Deux fichiers sont générés :

  • household_wrap.c contient tous les wrappers autour de nos structures et fonctions, écrit en C.
  • household.py contient le code qui s’appuie sur un _household.so (pas encore compilé) et va exporter les classes et fonctions Python que l’on veut.

Si on ne prend que les prototypes, voici le code Python généré :

class Person(_object):

    __swig_destroy__ = _household.delete_Person
    __del__ = lambda self: None

    def __init__(self, *args):
        # ...

    def __eq__(self, that):
        # ...

    def say_hello(self):
        # ...

    def clone(self):
        # ...

class Household(_object):

    __swig_destroy__ = _household.delete_Household
    __del__ = lambda self: None

    def __init__(self):
        # ...

    def add_person(self, p):
        # ...

    def remove_person(self, p):
        # ...

    def present_household(self):
        # ...

def create_household():
    # ...

J’ai enlevé toute la bidouille interne de Swig, pour ne laisser que l’interface exposée. Comme on peut le voir, cela correspond à ce que l’on voulait à la base, c’est donc une bonne chose !

Pour la partie destructeur, Swig se débrouille tout seul et surcharge de lui même le __del__ de Python. On peut compiler le code avec gcc et tester notre script de test du début avec ces commandes :

gcc -fpic -c household.c household_wrap.c -I/usr/include/python3.6m/
gcc -shared household.o household_wrap.o -o _household.so
python3 main.py

Rien d’exceptionnel, il faut juste lui donnez à manger le Python.h qui correspond à la version de Python visée (3.6 dans mon cas). Et miracle, on voit que tout fonctionne !

Maintenant que l’on a une base fonctionnelle, on peut s’intéresser à l’optimisation du code généré. En effet, par défaut Swig est compatible avec beaucoup de versions de Python (jusqu’à 2.2, c’est dire !) et rajoute pas mal de couches de traitement. Par exemple, voici la pile d’appel lorsque l’on appelle tux.say_hello() depuis le main:

say_hello
_household.Person_say_hello(self) // Fonction exportée par Swig, pas la notre
_household._wrap_Person_say_hello(self, *args)
_household.Person_say_hello(self) // Enfin notre implémentation

En fait, actuellement Swig se contente de définir des fonctions globale (les PyMethodDef) et d’appeler ces fonctions depuis la couche objet qu’il a généré en Python. Cependant, Swig peut aussi lui dire d’exporter des classes directement depuis le C. En fait, il s’agit plutôt de définir de nouveaux types, et pour cela définit des PyTypeObject, auxquels il accole directement les méthodes et un certains nombre d’autre fonctionnalités de base.

Pour cela, il suffit juste d’appeler swig avec le flag -builtin. On recompile tout, on re-teste, et … ça ne marche pas. Ou en tout cas, plus comme on voulait. Si on reprend les trois premiers tests, voici ce que l’on obtient :

print(clone == tux) # False
print(bulb_9 == bulb_7) # False
print(bulb_9 == squi_9) # False

Je la fais courte : l’opérateur d’égalité n’est plus pris en compte, la méthode __eq__ que l’on a définit ne surcharge plus l’égalité de base. En gros, lorsque l’on déclare un nouveau type Python depuis C, on lui fournit un certain nombre de fonctionnalités appelées slots. Il y en a quelques dizaines, et ils représentent les opérations primitives que ce type aura. Par exemple, il y a un slot pour faire un type hashable (tp_hash), lui donner des getattr et setattr, une représentation en chaîne de caractères, et plein d’autres. Il se trouve que pour définir un objet comme comparable, il faut lui fournir une fonction dans le slot tp_richcompare.

Swig le fait naturellement, mais la fonction fournie ne fait que de la comparaison de références. Ce qu’il nous faudrait, c’est pouvoir réécrire simplement le cas de l’égalité et le remplacer par notre propre code. Il se trouve que Swig a une feature qui permet de faire justement ça ! On peut lui spécifier une fonction a appeler par opération de comparaison (l’ensemble des opérations étant Py_LT, Py_LE, Py_EQ, Py_NE, Py_GE, Py_GT).

Voici le fragment d’interface changé.

typedef struct {
    ...
    %extend {
        ...
        /* Overload the equality operator */
        #ifdef SWIGPYTHON
            %feature("python:compare", "Py_EQ") equal;
        #endif
        long equal(Person* that);
    }
} Person;

Si on regarde le code généré par Swig, on peut voir que notre cas est traité ici :

SwigPyBuiltin__Person_richcompare(PyObject *self, PyObject *other, int op) {
    // ...
    switch (op) {
        case Py_EQ : result = _wrap_Person_equal(self, tuple); break;
        default : break;
    }
    // ...
}

On peut recompiler le code, re-tester … et se rendre compte qu’il y a encore un léger souci. En effet, au lieu de renvoyer les booléens True et False, la comparaison de deux objets Person renvoie 0 ou 1. Il faut non seulement dire à Swig comment il doit gérer l’égalité, mais aussi comment il doit interprêter le résultat qu’il obtient !

Malheureusement, pas d’option magique ici, c’est l’heure de sortir la grosse artillerie : les typemaps. On ne rentrera pas trop dans le détail, la documentation officielle le fait très bien.

Une typemap, c’est simplement une manière de définir une opération additionnelle sur la valeur de retour ou les arguments d’une fonction. Nous, on veut simplement ajouter une fonction pour exécuter une conversion entre le retour de notre fonction d’égalité et le passage de la valeur dans la variable result.

#ifdef SWIGPYTHON
    %feature("python:compare", "Py_EQ") equal;
    %typemap(out) int {
        $result = PyBool_FromLong($1);
    }
#endif
long equal(Person* that);

Le type de la typemap, out, indique que l’on va opérer sur une valeur de retour. Le long va être utilisé pour faire du pattern matching : notre typemap va être exécutée sur toutes les fonctions dont les types de retour sont des int. Comme on en a une seule, tout se passe bien. Ensuite, la variable $result permet de stocker la sortie de la typemap (rien à voir avec la variable result du code C). Enfin, on appelle la fonction PyBool_FromLong qui transforme un entier en un PyObject booléen et l’applique au premier argument de la fonction.

On a aussi la possibilité de faire un typedef long bool et définir la typemap sur le type bool pour éviter les effets de bords impromptus. Finalement, on peut aussi ajouter le flag -O à la compilation pour que Swig fasse d’autres optimisations avec le code généré, et ça ne casse rien de ce que l’on a déjà.

On peut maintenant recompiler, re-tester, et s’apercevoir que tout marche. On en a fini avec les bindings Python, on peut s’intéresser à les compiler et les installer plus proprement :-)

Etape 3 : Ajout de setuptools

Bon, on a fait la partie difficile, le reste c’est simplement jouer avec des fichiers de configuration pour obtenir le résultat souhaité. Il y a quelques manières de packager des projets Python, et toute cette partie va se baser sur le Python Packaging User Guide.

Le packaging d’un projet repose pour beaucoup sur le fichier setup.py. C’est dans ce fichier que sont contenus toutes les informations sur le projet, les dépendances et les fichiers nécessaires à la compilation. Voici un petit exemple pour le module :

#!/usr/bin/python

"""
setup.py file for Household module
"""

from setuptools import setup, Extension


household_module = Extension("_household",
                             sources=["household_wrap.c", "household.c"],
                             )

setup(name="household",
      version="1.0.0",
      author="Tristan Claverie",
      author_email="public@tclaverie.eu",
      license="MIT",
      description="Example module for Python bindings generation",
      ext_modules=[household_module],
      py_modules=["household"],
      )

Il n’y a pas grand chose à ajouter à cela, le nom des options s’explique de lui-même. Il y a probablement des subtilités présentes, mais je me suis contenté de trouver un ensemble d’option simple et clair, sans fouiller toutes les options disponibles.

Pour compiler notre extension via setuptools, il suffit de taper la commande ./setup.py build, et tout se fait tout seul. Magique ! Un dossier build est créé, qui contient la librairie partagée et une copie du fichier Python généré par Swig.

Ensuite, l’installation se fait tout aussi simplement via ./setup.py install. Il est possible qu’il demande les droits root, selon là où se trouve le dossier d’installation Python sur votre système.

Etape 4 : Passer à l’ère industrielle

Tout ça c’est bien gentil, mais il n’y a rien d’automatique, aucune architecture et toutes les opérations sont manuelles. On commence par changer l’architecture des dossiers, histoire d’organiser un peu le code. Assez logiquement, on va mettre tout le code C dans lib. On laisse setup.py à la racine et main.py n’a plus rien à faire dans le projet. Enfin, le fichier C de swig va se retrouver aussi dans lib, et le fichier Python va aller dans src.

Un simple Makefile pour faire le travail :

default: build

swig:
	swig -builtin -Wall -o lib/household_py_wrap.c -outdir src -python lib/household.i

build: swig
	python setup.py build

install:
	python setup.py install

uninstall:
	pip uninstall household

clean:
	rm -rf src/household.py lib/household_py_wrap.c build/* dist household.egg-info

Bon, en vrai j’ai essayé CMake. Et j’ai assez vite abandonné, quand j’ai vu le bordel que c’est. Dans l’idée, CMake est utilisé pour faire du multiplate-forme en générant des Makefiles avec toutes les bonnes options. Mais ici, setuptools fait déjà ce travail nativement ! L’intérêt que CMake peut avoir, c’est pour intégrer l’extension Python dans un bien plus gros projet (j’imagine), mais pour un module auto-suffisant cela ne sert pas à grand chose (de mon point de vue).

Et j’en ai eu marre de scroller du code sur github pour comprendre comment il marchait, ça a beaucoup joué aussi.

Micro-conclusion

Quoi qu’il en soit, j’éspère que vous aurez appris deux-trois petites choses sur les extensions natives en Python. L’idée de base est de fournir un exemple un peu plus complet que le copier-coller de la documentation de Swig. Il se trouve que l’on arrive très vite dans des situations bizarres, et voir toujours le même exemple minimaliste n’aide pas vraiment.

Cela dit, Swig est au final assez simple à prendre en main, et si on a pas de réponse à une question, le code source sur github est quand même très lisible (notamment pour avoir la liste des features disponibles). Sinon,l’ensemble est relativement modulable quand même. Je ne l’ai pas montré ici, mais il est aussi possible de rajouter des morceaux de code Python directement dans l’interface, pour une fonction par exemple. Je ne l’ai pas fait parce que je voulais l’interface la plus multi-langages possible, mais la possibilité existe.


Back to posts