Appeler du code C depuis Python avec ctypes


On vous a dit et répété que Python c’était un super langage de glu et que ça pouvait très facilement s’interfacer avec les binaires produits par du C. Mais jusqu’à quel point ?

En vérité c’est extrêmement simple : Python permet de se mapper directement sur un .dll ou un .so, et d’appeler n’importe quelle fonction qu’il contient depuis le code Python comme si c’était une fonction normale. Il n’y a rien à installer, c’est fourni d’office.

On peut tester ça facilement. Je ponds une lib d’une folle puissance grâce à mes talents de codeurs C internationalement connus dans le quartier :

#include<stdio.h>
 
/*
Attend un pointeur sur un array de caractères (une chaîne en C) 
et l'affiche.
*/
dit_papa(char * p)
{
    printf("%s\n", p);
}
 
 
/*
Attend deux entiers et les multiple
*/
multiplier(long a, long b)
{
    return a * b;
}
 
/*
Attend un pointeur de pointeur sur un array de char
parce qu'on aime les risques.
*/
jakadi(char ** p)
{
    printf("%s\n", *p);
}

On compile tout ça. Comme je suis sous Nunux, j’utilise GCC et j’obtiens un .so, mais sous Windows c’est pareil avec VisualStudio et les .dll.

gcc -shared -Wl,-soname,ZeLib -o ZeLib.so -fPIC ZeLib.c

ZeLib.so est prête et frétille d’impatience à l’idée de vous servir. Il ne reste plus qu’à lancer le shell Python…

D’abord on fait ses imports, c’est dans la lib standard tout ça :

>>> import ctypes

Ensuite on se bind sur le binaire, il faut préciser un chemin absolu sinon ça ne marche pas :

>>> zelib = ctypes.CDLL("/home/sam/Work/projet/ZeLib.so")

Et derrière on peut appeler une fonction (Python fait la conversion entre tous les types de bases Python et C) :

>>> res = zelib.multiplier(2, 3)
>>> print res
6

Si on veut faire des chaînes, on ne peut pas passer de l’unicode. Comme mon shell a toutes les chaînes en unicode par défaut, je dois encoder dans le charset de sortie (sur mon système, c’est UTF8):

>>> zelib.dit_papa("papa".encode('utf8'))
papa
5

Notez au passage que la fonction retourne quelque chose même si je n’ai pas précisé de valeur de retour. Du coup j’aurais mieux fait de mettre un bon return 0 à la fin.

Si on veut appeler une fonction qui attend un pointeur, il faut d’abord caster son type en un type C, puis appeler by_ref dessus, qui va passer l’argument par référence, plutôt que par valeur :

>>> from ctypes import *
>>> s = ctypes.c_char_p('kiwi'.encode('utf8'))
>>> zelib.jakadi(byref(s))
kiwi
5

Voilà.

Voilà, voilà.

Bon attention quand même, le C n’est pas aussi conciliant que le Python : le debug est plus dur (pas de stacktrace, mais un bon vieux core dump), pas de completion dans ipython, et on peut même planter la VM si on se débrouille bien :-) N’oubliez pas non plus que Python 64 bits ne peut pas taper dans des DLL 32 bits et inversement.

P.S: je ne vais pas mettre le code C à télécharger et le code Python, franchement, il est pas énorme. Donc petite exception dans cet article : y a rien à DL et la syntaxe est pas à base de comments.

32 thoughts on “Appeler du code C depuis Python avec ctypes

  • François

    Dans les libs scientifiques, notamment celles basées sur numpy, on utilise beaucoup cython. C’est très facile vu que c’est à mi-chemin entre python et C. Est-ce que c’est utilisé par des devs python non scientifiques, lorsque du code performant est nécessaire et pas déjà écrit en C ?

  • Sam Post author

    Bien entendu : manipulation d’image, process de grosses données, event machines performances… Les extensions en C sont très utiles.

  • Etienne

    Manifestement t’as commencé par une fonction qui additionne, puis tu t’es dit qu’une multiplication serait plus impressionnante… et le nom est resté…

  • FX

    C’est souvent très pratique dès que tu veux optimiser un tout petit point chaud… mais en général j’utilise jamais Cython ou Ctypes quand je manipule des tableaux NumPy. Je devrais peut-être m’y mettre…

  • Sam Post author

    Numpy est déjà tellement optimisé, il ne doit pas y avoir souvent besoin de faire plus de C derrière.

  • FX

    @Sam : Les trois cas où j’ai parfois besoin de le faire :

    * Une fonction qui n’est pas implémentée dans NumPy (comme un traitement un peu bizarrement foutu sur des grosses images)
    * Certaines fonctions de NumPy/SciPy prennent un pointeur vers une fonction C en argument pour s’éviter de se manger un call Python à chaque pixel d’une image (comme scipy.ndimage.interpolation.geometric_transform)
    * Utiliser une bibliothèque déjà implémentée en C

    Sinon, la plupart du temps il vaut beaucoup, beaucoup mieux faire du code Python propre et s’occuper après de passer en C certaines petites parties! ;)

  • Lyyn

    Marrant, j’regardais justement hier ctypes en me disant “oh, ça permet même d’utiliser les .dll” mais j’avais la flemme de lire. Avec votre article, ça va être bien plus agréable !
    <3

  • Pocket Tiger

    Ça veux dire qu’on peut faire SegFault/BusError du Python ?

  • François

    Numpy est déjà tellement optimisé, il ne doit pas y avoir souvent besoin de faire plus de C derrière.

    Si c’était le cas, on ne coderait pas en Cython dans des libs comme scikit-image. En fait, numpy c’est bien optimisé quand tu fais un gentil calcul matriciel et que tu évites toutes les boucles for déjà codées en C derrière, mais dans le cas où il faut écrire ses propres boucles, ca reste utile.

    les 2 cents de ma petite expérience.

  • Etienne

    En tout cas Sam, si t’as autre chose sur le sujet des rapports python – C/C++, genre ctypes, Cython et autres, envoie, tu feras au moins un heureux.

  • Sam Post author

    Je suis un pitoyable codeur C donc probablement pas la meilleure source, mais si quelques scientifiques désirent élargir la question, on publiera avec plaisir.

  • kontre

    Pypy ne supporte pas bien numpy (même si ça progresse), c’est pas bon pour les scientifiques (pour les autre je dis pas).

    Je confirme pour numpy et cython. Dès qu’un algo ne se vectorise pas, il faut faire des boucles. Et les grosses boucles python, ben c’est lent, comparé au C. La dernière fois que j’ai utilisé cython, j’ai gagné un facteur 100 simplement en rajoutant quelques types.

    @FX si tu veux accélérer ton code numpy, essaie cython, c’est vraiment fait pour.

    Il faudrait que je me motive à faire un article sur cython, mais rapidement voilà quelques avantages :
    – le code python est valide en cython. Ça veut dire qu’on peut transformer du python en cython juste en changeant l’extension, et qu’écrire du cython c’est écrire du python. On garde aussi toute l’introspection…
    – le code est transformé en C de manière très optimisée, mais on conserve les exceptions, on évite les buffer overflow, segmentation fault…
    – il est possible d’appeler du C directement depuis cython (ça j’ai pas trop fait encore, perso)
    – les tableaux numpy sont mappés en tableaux C
    – on peut soit compiler à l’installation via un setup.py, soit compiler à la volée, ce qui est juste parfait pour développer (c’est mis en cache, on ne recompile pas si rien n’a changé).
    – si il y a une exception, la trace indique la ligne du fichier C et celle du fichier .pyx d’origine

    Il n’y a bien sûr strictement absolument aucun désavantage ! :P

  • reg

    Bon, le plus souvent, on utilise pas ctypes pour un développement nouveau mais quand on veut utiliser une lib existante en C/C++. Car oui, ça marche aussi avec C++ mais si c’est beaucoup plus relou car il faut faire un wrapper à grand renfort d’extern et de simuler du procédural ; mais ça marche.

    Je déconseille l’utilisation de ctypes en direct! Prenez un peu de temps pour en faire une interface python indépendante et propre ; vous le le regretterez pas car ainsi quand ça vous pètera au visage, vous saurez clairement d’où ça vient.

  • FX

    @Kontre: Faut vraiment que je me penche dessus. J’ai un petit a priori négatif sur Cython pour le moment, j’ai un peu l’impression que c’est pas super stable. Je sais bien que des tas de trucs (comme SciPy) l’utilisent, mais j’hésite quand même.

    Au niveau déploiment, je suppose que tu ajoutes juste une extension dans ton setup.py? Sinon tu l’utilises pour quel genre de projets?

    Et puis je rejoins Etienne, si tu as des histoires à raconter sur des questions de Python/C/Ctypes/Cython, Sam, je suis preneur aussi!

  • kontre

    La plupart des gens rajoutent une extension dans setup.py, oui. Moi je fais de la recherche, donc je développe pour ma boite en interne et mes programmes sont utilisés par peut-être 5 personnes. J’ai bon espoir que ça monte autour de 10 ! ^^ Tout ça pour dire que j’ai pas de problème de déploiement. Je n’ai qu’un module en cython, mais il est central pour l’appli en question (affichage d’images de plusieurs Go) et je n’ai aucun souci de stabilité avec. J’atteins la vitesse d’exécution du même programme écrit en C, point de vue perfo ça dépote.

    Pour l’utilisation, j’appelle le module comme un module python, avec pyximport : http://docs.cython.org/src/userguide/source_files_and_compilation.html#pyximport. Ça me permet notamment d’installer le module sur ma machine en mode “dev” (avec pip -e module par exemple) et le module est compilé automatiquement à l’import si besoin. Pour les tests et le développement c’est top.

    Pour éviter les soucis de compilation, j’ai utilisé ce qui est indiqué dans la partie “MinGW + NumPy + pyximport at runtime” sur la page http://wiki.cython.org/InstallingOnWindows.

  • FX

    @Kontre: Héhé, ça conviendrait parfaitement à mon boulot, merci pour les liens, je vais jeter un oeil quand j’aurai le temps!

    Nous on ne va pas aussi loin que plusieurs Go, mais on a des TIFF de quelques 500Mo, donc les extensions C c’est carrément utile ;)

  • FX

    Je connaissais pas non plus. Nous a priori on n’a pas trop de libs existantes, tout le code est entièrement fait main avec amour, donc Cython ou C c’est bien plus approprié ;)

    (D’ailleurs, je suis en train de commencer à tester Cython, c’est vraiment sympa, tu fais la même chose qu’en C en plus sûr et en 3 fois moins de lignes, mais ça manque un poil de doc pour le moment…)

  • Joks

    Bonjour.

    tout d’abord je tiens a vous remercier pour cet EXCELENT tuto ! simple, et tres bien expliqué !

    J’ai crée une DLL qui fonctionne. je l’ai tester avec un autre programme en C, et il arrive bien à utiliser les fonction de cette DLL.
    Néanmoins cela ne fonctionne pas lors de l’interfaçage avec python…
    toutes vos étapes fonctionnent jusqu’à ce que j’éssaie d’utiliser une fonction de la dll :
    res = zelib.addition(2, 3)
    me renvoie : AttibuteError: function ‘multiplier’ not found

    Auriez-vous une solution ? car cela fait plusieurs jours que je n’en trouve pas… (j’ai tenté de jeter un oeil à la doc python mais sans résultats…) ^^’

    Je vous en serais infiniment reconnaissant si vous pouviez m’aider.

    cordialement.

  • Sam Post author

    D’abord, je t’invite à lire ceci. Ensuite, je te recommande de l’appliquer sur le forum de l’afpy, puisque des commentaires de blog ne sont pas un bon moyen de communiquer pour aider au debug.

  • Soso

    Bonjour. Bon artircle !

    J’aimerais comprend comme passer une liste d’int() python a ma fonction C.

    en C

    maFonction (int **buffer1, int **buffer2, int *size){}

    appel python

    maLib.maFonction(self.buffer1, self.buffer2, self.size)

    Msg d’erreur python

    Traceback (most recent call last):
    
    File "monFichier.py", line 37
    
    maLib.maFonction(self.buff1, self.buff2, c_int(self.size))
    
    ctypes.ArgumentError: argument 1: : Don't know how to convert parameter 1

    Merci par avance :)

  • Sam Post author

    Il faut créer un array compatible avec du code C:

    >>> import ctypes
    >>> data = [1, 2, 3]
    >>> # création d'un array d'int de taille 3
    >>> TypeArray = ctypes.c_int * len(data)
    >>> array = TypeArray(*data)
    >>> array
    <__main__.c_int_Array_3 object at 0x7ff84f9f3e18>
    >>> array[0]
    1
  • lesjj

    Bonjour,

    Mon ordinateur est en 64 bits. J’ai une extension pour QGIS qui contient une DLL ‘libmgrs.dll’. Quand je tente d’instraller ce plugin, j’ai une erreur qui s’affiche “WindowsError: [Error 193] %1 n’est pas une application Win32 valide”. La ligne incriminée contient l’exploitation de cette dll. Que dois-je faire pour que cela fonctionne?

  • Sam Post author

    Bonjour, pour les demandes d’aide, c’est par ici : indexerror.net.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.