File .mpy di MicroPython¶
MicroPython definisce il concetto di file .mpy, ovvero un formato di file contenitore binario che racchiude codice precompilato e che può essere importato come un normale modulo .py. Il file foo.mpy può essere importato tramite import foo, purché foo.mpy possa essere trovato nel modo consueto dal meccanismo di importazione. Di norma, ogni directory elencata in sys.path viene cercata in ordine. Quando si cerca in una particolare directory, viene cercato per primo foo.py e, se non viene trovato, viene cercato foo.mpy; la ricerca prosegue poi nella directory successiva se nessuno dei due viene trovato. Di conseguenza, foo.py ha la precedenza su foo.mpy.
Questi file .mpy possono contenere bytecode che viene solitamente generato da file sorgente Python (file .py) tramite il programma mpy-cross. Per alcune architetture un file .mpy può anche contenere codice macchina nativo, che può essere generato in vari modi, in particolare a partire da codice sorgente C.
Il compilatore mpy-cross¶
mpy-cross è il cross-compilatore che trasforma un file sorgente .py in un contenitore binario .mpy pronto per essere importato sulla cam. Fa parte dell’albero dei sorgenti di MicroPython (lo stesso usato per compilare il firmware della cam) ed è inoltre pubblicato come pacchetto pip per l’uso lato host senza un checkout completo del firmware:
$ pip install --user mpy-cross
Oppure tramite pipx:
$ pipx install mpy-cross
Una volta installato, invocalo su un singolo file sorgente:
$ mpy-cross foo.py
Questo produce foo.mpy nella directory corrente, pronto per essere copiato sul filesystem della cam insieme ad altri moduli o per essere passato a un’immagine ROMFS.
Le opzioni da riga di comando più utili:
-o <path>– percorso di output per il.mpygenerato (per impostazione predefinita è il nome del file di input con l’estensione sostituita;-o -scrive su stdout).-O<n>– livello di ottimizzazione da0a3. Il valore predefinito0conserva le asserzioni e le informazioni complete sulla posizione nel sorgente;3rimuove asserzioni e docstring e riscrive i blocchiif __debug__. Il livello controlla la stessa interfacciamicropython.opt_levelesposta dal runtime.-march=<arch>– architettura nativa di destinazione per le funzioni decorate con@nativee@viper. Necessaria quando il sorgente usa tali decoratori. Il valore deve corrispondere alla classe di MCU della cam: scegline uno dall’elenco stampato dampy-cross --help, oppure leggilo dalla cam a runtime consys.implementation._mpy.-s <path>– stringa del percorso del sorgente incorporata nelle informazioni di debug del.mpy. Utile quando il percorso su disco differisce dal percorso di importazione sotto cui il file dovrebbe comparire nei traceback.-X emit=bytecode|native|viper– sceglie l’emettitore predefinito per l’intero modulo (un’alternativa, a livello dell’intero modulo, ai decoratori@native/@viperper singola funzione).--version– stampa la versione del formato.mpyemessa da questo binario. Tale numero deve corrispondere alla versione supportata dal runtime della cam (vedi la tabella delle release più avanti), altrimenti l’importazione solleveràValueError('incompatible .mpy file').
Esegui mpy-cross --help per l’elenco completo dei flag.
Il pacchetto pip espone anche una piccola API in forma di modulo Python, in modo che gli script di build possano pilotare il compilatore in-process invece di lanciare manualmente un sottoprocesso:
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 sono i tre punti di ingresso; mpy_cross.CrossCompileError veicola lo stderr del compilatore quando qualcosa va storto. Le costanti di architettura (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, ecc.) corrispondono alle stringhe accettate dal flag -march.
Versionamento e compatibilità dei file .mpy¶
Un dato file .mpy può essere compatibile o meno con un dato sistema MicroPython. La compatibilità si basa su quanto segue:
Versione del file .mpy: la versione del file deve corrispondere alla versione supportata dal sistema che lo carica.
Sotto-versione del file .mpy: se il file .mpy contiene codice macchina nativo, allora la sotto-versione del file deve corrispondere alla versione supportata dal sistema che lo carica. Altrimenti, se nel file .mpy non è presente codice macchina nativo, la sotto-versione viene ignorata durante il caricamento.
Bit dei piccoli interi: il file .mpy richiederà un numero minimo di bit in uno small integer e il sistema che lo carica deve supportare almeno questo numero di bit.
Architettura nativa: se il file .mpy contiene codice macchina nativo, allora specificherà l’architettura di tale codice macchina e il sistema che lo carica deve supportare l’esecuzione del codice di quell’architettura.
Se un sistema MicroPython supporta l’importazione di file .mpy, allora il campo sys.implementation._mpy esisterà e restituirà un intero che codifica la versione (gli 8 bit inferiori), le funzionalità e l’architettura nativa.
Il tentativo di importare un file .mpy che fallisce uno dei primi quattro test solleverà ValueError('incompatible .mpy file'). Il tentativo di importare un file .mpy che fallisce il test dell’architettura nativa (se contiene codice macchina nativo) solleverà ValueError('incompatible .mpy arch').
Se l’importazione di un file .mpy fallisce, prova quanto segue:
Determina la versione .mpy e i flag supportati dal tuo sistema MicroPython eseguendo:
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()
Verifica la validità del file .mpy ispezionando i primi due byte del file. Il primo byte dovrebbe essere una “M” maiuscola e il secondo byte sarà il numero di versione, che dovrebbe corrispondere alla versione del sistema indicata sopra. Se non corrisponde, ricompila il file .mpy.
Verifica se la versione .mpy del sistema corrisponde alla versione emessa da
mpy-crossusato per compilare il file .mpy, ottenibile conmpy-cross --version. Se non corrisponde, ricompilampy-crossdal repository Git effettuandone il checkout al tag (o all’hash) riportato dampy-cross --version.Assicurati di usare i flag corretti di
mpy-cross, ottenibili con il codice sopra, oppure ispezionando la variabile MakefileMPY_CROSS_FLAGSper la porta che stai usando.Se il terzo byte del file .mpy ha il bit #6 impostato, verifica se i bit del vuint dei flag specifici dell’architettura codificati siano compatibili con il target su cui stai importando il file.
La tabella seguente mostra la corrispondenza tra le release di MicroPython e la versione .mpy.
Release di MicroPython |
Versione .mpy |
|---|---|
v1.23.0 e successive |
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 |
Per completezza, la tabella successiva mostra il commit Git del repository principale di MicroPython in corrispondenza del quale è stata modificata la versione .mpy.
Cambio di versione .mpy |
Commit Git |
|---|---|
da 6.2 a 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
da 6.1 a 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
da 6 a 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
da 5 a 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
da 4 a 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
da 3 a 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
da 2 a 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
da 1 a 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
da 0 a 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
versione iniziale 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
Codifica binaria dei file .mpy¶
I file .mpy di MicroPython sono un formato contenitore binario con oggetti di codice (bytecode e codice macchina nativo) memorizzati internamente in una gerarchia annidata. Il codice del modulo esterno viene memorizzato per primo, seguito dai suoi figli. Ogni figlio può avere ulteriori figli, ad esempio nel caso di una classe con metodi o di una funzione che definisce una lambda o una comprehension. Per mantenere i file di piccole dimensioni fornendo al contempo un ampio intervallo di valori possibili, in molti punti si usa il concetto di intero senza segno a codifica variabile (vuint). In modo simile alla codifica UTF-8, questa codifica memorizza 7 bit per byte con l’8° bit (MSB) impostato se seguono uno o più byte. I bit dell’intero senza segno sono memorizzati nel vuint in forma LSB.
Il livello superiore di un file .mpy è composto da tre parti:
L’header.
Le tabelle globali delle qstr e delle costanti.
Il raw-code per lo scope esterno del modulo. Questo scope esterno viene eseguito quando il file .mpy viene importato.
Puoi ispezionare il contenuto di un file .mpy usando mpy-tool.py, ad esempio (eseguito dalla radice del repository principale di MicroPython):
$ ./tools/mpy-tool.py -xd myfile.mpy
L’header¶
L’header del .mpy è:
dimensione |
campo |
|---|---|
byte |
valore 0x4d (ASCII “M”) |
byte |
numero di versione major del .mpy |
byte |
flag delle funzionalità, architettura nativa, numero di versione minor (nelle versioni precedenti erano flag delle funzionalità) |
byte |
numero di bit in un piccolo intero |
Il terzo byte è suddiviso come segue (MSB per primo):
bit |
significato |
|---|---|
7 |
riservato, deve essere 0 |
6 |
dopo l’header segue un vuint di flag specifici dell’architettura |
5..2 |
numero dell’architettura nativa |
1..0 |
numero di versione minor |
Flag specifici dell’architettura¶
Se il bit #6 del byte dei flag delle funzionalità dell’header è impostato, allora dopo l’header seguirà un vuint contenente informazioni opzionali specifiche dell’architettura. Il contenuto di questo intero dipende dall’architettura nativa a cui il file è destinato.
Attualmente viene usato per memorizzare quali estensioni del processore RISC-V il file MPY necessita per funzionare correttamente oltre a I, M, C e Zicsr. Le diverse varianti di ArmV7 sono identificate dal loro numero di architettura nativa, ma riutilizzare quel meccanismo complicherebbe le cose per RV32 e RV64.
I file MPY destinati a RV32 o RV64 che non necessitano di particolari estensioni del processore non devono fornire un intero di flag (oltre a impostare il bit appropriato nell’header). L’assenza di un valore di flag per i file MPY RV32 e RV64 viene usata per indicare che non sono necessarie estensioni specifiche, e risparmia un byte nel binario di output finale.
Vedi anche l’opzione da riga di comando -march-flags sia in mpy-tool.py sia in mpy-cross, e l’opzione da riga di comando --arch-flags in mpy_ld.py per impostare questo valore durante la creazione dei file MPY.
Le tabelle globali delle qstr e delle costanti¶
Un file .mpy contiene una singola tabella di qstr e una singola tabella di oggetti costanti. Queste sono globali al file .mpy e vengono referenziate da tutti gli oggetti raw-code annidati. La tabella delle qstr mappa il numero qstr interno (interno al file .mpy) al numero qstr risolto del runtime in cui il file .mpy viene importato. Questo collega il file .mpy al resto del sistema all’interno del quale viene eseguito. La tabella degli oggetti costanti viene popolata con i riferimenti a tutti gli oggetti costanti di cui il file .mpy ha bisogno.
dimensione |
campo |
|---|---|
vuint |
numero di qstr |
vuint |
numero di oggetti costanti |
… |
dati qstr |
… |
oggetti costanti codificati |
Elementi raw-code¶
Un elemento raw-code contiene codice, sia esso bytecode o codice macchina nativo. Il suo contenuto è:
dimensione |
campo |
|---|---|
vuint |
tipo, dimensione e presenza di elementi sub-raw-code |
… |
codice (bytecode o codice macchina) |
vuint |
numero di elementi sub-raw-code (solo se diverso da zero) |
… |
elementi sub-raw-code |
Il primo vuint in un elemento raw-code codifica il tipo di codice memorizzato in questo elemento (i due bit meno significativi), se questo raw-code ha figli (il terzo bit meno significativo) e la lunghezza del codice che segue (la quantità di RAM da allocare per esso).
Dopo il vuint viene il codice stesso. A meno che il tipo di codice non sia codice viper con rilocazioni, questo codice è dato costante e non necessita di modifiche.
Se questo raw-code ha figli (come indicato da un bit nel primo vuint), dopo il codice viene un vuint che conta il numero di elementi sub-raw-code.
Infine vengono memorizzati eventuali elementi sub-raw-code, in modo ricorsivo.