Fichiers .mpy de MicroPython

MicroPython définit le concept de fichier .mpy, un format de fichier conteneur binaire qui contient du code précompilé et qui peut être importé comme un module .py normal. Le fichier foo.mpy peut être importé via import foo, tant que foo.mpy peut être trouvé de la manière habituelle par le mécanisme d’importation. Habituellement, chaque répertoire listé dans sys.path est parcouru dans l’ordre. Lors de la recherche dans un répertoire donné, foo.py est recherché en premier et, s’il n’est pas trouvé, foo.mpy est ensuite recherché, puis la recherche se poursuit dans le répertoire suivant si aucun des deux n’est trouvé. Ainsi, foo.py a la priorité sur foo.mpy.

Ces fichiers .mpy peuvent contenir du bytecode qui est généralement généré à partir de fichiers source Python (fichiers .py) via le programme mpy-cross. Pour certaines architectures, un fichier .mpy peut également contenir du code machine natif, qui peut être généré de diverses manières, notamment à partir de code source C.

Le compilateur mpy-cross

mpy-cross est le compilateur croisé qui transforme un fichier source .py en un conteneur binaire .mpy prêt à être importé sur la caméra. Il fait partie de l’arborescence des sources de MicroPython (la même que celle utilisée pour compiler le micrologiciel de la caméra) et est également publié sous forme de paquet pip pour une utilisation côté hôte sans extraction complète du micrologiciel :

$ pip install --user mpy-cross

Ou via pipx :

$ pipx install mpy-cross

Une fois installé, invoquez-le sur un fichier source unique :

$ mpy-cross foo.py

Cela produit foo.mpy dans le répertoire courant, prêt à être copié sur le système de fichiers de la caméra à côté d’autres modules ou à être intégré dans une image ROMFS.

Les options de ligne de commande les plus utiles :

  • -o <path> – chemin de sortie pour le fichier .mpy généré (par défaut, le nom du fichier d’entrée avec l’extension remplacée ; -o - écrit sur la sortie standard).

  • -O<n> – niveau d’optimisation de 0 à 3. La valeur par défaut 0 préserve les assertions et l’intégralité des emplacements source ; 3 supprime les assertions et les docstrings et réécrit les blocs if __debug__. Le niveau contrôle la même interface micropython.opt_level que celle exposée par l’environnement d’exécution.

  • -march=<arch> – architecture native cible pour les fonctions décorées par @native et @viper. Requis lorsque la source utilise ces décorateurs. La valeur doit correspondre à la classe de microcontrôleur de la caméra : choisissez-la dans la liste affichée par mpy-cross --help, ou lisez-la sur la caméra à l’exécution avec sys.implementation._mpy.

  • -s <path> – chaîne de chemin source intégrée dans les informations de débogage du .mpy. Utile lorsque le chemin sur disque diffère du chemin d’importation sous lequel le fichier doit apparaître dans les traces de pile.

  • -X emit=bytecode|native|viper – choisit l’émetteur par défaut pour l’ensemble du module (une alternative par fonction aux décorateurs @native / @viper).

  • --version – affiche la version du format .mpy que ce binaire émet. Ce numéro doit correspondre à la version prise en charge par l’environnement d’exécution de la caméra (voir le tableau des versions ci-dessous), sinon l’importation lèvera ValueError('incompatible .mpy file').

Exécutez mpy-cross --help pour la liste complète des options.

Le paquet pip expose également une petite API de module Python afin que les scripts de compilation puissent piloter le compilateur dans le processus plutôt que de lancer manuellement un sous-processus

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 et mpy_cross.mpy_version sont les trois points d’entrée ; mpy_cross.CrossCompileError transporte la sortie d’erreur du compilateur en cas de problème. Les constantes d’architecture (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, etc.) correspondent aux chaînes acceptées par l’option -march.

Gestion des versions et compatibilité des fichiers .mpy

Un fichier .mpy donné peut être ou non compatible avec un système MicroPython donné. La compatibilité repose sur les éléments suivants :

  • Version du fichier .mpy : la version du fichier doit correspondre à la version prise en charge par le système qui le charge.

  • Sous-version du fichier .mpy : si le fichier .mpy contient du code machine natif, alors la sous-version du fichier doit correspondre à la version prise en charge par le système qui le charge. Sinon, s’il n’y a pas de code machine natif dans le fichier .mpy, la sous-version est ignorée lors du chargement.

  • Bits d’entier court : le fichier .mpy nécessitera un nombre minimal de bits dans un small integer et le système qui le charge doit prendre en charge au moins ce nombre de bits.

  • Architecture native : si le fichier .mpy contient du code machine natif, il spécifiera l’architecture de ce code machine et le système qui le charge doit prendre en charge l’exécution du code de cette architecture.

Si un système MicroPython prend en charge l’importation de fichiers .mpy, alors le champ sys.implementation._mpy existera et renverra un entier qui encode la version (les 8 bits de poids faible), les fonctionnalités et l’architecture native.

Tenter d’importer un fichier .mpy qui échoue à l’un des quatre premiers tests lèvera ValueError('incompatible .mpy file'). Tenter d’importer un fichier .mpy qui échoue au test d’architecture native (s’il contient du code machine natif) lèvera ValueError('incompatible .mpy arch').

Si l’importation d’un fichier .mpy échoue, essayez ce qui suit :

  • Déterminez la version .mpy et les indicateurs pris en charge par votre système MicroPython en exécutant

    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()
    
  • Vérifiez la validité du fichier .mpy en inspectant les deux premiers octets du fichier. Le premier octet devrait être un “M” majuscule et le deuxième octet sera le numéro de version, qui devrait correspondre à la version du système indiquée ci-dessus. S’il ne correspond pas, recompilez le fichier .mpy.

  • Vérifiez si la version .mpy du système correspond à la version émise par le mpy-cross qui a été utilisé pour compiler le fichier .mpy, obtenue par mpy-cross --version. Si elle ne correspond pas, recompilez mpy-cross à partir du dépôt Git extrait au tag (ou au hash) indiqué par mpy-cross --version.

  • Assurez-vous d’utiliser les bons indicateurs mpy-cross, trouvés par le code ci-dessus, ou en inspectant la variable Makefile MPY_CROSS_FLAGS pour le port que vous utilisez.

  • Si le bit #6 du troisième octet du fichier .mpy est activé, vérifiez si le vuint des bits d’indicateurs spécifiques à l’architecture encodés est compatible avec la cible sur laquelle vous importez le fichier.

Le tableau suivant montre la correspondance entre la version de MicroPython et la version .mpy.

Version de MicroPython

version .mpy

v1.23.0 et ultérieures

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

Par souci d’exhaustivité, le tableau suivant montre le commit Git du dépôt principal de MicroPython auquel la version .mpy a été modifiée.

changement de version .mpy

Commit Git

6.2 vers 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 vers 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 vers 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 vers 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 vers 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 vers 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 vers 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 vers 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 vers 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

version initiale 0

d8c834c95d506db979ec871417de90b7951edc30

Encodage binaire des fichiers .mpy

Les fichiers .mpy de MicroPython sont un format de conteneur binaire dans lequel les objets de code (bytecode et code machine natif) sont stockés en interne dans une hiérarchie imbriquée. Le code du module externe est stocké en premier, puis ses enfants suivent. Chaque enfant peut avoir d’autres enfants, par exemple dans le cas d’une classe ayant des méthodes, ou d’une fonction définissant une lambda ou une compréhension. Pour conserver des fichiers de petite taille tout en offrant une large plage de valeurs possibles, il utilise à de nombreux endroits le concept d’entier non signé à encodage variable (vuint). Comme l’encodage UTF-8, cet encodage stocke 7 bits par octet, le 8e bit (MSB) étant activé si un ou plusieurs octets suivent. Les bits de l’entier non signé sont stockés dans le vuint sous forme LSB.

Le niveau supérieur d’un fichier .mpy se compose de trois parties :

  • L’en-tête.

  • Les tables globales de qstr et de constantes.

  • Le raw-code de la portée externe du module. Cette portée externe est exécutée lorsque le fichier .mpy est importé.

Vous pouvez inspecter le contenu d’un fichier .mpy en utilisant mpy-tool.py, par exemple (à exécuter depuis la racine du dépôt principal de MicroPython)

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

L’en-tête

L’en-tête .mpy est :

taille

champ

octet

valeur 0x4d (ASCII “M”)

octet

numéro de version majeure .mpy

octet

indicateurs de fonctionnalités, architecture native, numéro de version mineure (était les indicateurs de fonctionnalités dans les versions antérieures)

octet

nombre de bits dans un entier court

Le troisième octet est divisé comme suit (MSB en premier) :

bit

signification

7

réservé, doit être 0

6

un vuint d’indicateurs spécifiques à l’architecture suit l’en-tête

5..2

numéro d’architecture native

1..0

numéro de version mineure

Indicateurs spécifiques à l’architecture

Si le bit #6 de l’octet des indicateurs de fonctionnalités de l’en-tête est activé, alors un vuint contenant des informations spécifiques à l’architecture facultatives suivra l’en-tête. Le contenu de cet entier dépend de l’architecture native à laquelle le fichier est destiné.

Ceci est actuellement utilisé pour stocker quelles extensions de processeur RISC-V le fichier MPY nécessite pour fonctionner correctement, en plus de I, M, C et Zicsr. Les différentes variantes d’ArmV7 sont identifiées par leur numéro d’architecture native, mais réutiliser ce mécanisme compliquerait les choses pour RV32 et RV64.

Les fichiers MPY ciblant RV32 ou RV64 qui n’ont besoin d’aucune extension de processeur particulière n’ont pas besoin de fournir un entier d’indicateurs (en plus d’activer le bit approprié dans l’en-tête). L’absence d’une valeur d’indicateurs pour les fichiers MPY RV32 et RV64 est utilisée pour indiquer qu’aucune extension spécifique n’est nécessaire, et économise un octet dans le binaire de sortie final.

Voir aussi l’option de ligne de commande -march-flags dans mpy-tool.py et mpy-cross, ainsi que l’option de ligne de commande --arch-flags dans mpy_ld.py pour définir cette valeur lors de la création de fichiers MPY.

Les tables globales de qstr et de constantes

Un fichier .mpy contient une seule table de qstr et une seule table d’objets constants. Celles-ci sont globales au fichier .mpy, elles sont référencées par tous les objets raw-code imbriqués. La table de qstr fait correspondre le numéro de qstr interne (interne au fichier .mpy) au numéro de qstr résolu de l’environnement d’exécution dans lequel le fichier .mpy est importé. Cela relie le fichier .mpy au reste du système dans lequel il s’exécute. La table d’objets constants est remplie de références à tous les objets constants dont le fichier .mpy a besoin.

taille

champ

vuint

nombre de qstrs

vuint

nombre d’objets constants

données qstr

objets constants encodés

Éléments raw-code

Un élément raw-code contient du code, soit du bytecode, soit du code machine natif. Son contenu est :

taille

champ

vuint

type, taille et présence d’éléments sub-raw-code

code (bytecode ou code machine)

vuint

nombre d’éléments sub-raw-code (uniquement si non nul)

éléments sub-raw-code

Le premier vuint d’un élément raw-code encode le type de code stocké dans cet élément (les deux bits de poids faible), si ce raw-code a des enfants (le troisième bit de poids faible), et la longueur du code qui suit (la quantité de RAM à allouer pour celui-ci).

Après le vuint vient le code lui-même. Sauf si le type de code est du code viper avec relocalisations, ce code est constitué de données constantes et n’a pas besoin d’être modifié.

Si ce raw-code a des enfants (comme indiqué par un bit dans le premier vuint), après le code vient un vuint comptant le nombre d’éléments sub-raw-code.

Enfin, tous les éléments sub-raw-code sont stockés, de manière récursive.