Skip to main content
Niveau : Expert

Chapitre 42 : Écrire des Extensions C pour Python

Objectif

Ce module a pour but de vous donner un aperçu de haut niveau sur la manière d'étendre l'interpréteur CPython avec du code écrit en C. Vous apprendrez pourquoi et quand créer une extension C, et vous verrez la structure de base d'un module d'extension simple.

Note : Ce sujet est vaste et complexe. L'objectif ici est de comprendre les concepts et le processus, pas de maîtriser l'API C de Python en une seule leçon.

1. Pourquoi Écrire des Extensions C ?

Il y a deux raisons principales pour lesquelles on étend Python avec du C (ou C++, Rust, etc.) :

  1. Performance : Pour des tâches très gourmandes en calcul (CPU-bound), le code C compilé est des ordres de grandeur plus rapide que le code Python interprété. En écrivant les goulots d'étranglement en C, on peut accélérer considérablement une application. C'est la raison d'être de bibliothèques comme NumPy, SciPy, et Pandas.

  2. Intégration de bibliothèques existantes : Pour utiliser une bibliothèque C existante depuis Python sans avoir à la ré-implémenter. On écrit une "couche d'adaptation" (wrapper) qui expose les fonctions de la bibliothèque C au monde Python.

2. L'API C de Python

CPython fournit une API C (Python.h) qui permet au code C d'interagir avec l'interpréteur Python. Cette API permet de :

  • Créer et manipuler des objets Python (nombres, chaînes, listes, dictionnaires, etc.).
  • Appeler des fonctions Python depuis le C.
  • Définir de nouveaux types d'objets en C.
  • Gérer le comptage de références et le garbage collector.
  • Lever et attraper des exceptions Python.

Tous les objets en Python sont représentés en C par une structure PyObject *. C'est un pointeur vers une structure générique qui contient le compteur de références et un pointeur vers une structure de type spécifique.

3. Structure d'un Module d'Extension Simple

Créons un module C simple nommé monmodule qui expose une seule fonction, monmodule.addition(a, b).

Le processus implique trois parties principales :

  1. Écrire la fonction C qui sera appelée.
  2. Définir la table des méthodes du module.
  3. Écrire la fonction d'initialisation du module.

Étape 1 : Le Fichier Source C (monmodule.c)

#define PY_SSIZE_T_CLEAN
#include <Python.h> // Doit être inclus en premier

// 1. La fonction C qui implémente la logique
// Le nom est statique pour qu'il ne soit pas visible en dehors de ce fichier.
static PyObject* monmodule_addition(PyObject* self, PyObject* args) {
long a, b;
long resultat;

// PyArg_ParseTuple convertit les arguments Python (un tuple) en valeurs C.
// "ll" signifie qu'on attend deux entiers longs (long).
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL; // Le parsing a échoué, une exception a été levée.
}

// La logique métier (ici, une simple addition)
resultat = a + b;

// PyLong_FromLong convertit le résultat C en un objet entier Python.
return PyLong_FromLong(resultat);
}

// 2. La table des méthodes du module
// C'est un tableau qui liste les fonctions exportées par le module.
static PyMethodDef MonModuleMethods[] = {
// { "nom_en_python", fonction_c, convention_d_appel, "docstring" }
{"addition", monmodule_addition, METH_VARARGS, "Additionne deux entiers."},
{NULL, NULL, 0, NULL} // Sentinelle de fin de tableau
};

// 3. La structure de définition du module
static struct PyModuleDef monmodule_definition = {
PyModuleDef_HEAD_INIT,
"monmodule", // Nom du module
"Un module d'exemple qui additionne des entiers.", // Docstring du module
-1,
MonModuleMethods
};

// 4. La fonction d'initialisation du module
// Cette fonction est appelée lorsque Python fait 'import monmodule'.
// Le nom DOIT être PyInit_<nom_du_module>.
PyMODINIT_FUNC PyInit_monmodule(void) {
return PyModule_Create(&monmodule_definition);
}

Analyse du code :

  • monmodule_addition : C'est le "wrapper". Il prend deux PyObject* en argument. self est l'objet module, et args est un tuple contenant les arguments passés depuis Python.
  • PyArg_ParseTuple : C'est la fonction standard pour extraire des valeurs C à partir du tuple d'arguments Python.
  • PyLong_FromLong : C'est la fonction pour créer un objet entier Python à partir d'un long C. Il existe des fonctions similaires pour tous les types (PyFloat_FromDouble, PyUnicode_FromString, etc.).
  • MonModuleMethods : Ce tableau fait le lien entre le nom de la fonction en Python ("addition") et le pointeur vers la fonction C qui l'implémente (monmodule_addition).
  • PyInit_monmodule : C'est le point d'entrée du module. Python l'appelle lors de l'import.

4. Compiler l'Extension

Compiler une extension C pour Python est la partie la plus délicate. La méthode standard est d'utiliser le module distutils ou setuptools de Python.

On crée un fichier setup.py :

# setup.py
from distutils.core import setup, Extension

# Décrit notre module d'extension
mon_module_extension = Extension(
'monmodule', # Nom du module tel qu'importé en Python
sources=['monmodule.c'] # Liste des fichiers source C
)

# Fonction setup
setup(
name='MonModule',
version='1.0',
description='Un module d\'exemple',
ext_modules=[mon_module_extension] # Liste des extensions à compiler
)

Ensuite, on compile depuis la ligne de commande :

python setup.py build

Cette commande va :

  1. Trouver le compilateur C approprié sur votre système (GCC, Clang, MSVC...).
  2. Compiler monmodule.c en un fichier objet.
  3. Lier le fichier objet avec la bibliothèque Python pour créer une bibliothèque partagée.
    • Sur Linux : monmodule.so
    • Sur macOS : monmodule.so
    • Sur Windows : monmodule.pyd

Le fichier compilé se trouvera dans un sous-dossier build/lib....

Pour une installation propre, on utilise :

python setup.py install
# ou pour le développement :
python setup.py develop
# ou pour créer une wheel (paquet distribuable) :
pip wheel .

5. Utiliser l'Extension en Python

Une fois le module compilé et installé dans l'environnement, on peut l'utiliser comme n'importe quel autre module Python :

import monmodule

# Afficher la docstring du module
print(monmodule.__doc__)
# Un module d'exemple qui additionne des entiers.

# Afficher la docstring de la fonction
print(monmodule.addition.__doc__)
# Additionne deux entiers.

# Appeler la fonction C
resultat = monmodule.addition(10, 25)
print(f"Le résultat est : {resultat}") # Le résultat est : 35

# Essayer avec des types incorrects
try:
monmodule.addition("a", "b")
except TypeError as e:
print(f"Erreur attrapée : {e}")
# PyArg_ParseTuple a levé une TypeError automatiquement.

6. Libérer le GIL

Pour les tâches CPU-bound, le vrai gain de performance vient de la capacité du code C à libérer le GIL, permettant à d'autres threads Python de s'exécuter.

On utilise une paire de macros pour cela :

static PyObject* ma_fonction_cpu_intensive(PyObject* self, PyObject* args) {
// ... parser les arguments ...

Py_BEGIN_ALLOW_THREADS
// --- Début de la section sans GIL ---

// Ici, on ne peut PAS utiliser l'API C de Python.
// On travaille uniquement avec des variables et des structures de données C.
// C'est ici qu'on place le calcul lourd.

// --- Fin de la section sans GIL ---
Py_END_ALLOW_THREADS

// ... créer l'objet Python de retour ...
return resultat_python;
}

Pendant que le code entre Py_BEGIN_ALLOW_THREADS et Py_END_ALLOW_THREADS s'exécute, le GIL est libéré. Si ce code est dans un thread, la PVM peut scheduler un autre thread Python pour qu'il s'exécute en parallèle.

7. Alternatives Modernes

Écrire des extensions C à la main est puissant mais verbeux et sujet aux erreurs (notamment la gestion manuelle des références). Des outils modernes simplifient grandement ce processus :

  • Cython : Un sur-ensemble de Python qui permet d'ajouter des types C statiques au code Python. Cython "compile" ce code en un module C optimisé. C'est l'outil le plus populaire pour accélérer du code Python.
  • pybind11 : Une bibliothèque C++ "header-only" qui simplifie grandement la création de wrappers pour du code C++.
  • CFFI (C Foreign Function Interface) : Permet d'appeler du code C directement depuis Python, sans écrire de code C "wrapper". Idéal pour intégrer des bibliothèques C existantes.
  • Rust avec PyO3 ou Maturin : Rust est un langage moderne et sûr qui devient une alternative très populaire au C/C++ pour écrire des extensions Python performantes.

Conclusion

Écrire des extensions C est la méthode ultime pour repousser les limites de performance de Python et pour s'interfacer avec l'écosystème C existant. Bien que le processus manuel soit complexe, il offre un contrôle total sur le comportement du code. Comprendre les bases de l'API C de Python vous donne une vision plus profonde du fonctionnement de l'interpréteur et vous permet d'apprécier la "magie" derrière les grandes bibliothèques scientifiques comme NumPy. Pour des besoins pratiques, des outils comme Cython ou pybind11 sont souvent des choix plus productifs.


Exercice 01 : Création et Compilation d'un Module C Simple

Objectif

Cet exercice a pour but de vous guider à travers les étapes complètes de création, compilation et utilisation d'une extension C simple pour Python. Vous allez implémenter un module systeminfo qui expose une fonction get_platform() retournant la plateforme du système d'exploitation sur laquelle Python a été compilé.

Contexte

L'API C de Python fournit des macros et des variables pour obtenir des informations sur le système. La macro Py_GetPlatform() retourne une chaîne de caractères (const char*) décrivant la plateforme (ex: "linux", "darwin" pour macOS, "win32" pour Windows).

Vous allez créer un module C qui rend cette information accessible directement depuis Python.

Prérequis

  • Un compilateur C (GCC ou Clang sur Linux/macOS, MSVC sur Windows).
  • Les en-têtes de développement Python.
    • Sur Debian/Ubuntu : sudo apt-get install python3-dev
    • Sur Fedora/CentOS : sudo dnf install python3-devel
    • Sur macOS, ils sont généralement inclus avec les outils de ligne de commande Xcode.
    • Sur Windows, assurez-vous de cocher "Python development headers" lors de l'installation de Python.

Énoncé

Étape 1 : Créer la structure du projet

Créez un nouveau dossier pour votre projet et placez-y deux fichiers vides :

  • systeminfo_module.c
  • setup.py

Étape 2 : Écrire le code de l'extension C

Ouvrez systeminfo_module.c et écrivez le code C nécessaire. Inspirez-vous de l'exemple du cours.

  1. Inclure Python.h.
  2. Écrire la fonction C systeminfo_get_platform.
    • Elle ne prend pas d'arguments, donc PyObject* args ne sera pas utilisé.
    • Utilisez Py_GetPlatform() pour obtenir la chaîne de caractères de la plateforme.
    • Utilisez PyUnicode_FromString() pour convertir cette chaîne C en un objet chaîne de caractères Python.
    • Retournez cet objet Python.
  3. Créer la table des méthodes SystemInfoMethods.
    • Déclarez une seule méthode :
      • Nom Python : "get_platform"
      • Fonction C : systeminfo_get_platform
      • Type d'appel : METH_NOARGS (car la fonction ne prend pas d'arguments).
      • Docstring : "Retourne la plateforme du système (ex: 'linux', 'darwin')."
  4. Créer la structure de définition du module systeminfo_definition.
    • Nom du module : "systeminfo"
    • Docstring du module : "Un module d'exemple pour obtenir des informations système."
  5. Écrire la fonction d'initialisation PyInit_systeminfo.

Étape 3 : Écrire le script de compilation

Ouvrez setup.py et écrivez le script pour distutils.

  1. Importez setup et Extension depuis distutils.core.
  2. Créez un objet Extension :
    • Nom du module : "systeminfo"
    • Fichiers source : ['systeminfo_module.c']
  3. Appelez setup() en lui passant les informations nécessaires, y compris votre objet Extension dans la liste ext_modules.

Étape 4 : Compiler et Installer le module

Ouvrez un terminal dans le dossier de votre projet.

  1. Compiler le module.

    python setup.py build

    Vérifiez qu'un dossier build a été créé et qu'il contient votre bibliothèque partagée (.so ou .pyd).

  2. Installer le module dans votre environnement de développement. Cette commande crée un lien symbolique vers le module compilé, ce qui vous permet de le tester sans avoir à le réinstaller après chaque modification.

    python setup.py develop

Étape 5 : Tester le module en Python

  1. Lancez un interpréteur Python depuis le même terminal.

  2. Importez et utilisez votre module.

    import systeminfo

    # Testez la fonction
    platform = systeminfo.get_platform()
    print(f"La plateforme détectée par le module C est : '{platform}'")

    # Vérifiez la docstring
    print("\nDocstring du module:")
    print(systeminfo.__doc__)

    print("\nDocstring de la fonction:")
    print(systeminfo.get_platform.__doc__)

Résultat Attendu

L'exécution du script de test Python doit afficher la plateforme de votre système, ainsi que les docstrings que vous avez définies dans votre code C.

La plateforme détectée par le module C est : 'darwin'

Docstring du module:
Un module d'exemple pour obtenir des informations système.

Docstring de la fonction:
Retourne la plateforme du système (ex: 'linux', 'darwin').
Cliquez ici pour voir un exemple de code de solution

Fichier systeminfo_module.c :

#define PY_SSIZE_T_CLEAN
#include <Python.h>

// 1. Implémentation de la fonction
static PyObject* systeminfo_get_platform(PyObject* self, PyObject* args) {
// Pas besoin de PyArg_ParseTuple car on n'a pas d'arguments (METH_NOARGS)

// Py_GetPlatform() retourne un const char*
const char* platform_str = Py_GetPlatform();

// Convertit la chaîne C en objet chaîne de caractères Python
return PyUnicode_FromString(platform_str);
}

// 2. Table des méthodes
static PyMethodDef SystemInfoMethods[] = {
{
"get_platform", // Nom en Python
systeminfo_get_platform, // Fonction C
METH_NOARGS, // Pas d'arguments
"Retourne la plateforme du système (ex: 'linux', 'darwin')." // Docstring
},
{NULL, NULL, 0, NULL} // Sentinelle
};

// 3. Définition du module
static struct PyModuleDef systeminfo_definition = {
PyModuleDef_HEAD_INIT,
"systeminfo", // Nom du module
"Un module d'exemple pour obtenir des informations système.", // Docstring
-1,
SystemInfoMethods
};

// 4. Fonction d'initialisation
PyMODINIT_FUNC PyInit_systeminfo(void) {
return PyModule_Create(&systeminfo_definition);
}

Fichier setup.py :

from distutils.core import setup, Extension

systeminfo_extension = Extension(
'systeminfo',
sources=['systeminfo_module.c']
)

setup(
name='SystemInfo',
version='1.0',
description='Module pour obtenir la plateforme système.',
ext_modules=[systeminfo_extension]
)