Framework permettant de créer votre propre extension C pour Python

Commençons par le code source.
Il existe deux méthodes : la méthode en une seule phase et la méthode en plusieurs phases.

La méthode en une seule phase n'est plus recommandée (est-elle obsolète ? Je n'en suis pas encore sûr) ==> Pourquoi ne pas créer un programme permettant de migrer le code source de la méthode en une seule phase vers la méthode en plusieurs phases ? C'est peut-être possible ? Mais c'est un autre sujet.


Concentrons-nous sur le code source : voici un modèle générique. Commençons par nous exercer sur le code pour comprendre comment procéder.

Et au cœur de Python :

  • Include/pyport.h contient le commentaire : « Désormais, PY_SSIZE_T_CLEAN est obligatoire. Ceci est uniquement pour des raisons de compatibilité ascendante. »
  • et il définit également : typedef Py_ssize_t Py_ssize_clean_t;

La règle pratique est donc la suivante :

  • avant Python 3.13 : le définir avant #include <Python.h> pour utiliser les API avec #
  • à partir de Python 3.13 : ce n’est plus strictement nécessaire, mais on le conserve pour des raisons de compatibilité

En bref, PY_SSIZE_T_CLEAN cela sert à garantir le bon type Py_ssize_t dans les liaisons C/extensions, et le dépôt le met surtout en avant dans la documentation et les exemples, pas dans le corps de l’en-tête principal.

#ifdef PY_SSIZE_T_CLEAN est une condition de compilation utilisée par l’API C de CPython pour basculer certaines fonctions vers les Py_ssize_tvariantes basées sur .

En pratique, le modèle courant est le suivant :

C

#define PY_SSIZE_T_CLEAN
#include <Python.h>

Ainsi, les API qui prennent en paramètre une longueur/un nombre ou une taille de tampon utilisent Py_ssize_t au lieu de int pour certaines unités de format telles que :

  • s#
  • y#
  • z#
  • et d’autres #formats d’analyse d’arguments basés sur

Exemple :

C

char *buf;
Py_ssize_t len;

PyArg_ParseTuple(args, "s#", &buf, &len);

Sans PY_SSIZE_T_CLEAN, certaines anciennes versions de Python pouvaient considérer la longueur comme int au lieu de Py_ssize_t, ce qui n’est pas idéal pour les grandes tailles et constitue une source de problèmes d’incompatibilité ABI/API.

Remarques importantes concernant CPython :

  • Sous Python 3.12 et versions antérieures, cette macro est nécessaire pour utiliser les # variantes en toute sécurité.
  • Sous Python 3.13 et versions ultérieures, elle n’est plus nécessaire dans la plupart des cas.
  • Dans le code CPython plus récent, elle est considérée comme une macro de compatibilité : le moteur la prend toujours en charge, mais le code moderne utilise principalement les Py_ssize_tsans avoir besoin d’elle.

Vous pouvez également le constater dans la documentation de CPython :

  • Doc/c-api/arg.rst il est indiqué qu’elle doit être définie sous Python 3.12 et versions antérieures pour toutes les # variantes de format.
  • Include/pyport.h indique que cela n’est désormais conservé que pour des raisons de compatibilité ascendante.

En résumé :

  • PY_SSIZE_T_CLEAN = « utiliser Py_ssize_t pour les arguments de type taille/nombre de l’API C »
  • Cela concerne principalement l’analyse des arguments et les API de tampon
  • Il ne s’agit pas d’un indicateur d’exécution, mais simplement d’une macro qui modifie le contrat de l’API C lors de la compilation

Si vous le souhaitez, je peux également vous indiquer précisément quelles fonctions de l’API C de CPython sont concernées et comment les utiliser Py_ssize_t correctement dans un module d’extension.


Py_ssize_t n’est pas défini en fonction de PY_SSIZE_T_CLEAN. Il est défini dans Include/pyport.h. La macro PY_SSIZE_T_CLEAN modifie plutôt la fonction d’analyse syntaxique utilisée, et donc le type attendu pour les longueurs des formats # comme s# ou y#.huggingface

Définition de Py_ssize_t

Dans Include/pyport.h :

ctypedef ssize_t Py_ssize_t;

lorsque la plateforme fournit ssize_t. Sinon, CPython choisit un type entier signé de même taille que size_t :

csizeof(Py_ssize_t) == sizeof(size_t)

Donc :

textPy_ssize_t : toujours défini dans Include/pyport.h
PY_SSIZE_T_CLEAN : contrôle le comportement des formats "#"

Où intervient PY_SSIZE_T_CLEAN

La macro est utilisée dans Include/modsupport.h. Elle redirige les fonctions d’analyse vers leurs variantes _SizeT :

c#ifdef PY_SSIZE_T_CLEAN
#define PyArg_Parse                 _PyArg_Parse_SizeT
#define PyArg_ParseTuple            _PyArg_ParseTuple_SizeT
#define PyArg_ParseTupleAndKeywords _PyArg_ParseTupleAndKeywords_SizeT
#define Py_VaBuildValue             _Py_VaBuildValue_SizeT
#endif

Ainsi, avec :

c#define PY_SSIZE_T_CLEAN
#include <Python.h>

ce code :

cPyArg_ParseTuple(args, "y#", &data, &length);

appelle en réalité :

c_PyArg_ParseTuple_SizeT(args, "y#", &data, &length);

Effet sur les formats #

Macro définie ?Fonction appeléeType attendu pour s#, y#, etc.
NonPyArg_ParseTupleint
Oui_PyArg_ParseTuple_SizeTPy_ssize_t

huggingface+1

Exemple :

c#define PY_SSIZE_T_CLEAN
#include <Python.h>

const char *data;
Py_ssize_t length;

PyArg_ParseTuple(args, "y#", &data, &length);

Ici, length il doit s'agir d'un Py_ssize_t.cmi.ac+1

Où est implémenté le comportement

L’implémentation se trouve dans :

textPython/getargs.c

notamment dans les fonctions de conversion telles que convertsimple(). C’est là que CPython vérifie, pour les anciennes versions, que la macro a bien été définie avant d’accepter un format # :

c#define REQUIRE_PY_SSIZE_T_CLEAN 
if (!(flags & FLAG_SIZE_T)) { 
    PyErr_SetString(PyExc_SystemError, 
        "PY_SSIZE_T_CLEAN macro must be defined for '#' formats"); 
    return NULL; 
}

Résumé des fichiers

Fichier CPythonRôle
Include/pyport.hDéfinit Py_ssize_t, PY_SSIZE_T_MAX, PY_SSIZE_T_MIN
Include/Python.hInclut pyport.h, modsupport.h, etc.
Include/modsupport.hUtilise PY_SSIZE_T_CLEAN pour rediriger vers les variantes _SizeT
Python/getargs.cImplémente l'analyse et le traitement des formats #
Python/modsupport.cImplémente notamment Py_BuildValue()

huggingface+1

Depuis Python 3.13

Depuis Python 3.13, PY_SSIZE_T_CLEAN n’est plus nécessaire pour les formats # : Py_ssize_t est désormais le comportement par défaut. La macro reste recommandée dans le code source pour des raisons de compatibilité avec les versions antérieures.github

En un mot

Py_ssize_t est défini dans Include/pyport.h indépendamment de PY_SSIZE_T_CLEAN ; cette macro, traitée dans Include/modsupport.h et Python/getargs.c, sélectionne les variantes _SizeT de PyArg_Parse*() afin que les formats # attendent un Py_ssize_t plutôt qu’un int.

Code C
#define PY_SSIZE_T_CLEAN
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string.h>

/* ------------------------------------------------------------------ */
/* 1. État du module                                                   */
/* ------------------------------------------------------------------ */

typedef struct {
    long counter;
    char* message;
} module_state;

/* ------------------------------------------------------------------ */
/* 2. Fonctions exposées à Python                                      */
/* ------------------------------------------------------------------ */

static PyObject*
add(PyObject* self, PyObject* args)
{
    long a, b;

    if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
        return NULL;
    }

    return PyLong_FromLong(a + b);
}

static PyObject*
increment_counter(PyObject* self, PyObject* args)
{
    module_state* state;

    if (!PyArg_ParseTuple(args, "")) {
        return NULL;
    }

    state = (module_state*)PyModule_GetState(self);

    if (state == NULL) {
        return NULL;
    }

    state->counter++;

    return PyLong_FromLong(state->counter);
}

static PyObject*
get_counter(PyObject* self, PyObject* args)
{
    module_state* state;

    if (!PyArg_ParseTuple(args, "")) {
        return NULL;
    }

    state = (module_state*)PyModule_GetState(self);

    if (state == NULL) {
        return NULL;
    }

    return PyLong_FromLong(state->counter);
}

static PyObject*
set_message(PyObject* self, PyObject* args)
{
    const char* text;
    module_state* state;
    char* copy;

    if (!PyArg_ParseTuple(args, "s", &text)) {
        return NULL;
    }

    state = (module_state*)PyModule_GetState(self);

    if (state == NULL) {
        return NULL;
    }

    copy = PyMem_Malloc(strlen(text) + 1);

    if (copy == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    strcpy(copy, text);

    PyMem_Free(state->message);
    state->message = copy;

    Py_RETURN_NONE;
}

static PyObject*
get_message(PyObject* self, PyObject* args)
{
    module_state* state;

    if (!PyArg_ParseTuple(args, "")) {
        return NULL;
    }

    state = (module_state*)PyModule_GetState(self);

    if (state == NULL) {
        return NULL;
    }

    if (state->message == NULL) {
        Py_RETURN_NONE;
    }

    return PyUnicode_FromString(state->message);
}

static PyObject*
divide(PyObject* self, PyObject* args)
{
    long a, b;

    if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
        return NULL;
    }

    if (b == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
        return NULL;
    }

    return PyFloat_FromDouble((double)a / (double)b);
}

/* ------------------------------------------------------------------ */
/* 3. Table des méthodes                                               */
/* ------------------------------------------------------------------ */

static PyMethodDef mymodule_methods[] = {
    {"add", add, METH_VARARGS,
     "add(a, b) -> intnnReturn the sum of two integers."},

    {"divide", divide, METH_VARARGS,
     "divide(a, b) -> floatnnReturn a divided by b."},

    {"increment_counter", increment_counter, METH_VARARGS,
     "increment_counter() -> intnnIncrement and return the module counter."},

    {"get_counter", get_counter, METH_VARARGS,
     "get_counter() -> intnnReturn the current module counter."},

    {"set_message", set_message, METH_VARARGS,
     "set_message(text) -> NonennStore a message in the module state."},

    {"get_message", get_message, METH_VARARGS,
     "get_message() -> str or NonennReturn the stored message."},

    {NULL, NULL, 0, NULL}
};

/* ------------------------------------------------------------------ */
/* 4. Slot Py_mod_exec : initialisation du module                      */
/* ------------------------------------------------------------------ */

static int
mymodule_exec(PyObject* module)
{
    module_state* state;
    PyObject* my_error;

    /* Récupère l’état alloué par CPython grâce à m_size. */
    state = (module_state*)PyModule_GetState(module);

    if (state == NULL) {
        return -1;
    }

    /* Initialisation explicite de l’état. */
    state->counter = 0;
    state->message = NULL;

    /* Ajout des fonctions. */
    if (PyModule_AddFunctions(module, mymodule_methods) < 0) {
        return -1;
    }

    /* Ajout d’une constante. */
    if (PyModule_AddIntConstant(module, "VERSION", 1) < 0) {
        return -1;
    }

    /* Création et ajout d’une exception personnalisée. */
    my_error = PyErr_NewException("mymodule.MyError", NULL, NULL);

    if (my_error == NULL) {
        return -1;
    }

    if (PyModule_AddObjectRef(module, "MyError", my_error) < 0) {
        Py_DECREF(my_error);
        return -1;
    }

    Py_DECREF(my_error);

    return 0;
}

/* ------------------------------------------------------------------ */
/* 5. Libération de l’état                                             */
/* ------------------------------------------------------------------ */

static void
mymodule_free(void* raw_state)
{
    module_state* state = (module_state*)raw_state;

    if (state != NULL) {
        PyMem_Free(state->message);
        state->message = NULL;
    }
}

/* ------------------------------------------------------------------ */
/* 6. Définition multi-phase du module                                 */
/* ------------------------------------------------------------------ */

static struct PyModuleDef mymodule_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "mymodule",
    .m_doc = "Exemple complet de module Python en C avec initialisation multi-phase.",
    .m_size = sizeof(module_state),
    .m_methods = mymodule_methods,
    .m_slots = (PyModuleDef_Slot[]) {
        {Py_mod_exec, mymodule_exec},
        {0, NULL}
    },
    .m_free = mymodule_free
};

/* ------------------------------------------------------------------ */
/* 7. Point d’entrée obligatoire                                       */
/* ------------------------------------------------------------------ */

PyMODINIT_FUNC
PyInit_mymodule(void)
{
    return PyModuleDef_Init(&mymodule_module);
}</kbd>