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.
  •  : 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.
  • 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 et string , 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:

#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
", 
          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.

>>> 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 version
    • print_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()


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.
  • 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 ;)