Empacotando extensões binárias#

Status da página:

Incompleta

Última revisão:

2013-12-08

Uma das características do interpretador de referência CPython é que, além de permitir a execução do código Python, também expõe uma API C rica para uso por outro software. Um dos usos mais comuns dessa API C é criar extensões C importáveis que permitem coisas que nem sempre são fáceis de alcançar em código puro Python.

Uma visão geral de extensões binárias#

Casos de uso#

Os casos de uso típicos para extensões binárias se dividem em apenas três categorias convencionais:

  • Módulos aceleradores: esses módulos são completamente autocontidos e são criados exclusivamente para executar mais rápido do que o código puro Python equivalente é executado em CPython. Idealmente, os módulos aceleradores sempre terão um Python equivalente puro para usar como fallback se a versão acelerada não estiver disponível em um determinado sistema. A biblioteca padrão do CPython faz uso extensivo de módulos aceleradores. Exemplo: Ao importar datetime, Python recorre ao módulo datetime.py se a implementação C ( _datetimemodule.c) não estiver disponível.

  • módulos wrapper: esses módulos são criados para expor as interfaces C existentes ao código Python. Eles podem expor a interface C subjacente diretamente ou então expor uma API mais “Pythônica” que faz uso dos recursos da linguagem Python para tornar a API mais fácil de usar. A biblioteca padrão do CPython faz uso extensivo de módulos wrapper. Exemplo: functools.py é um módulo Python wrapper para _functoolsmodule.c.

  • acesso de baixo nível ao sistema: esses módulos são criados para acessar os recursos de baixo nível do tempo de execução do CPython, do sistema operacional ou do hardware subjacente. Por meio do código específico da plataforma, os módulos de extensão podem alcançar coisas que não são possíveis no código puro Python. Vários módulos de biblioteca padrão do CPython são escritos em C para acessar as partes internas do interpretador que não são expostas no nível da linguagem. Exemplo: sys, que vem de sysmodule.c.

    Um recurso particularmente notável das extensões C é que, quando não precisam chamar de volta para o tempo de execução do interpretador, podem liberar o bloqueio do interpretador global do CPython em torno de operações de longa duração (independentemente de essas operações serem vinculados à CPU ou à E/S).

Nem todos os módulos de extensão se encaixam perfeitamente nas categorias acima. Os módulos de extensão incluídos com NumPy, por exemplo, abrangem todos os três casos de uso – eles movem loops internos para C por motivos de velocidade, agrupam bibliotecas externas escritas em C, FORTRAN e outras linguagens e usam interfaces de sistema de baixo nível para CPython e o sistema operacional subjacente para oferecer suporte à execução simultânea de operações vetorizadas e para controlar rigidamente o layout de memória exato dos objetos criados.

Desvantagens#

A principal desvantagem de usar extensões binárias é o fato de tornar a distribuição subsequente do software mais difícil. Uma das vantagens de usar o Python é que ele é amplamente multiplataforma, e as linguagens usadas para escrever módulos de extensão (normalmente C ou C++, mas na verdade qualquer linguagem que possa se ligar à API C do CPython) normalmente exigem que binários personalizados podem ser criados para plataformas diferentes.

Isso significa que extensões binárias:

  • exigem que os usuários finais sejam capazes de criá-las a partir do código-fonte, ou então que alguém publique binários pré-construídos para plataformas comuns

  • pode não ser compatível com diferentes compilações do interpretador de referência do CPython

  • frequentemente não funciona corretamente com interpretadores alternativos como PyPy, Iron Python ou Jython

  • se codificado manualmente, torna a manutenção mais difícil, exigindo que os mantenedores estejam familiarizados não apenas com Python, mas também com a linguagem usada para criar a extensão binária, bem como com os detalhes da API C do CPython.

  • se uma implementação de fallback de puro Python for fornecida, torna a manutenção mais difícil, exigindo que as alterações sejam implementadas em dois lugares, e introduzindo complexidade adicional no conjunto de testes para garantir que ambas as versões sejam sempre executadas.

Outra desvantagem de depender de extensões binárias é que mecanismos de importação alternativos (como a capacidade de importar módulos diretamente de arquivos zip) geralmente não funcionam para módulos de extensão (já que os mecanismos de carregamento dinâmico na maioria das plataformas só podem carregar bibliotecas do disco).

Alternativas para módulos aceleradores codificados manualmente#

Quando os módulos de extensão estão apenas sendo usados para tornar o código executado mais rápido (após a criação de perfil ter identificado o código onde o aumento de velocidade vale um esforço de manutenção adicional), uma série de outras alternativas também devem ser consideradas:

  • procure alternativas otimizadas existentes. A biblioteca padrão do CPython inclui uma série de estruturas de dados e algoritmos otimizados (especialmente nos módulos embutidos e collections e itertools). O índice de Pacotes do Python também oferece alternativas adicionais. Às vezes, a escolha apropriada de biblioteca padrão ou módulo de terceiros pode evitar a necessidade de criar seu próprio módulo acelerador.

  • para aplicações de longa execução, o interpretador PyPy compilado por JIT pode oferecer uma alternativa adequada ao tempo de execução padrão do CPython. A principal barreira para a adoção do PyPy é normalmente a dependência de outros módulos de extensão binários – enquanto o PyPy emula a API C do CPython, os módulos que dependem disso causam problemas para o PyPy de JIT e a camada de emulação pode frequentemente expor defeitos latentes na extensão módulos que o CPython atualmente tolera (frequentemente em torno de erros de contagem de referências – um objeto com uma referência ativa em vez de duas geralmente não quebrará nada, mas nenhuma referência em vez de uma é um grande problema).

  • Cython é um compilador estático maduro que pode compilar a maioria dos códigos Python para módulos de extensão C. A compilação inicial fornece alguns aumentos de velocidade (contornando a camada de interpretador do CPython) e os recursos opcionais de tipagem estática do Cython podem oferecer oportunidades adicionais para aumentos de velocidade. O uso do Cython ainda carrega as desvantagens associadas ao uso de extensões binárias, mas tem o benefício de ter uma barreira de entrada reduzida para programadores Python (em relação a outras linguagens como C ou C++).

  • Numba é uma ferramenta mais recente, criada por membros da comunidade científica do Python, que visa alavancar o LLVM para permitir a compilação seletiva de partes de uma aplicação Python para código de máquina nativo em tempo de execução. Requer que o LLVM esteja disponível no sistema onde o código está sendo executado, mas pode fornecer aumentos de velocidade significativos, especialmente para operações que são passíveis de vetorização.

Alternativas para módulos wrapper codificados manualmente#

A ABI C (Interface Binária de Aplicação) é um padrão comum para compartilhamento de funcionalidade entre várias aplicações. Um dos pontos fortes da API C do CPython (Interface de Programação de Aplicação) é permitir que os usuários do Python aproveitem essa funcionalidade. No entanto, empacotar módulos manualmente é muito tedioso, portanto, várias outras abordagens alternativas devem ser consideradas.

As abordagens descritas abaixo não simplificam o caso de distribuição, mas podem reduzir significativamente a carga de manutenção de manter os módulos wrapper atualizados.

  • Além de ser útil para a criação de módulos aceleradores, o Cython também é amplamente utilizado para criar módulos wrapper para APIs C ou C++. Envolve o encapsulamento manual das interfaces, o que oferece uma ampla gama de liberdade no design e otimização do código do encapsulamento, mas pode não ser uma boa opção para encapsular APIs muito grandes rapidamente. Veja a lista de ferramentas de terceiros para encapsulamento automático com Cython. Ele também oferece suporte a implementações Python orientadas para o desempenho que fornecem uma API C semelhante a CPython, como PyPy e Pyston.

  • pybind11 é uma biblioteca C++11 pura que fornece uma interface C++ limpa para a API C do CPython (e PyPy). Não requer uma etapa de pré-processamento; ele é escrito inteiramente em C++ modelado. Auxiliares são incluídos para construções de Setuptools ou CMake. Foi baseado em Boost.Python, mas não requer as bibliotecas Boost ou BJam.

  • cffi é um projeto criado por alguns dos desenvolvedores PyPy para torná-lo simples para desenvolvedores que já conhecem Python e C expor seus módulos C a aplicações Python. Também torna relativamente simples fazer um wrapper de um módulo C com base em seus arquivos de cabeçalho, mesmo que você não conheça C.

    Uma das principais vantagens do cffi é que ele é compatível com o PyPy de JIT, permitindo que os módulos wrapper de CFFI participem totalmente das otimizações JIT de rastreamento do PyPy.

  • SWIG é um wrapper gerador de interface que permite uma variedade de linguagens de programação, incluindo Python, para fazer interface com código C e C++.

  • O módulo ctypes da biblioteca padrão, embora útil para obter acesso a interfaces de nível C quando as informações do cabeçalho não estão disponíveis, sofre do fato de operar exclusivamente no nível ABI do C e, portanto, não tem verificação automática de consistência entre os interface realmente sendo exportada pela biblioteca e aquela declarada no código Python. Em contraste, as alternativas acima são todas capazes de operar no nível API do C, usando arquivos de cabeçalho C para garantir consistência entre a interface exportada pela biblioteca sendo empacotada e aquela esperada pelo módulo empacotador Python. Enquanto cffi pode operar diretamente no nível ABI do C, ele sofre dos mesmos problemas de inconsistência de interface que ctypes quando é usado dessa forma.

Alternativas para acesso de baixo nível ao sistema#

Para aplicações que precisam de acesso de baixo nível ao sistema (independentemente do motivo), um módulo de extensão binária geralmente é a melhor maneira de fazer isso. Isso é particularmente verdadeiro para o acesso de baixo nível ao próprio tempo de execução do CPython, uma vez que algumas operações (como liberar o Bloqueio Global do Interpretador) são simplesmente inválidas quando o interpretador está executando o código, mesmo se um módulo como ctypes ou cffi é usado para obter acesso às interfaces API C relevantes.

Para casos em que o módulo de extensão está manipulando o sistema operacional ou hardware subjacente (em vez do tempo de execução do CPython), às vezes pode ser melhor apenas escrever uma biblioteca C comum (ou uma biblioteca em outra linguagem de programação de sistemas como C++ ou Rust que pode exportar uma ABI compatível com C) e, em seguida, usar uma das técnicas de agrupamento descritas acima para tornar a interface disponível como um módulo Python importável.

Implementando extensões binárias#

O guia Estendendo e Incorporando do CPython inclui uma introdução a escrever um módulo de extensão personalizado no C.

CORRIGIR: Elaborar que tudo isso é uma das razões pelas quais você provavelmente não deseja codificar manualmente seus módulos de extensão :)

Ciclo de vida dos módulos de extensão#

CORRIGIR: Esta seção precisa ser mais detalhada.

Implicações do estado estático compartilhado e subinterpretadores#

CORRIGIR: Esta seção precisa ser mais detalhada.

Implicações do GIL#

CORRIGIR: Esta seção precisa ser mais detalhada.

APIs de alocação de memória#

CORRIGIR: Esta seção precisa ser mais detalhada.

Compatibilidade da ABI#

A API C do CPython não garante a estabilidade da ABI entre versões secundárias (3.2, 3.3, 3.4, etc.). Isso significa que, normalmente, se você construir um módulo de extensão em uma versão do Python, é garantido que ele funcionará apenas com a mesma versão secundária do Python e não com quaisquer outras versões secundárias.

Python 3.2 introduziu a API Limitada (“Limited API”), que é um subconjunto bem definido da API C do Python. Os símbolos necessários para a API Limitada formam a “ABI estável”, que é garantidamente compatível com todas as versões do Python 3.x. Wheels contendo extensões construídas na ABI estável usam a tag de ABI abi3, para refletir que são compatíveis com todas as versões do Python 3.x.

A página Estabilidade da API C do CPython fornece informações detalhadas sobre as garantias de estabilidade da API/ABI, como usar a API Limitada e o conteúdo exato da “API Limitada”.

Construindo extensões binárias#

CORRIGIR: Cobrir os backends de construção disponíveis para construir extensões.

Construindo extensões para várias plataformas#

Se você planeja distribuir sua extensão, você deve fornecer wheels para todas as plataformas que pretende oferecer suporte. Geralmente são construídos em sistemas de integração contínua (CI). Existem ferramentas para lhe ajudar a construir binários altamente redistribuíveis de CI; estes incluem cibuildwheel e multibuild.

Para a maioria das extensões, você precisará construir wheels para todas as plataformas que pretende suportar. Isso significa que o número de wheels que você precisa construir é o produto de:

count(Python minor versions) * count(OS) * count(architectures)

Usar a ABI Estável do CPython pode ajudar a reduzir significativamente o número de wheels que você precisa fornecer, já que um único wheel em uma plataforma pode ser usada com todas as versões secundárias do Python; eliminando uma dimensão da matriz. Também elimina a necessidade de gerar novos wheels para cada nova versão secundária do Python.

Extensões binárias para Windows#

Antes que seja possível construir uma extensão binária, é necessário garantir que você tenha um compilador adequado disponível. No Windows, o Visual C é usado para construir o interpretador oficial do CPython e deve ser usado para construir extensões binárias compatíveis. Para configurar um ambiente de construção para extensões binárias, instale o Visual Studio Community Edition – qualquer versão recente já serve.

Uma ressalva: se você usar o Visual Studio 2019 ou posterior, sua extensão dependerá de um arquivo “extra”, VCRUNTIME140_1.dll, além do VCRUNTIME140.dll que todas as versões anteriores de 2015 dependem sobre. Isso adicionará um requisito extra ao uso de sua extensão em versões do CPython que não incluem este arquivo extra. Para evitar isso, você pode adicionar o argumento de tempo de compilação /d2FH4-. Versões recentes do Python podem incluir este arquivo.

Não é recomendável construir para Python antes de 3.5, porque as versões anteriores do Visual Studio não estão mais disponíveis na Microsoft. Se você precisa construir para versões mais antigas, você pode definir DISTUTILS_USE_SDK=1 and MSSdk=1 para forçar uma versão atualmente ativada do MSVC a ser encontrada, e você deve ter cuidado ao projetar sua extensão não alocar/liberar memória em bibliotecas diferentes, evitar depender de estruturas de dados alteradas e assim por diante. Ferramentas para gerar módulos de extensão geralmente evitam essas coisas para você.

Extensões binárias para Linux#

Os binários do Linux devem usar uma glibc suficientemente antiga para serem compatíveis com distribuições mais antigas. As imagens Docker do manylinux fornecem um ambiente de construção com uma glibc antiga o suficiente para oferecer suporte à maioria das distribuições Linux atuais em arquiteturas comuns.

Extensões binárias para macOS#

A compatibilidade binária no macOS é determinada pelo sistema mínimo alvo de implantação, por exemplo, 10.9, que geralmente é especificado com a variável de ambiente MACOSX_DEPLOYMENT_TARGET ao construir binários no macOS. Ao construir com setuptools / distutils, o alvo de implantação é especificado com o sinalizador --plat-name, por exemplo, macosx-10.9-x86_64. Para alvos de implantação comuns para distribuições do Python para macOS, consulte a Spinning Wheels na wiki do MacPython.

Publicando extensões binárias#

A publicação de extensões binárias por meio do PyPI usa os mesmos mecanismos de upload que a publicação de pacotes Python puros. Você constrói um arquivo wheel para sua extensão usando o backend de construção e carrega-o no PyPI usando twine.

Evite lançamentos somente binários#

É altamente recomendável que você publique suas extensões binárias, bem como o código-fonte usado para construí-las. Isso permite que os usuários criem a extensão a partir do código-fonte, se necessário. Notavelmente, isso é necessário para certas distribuições Linux que compilam a partir do código-fonte em seus próprios sistemas de compilação para os repositórios de pacotes de distribuição.

Ligação fraca#

CORRIGIR: Esta seção precisa ser mais detalhada.

Recursos adicionais#

O desenvolvimento e distribuição multiplataforma de módulos de extensão é um tópico complexo, portanto, este guia se concentra principalmente em fornecer indicadores para várias ferramentas que automatizam o tratamento dos desafios técnicos subjacentes. Os recursos adicionais nesta seção são destinados a desenvolvedores que procuram entender mais sobre as interfaces binárias subjacentes das quais esses sistemas dependem em tempo de execução.

Geração de wheel multiplataforma com scikit-build#

O pacote scikit-build ajuda a abstrair as operações de construção multiplataforma e fornece recursos adicionais ao criar pacotes de extensões binárias. Documentação adicional também está disponível no tempo de execução C, compilador e gerador de sistema de construção para módulos Python de extensão binária.

Introdução a módulos de extensão C/C++#

Para uma explicação mais detalhada de como os módulos de extensão são usados pelo CPython em um sistema Debian, consulte os seguintes artigos (todos em inglês):