Python et le C

Vincent Bernat

Note

Cet ar­ticle a été pu­blié dans GNU/Linux Ma­ga­zine n° 132 en 2010. Il est re­pro­duit ici avec de lé­gères re­touches cos­métiques.

Py­thon est un lan­gage par­ti­cu­liè­re­ment riche. Il dis­pose no­tam­ment d’une im­por­tante bi­blio­thèque stan­dard cou­vrant de larges be­soins. Tou­te­fois, il n’est pas rare de re­cou­rir à un mo­dule ex­terne. Par exemple, si votre ap­pli­ca­tion né­ces­site l’ac­cès à une base de don­nées Postgre­SQL, vous pou­vez re­cou­rir au mo­dule tiers psycopg2. Qu’ils soient ou non in­clus dans la bi­blio­thèque stan­dard, il existe deux grandes fa­milles de mo­dules : les mo­dules na­tifs, écrits en­tiè­re­ment en Py­thon et les ex­ten­sions, qui sont des mo­dules écrits dans un autre lan­gage, ty­pi­que­ment le C. Nous al­lons nous in­té­res­ser dans cet ar­ticle aux dif­fé­rentes fa­çons d’écrire une ex­ten­sion Python.

Une extension ou un module natif ?#

Tout d’abord, pour­quoi choi­sir d’écrire une ex­ten­sion plu­tôt qu’un mo­dule na­tif ? Il y a des avan­tages et des in­con­vé­nients aux deux ap­proches.

Un mo­dule na­tif ne né­ces­site pas de com­pi­la­tion et au­cun ou­til sup­plé­men­taire pour être fonc­tion­nel. Il est alors gé­né­ra­le­ment im­mé­dia­te­ment por­table et fonc­tion­nera de fa­çon iden­tique quel que soit l’OS : cela fonc­tion­nera aussi bien sous Mi­cro­soft Win­dows que sous une dis­tri­bu­tion GNU/Linux. En­fin, il est in­utile d’ap­prendre un autre lan­gage ! En écri­vant votre mo­dule di­rec­te­ment en Py­thon, vous dis­po­sez im­mé­dia­te­ment de ses qua­li­tés : ri­chesse, dy­na­misme et les nom­breux mo­dules et ex­ten­sions exis­tants sur les­quels s’appuyer.

Tou­te­fois, il n’est pas tou­jours pos­sible ou sou­hai­table d’écrire un mo­dule na­tif. Le cas le plus cou­rant et que nous al­lons ex­ploi­ter par la suite est l’uti­li­sa­tion d’une bi­blio­thèque écrite dans un autre lan­gage comme le C. Il va fal­loir faire le lien entre cette bi­blio­thèque et Py­thon. Une autre rai­son est la vi­tesse. L’in­ter­pré­teur Py­thon le plus cou­ram­ment uti­lisé (CPython) n’est pas en­core ter­ri­ble­ment ra­pide. Si la vi­tesse est un point im­por­tant, vous pou­vez avoir à ré­écrire cer­taines par­ties de votre ap­pli­ca­tion dans un lan­gage plus sta­tique mais plus ra­pide comme le C. En­fin, vous pou­vez avoir be­soin d’ac­cé­der à cer­taines fonc­tions de bas ni­veau, in­ac­ces­sibles ou dif­fi­ci­le­ment ac­ces­sibles en Py­thon, pour par exemple pi­lo­ter un ap­pa­reil élec­tro­nique sur une in­ter­face peu com­mune. No­tez que ce der­nier exemple est de moins en moins va­lable car Py­thon se dote d’ex­ten­sions pour com­bler ce be­soin, comme le pi­lo­tage des pé­ri­phé­riques USB.

La tortue#

Pour illus­trer cet exemple, ima­gi­nons que vous ve­nez de re­ce­voir une tor­tue ro­bot. Il s’agit d’un pe­tit ro­bot des­tiné à re­pro­duire le com­por­te­ment de la tor­tue du lan­gage LOGO. Le ro­bot est ca­pable d’in­ter­pré­ter un très grand nombre de com­mandes. On peut par exemple lui de­man­der d’avan­cer, de tour­ner à gauche, de bais­ser le crayon, de chan­ger de crayon, etc. Il s’in­ter­face avec un PC afin de pou­voir lui en­voyer les com­mandes adé­quates. L’in­ter­face se fait avec un bus de com­mande sur port série.

Bibliothèque en C#

Cette tor­tue est four­nie avec une bi­blio­thèque en C per­met­tant de la pi­lo­ter avec quelques fonc­tions. Il s’agit d’une bi­blio­thèque très simple. Nous al­lons donc ex­ploi­ter cette bi­blio­thèque plu­tôt que de ten­ter de la ré­im­planter.

L’interface en C#

Voici l’in­ter­face four­nie avec la bi­bliothèque :

#ifndef _TORTUE_H
#define _TORTUE_H

/* Codes d'erreur */
#define TURTLE_ERROR_NO_ERROR      0
#define TURTLE_ERROR_COMMUNICATION 1
#define TURTLE_ERROR_INVALID_VALUE 2
#define TURTLE_ERROR_NOT_PRESENT   3

/* Object opaque représentant une tortue */
struct turtle;

/* Fonctions disponibles */
struct turtle* turtle_init(int port);
int            turtle_send(struct turtle *, const char *);
int            turtle_close(struct turtle *);
int            turtle_error(struct turtle *);
const char*    turtle_model(struct turtle *);
long int       turtle_status(struct turtle *);

#endif

La bi­blio­thèque est ca­pable de pi­lo­ter si­mul­ta­né­ment plu­sieurs tor­tues. Celles-ci sont re­liées sur un bus et iden­ti­fiées par un nu­méro. La pre­mière tor­tue ob­tient le nu­méro 1, la sui­vante le nu­méro 2 et ainsi de suite.

La fonc­tion turtle_​init() per­met d’ini­tia­li­ser une tor­tue. On ob­tient alors une struc­ture opaque qui sera uti­li­sée par les autres fonc­tions pour in­di­quer sur quelle tor­tue on dé­sire tra­vailler. Si la tor­tue de­man­dée n’existe pas, cette fonc­tion re­tourne NULL. L’in­ter­face pro­po­sée ne per­met pas d’ob­te­nir de plus amples ren­sei­gne­ments au su­jet de cette er­reur à ce niveau.

Les fonc­tions sui­vantes ren­voient -1 ou NULL en cas d’er­reur. Il est alors pos­sible d’ob­te­nir da­van­tage de ren­sei­gne­ments sur l’er­reur à l’aide de la fonc­tion turtle_​error(). Il y a 3 er­reurs pos­sibles seulement.

La fonc­tion turtle_​send() per­met d’en­voyer un ordre à la tor­tue. Elle ren­voie 0 en cas de suc­cès. On lui four­nit une chaîne comme FORWARD 10 pour la faire avan­cer ou LEFT 50 pour la faire tour­ner. Le ro­bot s’oc­cupe d’in­ter­pré­ter lui-même l’ordre.

Pour éteindre la tor­tue et li­bé­rer toutes les res­sources as­so­ciées, on uti­lise la fonc­tion turtle_​close(). Elle ren­voie 0 en cas de succès.

La fonc­tion turtle_​model() ren­voie une chaîne in­di­quant le mo­dèle de la tor­tue. En­fin, la fonc­tion turtle_​status() ren­voie un en­tier en­co­dant l’état de la tor­tue. Cet état dé­pend du mo­dèle et la bi­blio­thèque en C ne nous four­nit au­cune in­di­ca­tion sur sa si­gni­fi­ca­tion. Pour avoir plus de dé­tails sur cette va­leur, il faut lire les spé­ci­fi­ca­tions du mo­dèle de tor­tue que l’on utilise.

L’implémentation#

À moins de dis­po­ser d’un conces­sion­naire Tor­tue 3000 à proxi­mité de chez vous, il n’est pas fa­cile de trou­ver la tor­tue dé­crite ci-des­sus. Pour fa­ci­li­ter l’ex­pé­ri­men­ta­tion, nous al­lons pro­po­ser une im­plé­men­ta­tion mi­ni­ma­liste de cette bi­blio­thèque afin de pou­voir l’uti­li­ser tout au long de l’article.

L’im­plé­men­ta­tion pro­po­sée ici est ca­pable de pi­lo­ter seule­ment 3 tor­tues. Une er­reur est ren­voyée si on tente de pi­lo­ter une autre tor­tue. La troi­sième tor­tue est de plus dé­fec­tueuse. Il n’est pas pos­sible de lui en­voyer des ordres. Chaque tor­tue est d’un mo­dèle différent.

#include <stdio.h>
#include <stdlib.h>
#include "tortue.h"

FILE *output = NULL;

struct turtle {
  int index;  /* Index de la tortue */
  int error;  /* Dernière erreur de la tortue */
};

struct turtle*
turtle_init(int port)
{
  struct turtle *t;
  if ((port < 1) || (port > 3))
    return NULL;
  t = malloc(sizeof(struct turtle));
  if (!t) return NULL;
  t->index = port;
  t->error = 0;
  fprintf(output, "Open turtle %d\n", t->index);
  fflush(output);
  return t;
}

int
turtle_send(struct turtle *t, const char *command)
{
  if (t->index == 3) {
    t->error = TURTLE_ERROR_COMMUNICATION;
    return -1;
  }
  fprintf(output, "Command for turtle %d: %s\n", t->index, command);
  fflush(output);
  return 0;
}

int
turtle_close(struct turtle *t)
{
  free(t);
  return 0;
}

int
turtle_error(struct turtle *t)
{
  return t->error;
}

const char*
turtle_model(struct turtle *t)
{
  switch (t->index) {
  case 1: return "T1988";
  case 2: return "T2000";
  default: return "T3000";
  }
}

long int
turtle_status(struct turtle *t)
{
  switch (t->index) {
  case 1: return 458751;
  case 2: return 812;
  default: return 0;
  }
}

void __attribute__ ((constructor))
my_init() {
  output = fdopen(3, "a");
  if (!output) output = stderr;
}

Nous pou­vons com­pi­ler notre bi­blio­thèque avec les com­mandes suivantes :

$ gcc -O2 -Wall -fPIC -shared -Wl,-soname,libtortue.so.1 \
      -o libtortue.so.1.0.0
$ ln -s libtortue.so.1.0.0 libtortue.so.1
$ ln -s libtortue.so.1 libtortue.so

Cette bi­blio­thèque a une pe­tite bi­zar­re­rie qui va nous per­mettre de la tes­ter plus fa­ci­le­ment. Si le des­crip­teur 3 existe, elle va l’uti­li­ser pour im­pri­mer les mes­sages de diag­nos­tic. Il est alors pos­sible d’in­ter­cep­ter ces mes­sages pour vé­ri­fier le bon fonc­tion­ne­ment de notre bi­bliothèque.

Avant de pas­ser au Py­thon, es­sayons d’uti­li­ser notre bi­blio­thèque avec un simple pro­gramme de test.

/* sample.c */
#include <assert.h>
#include <stdlib.h>
#include "tortue.h"

int main() {
  struct turtle *t;
  assert((t = turtle_init(1)) != NULL);
  assert(turtle_send(t, "GO 10") == 0);
  assert(turtle_send(t, "LEFT 50") == 0);
  assert(turtle_send(t, "GO 40") == 0);
  assert(turtle_close(t) == 0);
  return 0;
}

Com­pi­lons et exécutons.

$ gcc -Wall -O2 -o sample sample.c -L. -ltortue
$ LD_LIBRARY_PATH=. ./sample
Open turtle 1
Command for turtle 1: GO 10
Command for turtle 1: LEFT 50
Command for turtle 1: GO 40

Tout semble fonc­tion­ner comme on l’attend !

L’interface Python#

Vous voilà donc en pos­ses­sion d’une su­perbe tor­tue et de quoi la pro­gram­mer ai­sé­ment en C. Tou­te­fois, vous des­ti­nez cette tor­tue à un pu­blic dé­si­reux de la pro­gram­mer en Py­thon plu­tôt qu’en C. Il y a alors deux so­lu­tions pos­sibles : ré­im­plé­men­ter la bi­blio­thèque en un mo­dule na­tif Py­thon (en re­gar­dant son code source ou en écou­tant ce qui se passe sur le port sé­rie) ou en créant une ex­ten­sion Py­thon. Nous nous orien­tons pour cet ar­ticle vers la se­conde solution.

Avant de se lan­cer dans le code de l’ex­ten­sion, il est pré­fé­rable de dé­fi­nir quelle in­ter­face nous dé­si­rons ob­te­nir. Dans les grandes lignes, nous vou­lons un ob­jet Turtle avec une mé­thode send() et des at­tri­buts model et status. Les er­reurs de­vront être conver­ties en ex­ceptions.

Afin de vé­ri­fier que l’in­ter­face que l’on va conce­voir est adap­tée à ce que nous vou­drons faire par la suite, il est gé­né­ra­le­ment utile d’écrire quelques lignes d’uti­li­sa­tion de l’ex­ten­sion plu­tôt que de fon­cer tête bais­sée dans le code. Gé­né­ra­le­ment, c’est à ce ni­veau qu’on se rend compte si l’in­ter­face est com­plète et pra­tique à uti­li­ser. Une autre ap­proche est d’écrire dès le dé­but des tests uni­taires. On pourra ainsi vé­ri­fier que le code ré­pond bien à nos at­tentes. Nous pour­rons aussi vé­ri­fier que les dif­fé­rentes im­plé­men­ta­tions que nous al­lons pro­po­ser par la suite sont bien équi­va­lentes. Lan­çons-nous sans plus at­tendre dans l’écri­ture de ces tests !

#!/usr/bin/python

import unittest
import os

r, w = os.pipe()
os.dup2(r, 200)
os.close(r)
os.dup2(w, 3)
os.close(w)
output = os.fdopen(200)

from tortue import Turtle
from tortue import TurtleException

class TestTurtle(unittest.TestCase):

    def test_turtle(self):
        t1 = Turtle(1)
        self.assertEqual(output.readline().strip(), "Open turtle 1")

    def test_send(self):
        t1 = Turtle(1)
        output.readline()
        t1.send("GO 10")
        self.assertEqual(output.readline().strip(), "Command for turtle 1: GO 10")

    def test_model(self):
        t1 = Turtle(1)
        output.readline()
        self.assertEqual(t1.model, "T1988")
        t2 = Turtle(2)
        output.readline()
        self.assertEqual(t2.model, "T2000")

    def test_status(self):
        t1 = Turtle(1)
        self.assertEqual(t1.status["ready"], True)
        self.assertEqual(t1.status["distance"], 458)
        self.assertEqual(t1.status["speed"], 75)

    def test_exception(self):
        t3 = Turtle(3)
        output.readline()
        self.assertRaises(TurtleException, t3.send, "GO 10")

if __name__ == "__main__":
    unittest.main()

Nous com­men­çons par ef­fec­tuer une pe­tite ma­ni­pu­la­tion au ni­veau des des­crip­teurs de fi­chier pour pou­voir lire ce qui sort du des­crip­teur 3. Cela va nous per­mettre de contrô­ler le bon fonc­tion­ne­ment de notre ex­ten­sion. Nous im­por­tons en­suite l’ex­ten­sion que nous sou­hai­tons tes­ter. Nous ef­fec­tuons en­suite les 5 tests né­ces­saires pour va­li­der notre in­terface.

L’implémentation en Python#

Notre bi­blio­thèque en C est très simple. Nous au­rions pu l’écrire di­rec­te­ment sous forme de mo­dule Py­thon. Ce n’est pas le but de l’exer­cice mais afin de bien mon­trer que les tests fonc­tionnent avant même d’écrire notre pre­mière ver­sion de l’ex­ten­sion, nous al­lons écrire la ver­sion en Py­thon. Pour ce faire, nous créons un ré­per­toire tortue dans le­quel nous pla­çons un fi­chier __init__.py conte­nant sim­ple­ment from native import *. Le but de ce fi­chier est d’ai­guiller notre mo­dule tortue sur la bonne ver­sion de l’ex­ten­sion. Dans ce ré­per­toire, nous pla­çons aussi le code du mo­dule na­tif dans le fi­chier native.py.

import os
import sys

from status import TurtleStatus

try:
    output = os.fdopen(3, "a")
except OSError:
    output = sys.stdout

class Turtle:
    def __init__(self, nb):
        print >> output, "Open turtle %d" % nb
        output.flush()
        self.nb = nb
        if nb == 1:
            self.model = "T1988"
            self.status = TurtleStatus(self.model, 458751)
        elif nb == 2:
            self.model = "T2000"
            self.status = TurtleStatus(self.model, 812)
        else:
            self.model = "T3000"
            self.status = TurtleStatus(self.model, 0)

    def send(self, cmd):
        if self.nb == 3:
            raise TurtleException("communication error")
        print >> output, "Command for turtle 1: %s" % cmd
        output.flush()

class TurtleException(Exception):
    pass

Ce mo­dule fait ap­pel à un mo­dule ad­di­tion­nel dont le rôle est de dé­chif­frer l’état du ro­bot en fonc­tion de son mo­dèle et de la va­leur nu­mé­rique de l’état. On dé­sire en ef­fet ob­te­nir un état sous forme de dic­tion­naire, plus li­sible qu’une va­leur nu­mé­rique. Comme le dé­co­dage de l’état du ro­bot peut évo­luer avec les nou­veaux mo­dèles, il nous ap­pa­raît plus simple de co­der ce­lui-ci en Py­thon plu­tôt que de l’in­clure dans l’ex­ten­sion. Bien sûr, par la suite, cela aura aussi une vertu pé­da­go­gique ! Le contenu du fi­chier status.py est le suivant :

class TurtleStatus(dict):

    def __init__(self, model, status):
        if model == "T1988":
            dict.__init__(
                self,
                {
                    "ready": (status % 10 == 1),
                    "distance": status / 1000,
                    "speed": (status / 10) % 100,
                },
            )
        else:
            dict.__init__(
                self, {"ready": (status & 2) != 0, "distance": (status & 0xFF0) >> 8}
            )

Une fois tout ceci en place, nous pou­vons lan­cer nos tests et tout de­vrait réussir !

$ python turtletest.py
----------------------
Ran 5 tests in 0.000s

OK

Nous n’avons ce­pen­dant pas uti­lisé la bi­blio­thèque en C. Si nous es­sayons de pi­lo­ter de vraies tor­tues, il ne va pas se pas­ser grand-chose. Il est donc temps d’écrire l’ex­ten­sion en uti­li­sant la bi­blio­thèque C.

Écriture de l’extension Python#

Il y a plu­sieurs mé­thodes pour écrire une ex­ten­sion Py­thon. Nous al­lons en voir 4 : l’uti­li­sa­tion de ctypes, l’uti­li­sa­tion de SWIG, l’uti­li­sa­tion de Py­rex et en­fin l’écri­ture de l’ex­ten­sion di­rec­te­ment en C avec l’API Python/C.

Utilisation de ctypes#

Le mo­dule ctypes per­met de s’in­ter­fa­cer avec une bi­blio­thèque et d’en uti­li­ser les fonc­tions. Son uti­li­sa­tion a déjà été dé­taillée dans un ar­ticle de Vic­tor Stin­ner dans un pré­cé­dent hors-sé­rie. Le grand avan­tage de cette ap­proche est de pou­voir ma­ni­pu­ler di­rec­te­ment la bi­blio­thèque sous sa forme com­pi­lée. On ob­tient ainsi une ex­ten­sion qui ne né­ces­site au­cune com­pi­la­tion pour être uti­li­sable. L’in­con­vé­nient est qu’il n’y a pas de fi­let de sé­cu­rité : si on se trompe ou si l’in­ter­face bi­naire de la bi­blio­thèque change, on aura gé­né­ra­le­ment droit à un seg­fault de l’in­ter­pré­teur Python.

L’uti­li­sa­tion du mo­dule ctypes est ex­trê­me­ment simple. On charge la bi­blio­thèque en mé­moire avec CDLL() puis on peut ap­pe­ler di­rec­te­ment les fonc­tions conte­nues dans la bi­blio­thèque en les consi­dé­rant comme des mé­thodes de l’ob­jet ob­tenu. Dans les cou­lisses, le mo­dule va se char­ger de trou­ver la si­gna­ture de la fonc­tion C à par­tir de la fa­çon dont vous in­vo­quez la mé­thode Python.

$ LD_LIBRARY_PATH=. python
Python 2.6.6 (r266:84292, Sep 14 2010, 08:45:25)
[GCC 4.4.5 20100909 (prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import *
>>> libtortue = CDLL("libtortue.so.1")
>>> t1 = libtortue.turtle_init(1)
Open turtle 1
>>> libtortue.turtle_model(t1)
-1600443770

Ce­pen­dant, tout ne peut pas se dé­rou­ler par ma­gie. Il fau­dra dans cer­tains cas ai­der un peu le mo­dule. Par exemple, par dé­faut, il est at­tendu que les fonc­tions C re­tournent un int. C’est pour cette rai­son que nous ob­te­nons -1600443770 dans l’exemple ci-des­sus. Il s’agit de la conver­sion du poin­teur sur une chaîne de ca­rac­tères en un en­tier. Il va fal­loir donc ai­der un peu ctypes en lui in­di­quant les va­leurs de retour.

>>> libtortue.turtle_init.restype = c_void_p
>>> libtortue.turtle_model.restype = c_char_p
>>> t1 = libtortue.turtle_init(1)
Open turtle 1
>>> libtortue.turtle_model(t1)
'T1988'

Bien en­tendu, il ne s’agit pas de se trom­per. Dans le cas contraire, la sanc­tion est im­mé­diate. C’est pour cette rai­son que le dé­ve­lop­pe­ment d’une ex­ten­sion avec ctypes est fra­gile et ne de­vrait per­mettre que de mon­ter un pre­mier prototype.

>>> libtortue.turtle_model(1)
sh: segmentation fault  LD_LIBRARY_PATH=. python

Le mo­dule ctypes per­met de nom­breuses autres ma­ni­pu­la­tions sur les struc­tures en C. Ce­pen­dant, nous n’en avons pas be­soin pour écrire notre ex­tension.

from ctypes import *
from status import TurtleStatus

libtortue = CDLL("libtortue.so.1")
libtortue.turtle_init.restype = c_void_p
libtortue.turtle_model.restype = c_char_p
libtortue.turtle_status.restype = c_long

class Turtle(object):
    def __init__(self, nb):
        t = libtortue.turtle_init(nb)
        if t == 0:
            raise TurtleException("unable to create turtle %d" % nb)
        self.t = c_void_p(t)

    def send(self, cmd):
        result = libtortue.turtle_send(self.t, cmd)
        if result != 0:
            raise TurtleException("got error %d" % libtortue.turtle_error(self.t))

    def __getattribute__(self, attr):
        if attr == "model":
            return libtortue.turtle_model(self.t)
        if attr == "status":
            s = libtortue.turtle_status(self.t)
            if s == -1:
                raise TurtleException("got error %d" % libtortue.turtle_error(self.t))
            return TurtleStatus(self.model, s)
        return object.__getattribute__(self, attr)

    def __del__(self):
        libtortue.turtle_close(self.t)

class TurtleException(Exception):
    pass

Nom­mez ce nou­veau fi­chier mctypes.py puis mo­di­fiez le fi­chier __init__.py pour im­por­ter ce fi­chier à la place de native.py. Les tests de­vraient pas­ser sans problème !

No­tez com­ment l’in­té­gra­tion est simple. La mé­thode send() ap­pelle sim­ple­ment la fonc­tion C send() et trans­forme l’er­reur éven­tuelle en ex­cep­tion. Nous avons éga­le­ment fait usage de la mé­thode __getattribute__ qui per­met d’in­ter­cep­ter les ac­cès aux at­tri­buts afin de four­nir dy­na­mi­que­ment le bon mo­dèle et sur­tout le bon état.

Le mo­dule ainsi construit est ce­pen­dant très fra­gile. Si une er­reur se glisse, le pro­gramme se ter­mi­nera sim­ple­ment sous forme d’un seg­fault, ce qui est as­sez in­ha­bi­tuel en Py­thon. C’est le lot de l’in­ter­fa­çage de Py­thon et C mais le pro­blème est ici ag­gravé par l’ab­sence de toute com­pi­la­tion qui per­met­trait de dé­tec­ter cer­taines er­reurs, y com­pris dans des par­ties du code non exé­cu­tées dans les tests.

Utilisation de SWIG#

SWIG si­gni­fie Sim­pli­fied Wrap­per and In­ter­face Ge­ne­ra­tor. Son but est de construire très ra­pi­de­ment à par­tir d’un fi­chier d’in­ter­face (notre tortue.h) une ex­ten­sion Python.

Lan­çons-nous dans le vif du su­jet. Pour uti­li­ser SWIG, il faut écrire un fi­chier d’in­ter­face. SWIG va lire ce­lui-ci pour gé­né­rer une ex­ten­sion écrite en C que l’on pourra en­suite com­pi­ler. Voici le fi­chier d’in­ter­face le plus court que nous pou­vons écrire :

%module swig
%{
#include "../tortue.h"
%}

%include "../tortue.h"

Un fi­chier d’in­ter­face est un fi­chier conte­nant des di­rec­tives C ainsi que des di­rec­tives spé­ci­fiques à SWIG com­men­çant par le signe %. La di­rec­tive %module in­dique le nom de l’ex­ten­sion que nous vou­lons construire. Le bloc qui suit, dé­li­mité par %{ et %} per­met d’in­clure du code ar­bi­traire dans l’ex­ten­sion en C gé­néré par SWIG. Nous l’uti­li­sons pour in­clure l’en­tête de notre bi­blio­thèque comme on l’a fait pour le pro­gramme d’exemple écrit en C. La di­rec­tive %include per­met d’in­clure un fi­chier ar­bi­traire dans notre fi­chier d’in­ter­face. Son for­mat doit être com­pré­hen­sible par SWIG. Notre fi­chier d’en­tête tortue.h est suf­fi­sam­ment simple pour être com­pris di­rec­te­ment par SWIG, on peut donc l’in­clure di­rec­te­ment. S’il était trop com­plexe ou conte­nant des sym­boles que l’on ne veut pas rendre ac­ces­sible dans l’ex­ten­sion Py­thon, il au­rait fallu écrire di­rec­te­ment les dé­cla­ra­tions de fonctions.

Nous al­lons main­te­nant com­pi­ler le fi­chier d’in­ter­face en un fi­chier C. Ce fi­chier C sera en­suite com­pilé en une ex­ten­sion Py­thon que l’on pourra uti­li­ser de­puis l’in­ter­préteur.

$ cd tortue
$ swig -python swig.i
$ gcc -Wall -O2 -c -fPIC swig_wrap.c $(python-config --cflags)
$ gcc -shared swig_wrap.o -L.. -ltortue -o _swig.so
$ cd ..
$ LD_LIBRARY_PATH=. python
>>> from tortue import swig
>>> [f for f in dir(swig) if f.startswith("turtle_")]
['turtle_close', 'turtle_error', 'turtle_init', 'turtle_model', 'turtle_send', 'turtle_status']
>>> a = swig.turtle_init(1)
>>> swig.turtle_send(a, "GO 10")
Command for turtle 1: GO 10
0

SWIG a converti cha­cune des dé­cla­ra­tions en une fonc­tion Py­thon que l’on peut uti­li­ser comme la fonc­tion C cor­res­pon­dante. On ob­tient ainsi une in­ter­face iden­tique de ce que l’on peut ob­te­nir di­rec­te­ment avec le mo­dule ctypes avec ce­pen­dant une meilleure vé­ri­fi­ca­tion des ar­gu­ments. Il y a tou­jours pos­si­bi­lité d’ob­te­nir un seg­fault, mais dans la plu­part des cas, les er­reurs sont dé­tec­tées cor­rectement :

>>> swig.turtle_model(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in >module<
TypeError: in method 'turtle_model', argument 1 of type 'struct turtle *'

Le tra­vail n’est ce­pen­dant pas fini. L’ex­ten­sion gé­né­rée par SWIG ne res­pecte pas du tout notre in­ter­face. Ce­pen­dant, l’in­ter­face ob­te­nue étant qua­si­ment iden­tique à celle ob­te­nue avec le mo­dule ctypes, on va re­prendre le fi­chier mctypes.py et le co­pier en mswig.py. Seul le dé­but change :

import swig as libtortue
from status import TurtleStatus

class Turtle(object):
    def __init__(self, nb):
        t = libtortue.turtle_init(nb)
        if t == 0:
            raise TurtleException("unable to create turtle %d" % nb)
        self.t = t

Le reste est stric­te­ment iden­tique. On mo­di­fie __init__.py pour uti­li­ser ce mo­dule et on re­lance les tests. Tout doit pas­ser cor­rectement.

SWIG dis­pose d’un cer­tain nombre de fonc­tion­na­li­tés pour pro­duire di­rec­te­ment une ex­ten­sion uti­li­sable sans wrap­per sup­plé­men­taire comme on a dû faire ici. Il est par exemple ca­pable de gé­né­rer des ex­cep­tions se­lon le code de re­tour des fonc­tions. Il est éga­le­ment ca­pable de trans­for­mer des struc­tures en ob­jet. En­fin, une der­nière fonc­tion­na­lité per­met de mo­di­fier les fonc­tions gé­né­rées en leur ajou­tant du code sup­plé­men­taire. Cette der­nière fonc­tion­na­lité nous se­rait né­ces­saire si on vou­lait ren­voyer une ins­tance de TurtleStatus lors de l’ac­cès à l’at­tri­but status. Nous n’al­lons pas dé­tailler ici l’en­semble de ces fonc­tion­na­li­tés car elles né­ces­sitent un cer­tain nombre de connais­sances sur l’API Py­thon et SWIG n’est gé­né­ra­le­ment pas uti­lisé pour pro­duire di­rec­te­ment une in­ter­face de haut ni­veau à par­tir d’une bi­blio­thèque en C de bas ni­veau. Re­gar­dons sim­ple­ment com­ment ob­te­nir un ob­jet Turtle plu­tôt qu’une suite de fonctions :

// swig.i
%module swig
%{
#include "../tortue.h"

  typedef struct {
    struct turtle *t;
  } Turtle;

  Turtle *new_Turtle(int port) {
    Turtle *t;
    t = malloc(sizeof(Turtle));
    t->t = turtle_init(port);
    return t;
  }

  void delete_Turtle(Turtle *t) {
    turtle_close(t->t);
    free(t);
  }

  void Turtle_send(Turtle *t, char *cmd) {
    turtle_send(t->t, cmd);
  }

  const char *Turtle_model_get(Turtle *t) {
    return turtle_model(t->t);
  }

  const int Turtle_status_get(Turtle *t) {
    return turtle_status(t->t);
  }
%}

typedef struct {
  %extend {
    Turtle(int);
    ~Turtle();
    void send(char *cmd);
    %immutable;
    const char *model;
    const int status;
  }
} Turtle;

Dans la par­tie C, nous dé­cla­rons une nou­velle struc­ture Turtle conte­nant juste l’iden­ti­fiant de la tor­tue. Dans la par­tie SWIG, nous uti­li­sons la di­rec­tive %extend afin de trans­for­mer cette struc­ture en classe et d’ajou­ter des mé­thodes et des at­tri­buts res­pec­tant notre in­ter­face. Nous avons ainsi un construc­teur (équi­valent de __init__), un des­truc­teur (équi­valent de __del__), la fonc­tion d’en­voi et les deux at­tri­buts model et status. Ces der­niers sont mar­qués comme %immutable pour in­di­quer que l’on ne va pas écrire de fonc­tion per­met­tant de les mo­di­fier. SWIG s’at­tend à dis­po­ser des fonc­tions cor­res­pon­dantes dé­fi­nies dans la partie C.

Cette nou­velle ex­ten­sion ne passe pas nos tests. La ges­tion des ex­cep­tions est ab­sente. Il fau­drait dé­cla­rer une nou­velle ex­cep­tion et l’uti­li­ser dans cha­cune des mé­thodes. La dé­cla­ra­tion et l’uti­li­sa­tion des ex­cep­tions fait ap­pel à des fonc­tions de l’API Py­thon que nous ver­rons par la suite. L’at­tri­but status est un en­tier alors que nous at­ten­dions une ins­tance de TurtleStatus. Il nous fau­drait im­por­ter le mo­dule conte­nant cet ob­jet puis ins­tan­cier cet ob­jet. Il nous fau­drait en­core faire ap­pel à des fonc­tions de l’API Py­thon que nous ver­rons par la suite.

SWIG est très in­té­res­sant si on a une bi­blio­thèque C avec beau­coup de fonc­tions à conver­tir, comme une bi­blio­thèque OpenGL. Un autre avan­tage de SWIG est sa com­pa­ti­bi­lité avec un grand nombre de ver­sions de Py­thon, de­puis la ver­sion 2.0 jusqu’aux versions 3.

Quand on es­saie d’al­ler un peu plus loin, il est né­ces­saire de faire ap­pel di­rec­te­ment à cer­taines fonc­tions de l’API Py­thon. L’uti­li­sa­tion avan­cée de SWIG peut donc de­ve­nir ra­pi­de­ment plus dif­fi­cile. Pour ces rai­sons, les bi­blio­thèques uti­li­sant SWIG sont gé­né­ra­le­ment de bas ni­veau : elles four­nissent un équi­valent Py­thon des fonc­tions en C, avec éven­tuel­le­ment une ges­tion des ex­cep­tions. L’ajout d’une in­ter­face plus py­tho­nique se fait en écri­vant un mo­dule ad­di­tion­nel en Py­thon uti­li­sant l’ex­ten­sion pro­duite par SWIG. Ainsi, au­cune connais­sance de l’API Py­thon n’est réel­le­ment né­ces­saire pour uti­li­ser SWIG.

Utilisation de Pyrex#

Il est pos­sible de com­bi­ner la grande li­berté d’uti­li­sa­tion du mo­dule ctypes avec la sé­cu­rité d’un com­pi­la­teur. Cette so­lu­tion s’ap­pelle Py­rex qui est un com­pi­la­teur de code Py­thon en C qui dis­pose de fonc­tion­na­li­tés per­met­tant d’uti­li­ser di­rec­te­ment des données C.

Py­rex prend en en­trée du code Py­thon et va le trans­for­mer en code C. Quelques li­mi­ta­tions sont pré­sentes, mais un code Py­thon exis­tant peut gé­né­ra­le­ment être trans­formé en C par Py­rex. Py­rex sait éga­le­ment uti­li­ser des don­nées et des fonc­tions en C. Il de­vient alors pos­sible de mé­lan­ger du code C et du code Py­thon, un peu à la ma­nière du mo­dule ctypes. L’ex­ten­sion que nous al­lons écrire est d’ailleurs très proche de ce que l’on peut faire avec le mo­dule ctypes :

cdef extern from "../tortue.h":
  struct turtle
  turtle* turtle_init(int port)
  int turtle_send(turtle *, char *)
  int turtle_close(turtle *)
  int turtle_error(turtle *)
  char* turtle_model(turtle *)
  long int turtle_status(turtle *)

from status import TurtleStatus

cdef class Turtle:

    cdef turtle *t

    def __cinit__(self, port):
        cdef turtle* t
        t = turtle_init(port)
        if (t == NULL):
            raise TurtleException("unable to create turtle %d" % port)
        self.t = t

    def send(self, cmd):
        cdef int result
        result = turtle_send(self.t, cmd)
        if result != 0:
            raise TurtleException("got error %d" % turtle_error(self.t))

    def __getattr__(self, attr):
        cdef int s
        if attr == "model":
            return turtle_model(self.t)
        if attr == "status":
            s = turtle_status(self.t)
            if s == -1:
                raise TurtleException("got error %d" % turtle_error(self.t))
            return TurtleStatus(self.model, s)
        return object.__getattribute__(self, attr)

    def __dealloc__(self):
        turtle_close(self.t)

class TurtleException(Exception):
    pass

En tête du fi­chier, on im­porte les fonc­tions qui nous in­té­ressent. La syn­taxe n’est ni du Py­thon, ni du C. Il n’est pas pos­sible d’im­por­ter tout un tas de fonc­tions sans les énu­mé­rer une à une. On dé­clare d’abord la struc­ture struct turtle. Elle est ré­fé­ren­cée par la suite sous le nom turtle. On dé­fi­nit en­suite chaque fonc­tion en sui­vant au mieux le pro­to­type de la fonc­tion C. Py­rex n’aime pas le mot clef const ; on ne l’uti­lise donc pas. Une fois les fonc­tions C ainsi dé­fi­nies, on peut y faire ap­pel n’im­porte où dans le code.

Sans pré­ci­sions sup­plé­men­taires, toutes les va­riables sont ré­pu­tées conte­nir des ob­jets Py­thon. Si be­soin et si pos­sible, Py­rex as­su­rera la conver­sion au­to­ma­tique des don­nées C en ob­jets Py­thon. Pour les don­nées qui ne peuvent pas être conver­ties en Py­thon ou pour les­quelles on ne sou­haite pas de conver­sion en Py­thon (pour des rai­sons de per­for­mance par exemple), il est né­ces­saire de les dé­cla­rer à l’aide du mot clef cdef. No­tons que la classe en­tière est dé­cla­rée avec le mot clef cdef. En ef­fet, on dé­sire sto­cker dans la classe le poin­teur vers notre tor­tue et il n’est pas pos­sible de sto­cker une don­née C dans un ob­jet Py­thon. Les mé­thodes __init__ et __del__ de­viennent __cinit__ et __dealloc__ (sa­chant qu’il faut évi­ter de faire trop de choses dans cette der­nière). Si la bi­blio­thèque en C uti­li­sait un en­tier plu­tôt qu’une struc­ture pour iden­ti­fier les tor­tues, il au­rait été pos­sible d’uti­li­ser une classe Py­thon clas­sique. En­fin, no­tons le mé­lange cu­rieux de l’uti­li­sa­tion de __getattr__ et __getattribute__ pour une classe n’hé­ri­tant pas de object. C’est une par­ti­cu­la­rité de Pyrex.

Es­sayons de com­pi­ler ce code après avoir mo­di­fié __init__.py pour uti­li­ser l’ex­ten­sion écrite avec l’aide de pyrex :

$ pyrexc pyrex.pyx
$ gcc -shared -fPIC -O2 -Wall $(python-config --cflags) pyrex.c \
      -L.. -ltortue -o pyrex.so
$ (cd .. ; LD_LIBRARY_PATH=. python turtletest.py)
.....
----------------------
Ran 5 tests in 0.000s

OK

Py­rex dis­pose donc des avan­tages de SWIG (com­pi­la­tion, vé­ri­fi­ca­tion des pa­ra­mètres des fonc­tions) et des avan­tages du mo­dule ctypes (mé­lange avec du code Py­thon). Il n’y a tou­te­fois pas de gé­né­ra­tion au­to­ma­tique de toutes les fonc­tions d’une bi­blio­thèque et il est donc fas­ti­dieux de conver­tir ainsi une bi­blio­thèque conséquente.

Utilisation de l’API Python/C#

Une der­nière op­tion s’offre à nous pour écrire notre ex­ten­sion : l’uti­li­sa­tion de l’API Python/C qui per­met d’écrire toute l’ex­ten­sion di­rec­te­ment en C. Outre le côté di­dac­tique d’écrire di­rec­te­ment en C l’ex­ten­sion Py­thon, il existe plu­sieurs cô­tés pra­tiques pour se pen­cher sur cette ap­proche. Tout d’abord, des ou­tils comme SWIG peuvent né­ces­si­ter d’écrire un peu plus de code C qu’on ne le sou­hai­te­rait, code qui va né­ces­si­ter de bien com­prendre l’API Py­thon. En­suite, si vous ren­con­trez un seg­fault ou autre bug, que ce soit avec Py­rex ou SWIG, il fau­dra re­gar­der et de pré­fé­rence com­prendre le code C gé­néré. En­fin, vous pou­vez pré­fé­rer le code écrit à la main à ce­lui gé­néré au­to­ma­ti­que­ment pour di­verses rai­sons : conci­sion, ra­pi­dité, flexi­bi­lité. No­tez que l’in­té­gra­tion d’une ex­ten­sion Py­thon dans la bi­blio­thèque stan­dard ne s’ac­com­mode pas de code gé­néré au­to­ma­ti­quement.

Gestion de la mémoire#

En C, la ges­tion de la mé­moire est gé­né­ra­le­ment lais­sée à la dis­cré­tion du pro­gram­meur. On al­loue un es­pace mé­moire pour conte­nir des don­nées à un en­droit, il faut pen­ser à li­bé­rer cet es­pace mé­moire à un autre. Le pro­gram­meur est seul maître à bord et doit adop­ter sa propre mé­tho­do­lo­gie pour ne pas ou­blier de désal­louer les es­paces mé­moire in­uti­li­sés. Il peut ef­fec­tuer cette ges­tion ma­nuel­le­ment ou faire ap­pel à un mé­ca­nisme de ges­tion de la mé­moire tel qu’un ra­masse-miettes. En Py­thon, la ges­tion de la mé­moire est au­to­ma­tique. L’in­ter­pré­teur tient à jour un comp­teur pour chaque ob­jet. Ce comp­teur est in­cré­menté si l’ob­jet est ré­fé­rencé par une va­riable, dé­cré­menté quand ce n’est plus le cas. Ar­rivé à zéro, l’ob­jet est libéré.

Lors de l’écri­ture d’une ex­ten­sion en C, les ob­jets que l’on va créer et ma­ni­pu­ler peuvent être com­mu­ni­qués au code Py­thon. Il va donc fal­loir uti­li­ser ce mé­ca­nisme de comp­teur pour chaque ob­jet que l’on va ma­ni­pu­ler. Il fau­dra in­cré­men­ter le comp­teur si on veut gar­der un ob­jet et le dé­cré­men­ter quand on n’en a plus besoin.

Toute la dif­fi­culté consiste à sa­voir s’il est né­ces­saire ou non de tou­cher au comp­teur d’un ob­jet ou si une autre fonc­tion s’en est char­gée. Il existe heu­reu­se­ment quelques conven­tions de fa­çon à ce que l’exer­cice pa­raisse na­tu­rel avec un peu d’ha­bi­tude. Tout tourne au­tour de la no­tion de pos­ses­sion d’une ré­fé­rence à un ob­jet. Quand on pos­sède une ré­fé­rence à un ob­jet, il est né­ces­saire, quand on n’a plus be­soin de celle-ci de la li­bé­rer en dé­cré­men­tant le comp­teur de l’ob­jet ou d’en trans­fé­rer la pro­priété à une autre fonc­tion. In­ver­se­ment, quand on veut uti­li­ser un ob­jet, il est né­ces­saire de s’as­su­rer que l’on pos­sède bien la ré­fé­rence de l’ob­jet qu’on manipule.

Quand on fait ap­pel à une fonc­tion et que celle-ci ren­voie un ob­jet, il y a deux pos­si­bi­li­tés. Soit la fonc­tion nous ren­voie une nou­velle ré­fé­rence à l’ob­jet, c’est-à-dire nous dé­légue la pro­priété de la ré­fé­rence à l’ob­jet et dans ce cas, il ne faut pas in­cré­men­ter le comp­teur de l’ob­jet si on sou­haite le gar­der mais le dé­cré­men­ter quand on n’en a plus be­soin. Soit la fonc­tion nous prête une ré­fé­rence à l’ob­jet. C’est le cas in­verse. Il faut in­cré­men­ter le comp­teur si l’on sou­haite gar­der cette ré­fé­rence (afin de pos­sé­der sa propre ré­fé­rence) et si ce n’est pas le cas, il est in­utile de dé­cré­men­ter le compteur.

In­ver­se­ment, quand on donne une ré­fé­rence à un ob­jet à une fonc­tion, celle-ci peut adop­ter deux com­por­te­ments. Soit elle vole la ré­fé­rence à cet ob­jet, c’est-à-dire que l’ap­pe­lant n’a plus la pro­priété de la ré­fé­rence à l’ob­jet, soit elle ne la vole pas et l’ap­pe­lant doit se dé­bar­ras­ser de cette ré­fé­rence s’il ne sou­haite plus uti­li­ser l’objet.

Com­men­çons par les cas simples. Sauf in­di­ca­tion contraire dans la do­cu­men­ta­tion, une fonc­tion ap­pe­lée ne vole pas la ré­fé­rence à un ob­jet. Si la fonc­tion né­ces­site de gar­der une ré­fé­rence sur l’ob­jet, elle in­cré­men­tera elle-même le comp­teur de l’ob­jet. L’ap­pe­lant doit donc se dé­bar­ras­ser lui-même de la ré­fé­rence sur l’ob­jet dans la plu­part des cas. Les deux fonc­tions connues pour vo­ler la ré­fé­rence sont PyList_​SetItem() et PyTuple_​SetItem() qui per­mettent de pla­cer l’ob­jet dans une liste ou dans un tuple.

En ce qui concerne le trans­fert ou l’em­prunt de la ré­fé­rence, une règle gé­né­rale est que la créa­tion d’un ob­jet en­traîne le trans­fert de la pro­priété de la ré­fé­rence cor­res­pon­dante vers l’ap­pe­lant tan­dis que la consul­ta­tion d’une pro­priété consiste sim­ple­ment en un em­prunt. Par exemple, si on crée une nou­velle liste avec PyList_​New(), la pro­priété de la ré­fé­rence ren­voyée est trans­fé­rée à l’ap­pe­lant. Ce­lui-ci n’a pas be­soin d’in­cré­men­ter le comp­teur de l’ob­jet. On dit dans ce cas que l’ap­pe­lant ob­tient une nou­velle ré­fé­rence. Par contre, quand on consulte un élé­ment d’une liste avec PyList_​GetItem(), l’ap­pe­lant em­prunte la ré­fé­rence ren­voyée. S’il sou­haite conser­ver cette ré­fé­rence, il doit in­cré­men­ter lui-même le comp­teur de l’ob­jet. En cas de doute, la do­cu­men­ta­tion in­dique à chaque fois qu’une fonc­tion ren­voie un ob­jet Py­thon si la res­pon­sa­bi­lité est trans­fé­rée (nou­velle ré­fé­rence) ou em­prun­tée dans la do­cu­men­ta­tion. Les fonc­tions C ap­pe­lées de­puis Py­thon doivent re­tour­ner de nou­velles ré­férences.

Il n’est pas for­cé­ment utile d’in­cré­men­ter le comp­teur avant d’uti­li­ser un ob­jet. Par exemple, si cet ob­jet est l’ar­gu­ment d’une fonc­tion ap­pe­lée de­puis l’in­ter­pré­teur Py­thon, il est ga­ranti que la ré­fé­rence sur ce­lui-ci sera va­lide pen­dant toute la du­rée de vie de la fonc­tion. Il n’est donc né­ces­saire d’in­cré­men­ter le comp­teur que lorsque l’on dé­sire gar­der cette ré­fé­rence au-delà de la vie de la fonc­tion. Quand la ré­fé­rence pro­vient d’une fonc­tion à la­quelle on l’em­prunte, il convient d’être plus pru­dent. Il est pos­sible que cette ré­fé­rence de­vienne in­va­lide avant la fin de la fonc­tion. En ef­fet, cer­taines fonc­tions peuvent dé­clen­cher du code qui va li­bé­rer la ré­fé­rence en ques­tion. Il est donc pré­fé­rable, dans ce cas, d’in­cré­men­ter le comp­teur de l’objet.

Mais donc, com­ment ma­ni­pule-t-on ce comp­teur ? Il existe plu­sieurs ma­cros. Py_​INCREF per­met d’in­cré­men­ter le comp­teur, Py_​DECREF per­met de le dé­cré­men­ter et Py_​XDECREF fait de même à condi­tion que l’ob­jet ne soit pas NULL (au­quel cas, la ma­cro ne fait rien).

Présentation de l’API#

La ges­tion mé­moire consti­tue un point cen­tral pour bien uti­li­ser l’API Py­thon. N’hé­si­tez pas à re­lire la sec­tion pré­cé­dente si cela vous semble flou. Le reste de l’API Py­thon est beau­coup plus simple à com­prendre.

De­puis le code de votre ex­ten­sion en C, vous al­lez re­ce­voir des ob­jets Py­thon, les ma­ni­pu­ler et les ren­voyer. Tout ce qui pro­vient de l’in­ter­pré­teur Py­thon ou qui lui est des­tiné est un ob­jet Py­thon, y com­pris les en­tiers ou les chaînes de ca­rac­tères. Quand vous ma­ni­pu­lez un ob­jet Py­thon en C, vous avez une ré­fé­rence sur cet ob­jet sous la forme d’un poin­teur PyObject *. Tous les ob­jets que vous al­lez ma­ni­pu­ler ne sont pas équi­va­lents mais ils sont tou­jours re­pré­sen­tés sous la forme d’un poin­teur PyObject *.

Les fonc­tions ma­ni­pu­lant des ob­jets sont pré­fixées se­lon le type d’ob­jet qu’elles vont ma­ni­pu­ler. Par exemple, les fonc­tions com­men­çant par PyList_ ma­ni­pulent des listes tan­dis que les fonc­tions com­men­çant par PyInt_ ma­ni­pulent des en­tiers. Pour chaque type, on dis­pose gé­né­ra­le­ment de fonc­tions pour créer un ob­jet, vé­ri­fier le type d’un ob­jet (est-ce que la don­née qu’on me donne en ar­gu­ment est bien un en­tier ?), conver­tir un ob­jet en un autre. Tout ce que vous pou­vez faire avec les ob­jets de­puis l’in­ter­pré­teur Py­thon a une fonc­tion équi­va­lente dans l’API.

Il existe deux fonc­tions qui se­ront uti­li­sées très ré­gu­liè­re­ment. La pre­mière est PyArg_​ParseTuple() qui per­met de vé­ri­fier que les ar­gu­ments d’une fonc­tion sont bien du type at­tendu et de les sto­cker dans un en­semble de va­riables, en un seul ap­pel. L’ap­pe­lant a-t-il bien fourni un en­tier puis une liste puis op­tion­nel­le­ment un autre en­tier ? Dans ce cas, sto­cker chaque va­leur dans telle et telle va­riable. La se­conde fonc­tion est Py_​BuildValue() qui per­met de construire fa­ci­le­ment cer­tains ob­jets simples comme un en­tier, un couple conte­nant un en­tier et une chaîne de ca­ractères.

La gestion des erreurs#

Py­thon dis­pose d’un mé­ca­nisme d’ex­cep­tions. Quand une er­reur sur­vient dans une ex­ten­sion, celle-ci doit re­mon­ter une ex­cep­tion. L’API Py­thon per­met de gé­rer les cas d’er­reurs de ma­nière as­sez simple. La règle gé­né­rale est que quand une fonc­tion qui doit re­tour­ner à l’in­ter­pré­teur une ré­fé­rence sur un ob­jet re­tourne en réa­lité NULL, une ex­cep­tion sera gé­né­rée. No­tam­ment, si une fonc­tion de l’API Py­thon vous re­tourne NULL et que vous ne sou­hai­tez pas gé­rer ce cas d’er­reur, re­tour­nez éga­le­ment NULL et l’ex­cep­tion sera pro­pa­gée avec le mes­sage d’er­reur adé­quat. Quand une fonc­tion ne re­tourne pas un ob­jet Py­thon, il faut se ré­fé­rer à la do­cu­men­ta­tion pour voir com­ment sont gé­rés les cas d’er­reur. Dans tous les cas, on peut faire ap­pel à PyErr_​Occurred() pour sa­voir si on est ac­tuel­le­ment dans un cas d’erreur.

Quand vous vou­lez gé­né­rer une ex­cep­tion, il vous faut non seule­ment re­tour­ner NULL (ou -1 dans le cas de la plu­part des fonc­tions qui re­tournent un en­tier) à l’in­ter­pré­teur, mais aussi in­di­quer quelle ex­cep­tion vous vou­lez faire re­mon­ter. Pour ce faire, vous pou­vez uti­li­ser les fonc­tions com­men­çant par PyErr_ comme PyErr_​NoMemory(), PyErr_​SetString() ou PyErr_​FormatString().

Écriture de l’extension#

Nous al­lons nous lan­cer dou­ce­ment dans l’écri­ture de notre ex­ten­sion. Voici une pre­mière ver­sion de celle-ci conte­nant son ini­tia­li­sa­tion et la dé­cla­ra­tion de l’ex­cep­tion TurtleException :

#include <Python.h>

static PyObject *TurtleException;

PyMODINIT_FUNC
initpythonc(void)
{
    PyObject *m;

    m = Py_InitModule("pythonc", NULL);
    if (!m)
        return;

    TurtleException = PyErr_NewException("pythonc.TurtleException", NULL, NULL);
    if (!TurtleException)
      return;
    Py_INCREF(TurtleException);
    PyModule_AddObject(m, "TurtleException", TurtleException);
}

L’en­tête Python.h contient les dé­cla­ra­tions né­ces­saires pour uti­li­ser l’API Py­thon. On dé­clare en­suite l’ob­jet TurtleException qui pourra être uti­lisé dans le reste de l’ex­ten­sion. La fonc­tion mar­quée par PyMODINIT_​FUNC re­pré­sente la fonc­tion char­gée d’ini­tia­li­ser le mo­dule. Un mo­dule est aussi un ob­jet Py­thon. On l’ini­tia­lise avec Py_​InitModule(). Le pre­mier pa­ra­mètre est le nom du mo­dule et le se­cond est l’en­semble des mé­thodes du mo­dule. Ac­tuel­le­ment, on ne dis­pose d’au­cune mé­thode. No­tez en­suite la ges­tion des er­reurs propre à l’API. Si Py_​InitModule a re­tourné NULL, c’est qu’une er­reur est sur­ve­nue. Le cas de l’ini­tia­li­sa­tion du mo­dule est un peu par­ti­cu­lier puisque la fonc­tion ne re­tourne pas un ob­jet. On se contente de sor­tir de la fonc­tion. L’in­ter­pré­teur Py­thon dé­tec­tera le cas d’er­reur et re­mon­tera une ex­ception.

Nous re­ve­nons à notre ob­jet TurtleException. La fonc­tion PyErr_​NewException() va nous per­mettre de créer une nou­velle ex­cep­tion. En­core une fois, on vé­ri­fie si on ne nous a pas re­tourné NULL et dans ce cas, in­utile d’al­ler plus loin. Sui­vant la lo­gique dé­crite au­pa­ra­vant, PyErr_​NewException() doit nous trans­fé­rer une nou­velle ré­fé­rence sur l’ob­jet créé. La do­cu­men­ta­tion confirme que c’est le cas. Pour­quoi donc in­cré­men­tons-nous le comp­teur de l’ob­jet ? La fonc­tion sui­vante, PyModule_​AddObject() dont le rôle est d’ajou­ter un ob­jet au mo­dule (sous le nom de son choix), vole la ré­fé­rence à l’ob­jet. Or nous vou­lons gar­der cette ex­cep­tion car nous al­lons l’uti­li­ser dans le reste de l’ex­ten­sion. Si l’in­ter­pré­teur dé­cide de re­ti­rer l’ex­cep­tion du mo­dule (avec del pythonc.​TurtleException par exemple), nous pour­rions perdre la seule ré­fé­rence sur l’ex­cep­tion et l’ob­jet se­rait désalloué.

Com­pi­lons notre nou­velle ex­tension :

$ gcc -shared -fPIC -O2 -Wall \
     $(python-config --cflags) pythonc.c -L.. -ltortue \
     -o pythonc.so
$ (cd .. ; LD_LIBRARY_PATH=. python )
>>> from tortue import pythonc
>>> dir(pythonc)
['TurtleException', '__doc__', '__file__', '__name__', '__package__']
>>> raise pythonc.TurtleException
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
pythonc.TurtleException

Nous de­vons en­suite ajou­ter notre classe Turtle au mo­dule. Pour faire ceci, il faut d’abord dé­cla­rer une struc­ture qui re­pré­sen­tera une ins­tance de la classe et donc l’ob­jet Py­thon cor­res­pon­dant. Elle contien­dra no­tam­ment le comp­teur per­met­tant de suivre le nombre de ré­fé­rences ac­tives sur l’ob­jet mais aussi tout ce que vous ju­ge­rez utile d’as­so­cier avec chaque ins­tance. Dans notre cas, notre struc­ture est dé­cla­rée comme ceci :

#include "../tortue.h"

typedef struct {
  PyObject_HEAD
  struct turtle *t;
} TurtleObject;

La ma­cro PyObject_​HEAD se charge d’in­clure tout ce qui est né­ces­saire pour que cette struc­ture puisse se com­por­ter comme un ob­jet Py­thon, dont le comp­teur de ré­fé­rences. Ainsi, une va­riable qui aura pour type cette struc­ture pourra être trans­for­mée en une va­riable de type PyObject. Nous ajou­tons en­suite les élé­ments né­ces­saires à chaque ins­tance. Dans notre cas, il s’agit de la ré­fé­rence vers la tortue.

Nous de­vons en­suite dé­fi­nir toutes les opé­ra­tions pos­sibles sur notre ob­jet ainsi que ses ca­rac­té­ris­tiques es­sen­tielles. Pour ce faire, on dé­fi­nit une va­riable de type PyTypeObject qui re­pré­sen­tera la classe :

static PyTypeObject TurtleType = {
  PyObject_HEAD_INIT(NULL)
  0,                            /*ob_size*/
  "pythonc.Turtle",             /*tp_name*/
  sizeof(TurtleObject),         /*tp_basicsize*/
  0,                            /*tp_itemsize*/
  (destructor)Turtle_dealloc,   /*tp_dealloc*/
  0,                            /*tp_getattr*/
  0,                            /*tp_setattr*/
  0,                            /*tp_compare*/
  0,                            /*tp_repr*/
  0,                            /*tp_as_number*/
  0,                            /*tp_as_sequence*/
  0,                            /*tp_as_mapping*/
  0,                            /*tp_hash */
  0,                            /*tp_call*/
  0,                            /*tp_str*/
  0,                            /*tp_getattro*/
  0,                            /*tp_setattro*/
  0,                            /*tp_as_buffer*/
  Py_TPFLAGS_DEFAULT,           /*tp_flags*/
  "Turtle objects",             /*tp_doc*/
  0,                            /*tp_traverse*/
  0,                            /*tp_clear*/
  0,                            /*tp_richcompare*/
  0,                            /*tp_weaklistoffset*/
  0,                            /*tp_iter*/
  0,                            /*tp_iternext*/
  0,                            /*tp_methods*/
  0,                            /*tp_members*/
  0,                            /*tp_getset*/
  0,                            /*tp_base*/
  0,                            /*tp_dict*/
  0,                            /*tp_descr_get*/
  0,                            /*tp_descr_set*/
  0,                            /*tp_dictoffset*/
  (initproc)Turtle_init,        /*tp_init*/
  0,                            /*tp_alloc*/
  PyType_GenericNew,            /*tp_new*/
};

Pour le mo­ment, notre type ne per­met pas de faire grand-chose. On lui donne un nom, la taille de l’ob­jet qui lui cor­res­pond et une chaîne de do­cu­men­ta­tion. Il n’a pour le mo­ment ni mé­thode, ni at­tri­but. On doit ce­pen­dant ex­pli­quer com­ment créer une nou­velle ins­tance de notre ob­jet à l’aide de Turtle_​init(). Cette fonc­tion jouera le rôle de __init__(). Voyons son code :

static int
Turtle_init(TurtleObject *self, PyObject *args, PyObject *kwds)
{
  int port;

  if (!PyArg_ParseTuple(args, "i", &port))
    return -1;

  self->t = turtle_init(port);
  if (!self->t) {
    PyErr_Format(TurtleException, "unable to create turtle %d", port);
    return -1;
  }

  return 0;
}

Re­mar­quons d’abord que cette fonc­tion ne re­tourne pas un PyObject *, tout comme un construc­teur d’une classe Py­thon. En de­hors de cela, la si­gna­ture de la fonc­tion est clas­sique. Toutes les fonc­tions se­ront ap­pe­lées avec trois ar­gu­ments : un poin­teur sur l’ins­tance, un poin­teur sur une liste d’ar­gu­ments po­si­tion­nels et un poin­teur sur un dic­tion­naire d’ar­gu­ments nom­més. Gé­né­ra­le­ment, les fonc­tions ren­voient un ob­jet Py­thon. Ce n’est pas le cas ici.

On com­mence par re­gar­der les ar­gu­ments ob­te­nus. On dé­sire que notre construc­teur soit ap­pelé avec comme seul et unique ar­gu­ment un en­tier re­pré­sen­tant le port de la tor­tue. Le rôle de PyArg_​ParseTuple() est jus­te­ment de vé­ri­fier que les ar­gu­ments po­si­tion­nels four­nis sont bien au nombre de 1 et re­pré­sentent un en­tier. Si on at­ten­dait un ob­jet Py­thon, il au­rait fallu mettre O plu­tôt que i. Il au­rait alors fallu éga­le­ment vé­ri­fier nous-même que l’ob­jet fourni est bien du type at­tendu. Par exemple, si on at­ten­dait une liste, la fonc­tion PyList_​Check() nous au­rait confirmé si nous avions bien une liste. Si l’uti­li­sa­teur four­nit plus d’un ar­gu­ment ou que ce­lui-ci n’est pas un en­tier, PyArg_​ParseTuple() va gé­né­rer une ex­cep­tion et ren­voyer NULL. Dans ce cas, on se contente de pro­pa­ger cette ex­cep­tion en ren­voyant -1. No­tons que si l’uti­li­sa­teur four­nit des ar­gu­ments nom­més, ceux-ci sont igno­rés. On pour­rait rem­pla­cer l’ap­pel à PyArg_​ParseTuple() par un ap­pel à PyArg_​ParseTupleAndKeywords() pour être plus strict.

Nous fai­sons en­suite ap­pel à notre bi­blio­thèque en C pour ini­tia­li­ser la tor­tue. Si l’ini­tia­li­sa­tion échoue, on gé­nère une ex­cep­tion à l’aide de PyErr_​Format() et en re­tour­nant -1. Dans le cas contraire, 0 est re­tourné pour in­di­quer le suc­cès de l’opération.

Nous de­vons aussi dé­fi­nir la fonc­tion Turtle_​dealloc() qui est ap­pe­lée lorsque l’ob­jet doit être désal­loué. Dans cette fonc­tion, nous fai­sons ap­pel à turtle_​close() :

static void
Turtle_dealloc(TurtleObject *self)
{
  if (self->t)
    turtle_close(self->t);
  self->ob_type->tp_free((PyObject*)self);
}

Si nous com­pi­lons et lan­çons nos tests à ce stade, le pre­mier test passe. Nous sommes sur la bonne voie ! Nous al­lons pou­voir ajou­ter la mé­thode send() à notre ob­jet. Nous de­vons d’abord écrire la fonc­tion cor­res­pondante :

static PyObject *
Turtle_send(TurtleObject *self, PyObject *args, PyObject *kwds)
{
  const char *cmd = NULL;
  int result;
  if (!PyArg_ParseTuple(args, "s", &cmd))
    return NULL;

  result = turtle_send(self->t, cmd);
  if (result != 0) {
    PyErr_Format(TurtleException, "got error %d", turtle_error(self->t));
    return NULL;
  }
  Py_INCREF(Py_None);
  return Py_None;
}

Ici en­core, nous com­men­çons à vé­ri­fier nos ar­gu­ments. Nous at­ten­dons exac­te­ment une chaîne de ca­rac­tères. Si ce n’est pas le cas, nous trans­met­tons l’ex­cep­tion. Nous fai­sons en­suite ap­pel à la fonc­tion turtle_​send() de notre bi­blio­thèque et vé­ri­fions le ré­sul­tat. Si ce­lui-ci n’est pas sa­tis­fai­sant, nous gé­né­rons une ex­cep­tion. Dans le cas contraire, il nous faut ren­voyer None. Une fonc­tion Py­thon qui ne ren­voie pas de ré­sul­tat ren­voie None. Il ne faut pas ren­voyer NULL car cela cor­res­pon­drait à un cas d’er­reur ! No­tez éga­le­ment que nous in­cré­men­tons le comp­teur de l’ob­jet Py_​None qui est un ob­jet comme les autres (mais dont il n’existe qu’un exem­plaire). Rap­pe­lez-vous, par conven­tion, les mé­thodes doivent ren­voyer à l’in­ter­pré­teur une nou­velle ré­fé­rence à l’ob­jet. Il nous faut donc in­cré­men­ter le comp­teur de tout ob­jet Py­thon renvoyé.

Nous de­vons en­suite ré­fé­ren­cer cette nou­velle mé­thode pour notre ob­jet. Toutes les mé­thodes d’un ob­jet sont ré­fé­ren­cées dans une même struc­ture que nous dé­cla­rons ainsi :

static PyMethodDef Turtle_methods[] = {
    {"send", (PyCFunction)Turtle_send, METH_VARARGS,
     "Send a command to the turtle"
    },
    {NULL}  /* Sentinel */
};

Nous don­nons le nom de la mé­thode, la mé­thode à ap­pe­ler (dont la si­gna­ture doit sys­té­ma­ti­que­ment cor­res­pondre à la si­gna­ture d’une fonc­tion C ap­pe­lée de­puis l’in­ter­pré­teur Py­thon), les ar­gu­ments qu’elle at­tend (ici un nombre va­riable d’ar­gu­ments po­si­tion­nels mais pas d’ar­gu­ments nom­més) et en­fin une chaîne de do­cu­men­ta­tion. En­fin, dans la dé­fi­ni­tion du type TurtleType, nous rem­pla­çons la va­leur du membre tp_​methods par cette struc­ture (au lieu de 0).

Après com­pi­la­tion, il ne reste que les tests concer­nant les at­tri­buts model et status qui échouent. Nous al­lons donc écrire les fonc­tions qui per­mettent d’ac­cé­der à ces at­tri­buts. Com­men­çons par l’at­tri­but le plus simple :

static PyObject *
Turtle_getmodel(TurtleObject *self, void *closure)
{
  return PyString_FromString(turtle_model(self->t));
}

La fonc­tion qui per­met d’ac­cé­der à l’at­tri­but model est très simple. Elle se contente de créer un nou­vel ob­jet Py­thon de type chaîne construit à par­tir de la va­leur ren­voyée par la fonc­tion C turtle_​model(). La fonc­tion PyString_​FromString() nous ob­tient une nou­velle ré­fé­rence. Nous fai­sons sim­ple­ment pas­ser cette ré­fé­rence à l’ap­pe­lant. In­utile donc de tou­cher au comp­teur de l’ob­jet. À no­ter qu’à la place de PyString_​FromString(), il au­rait été aussi pos­sible de faire ap­pel à Py_​BuildValue().

L’en­semble des fonc­tions per­met­tant de ma­ni­pu­ler les at­tri­buts sont re­grou­pés au sein d’une même struc­ture que nous dé­fi­nis­sons ainsi :

static PyGetSetDef Turtle_getseters[] = {
        {"model",
         (getter)Turtle_getmodel, NULL,
         "model", NULL},
        {NULL}                  /* Sentinel */
};

Pour chaque at­tri­but, nous don­nons un nom puis la fonc­tion qui per­met d’ob­te­nir la va­leur de cet at­tri­but, la fonc­tion qui per­met de mo­di­fier cette va­leur (NULL dans notre cas), la chaîne de do­cu­men­ta­tion ainsi que des don­nées sup­plé­men­taires qui se­raient pas­sées en der­nier pa­ra­mètre des fonc­tions. Dans la dé­fi­ni­tion du type TurtleType, nous rem­pla­çons la va­leur du membre tp_​getset par cette structure.

Après com­pi­la­tion, seul un der­nier test échoue ! Il nous faut donc ajou­ter le sup­port de l’at­tri­but status. Rap­pe­lez-vous cet at­tri­but est ob­tenu en re­tour­nant une ins­tance de TurtleStatus. Il va donc nous fal­loir im­por­ter le mo­dule cor­res­pon­dant et ins­tan­cier la classe en ques­tion, ce qui re­vient à ap­pe­ler du code Py­thon de­puis notre code C. Voici com­ment im­por­ter le module :

    StatusModule = PyImport_ImportModule("tortue.status");
    if (!StatusModule)
      return;

Ce code est à ajou­ter à la fin de l’ini­tia­li­sa­tion du mo­dule. La fonc­tion PyImport_​ImportModule() nous ren­voie une nou­velle ré­fé­rence. On n’ajoute pas cette ré­fé­rence au mo­dule et on conserve donc la pro­priété de celle-ci. Il est donc in­utile d’in­cré­men­ter le comp­teur. StatusModule est dé­claré en va­riable glo­bale de type static PyObject *. Nous al­lons dé­sor­mais pou­voir ap­pe­ler les fonc­tions de ce mo­dule dans la fonc­tion qui per­met d’ac­cé­der à l’at­tri­but status :

static PyObject *
Turtle_getstatus(TurtleObject *self, void *closure)
{
  long int status = turtle_status(self->t);
  const char *model = turtle_model(self->t);
  PyObject *cstatus, *istatus;
  if (status == -1) {
    PyErr_Format(TurtleException, "got error %d", turtle_error(self->t));
    return NULL;
  }
  cstatus = PyObject_GetAttrString(StatusModule, "TurtleStatus");
  if (!cstatus)
    return NULL;
  istatus = PyObject_CallFunction(cstatus, "sl", model, status);
  Py_DECREF(cstatus);
  if (!status)
    return NULL;
  return istatus;
}

On stocke tout d’abord le mo­dèle et l’état cou­rant dans deux va­riables. Si la fonc­tion turtle_​status() re­tourne une er­reur, nous le­vons une ex­cep­tion. En­suite, nous ré­cu­pé­rons dans cstatus la classe TurtleStatus de­puis le mo­dule que nous avons im­porté pré­cé­dem­ment. Un mo­dule étant un ob­jet clas­sique, on uti­lise la fonc­tion gé­né­rique PyObject_​GetAttrString() qui nous ren­voie une nou­velle ré­fé­rence sur l’ob­jet en ques­tion. No­tons que par la suite, nous n’au­rons plus be­soin de cette ré­fé­rence et donc, si­tôt uti­li­sée, nous nous la li­bé­rons en dé­cré­men­tant le compteur.

Nous pos­sé­dons donc main­te­nant une ré­fé­rence sur la classe que nous dé­si­rons ins­tan­cier. Une ins­tan­cia­tion consiste en fait à consi­dé­rer la classe comme une fonc­tion et à l’exé­cu­ter. L’API Py­thon dis­pose de la fonc­tion PyObject_​CallFunction() à cet ef­fet. Nous lui four­nis­sons la ré­fé­rence que nous avons sur la classe, le for­mat des ar­gu­ments (une chaîne et un en­tier long) ainsi que les ar­gu­ments en eux-mêmes. La fonc­tion PyObject_​CallFunction() s’oc­cupe de tout pour nous : elle trans­forme nos va­riables en ob­jets Py­thon et ins­tan­cie la classe. En cas d’échec, nous ob­te­nons NULL et nous pro­pa­geons donc l’ex­cep­tion. En cas de suc­cès, nous ob­te­nons une nou­velle ré­fé­rence sur l’ins­tance qui nous in­té­resse et nous trans­fé­rons celle-ci à l’appelant.

Pour ter­mi­ner, il convient d’ajou­ter la ré­fé­rence à cette fonc­tion dans la struc­ture dans la­quelle nous avions dé­claré le pré­cé­dent at­tri­but. Celle-ci devient :

static PyGetSetDef Turtle_getseters[] = {
        {"model",
         (getter)Turtle_getmodel, NULL,
         "model", NULL},
        {"status",
         (getter)Turtle_getstatus, NULL,
         "status", NULL},
        {NULL}                  /* Sentinel */
};

Après com­pi­la­tion, tous les tests passent dé­sor­mais. Notre mo­dule est donc fonc­tion­nel ! L’API Py­thon est très riche et nous n’en avons ef­fleuré qu’une par­tie. Cela de­vrait vous don­ner les bases né­ces­saires pour al­ler plus loin.

Automatiser la compilation#

La plu­part des so­lu­tions qui ont été pré­sen­tées dans cet ar­ticle né­ces­sitent une phase de com­pi­la­tion. Nous avons lancé des com­mandes ma­nuel­le­ment pour ob­te­nir les dif­fé­rentes ex­ten­sions Py­thon. Sa­chez que le mo­dule distutils four­nit le né­ces­saire pour au­to­ma­ti­ser cette com­pi­la­tion et l’in­té­grer cor­rec­te­ment dans le sys­tème de construc­tion et d’ins­tal­la­tion des mo­dules Python.

Pour une ex­ten­sion écrite di­rec­te­ment en C, vous pou­vez ajou­ter l’ar­gu­ment ext_​modules à l’ap­pel de la fonc­tion setup() afin de com­pi­ler votre ex­ten­sion. Dans notre cas :

from distutils.core import setup, Extension
setup(name="tortue", version="1.0",
      ext_modules=[Extension("tortue.pythonc",
                             libraries = ['tortue']
                             sources = ['tortue/pythonc.c'])])

SWIG et Py­rex pro­posent des ex­ten­sions à distutils qui per­mettent d’ar­ri­ver à un ré­sul­tat si­mi­laire. Consul­tez leurs do­cu­men­tations.

Conclusion#

In­ter­fa­cer une bi­blio­thèque en C avec du code Py­thon n’est fi­na­le­ment pas si com­pli­qué. Se­lon vos af­fi­ni­tés, de nom­breuses so­lu­tions s’offrent à vous. SWIG vous per­met de gé­né­rer fa­ci­le­ment un équi­valent Py­thon de votre bi­blio­thèque en C. Charge à vous de com­plé­ter le ré­sul­tat avec un peu de Py­thon pour rendre la bi­blio­thèque ob­te­nue un peu plus py­tho­nique. Py­rex vous per­met de mé­lan­ger du code Py­thon avec des don­nées et des fonc­tions C tout en gar­dant une cer­taine sé­cu­rité. La connais­sance de l’API Py­thon n’est pas né­ces­saire pour conce­voir des ex­ten­sions évo­luées. Le mo­dule ctypes vous per­met d’ar­ri­ver à un ré­sul­tat si­mi­laire sans dis­po­ser des sources de la bi­blio­thèque. Il né­ces­site quelques pré­cau­tions mais per­met d’ob­te­nir très ra­pi­de­ment un ré­sul­tat que l’on pourra en­suite conso­li­der avec les autres ou­tils. En­fin, pour les plus aven­tu­reux, la concep­tion d’une ex­ten­sion en uti­li­sant di­rec­te­ment l’API Python/C reste ac­ces­sible et vous au­to­rise toutes les fan­taisies.