Python >> Tutoriel Python >  >> Python

Extension de Python avec les bibliothèques C et le module "ctypes"

Un didacticiel de bout en bout expliquant comment étendre vos programmes Python avec des bibliothèques écrites en C, à l'aide du module intégré "ctypes".

Le ctypes intégré module est une fonctionnalité puissante de Python, vous permettant d'utiliser des bibliothèques existantes dans d'autres langages en écrivant de simples wrappers en Python lui-même.

Malheureusement, il peut être un peu délicat à utiliser. Dans cet article, nous allons explorer certaines des bases de ctypes . Nous couvrirons :

  • Charger les bibliothèques C
  • Appeler une fonction C simple
  • Transmettre des chaînes mutables et immuables
  • Gérer la mémoire

Commençons par jeter un coup d'œil à la bibliothèque C simple que nous allons utiliser et comment la construire, puis passons au chargement d'une bibliothèque C et à l'appel de ses fonctions.

Une bibliothèque C simple pouvant être utilisée à partir de Python

Tout le code pour construire et tester les exemples discutés ici (ainsi que le Markdown pour cet article) est engagé dans mon dépôt GitHub.

Je vais parcourir un peu la bibliothèque C avant d'aborder ctypes .

Le code C que nous utiliserons dans ce didacticiel est conçu pour être aussi simple que possible tout en démontrant les concepts que nous couvrons. Il s'agit plutôt d'un "exemple de jouet" et n'est pas destiné à être utile en soi. Voici les fonctions que nous allons utiliser :

int simple_function(void) {
    static int counter = 0;
    counter++;
    return counter;
}

Le simple_function La fonction renvoie simplement des nombres comptés. Chaque fois qu'il est appelé par incréments counter et renvoie cette valeur.

void add_one_to_string(char *input) {
    int ii = 0;
    for (; ii < strlen(input); ii++) {
        input[ii]++;
    }
}

Le add_one_to_string La fonction ajoute un à chaque caractère dans un tableau de caractères qui est transmis. Nous allons l'utiliser pour parler des chaînes immuables de Python et comment les contourner lorsque nous en avons besoin.

char * alloc_C_string(void) {
    char* phrase = strdup("I was written in C");
    printf("C just allocated %p(%ld):  %s\n",
           phrase, (long int)phrase, phrase);
    return phrase;
}

void free_C_string(char* ptr) {
    printf("About to free %p(%ld):  %s\n",
           ptr, (long int)ptr, ptr);
    free(ptr);
}

Cette paire de fonctions alloue et libère une chaîne dans le contexte C. Cela fournira le cadre pour parler de la gestion de la mémoire dans ctypes .

Enfin, nous avons besoin d'un moyen de construire ce fichier source dans une bibliothèque. Bien qu'il existe de nombreux outils, je préfère utiliser make , je l'utilise pour des projets comme celui-ci en raison de sa faible surcharge et de son omniprésence. Make est disponible sur tous les systèmes de type Linux.

Voici un extrait du Makefile qui construit la bibliothèque C dans un .so fichier :

clib1.so: clib1.o
    gcc -shared -o libclib1.so clib1.o

clib1.o: clib1.c
    gcc -c -Wall -Werror -fpic clib1.c

Le Makefile du référentiel est configuré pour créer et exécuter complètement la démo à partir de zéro ; il vous suffit d'exécuter la commande suivante dans votre shell :

$ make

Charger une bibliothèque C avec le module "ctypes" de Python

Ctypes vous permet de charger une bibliothèque partagée ("DLL" sous Windows) et d'accéder directement aux méthodes depuis celle-ci, à condition de prendre soin de "marshaler" correctement les données.

La forme la plus basique de ceci est :

import ctypes

# Load the shared library into c types.
libc = ctypes.CDLL("./libclib1.so")

Notez que cela suppose que votre bibliothèque partagée se trouve dans le même répertoire que votre script et que vous appelez le script depuis ce répertoire. Il y a beaucoup de détails spécifiques au système d'exploitation autour des chemins de recherche de bibliothèque qui sortent du cadre de cet article, mais si vous pouvez empaqueter le .py à côté de la bibliothèque partagée, vous pouvez utiliser quelque chose comme ceci :

libname = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "libclib1.so"))

libc = ctypes.CDLL(libname)

Cela vous permettra d'appeler le script depuis n'importe quel répertoire.

Une fois que vous avez chargé la bibliothèque, elle est stockée dans un objet Python qui a des méthodes pour chaque fonction exportée.

Appeler des fonctions simples avec des ctypes

La grande chose à propos de ctypes c'est qu'il rend les choses simples assez simples. Appeler simplement une fonction sans paramètres est trivial. Une fois que vous avez chargé la bibliothèque, la fonction n'est qu'une méthode de l'objet de la bibliothèque.

import ctypes

# Load the shared library into c types.
libc = ctypes.CDLL("./libclib1.so")

# Call the C function from the library
counter = libc.simple_function()

Vous vous souviendrez que la fonction C que nous appelons renvoie le nombre de nombres comme int objets. Encore une fois, ctypes rend les choses faciles faciles - la transmission des ints fonctionne de manière transparente et fait à peu près ce que vous attendez.

Traitement des chaînes mutables et immuables en tant que paramètres de ctypes

Alors que les types de base, les entiers et les flottants, sont généralement rassemblés par ctypes trivialement, les chaînes posent un problème. En Python, les chaînes sont immuables , ce qui signifie qu'ils ne peuvent pas changer. Cela produit un comportement étrange lors du passage de chaînes dans ctypes .

Pour cet exemple, nous utiliserons le add_one_to_string fonction montrée dans la bibliothèque C ci-dessus. Si nous appelons ce passage dans une chaîne Python, il s'exécute, mais ne modifie pas la chaîne comme on pourrait s'y attendre. Ce code Python :

print("Calling C function which tries to modify Python string")
original_string = "starting string"
print("Before:", original_string)

# This call does not change value, even though it tries!
libc.add_one_to_string(original_string)

print("After: ", original_string)

Résultats dans cette sortie :

Calling C function which tries to modify Python string
Before: starting string
After:  starting string

Après quelques tests, je me suis prouvé que le original_string n'est pas du tout disponible dans la fonction C lors de cette opération. La chaîne d'origine était inchangée, principalement parce que la fonction C modifiait une autre mémoire, pas la chaîne. Ainsi, non seulement la fonction C ne fait pas ce que vous voulez, mais elle modifie également la mémoire qu'elle ne devrait pas, entraînant des problèmes potentiels de corruption de la mémoire.

Si nous voulons que la fonction C ait accès à la chaîne, nous devons faire un petit travail de tri en amont. Heureusement, ctypes rend cela assez facile aussi.

Nous devons convertir la chaîne d'origine en octets en utilisant str.encode , puis passez ceci au constructeur pour un ctypes.string_buffer . String_buffers sont mutable, et ils sont passés à C en tant que char * comme vous vous en doutez.

# The ctypes string buffer IS mutable, however.
print("Calling C function with mutable buffer this time")

# Need to encode the original to get bytes for string_buffer
mutable_string = ctypes.create_string_buffer(str.encode(original_string))

print("Before:", mutable_string.value)
libc.add_one_to_string(mutable_string)  # Works!
print("After: ", mutable_string.value)

L'exécution de ce code imprime :

Calling C function with mutable buffer this time
Before: b'starting string'
After:  b'tubsujoh!tusjoh'

Notez que le string_buffer est imprimé sous la forme d'un tableau d'octets du côté Python.

Spécification des signatures de fonction dans les ctypes

Avant de passer au dernier exemple de ce didacticiel, nous devons prendre un bref à part et parler de la façon dont ctypes passe des paramètres et renvoie des valeurs. Comme nous l'avons vu ci-dessus, nous pouvons spécifier le type de retour si nécessaire.

Nous pouvons faire une spécification similaire des paramètres de la fonction. Ctypes déterminera le type du pointeur et créera un mappage par défaut vers un type Python, mais ce n'est pas toujours ce que vous voulez faire. De plus, fournir une signature de fonction permet à Python de vérifier que vous transmettez les bons paramètres lorsque vous appelez une fonction C, sinon des choses folles peuvent se produire.

Parce que chacune des fonctions de la bibliothèque chargée est en fait un objet Python qui a ses propres propriétés, spécifier la valeur de retour est assez simple. Pour spécifier le type de retour d'une fonction, vous obtenez l'objet fonction et définissez le restype propriété comme celle-ci :

alloc_func = libc.alloc_C_string
alloc_func.restype = ctypes.POINTER(ctypes.c_char)

De même, vous pouvez spécifier les types de tous les arguments transmis à la fonction C en définissant la propriété argtypes sur une liste de types :

free_func = libc.free_C_string
free_func.argtypes = [ctypes.POINTER(ctypes.c_char), ]

J'ai trouvé plusieurs méthodes astucieuses différentes dans mes études pour simplifier leur spécification, mais en fin de compte, elles se résument toutes à ces propriétés.

Les bases de la gestion de la mémoire dans ctypes

L'une des grandes caractéristiques du passage de C à Python est que vous n'avez plus besoin de passer du temps à gérer manuellement la mémoire. La règle d'or quand on fait ctypes , ou tout marshalling inter-langage est que le langage qui alloue la mémoire doit également libérer la mémoire .

Dans l'exemple ci-dessus, cela a plutôt bien fonctionné car Python a alloué les tampons de chaîne que nous faisions circuler afin de pouvoir ensuite libérer cette mémoire lorsqu'elle n'était plus nécessaire.

Cependant, il est souvent nécessaire d'allouer de la mémoire en C, puis de la transmettre à Python pour certaines manipulations. Cela fonctionne, mais vous devez suivre quelques étapes supplémentaires pour vous assurer que vous pouvez renvoyer le pointeur de mémoire à C afin qu'il puisse le libérer lorsque nous aurons terminé.

Pour cet exemple, j'utiliserai ces deux fonctions C, alloc_C_string et free_C_string . Dans l'exemple de code, les deux fonctions affichent le pointeur de mémoire qu'elles manipulent pour indiquer clairement ce qui se passe.

Comme mentionné ci-dessus, nous devons être en mesure de conserver le pointeur réel vers la mémoire que alloc_C_string alloué afin que nous puissions le retransmettre à free_C_string . Pour ce faire, nous devons indiquer à ctype que alloc_C_string doit renvoyer un ctypes.POINTER à un ctypes.c_char . Nous l'avons vu plus tôt.

Le ctypes.POINTER les objets ne sont pas trop utiles, mais ils peuvent être convertis en objets utiles. Une fois que nous avons converti notre chaîne en un ctypes.c_char , nous pouvons accéder à son attribut value pour obtenir les octets en Python.

Mettre tout cela ensemble ressemble à ceci :

alloc_func = libc.alloc_C_string

# This is a ctypes.POINTER object which holds the address of the data
alloc_func.restype = ctypes.POINTER(ctypes.c_char)

print("Allocating and freeing memory in C")
c_string_address = alloc_func()

# Wow we have the POINTER object.
# We should convert that to something we can use
# on the Python side
phrase = ctypes.c_char_p.from_buffer(c_string_address)

print("Bytes in Python {0}".format(phrase.value))

Une fois que nous avons utilisé les données que nous avons allouées en C, nous devons les libérer. Le processus est assez similaire, en spécifiant le argtypes attribut au lieu de restype :

free_func = libc.free_C_string
free_func.argtypes = [ctypes.POINTER(ctypes.c_char), ]
free_func(c_string_address)

Module "ctypes" de Python – Conclusion

ctypes intégré de Python vous permet d'interagir assez facilement avec le code C de Python, en utilisant quelques règles de base pour vous permettre de spécifier et d'appeler ces fonctions. Cependant, vous devez faire attention à la gestion et à la propriété de la mémoire.

Si vous souhaitez voir et jouer avec le code que j'ai écrit en travaillant dessus, veuillez visiter mon dépôt GitHub.

Assurez-vous également de consulter la deuxième partie de ce didacticiel où vous en apprendrez plus sur les fonctionnalités avancées et les modèles d'utilisation du ctypes. bibliothèque pour interfacer Python avec du code C.