Seu primeiro módulo de extensão da API C

Este tutorial guiará você pela criação de um módulo de extensão simples para Python, escrito em C ou C++.

Utilizaremos diretamente a API C do Python de baixo nível. Para formas mais simples de criar módulos de extensão, consulte as ferramentas de terceiros recomendadas.

O tutorial pressupõe conhecimentos básicos de Python: você deve ser capaz de definir funções em código Python antes de começar a escrevê-las em C. Consulte O tutorial do Python para uma introdução ao próprio Python.

O tutorial deve ser acessível a qualquer pessoa capaz de escrever uma biblioteca básica em C. Embora mencionemos vários conceitos que não se espera que um iniciante em C conheça — como funções static ou declarações de vinculação (linkage) —, compreendê-los não é necessário para obter êxito.

Nosso foco será proporcionar a você uma noção de como é a API C do Python. Isso não ensinará conceitos importantes, como tratamento de erros e contagem de referências, que são abordados em capítulos posteriores.

Vamos supor que você utilize um sistema do tipo Unix (incluindo macOS e Linux) ou Windows. Em outros sistemas, talvez seja necessário ajustar alguns detalhes — por exemplo, o nome de um comando do sistema.

Você precisa ter um compilador C adequado e os cabeçalhos de desenvolvimento do Python instalados. No Linux, esses cabeçalhos geralmente estão em um pacote como python3-dev ou python3-devel.

Você precisa ser capaz de instalar pacotes Python. Este tutorial utiliza o pip <https://pip.pypa.io/>`__ (pip install), mas você pode substituí-lo por qualquer ferramenta capaz de realizar a construção e a instalação de projetos baseados em pyproject.toml, como uv (uv setup.py install). De preferência, mantenha um ambiente virtual ativado.

Nota

Este tutorial utiliza APIs que foram adicionadas no CPython 3.15. Para criar uma extensão compatível com versões anteriores do CPython, consulte uma versão anterior desta documentação.

Este tutorial utiliza sintaxe de C introduzida no C11 e no C++20. Se a sua extensão precisar ser compatível com padrões anteriores, consulte os tutoriais na documentação para o Python 3.14 ou versões anteriores.

O que faremos

Vamos criar um módulo de extensão chamado spam [1], que incluirá uma interface Python para a função system() da biblioteca padrão C. Essa função é definida em stdlib.h. Ela recebe uma string C como argumento, executa o argumento como um comando do sistema e retorna um valor de resultado como um inteiro. Uma página de manual para system() poderia resumi-la da seguinte forma:

#include <stdlib.h>
int system(const char *command);

Observe que, assim como muitas funções da biblioteca padrão da linguagem C, esta função já está disponível no Python. Em ambientes de produção, utilize os.system() ou subprocess.run() em vez do módulo que você escreverá aqui.

Queremos que esta função seja chamável a partir do Python da seguinte forma:

>>> import spam
>>> status = spam.system("whoami")
Nome do usuário
>>> status
0

Nota

O comando de sistema whoami exibe o seu nome de usuário. Ele é útil em tutoriais como este porque possui o mesmo nome tanto no Unix quanto no Windows.

Comece com os cabeçalhos

Comece criando um diretório para este tutorial e acessando-o na linha de comando. Em seguida, crie um arquivo chamado spammodule.c no seu diretório. [2]

Neste arquivo, incluiremos dois cabeçalhos: Python.h, para importar todas as declarações da API C do Python, e stdlib.h, para a função system(). [3]

Adicione as seguintes linhas a spammodule.c:

#include <Python.h>
#include <stdlib.h>     // para system()

Certifique-se de colocar stdlib.h — e quaisquer outras inclusões à biblioteca padrão — após Python.h. Em alguns sistemas, o Python pode definir diretivas de pré-processador que afetam os cabeçalhos padrão.

Executando sua ferramenta de construção

Com apenas as diretivas de inclusões (includes) implementadas, sua extensão não fará nada. Ainda assim, é um bom momento para compilá-la e tentar importá-la. Isso garantirá que sua ferramenta de construção esteja funcionando, permitindo que você faça e teste alterações incrementais à medida que acompanha o restante do texto.

O próprio CPython não inclui uma ferramenta para construir módulos de extensão; recomenda-se utilizar um projeto de terceiros para essa finalidade. Neste tutorial, utilizaremos o meson-python. (Caso queira utilizar outra ferramenta, consulte Appendix: Other build tools.)

O meson-python requer a definição de um “projeto” utilizando dois arquivos extras.

Primeiro, adicione um arquivo pyproject.toml com o seguinte conteúdo:

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[project]
# Placeholders de informações de projeto
# (altere isso antes de distribuir o módulo)
name = 'projetoexemplo'
version = '0'

Então, cria meson.build contendo o seguinte:

project('projetoexemplo', 'c')

py = import('python').find_installation(pure: false)

py.extension_module(
   'spam',          # nome do módulo Python importável
   'spammodule.c',  # o arquivo-fonte C
   install: true,
)

Nota

Consulte a documentação do meson-python para detalhes sobre a configuração.

Agora, construa e instale o projeto no diretório atual (.) via pip:

python -m pip -v install .

A opção -v (--verbose) faz com que o pip mostre a saída do compilador, o que é frequentemente útil durante o desenvolvimento.

Dica

Se você não tiver o pip instalado, execute python -m ensurepip, de preferência em um ambiente virtual. (Ou, se preferir outra ferramenta capaz de construir e instalar projetos baseados em pyproject.toml, utilize-a.)

Observe que precisará de executar este comando novamente sempre que alterar a sua extensão. Ao contrário do Python, o C tem um passo de compilação explícito.

Quando a sua extensão estiver compilada e instalada, inicie o Python e tente importá-la. Isto deverá falhar com a seguinte exceção:

>>> import spam
Traceback (most recent call last):
   ...
ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)

Gancho de exportação de módulo

A exceção que você recebeu ao tentar importar o módulo indicava que o Python está procurando uma “função de exportação de módulo”, também conhecida como gancho de exportação de módulo. Vamos definir uma.

Primeiro, adicione um protótipo embaixo das linhas #include:


PyMODEXPORT_FUNC PyModExport_spam(void);

Dica

O protótipo não é estritamente necessário, mas alguns compiladores modernos emitem avisos se ele não estiver presente. Geralmente, é melhor adicionar o protótipo do que desativar o aviso.

A macro PyMODEXPORT_FUNC declara o tipo de retorno da função e adiciona quaisquer declarações de vinculação especiais necessárias para tornar a função visível e utilizável quando o CPython a carrega.

Após o protótipo, adicione a função em si. Por enquanto, faça com que ela retorne NULL:

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return NULL;
}

Compile e carregue o módulo novamente. Você deverá receber um erro diferente desta vez.

>>> import spam
Traceback (most recent call last):
   ...
SystemError: module export hook for module 'spam' failed without setting an exception

Simplesmente retornar NULL não é o comportamento correto para um gancho de exportação, e o CPython reclama disso. Isso é bom — significa que o CPython encontrou a função! Agora, vamos fazer com que ela realize alguma tarefa útil.

A tabela de slots

Em vez de NULL, o gancho de exportação deve retornar as informações necessárias para criar um módulo. Vamos começar pelo básico: o nome e a docstring.

As informações devem ser definidas em um vetor de entradas PySlot, que são essencialmente pares chave-valor. Defina esse array logo antes do seu gancho de exportação:

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
   PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
   PySlot_STATIC_DATA(Py_mod_name, "spam"),
   PySlot_STATIC_DATA(Py_mod_doc, "Um módulo maravilhoso com um exemplo de função"),
   PySlot_END
};

A macro PySlot_STATIC_DATA é usada quando o valor do slot (aqui: &abi_info, "spam" e a docstring) é um ponteiro para dados constantes alocados estaticamente.

A macro PyABIInfo_VAR(abi_info); e o slot Py_mod_abi constituem um código padrão que ajuda a evitar que extensões compiladas para uma versão diferente do Python causem a falha do interpretador.

Para ambos Py_mod_name e Py_mod_doc, os valores são strings C — isto é, vetores de bytes terminados em NUL e codificados em UTF-8.

Observe a entrada sentinela PySlot_END ao final. Ela marca o fim do vetor. Se você esquecê-la, provocará um comportamento indefinido.

The array is defined as static – that is, not visible outside this .c file. This will be a common theme. CPython only needs to access the export hook; all global variables and all other functions should generally be static, so that they don’t clash with other extensions.

Return this array from your export hook instead of NULL:

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

Now, recompile and try it out:

>>> import spam
>>> print(spam)
<module 'spam' from '/home/encukou/dev/cpython/spam.so'>

You have an extension module! Try help(spam) to see the docstring.

The next step will be adding a function.

Exposing a function

To expose the system() C function directly to Python, we’ll need to write a layer of glue code to convert arguments from Python objects to C values, and the C return value back to Python.

One of the simplest ways to write glue code is a “METH_O” function, which takes two Python objects and returns one. All Python objects – regardless of the Python type – are represented in C as pointers to the PyObject structure.

Add such a function above the slots array:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   Py_RETURN_NONE;
}

For now, we ignore the arguments, and use the Py_RETURN_NONE macro, which expands to a return statement that properly returns a Python None object.

Recompile your extension to make sure you don’t have syntax errors. We haven’t yet added spam_system to the module, so you might get a warning that spam_system is unused.

Method definitions

To expose the C function to Python, you will need to provide several pieces of information in a structure called PyMethodDef [4]:

  • ml_name: the name of the Python function;

  • ml_doc: a docstring;

  • ml_meth: the C function to be called; and

  • ml_flags: a set of flags describing details like how Python arguments are passed to the C function. We’ll use METH_O here – the flag that matches our spam_system function’s signature.

Because modules typically create several functions, these definitions need to be collected in an array, with a zero-filled sentinel at the end. Add this array just below the spam_system function:


static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="Execute a shell command.",
    },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

As with module slots, a zero-filled sentinel marks the end of the array.

Next, we’ll add the method to the module. Add a Py_mod_methods slot to your PyMethodDef array:


PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
    PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
    PySlot_END
};

Recompile your extension again, and test it. Be sure to restart the Python interpreter, so that import spam picks up the new version of the module.

You should now be able to call the function:

>>> import spam
>>> print(spam.system)
<built-in function system>
>>> print(spam.system('whoami'))
None

Note that our spam.system does not yet run the whoami command; it only returns None.

Check that the function accepts exactly one argument, as specified by the METH_O flag:

>>> print(spam.system('too', 'many', 'arguments'))
Traceback (most recent call last):
   ...
TypeError: spam.system() takes exactly one argument (3 given)

Returning an integer

Now, let’s take a look at the return value. Instead of None, we’ll want spam.system to return a number – that is, a Python int object. Eventually this will be the exit code of a system command, but let’s start with a fixed value, say, 3.

The Python C API provides a function to create a Python int object from a C int value: PyLong_FromLong(). [5]

To call it, replace the Py_RETURN_NONE with the following 3 lines:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

Recompile, restart the Python interpreter again, and check that the function now returns 3:

>>> import spam
>>> spam.system('whoami')
3

Accepting a string

Finally, let’s handle the function argument.

Our C function, spam_system(), takes two arguments. The first one, PyObject *self, will be set to the spam module object. This isn’t useful in our case, so we’ll ignore it.

The other one, PyObject *arg, will be set to the object that the user passed from Python. We expect that it should be a Python string. In order to use the information in it, we will need to convert it to a C value – in this case, a C string (const char *).

There’s a slight type mismatch here: Python’s str objects store Unicode text, but C strings are arrays of bytes. So, we’ll need to encode the data, and we’ll use the UTF-8 encoding for it. (UTF-8 might not always be correct for system commands, but it’s what str.encode() uses by default, and the C API has special support for it.)

The function to encode a Python string into a UTF-8 buffer is named PyUnicode_AsUTF8AndSize() [6]. Call it like this:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

If PyUnicode_AsUTF8AndSize() is successful, command will point to the resulting C string – a zero-terminated array of bytes [7]. This buffer is managed by the arg object, which means we don’t need to free it, but we must follow some rules:

  • We should only use the buffer inside the spam_system function. After spam_system returns, arg and the buffer it manages might be garbage-collected.

  • We must not modify it. This is why we use const.

If PyUnicode_AsUTF8AndSize() was not successful, it returns a NULL pointer. When calling any Python C API, we always need to handle such error cases. The way to do this in general is left for later chapters of this documentation. For now, be assured that we are already handling errors from PyLong_FromLong() correctly.

For the PyUnicode_AsUTF8AndSize() call, the correct way to handle errors is returning NULL from spam_system. Add an if block for this:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg);
   if (command == NULL) {
      return NULL;
   }
   int status = 3;
   PyObject *result = PyLong_FromLong(status);
   return result;
}

To test that error handling works, compile again, restart Python so that import spam picks up the new version of your module, and try passing a non-string value to your function:

>>> import spam
>>> spam.system(3)
Traceback (most recent call last):
   ...
TypeError: bad argument type for built-in operation

Now, all that is left is calling the C library function system() with the char * buffer, and using its result instead of the 3:

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg);
   if (command == NULL) {
      return NULL;
   }
   int status = system(command);
   PyObject *result = PyLong_FromLong(status);
   return result;
}

Compile your module, restart Python, and test. This time, you should see your username – the output of the whoami system command:

>>> import spam
>>> result = spam.system('whoami')
User Name
>>> result
0

You can also test with other commands, like ls, dir, or one that doesn’t exist:

>>> import spam
>>> result = spam.system('nonexistent-command')
sh: line 1: nonexistent-command: command not found
>>> result
32512

The result

Congratulations! You have written a complete Python C API extension module, and completed this tutorial!

Here is the entire source file, for your convenience:

/// Includes

#include <Python.h>
#include <stdlib.h>     // for system()

/// Implementation of spam.system

static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
   const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
   if (command == NULL) {
      return NULL;
   }
   int status = system(command);
   PyObject *result = PyLong_FromLong(status);
   return result;
}

/// Module method table

static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="Execute a shell command.",
    },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

/// Module slot table

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
    PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
    PySlot_END
};

/// Export hook prototype

PyMODEXPORT_FUNC PyModExport_spam(void);

/// Module export hook

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

Appendix: Other build tools

You should be able to follow this tutorial – except the Running your build tool section itself – with a build tool other than meson-python.

The Python Packaging User Guide has a list of recommended tools; be sure to choose one for the C language.

Workaround for missing PyInit function

If your build tool output complains about missing PyInit_spam, add the following function to your module for now:

// A workaround
void *PyInit_spam(void) { return NULL; }

This is a shim for an old-style initialization function, which was required in extension modules for CPython 3.14 and below. Current CPython does not need it, but some build tools may still assume that all extension modules need to define it.

If you use this workaround, you will get the exception SystemError: initialization of spam failed without raising an exception instead of ImportError: dynamic module does not define module export function.

Compiling directly

Using a third-party build tool is heavily recommended, as it will take care of various details of your platform and Python installation, of naming the resulting extension, and, later, of distributing your work.

If you are building an extension for as specific system, or for yourself only, you might instead want to run your compiler directly. The way to do this is system-specific; be prepared for issues you will need to solve yourself.

Linux

On Linux, the Python development package may include a python3-config command that prints out the required compiler flags. If you use it, check that it corresponds to the CPython interpreter you’ll use to load the module. Then, start with the following command:

gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so

This should generate a spam.so file that you need to put in a directory on sys.path.

Notas de rodapé