Arquivos .mpy do MicroPython

O MicroPython define o conceito de arquivo .mpy, que é um formato de arquivo container binário contendo código pré-compilado e que pode ser importado como um módulo .py normal. O arquivo foo.mpy pode ser importado via import foo, desde que foo.mpy possa ser encontrado da maneira usual pelo mecanismo de importação. Normalmente, cada diretório listado em sys.path é pesquisado em ordem. Ao pesquisar um diretório específico, foo.py é procurado primeiro e, se não for encontrado, foo.mpy é procurado em seguida; a busca então continua no próximo diretório se nenhum dos dois for encontrado. Dessa forma, foo.py terá precedência sobre foo.mpy.

Esses arquivos .mpy podem conter bytecode, que normalmente é gerado a partir de arquivos-fonte Python (arquivos .py) por meio do programa mpy-cross. Para algumas arquiteturas, um arquivo .mpy também pode conter código de máquina nativo, que pode ser gerado de diversas formas, mais notavelmente a partir de código-fonte C.

O compilador mpy-cross

O mpy-cross é o compilador cruzado que transforma um arquivo-fonte .py em um container binário .mpy pronto para ser importado na cam. Ele faz parte da árvore de código-fonte do MicroPython (a mesma usada para compilar o firmware da cam) e também é publicado como um pacote pip para uso no lado do host sem a necessidade de um checkout completo do firmware:

$ pip install --user mpy-cross

Ou via pipx:

$ pipx install mpy-cross

Uma vez instalado, invoque-o em um único arquivo-fonte:

$ mpy-cross foo.py

Isso produz foo.mpy no diretório atual, pronto para ser copiado para o sistema de arquivos da cam junto com outros módulos ou para ser incorporado a uma imagem ROMFS.

As opções de linha de comando mais úteis:

  • -o <path> – caminho de saída para o .mpy gerado (o padrão é o nome do arquivo de entrada com a extensão substituída; -o - escreve na saída padrão).

  • -O<n> – nível de otimização de 0 a 3. O padrão 0 preserva as asserções e as localizações completas no código-fonte; 3 remove asserções e docstrings e reescreve os blocos if __debug__. O nível controla a mesma interface micropython.opt_level que o runtime expõe.

  • -march=<arch> – arquitetura nativa alvo para funções decoradas com @native e @viper. Obrigatória quando o código-fonte usa esses decoradores. O valor deve corresponder à classe do MCU da cam: escolha-o na lista que mpy-cross --help exibe, ou leia-o na cam em tempo de execução com sys.implementation._mpy.

  • -s <path> – string de caminho do código-fonte embutida nas informações de depuração do .mpy. Útil quando o caminho em disco difere do caminho de importação sob o qual o arquivo deve aparecer nos tracebacks.

  • -X emit=bytecode|native|viper – escolhe o emissor padrão para o módulo inteiro (uma alternativa por módulo aos decoradores @native / @viper aplicados por função).

  • --version – exibe a versão do formato .mpy que este binário emite. Esse número deve corresponder à versão suportada pelo runtime da cam (consulte a tabela de versões abaixo), caso contrário a importação levantará ValueError('incompatible .mpy file').

Execute mpy-cross --help para ver a lista completa de flags.

O pacote pip também expõe uma pequena API de módulo Python para que os scripts de build possam acionar o compilador no próprio processo, em vez de iniciar um subprocesso manualmente:

import mpy_cross

mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
                  march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)

mpy_cross.compile, mpy_cross.run e mpy_cross.mpy_version são os três pontos de entrada; mpy_cross.CrossCompileError carrega o stderr do compilador quando algo dá errado. As constantes de arquitetura (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, etc.) correspondem às strings que a flag -march aceita.

Versionamento e compatibilidade de arquivos .mpy

Um determinado arquivo .mpy pode ou não ser compatível com um determinado sistema MicroPython. A compatibilidade é baseada no seguinte:

  • Versão do arquivo .mpy: a versão do arquivo deve corresponder à versão suportada pelo sistema que o carrega.

  • Subversão do arquivo .mpy: se o arquivo .mpy contiver código de máquina nativo, então a subversão do arquivo deve corresponder à versão suportada pelo sistema que o carrega. Caso contrário, se não houver código de máquina nativo no arquivo .mpy, a subversão é ignorada no carregamento.

  • Bits de inteiro pequeno: o arquivo .mpy exigirá um número mínimo de bits em um small integer, e o sistema que o carrega deve suportar pelo menos essa quantidade de bits.

  • Arquitetura nativa: se o arquivo .mpy contiver código de máquina nativo, então ele especificará a arquitetura desse código de máquina, e o sistema que o carrega deve suportar a execução do código dessa arquitetura.

Se um sistema MicroPython suportar a importação de arquivos .mpy, então o campo sys.implementation._mpy existirá e retornará um inteiro que codifica a versão (8 bits inferiores), as funcionalidades e a arquitetura nativa.

Tentar importar um arquivo .mpy que falhe em um dos quatro primeiros testes levantará ValueError('incompatible .mpy file'). Tentar importar um arquivo .mpy que falhe no teste de arquitetura nativa (se ele contiver código de máquina nativo) levantará ValueError('incompatible .mpy arch').

Se a importação de um arquivo .mpy falhar, tente o seguinte:

  • Determine a versão e as flags do .mpy suportadas pelo seu sistema MicroPython executando:

    import sys
    sys_mpy = sys.implementation._mpy
    arch = [None, 'x86', 'x64',
        'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
        'xtensa', 'xtensawin', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F]
    print('mpy version:', sys_mpy & 0xff)
    print('mpy sub-version:', sys_mpy >> 8 & 3)
    print('mpy flags:', end='')
    if arch:
        print(' -march=' + arch, end='')
    if (sys_mpy >> 16) != 0:
        print(' -march-flags=' + (sys_mpy >> 16), end='')
    print()
    
  • Verifique a validade do arquivo .mpy inspecionando os dois primeiros bytes do arquivo. O primeiro byte deve ser um ‘M’ maiúsculo e o segundo byte será o número da versão, que deve corresponder à versão do sistema obtida acima. Se não corresponder, recompile o arquivo .mpy.

  • Verifique se a versão do .mpy do sistema corresponde à versão emitida pelo mpy-cross usado para compilar o arquivo .mpy, obtida por meio de mpy-cross --version. Se não corresponder, recompile o mpy-cross a partir do repositório Git com checkout na tag (ou hash) relatada por mpy-cross --version.

  • Certifique-se de estar usando as flags corretas do mpy-cross, encontradas pelo código acima, ou inspecionando a variável de Makefile MPY_CROSS_FLAGS para a porta que você está usando.

  • Se o terceiro byte do arquivo .mpy tiver o bit #6 definido, verifique se o vuint de bits de flags específicos da arquitetura codificado é compatível com o alvo no qual você está importando o arquivo.

A tabela a seguir mostra a correspondência entre a versão do MicroPython e a versão do .mpy.

Versão do MicroPython

versão do .mpy

v1.23.0 e superiores

6.3

v1.22.x

6.2

v1.20 - v1.21.0

6.1

v1.19.x

6

v1.12 - v1.18

5

v1.11

4

v1.9.3 - v1.10

3

v1.9 - v1.9.2

2

v1.5.1 - v1.8.7

0

Para fins de completude, a próxima tabela mostra o commit Git do repositório principal do MicroPython no qual a versão do .mpy foi alterada.

alteração da versão do .mpy

commit Git

6.2 para 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 para 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 para 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 para 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 para 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 para 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 para 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 para 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 para 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

versão inicial 0

d8c834c95d506db979ec871417de90b7951edc30

Codificação binária de arquivos .mpy

Os arquivos .mpy do MicroPython são um formato container binário com objetos de código (bytecode e código de máquina nativo) armazenados internamente em uma hierarquia aninhada. O código do módulo externo é armazenado primeiro, seguido por seus filhos. Cada filho pode ter outros filhos, por exemplo no caso de uma classe com métodos, ou de uma função que define uma lambda ou uma compreensão. Para manter os arquivos pequenos e ao mesmo tempo oferecer um amplo intervalo de valores possíveis, ele usa em muitos lugares o conceito de inteiro sem sinal codificado de forma variável (vuint). Semelhante à codificação UTF-8, essa codificação armazena 7 bits por byte, com o 8º bit (MSB) definido caso um ou mais bytes se sigam. Os bits do inteiro sem sinal são armazenados no vuint em formato LSB.

O nível superior de um arquivo .mpy consiste em três partes:

  • O cabeçalho.

  • As tabelas globais de qstr e de constantes.

  • O código bruto para o escopo externo do módulo. Esse escopo externo é executado quando o arquivo .mpy é importado.

Você pode inspecionar o conteúdo de um arquivo .mpy usando mpy-tool.py, por exemplo (executado a partir da raiz do repositório principal do MicroPython):

$ ./tools/mpy-tool.py -xd myfile.mpy

O cabeçalho

O cabeçalho do .mpy é:

tamanho

campo

byte

valor 0x4d (ASCII ‘M’)

byte

número da versão maior do .mpy

byte

flags de funcionalidades, arquitetura nativa, número da versão menor (era flags de funcionalidades em versões mais antigas)

byte

número de bits em um inteiro pequeno

O terceiro byte é dividido da seguinte forma (MSB primeiro):

bit

significado

7

reservado, deve ser 0

6

um vuint de flags específicos da arquitetura segue o cabeçalho

5..2

número da arquitetura nativa

1..0

número da versão menor

Flags específicos da arquitetura

Se o bit #6 do byte de flags de funcionalidades do cabeçalho estiver definido, então um vuint contendo informações opcionais específicas da arquitetura seguirá o cabeçalho. O conteúdo desse inteiro depende de para qual arquitetura nativa o arquivo se destina.

Isso é atualmente usado para armazenar quais extensões de processador RISC-V o arquivo MPY necessita para operar corretamente, além de I, M, C e Zicsr. Diferentes variantes do ArmV7 são identificadas pelo seu número de arquitetura nativa, mas reutilizar esse mecanismo complicaria as coisas para RV32 e RV64.

Arquivos MPY destinados a RV32 ou RV64 que não necessitam de nenhuma extensão de processador específica não precisam fornecer um inteiro de flags (além de definir o bit apropriado no cabeçalho). A ausência de um valor de flags para arquivos MPY de RV32 e RV64 é usada para indicar que nenhuma extensão específica é necessária, e economiza um byte no binário de saída final.

Consulte também a opção de linha de comando -march-flags tanto em mpy-tool.py quanto em mpy-cross, e a opção de linha de comando --arch-flags em mpy_ld.py para definir esse valor ao criar arquivos MPY.

As tabelas globais de qstr e de constantes

Um arquivo .mpy contém uma única tabela de qstr e uma única tabela de objetos constantes. Elas são globais ao arquivo .mpy e são referenciadas por todos os objetos de código bruto aninhados. A tabela de qstr mapeia o número interno de qstr (interno ao arquivo .mpy) para o número de qstr resolvido do runtime no qual o arquivo .mpy é importado. Isso vincula o arquivo .mpy ao restante do sistema dentro do qual ele é executado. A tabela de objetos constantes é preenchida com referências a todos os objetos constantes que o arquivo .mpy necessita.

tamanho

campo

vuint

número de qstrs

vuint

número de objetos constantes

dados de qstr

objetos constantes codificados

Elementos de código bruto

Um elemento de código bruto contém código, seja bytecode ou código de máquina nativo. Seu conteúdo é:

tamanho

campo

vuint

tipo, tamanho e se há elementos de código bruto secundários

código (bytecode ou código de máquina)

vuint

número de elementos de código bruto secundários (apenas se for diferente de zero)

elementos de código bruto secundários

O primeiro vuint em um elemento de código bruto codifica o tipo de código armazenado neste elemento (os dois bits menos significativos), se este código bruto tem filhos (o terceiro bit menos significativo) e o tamanho do código que se segue (a quantidade de RAM a ser alocada para ele).

Após o vuint vem o próprio código. A menos que o tipo de código seja código viper com realocações, esse código é dado constante e não precisa ser modificado.

Se este código bruto tiver filhos (conforme indicado por um bit no primeiro vuint), após o código vem um vuint contando o número de elementos de código bruto secundários.

Por fim, quaisquer elementos de código bruto secundários são armazenados, recursivamente.