Prototypons, petit patapon
Par Jean-Seb le dimanche 23 novembre 2008, 23:16 - Coding - Lien permanent
Prototypons! Avec Python!
Mais pourquoi donc avec Python ?
Parce que ça rime!
Remerciements à Frédéric Jolliton pour son aide
permanente.
Prototyper: quoi, quand, où, pourquoi, avec qui ?
- quoi : prototyper, c'est écrire du code vite fait pour vérifier qu'une idée est bonne (ou mauvaise, ce qui peut aussi s'avérer utile).
- quand : quand vous voulez ! Le plus tôt est peut être le mieux.
- où : dans vos projets dont la taille du listing dépasse une feuille A4.
- pourquoi :
parce qu'il faut tester!
( (c) F.J.)
- avec qui : avec un langage faisant office de glue , et collant rapidement les morceaux.
Ah bon! Et en clair ?
- L'idée est d'utiliser Python pour tester rapidement des routines en C.
- Cela évite d'avoir à écrire un cadre d'exécution complet en C.
- Cela permet également de continuer à exécuter le programme, tout en faisant
des modifications à la volée.
- Avec un squelette python de boucle principale, on peut modifier les routines satellites écrites en C sans tout réinitialiser à chaque fois.
Différence entre extending et embedding.
Extending
- extending : consiste à appeller du C depuis Python.
Nous avons deux possibilités:
- Créer un module Python, utilisé ensuite avec un simple
import module
. Ce module aura le même comportement que les modules Python habituels. - Utiliser ctypes pour appeller des fonctions C depuis Python. Ceci ne nécessite pas la création d'un module Python.
- Créer un module Python, utilisé ensuite avec un simple
- ctypes est beaucoup plus simple à mettre en oeuvre que la création d'un module.
- De plus, ctypes est intégré à Python depuis la version 2.5
Embedding
- embedding : consiste à faire tourner une machine
virtuelle Python dans un programme C.
- non traité dans ce billet.
Principe de l'extending avec ctypes
- Comme il faut bien commencer par quelque chose, autant faire au plus
simple.
- Nous utiliserons donc ctypes.
- On va utiliser une dll ou un .so (selon votre OS), que nous allons écrire en C.
- On importe cette bibliothèque depuis Python, qu'on peut ensuite utiliser comme n'importe quel module importé.
- Nous allons également écrire un wrapper dans Python, ce qui présentera
plusieurs avantages:
- Ne pas afficher les valeurs de retour de chaque sous-fonctions, lors de l'appel d'une fonction. En effet, les fonctions Python vont retourner "None" , ce qui ne sera pas affiché dans le shell interactif. Ceci est valable pour mon exemple, et n'est pas forcément souhaitable. Bien entendu, les valeurs renvoyées par la bibliothèque C sont renvoyées au wrapper, à charge pour lui de les traiter.
- Simplifier l'initialisation depuis Python: le wrapper comprendra du code d'initialisation.
- Masquer la complexité des appels en C: le wrapper se chargera de passer les paramètres éventuels, et de tester les codes de retour.
Faire appel au bon type
Définir un type
- ctypes permet de définir des types compatibles avec ceux du C.
- De manière générale, il suffit de rajouter le préfixe c_
au type C.
- Quelques exemples (voir la doc de ctypes) : c_char , c_int, c_long, c_float
Cas des pointeurs
- Les pointeurs :
- c_char_p et c_wchar_p : char * et wchar_t *
- c_void_p : void *
- Les types définis par ctypes sont tous mutables.
- Cela signifie que vous pouvez les modifier à la volée dans Python.
- Attention : la modification d'une zone mémoire
déréférencée par un pointeur ctypes crée une copie du contenu du
pointeur, et non pas une modification de la zone précédemment pointé.
- Pourquoi ? Parce que les strings ne sont pas mutables en Python (c_void_p est logé à la même enseigne que les strings).
- Si vous utilisez une fonction C qui a besoin d'un pointeur sur une zone
mémoire modifiable, il faut utiliser create_string_buffer
- L'objet créé est accédé via les méthodes
raw
etstring
, selon que vous voulez la représentation string ou pas (string : chaine terminée par zéro). - Je sens qu'un exemple serait le bienvenu:
- L'objet créé est accédé via les méthodes
#string python : pas mutable
>>> buffer_python="Jean-Seb"
>>> buffer_python[2]="x"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
#buffer ctypes : mutable
>>> buffer_c=ctypes.create_string_buffer("Jean-Seb")
>>> type(buffer_c)
<class 'ctypes.c_char_Array_9'>
>>> buffer_c.raw
'Jean-Seb\x00'
>>> buffer_c.raw="0wned"
>>> buffer_c.raw
'0wnedSeb\x00'
Les structures
- Vous pouvez directement définir et adresser des structures C en Python.
- Pour cela, il faut utiliser ctypes.Structure
- Il faut dériver de ctypes.Structure une nouvelle classe, et définir un attribut _fields_.
- _fields_ est un tuple contenant un champs nom et un champs type.
- Tous les types de ctypes peuvent être utilisés.
- Voici le code en C qui définit, puis affiche une structure
typedef struct {
char *name;
int age;
} MASTRUCT;
void print_struct(MASTRUCT *mastruct)
{
printf("je suis %s et j'ai %d ans\n",
mastruct->name, mastruct->age);
}
- Et voici comment créer une structure avec Python, pour la passer ensuite à
la fonction en C.
- On passe le pointeur à la structure en utilisant ctypes.byref . Cette fonction retourne un pointeur, mais ce pointeur est uniquement utilisable en tant qu'argument de fonction. Sinon, il faudra utiliser ctypes.pointer.
>>> import ctypes
>>> dll = ctypes.CDLL("./malib.so")
>>> class mastruct(ctypes.Structure) :
_fields_ = [ ("nom", ctypes.c_char_p),
("age", ctypes.c_int) ]
>>> ms = mastruct("Marcel", 42)
>>> print ms
<__main__.mastruct object at 0x2abfcecf6af0>
>>> print ms.age , ms.nom
42 Marcel
>>> dll.print_struct(ctypes.byref(ms))
je suis Marcel et j'ai 42 ans
Module util de ctypes
- ctypes possède un module fort utile, judicieusement nommé
util
import ctypes.util
- La fonction la plus utile de ce module est find_library,
qui permet d'avoir le nom complet d'une bibliothèque.
- Attention à ne pas ajouter
lib
sous Linux, sinon, la fonction ne trouvera rien.
- Attention à ne pas ajouter
>>> ctypes.util.find_library("c")
'libc.so.6'
La grande ruse du rechargement de bibliothèque.
Avertissement façon Java
- Pour recharger une bibliothèque, on utilise normalement un module python, que l'on rechargera en cas de modification de la bibliothèque.
- Bien que ce soit la méthode habituelle et recommandée, elle a pour inconvénient d'être plus contraignante que la méthode que je vais vous présenter.
- Pour le développement d'une DLL de centrale nucléaire ou d'un accélérateur de particule, je vous conseille toutefois la méthode classique.
- Concernant ce passage, Fred souhaite ajouter cela :
bon, en tout cas, je ne veux pas être associé a tes bidouilles avec dlopen/dlclose

Le problème
- Normalement, on ne peut importer une bibliothèque qu'une seule fois.
malib = ctypes.CDLL("./malib.so")- malib ne sera pas rechargée si vous appellez à nouveau le code ci-dessus.
- Ce comportement peut poser problème, notamment dans le cas d'une
bibliothèque en développement, ce qui est souvent le cas quand on fait du
prototypage.
- En effet, l'ajout ou la modification de fonctions dans la bibliothèque n'est pas pris en compte dans la même session Python.
- Cela ne pose pas de problèmes dans le cas d'un script Python, mais est très gênant si vous souhaitez garder un shell Python ouvert, et faire des modifications à la volée.
La solution
- Alors, on pleure ? Non. On ruse.
- L'astuce consiste à passer à ctypes.CDLL un handle sur une
bibliothèque déja ouverte.
- En cas de modification de cette bibliothèque, rien n'empêche de la fermer, puis de la recharger de la même manière.
- Les modifications seront bien prises en compte.
- Tout ceci nécessite l'utilisation de la bibliothèque libdl
- Nous l'initialisons de façon classique, et nous récupérons des pointeurs sur les fonctions que nous allons utiliser.
- Il convient de préciser le type de retour pour la fonction dlopen , qui par défaut est un type long. Nous voulons à la place un pointeur void, ce qui correspond au prototype de dlopen.
>>> import ctypes
>>> libdl = ctypes.CDLL("libdl.so.2")
>>> dlopen = libdl.dlopen #récupère un pointeur de fonction
>>> dlclose = libdl.dlclose
>>> print dlopen.restype
<class 'ctypes.c_long'>
>>> dlopen.restype = ctypes.c_void_p
- L'initialisation est terminée, nous allons maintenant utiliser
dlopen.
- dlopen sert à ouvrir une bibliothèque, et récupérer son handle.
- Le premier paramètre est évident: c'est le nom de la bibliothèque à ouvrir.
- Le second l'est moins, et correspond à un flag. Nous allons utiliser RTLD_NOW.
- La valeur des flags de la fonction est défini ici:
/usr/include/bits/dlfcn.h - Je vais rentrer en dur la valeur du flag, qui a l'air universelle. En cas de problèmes, vérifiez cette valeur.
- En résumé:
#define RTLD_NOW 0x00002 /* Immediate function call binding */
>>> monhandle = dlopen("./hello_extend.so", 2)
- Maintenant que nous avons obtenu un handle sur notre bibliothèque, nous
pouvons l'utiliser avec ctypes.CDLL
- Par défaut, CDLL s'occupe de l'ouverture de la bibliothèque.
- On peut toutefois lui passer en paramètre le handle d'une bibliothèque déja ouverte. Ainsi, il ne s'occupera pas de l'ouverture de la bibliothèque, et nous pourrons demander sa fermeture (ce qui est impossible si on délègue l'ouverture à CDLL).
>>> malib = ctypes.CDLL("./hello_extend.so", handle = monhandle)
- Voila, vous pouvez à présent utiliser normalement la bibliothèque.
- Procédure pour recharger la bibliothèque :
- faire un dlclose avec le handle de cette bibliothèque.
- obtenir un nouvea handle sur cette bibliothèque avec dlopen.
- utiliser ctypes.CDLL avec le nouveau handle en paramètre.
>>> dlclose(monhandle)
>>> monhandle = dlopen("./hello_extend.so",2)
>>> malib = ctypes.CDLL("./hello_extend.so", handle = monhandle)
Et pour MS-Windows ?
- Je n'ai pas testé. Voici quelques pistes pour ceux qui voudront s'y atteler.
- Le principe est le même que pour Unix, à part pour les noms de fonctions de
gestion de bibliothèque.
- Par exemple, dlopen deviendra LoadLibrary
- Bien entendu, les paramètres ne seront pas les mêmes, je vous laisse consulter le MSDN.
- Si vous faites le test, merci d'écrire un commentaire ou de m'envoyer un mail. Je complèterai le billet.
Mise en oeuvre : bibliothèque C utilisable depuis Python via un wrapper.
bibliothèque C : hello_extend.c
- Rien de particulier à signaler dans le fichier source qui exporte ces
fonctions:
get_version(): retourne un entier correspondant au numéro de versionprint_persion(): imprime sur la sortie standard le numéro de version.test_version(int version_attendue): teste la version de la bibliothèque.print_struct (MASTRUCT *mastruct): affiche une structure (détails dans la partie consacrée aux types).
- Compilez avec:
gcc -shared -fPIC -Wall -W hello_extend.c -o hello_extend.so- Pour les systèmes MS-Windows, enlevez le paramètre
-fPIC
- Le source en lui même est du C de base, je n'encombre pas le billet avec.
- Vous trouverez le fichier source dans l'archive en annexe.
Module d'importation Python : dll.py
- Ce module a été amplement décrit plus haut.
- Il sera appelé automatiquement lors de l'initialisation du wrapper Python.
- Vous n'avez donc pas à vous soucier de son initialisation.
- Par la suite, en cas de modification de la bibliothèque C, il faudra la
recharger
- depuis le module dll:
dll.restart() - ou depuis le wrapper (voir plus loin):
hello_wrapper.restart()
- depuis le module dll:
Wrapper Python : hello_wrapper.py
- Important : le nom du module Python doit être
différent du nom de la bibliothèque C (les extensions ne comptent pas pour la
différenciation).
- Dans le cas contraire, vous obtiendrez une erreur assez cryptique lors de l'import du wrapper dans Python.
- Le wrapper va appeler pour vous les fonctions de la bibliothèque, et tester les paramètres, ainsi que les codes de retour.
- Il est important de tester les paramètres depuis le code Python. Prenons
pour exemple la fonction d' affichage de structure.
- Le strict minimum dans le code C serait de tester un pointeur null et de ne pas exécuter la fonction en cas de pointeur invalide.
- Ce genre de test ne sert pas à grand chose. Même si le pointeur nul n'est
pas exclu, il y a plus de probabilités pour qu'un objet d'un type invalide soit
passé. Ce dernier cas
passerait
le test du pointeur nul, mais provoquera probablement un beau plantage. - Il faut donc tester depuis Python que l'on passe bien une structure valide,
afin de limiter les dégats potentiels. Je dis
limiter les dégats
, car le fait de passer une structure valide ne présume en rien de son contenu. - Voici un exemple de test de paramètre, pour la fonction d'affichage de structure: on teste si l'objet est bien la structure réglementaire.
def print_struct(obj):
if isinstance (obj, mastruct):
dll.malib.print_struct (ctypes.byref(obj))
else:
print "Objet invalide, ce n'est pas une structure <mastruct>"
print "type de l'objet :", type(obj)
- Les tests sur les codes de retour sont évidents, et n'appelent pas de commentaires particuliers.
Et enfin, appel du code depuis Python
import hello_wrapper- ensuite, vous pouvez appeller classiquement les fonctions.
- Par exemple:
hello_wrapper.print_version()
- En cas de modification de la bibliothèque, rechargez la depuis python avec
hello_wrapper.restart()
C'est terminé, mais quelle est donc cette perplexité ?
- Voici la fin d'un gros morceau. J'espère que c'était clair et que ça vous a plu.
- Je n'ai rien inventé, et tout se trouve dans le lien qui suit. Lisez au moins le tutorial, très bien fait:
- Après toute cette partie théorique, nous allons nous pencher sur la mise en
pratique des connaissances acquises.
- Cela fera l'objet d'un projet billet, je vous laisse digérer celui-ci en
attendant

- Cela fera l'objet d'un projet billet, je vous laisse digérer celui-ci en
attendant