Utilisation de Ctypes

Utilisation de Ctypes #

ctypes est un module Python, de la bibliothèque standard, qui permet, depuis un programme Python, d’appeler des fonctions et procédures présentes dans une bibliothèques compilées, généralement écrire en C (.dll sous Windows et .so sous Linux).

Documentation de ctypes : https://docs.python.org/3.4/library/ctypes.html

Cette documentation est succincte mais elle présente quelques exemples d’utilisation très simples.

Vous avez aussi une vidéo explicative (un peu ancienne) :

Écriture des fonctions en C #

L’objectif de ctypes étant en particulier de pouvoir utiliser des bibliothèques déjà compilées, il n’y a rien de particulier à faire pour écrire les fonctions C. Elles sont en C standard, sans déclaration particulière et n’utilisent pas de librairie spécifique.

Voici un exemple de bibliothèque, que nous appellerons tools et qui contient quelques fonctions simples illustrant les propos qui suivent :

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

void hello() {
    printf("Hello World\n");
    }

int ajoute_entiers(int a, int b) {
    return a + b;
    }
   
double moyenne(double a, double b) {
    return (a + b)/2;
}

double moyenne_tableau(int * tab, int n) {
    int i=0;
    double s = 0;
    for (i=0; i<n; i++){
        s += tab[i];
    }
    return s/n;
}

Récupérez ce fichier et compilez le sous forme d’une bibliothèque dynamique.

Compilation sous Unix :

$ gcc -shared -O2 -o tools.so -fPIC tools.c
Compilation sous Windows : Il faut construire une DLL (Shared Library), ce qui se fait sans difficulté avec Code::Blocks. Veillez toutefois, avec une version Python 64 bits, de compiler avec un mingw 64 bits (installé dans L:) (http://blog.security-helpzone.com/programmation/programmer-en-64x-avec-codeblocks.html)

Utilisation de la bibliothèque en Python #

Dans une session shell, testez :

>>> import ctypes as ct
>>> _lib = ct.cdll.LoadLibrary("./tools.so")
>>> _lib
<CDLL './tools.so', handle 202c9d0 at 7f1e6745c940>
>>> _lib.hello()
Hello World
12
>>> _lib.ajoute_entiers()
-2004708440
>>> _lib.ajoute_entiers(2,3)
5
>>> _lib.ajoute_entiers(2147483647,0) # Taille maxi d'un int 32 bits
2147483647
>>> _lib.ajoute_entiers(2147483647,2) # Oups...
-2147483647
>>> _lib.ajoute_entiers('ha! ha!','hello','world')
71587248

Si la fonction est utilisée correctement et ne prend en paramètre que des entiers, le résultat est OK. Sinon…

Il est possible de déclarer, côté Python, les types attendus :

>>> _lib.ajoute_entiers.argtypes = [ct.c_int, ct.c_int]

>>> _lib.ajoute_entiers('ha! ha!','hello','world')
ArgumentError: argument 1: <class 'TypeError'>: wrong type

Aaaaahhhh!!!!!

On peut de même annoncer le type de retour :

>>> _lib.ajoute_entiers.restype  = ct.c_int

Maintenant, les fonctions se comportent un peu plus sagement.

Voici un exemple de programme Python qui utilise correctement la bibliothèque C :

import ctypes as ct
_lib = ct.cdll.LoadLibrary("./tools.so")

_lib.moyenne.argtypes = [ct.c_double, ct.c_double]
_lib.moyenne.restype = ct.c_double

_lib.ajoute_entiers.argtypes = [ct.c_int, ct.c_int]
_lib.ajoute_entiers.restype = ct.c_int

_lib.moyenne_tableau.argtypes = [ct.POINTER(ct.c_int), ct.c_int]
_lib.moyenne_tableau.restype = ct.c_double


# Utilisation

r2 = _lib.moyenne_tableau((ct.c_int * 4)(3,1,6,11), 4)
print(r2)

Utilisation avec NumPy #

Une des raisons pour lesquels on voudrait pouvoir écrire du C (et donc utiliser ctypes) est l’optimisation de traitement coûteux contenant des boucles, comme des traitements sur les images.

Partant du principe que, côté Python, les images sont représentées par des tableaux NumPy, on cherche à écrire une fonction C qui prend un tableau NumPy en paramètre (un tableau tridimensionnel, puisque c’est une image).

Numpy propose quelques fonctions facilitant la conversation avec le C.

Supposons que nous ayons la fonction suivante qui assombrit une image (supposons que nous ne sachions pas que img2 = img1 // 3 fonctionnerait… ) :

def sombre(img1):
    img2 = img1.copy()
    mi, mj = img1.shape[:2]
    for i in range(mi):
        for j in range(mj):
            for k in range(3):
                img2[i,j,k] = img1[i,j,k] // 3
    return img2

La fonction précédente est très lente.

Nous écrirons son équivalent en C (à ajouter dans tools.c):

void sombre(unsigned char *i1, int mi, int mj, unsigned char* i2) {
    int i,j,k;
    int v;
    for (i=0;i<mi;i++) {
        for(j=0;j<mj;j++) {
            for(k=0;k<3;k++) {
                v = k+3*(i*mj+j);
                i2[v] = i1[v]/3;
            }
        }
    }
}
Cela nécessite un peut d’arithmétique sur les pointeurs, et il faut savoir à quel type de données correspond une composante et comment sont agencés les pixels de l’image.

Nous écrirons ensuite en Python :

import numpy as np
import ctypes as ct
_lib = ct.cdll.LoadLibrary("./tools.so")

_lib.sombre.argtypes = [np.ctypeslib.ndpointer(dtype = np.uint8),
                        ct.c_int, ct.c_int,
                        np.ctypeslib.ndpointer(dtype = np.uint8)]           
_lib.sombre.restype  = ct.c_void_p        

def sombre(img1):
    img2 = img1.copy()
    mi, mj = img1.shape[:2]
    _lib.sombre(img1, mi, mj, img2)
    return img2            

Le temps de calcul est typiquement divisé par plus de 100….