MicroPython .mpy ファイル

MicroPython では .mpy ファイルという概念が定義されています。これはプリコンパイル済みコードを保持するバイナリコンテナファイル形式で、通常の .py モジュールと同様にインポートできます。ファイル foo.mpy は、foo.mpy がインポート機構によって通常の方法で見つけられる限り、import foo 経由でインポートできます。通常、sys.path に列挙された各ディレクトリが順番に検索されます。特定のディレクトリを検索する際にはまず foo.py が探され、それが見つからない場合は foo.mpy が探されます。どちらも見つからない場合は次のディレクトリへ検索が続きます。このように、foo.pyfoo.mpy より優先されます。

これらの .mpy ファイルには bytecode を含めることができ、これは通常 mpy-cross プログラムによって Python ソースファイル(.py ファイル)から生成されます。一部のアーキテクチャでは、.mpy ファイルにネイティブマシンコードを含めることもでき、これはさまざまな方法で生成できますが、最も代表的には C ソースコードから生成されます。

mpy-cross コンパイラ

mpy-cross は、.py ソースファイルをカメラ上でインポートできる .mpy バイナリコンテナへと変換するクロスコンパイラです。これは MicroPython のソースツリー(カメラのファームウェアをビルドするのと同じもの)の一部であり、完全なファームウェアのチェックアウトなしにホスト側で使用できるよう pip パッケージとしても公開されています:

$ pip install --user mpy-cross

または pipx 経由で:

$ pipx install mpy-cross

インストールしたら、単一のソースファイルに対して実行します:

$ mpy-cross foo.py

これによりカレントディレクトリに foo.mpy が生成され、他のモジュールと共にカメラのファイルシステムへコピーしたり、ROMFS イメージへ取り込んだりする準備が整います。

最も有用なコマンドラインオプション:

  • -o <path> -- 生成される .mpy の出力パス(既定では入力ファイル名の拡張子を置き換えたもの。-o - は標準出力に書き出します)。

  • -O<n> -- 最適化レベル 0 から 3 まで。既定の 0 はアサーションと完全なソース位置情報を保持します。3 はアサーションとドキュメント文字列を取り除き、if __debug__ ブロックを書き換えます。このレベルはランタイムが公開しているのと同じ micropython.opt_level の機能を制御します。

  • -march=<arch> -- @native および @viper デコレータが付いた関数の対象ネイティブアーキテクチャ。ソースがこれらのデコレータを使用する場合に必須です。値はカメラの MCU クラスに一致している必要があります。mpy-cross --help が表示する一覧から選ぶか、実行時に sys.implementation._mpy でカメラから読み取ってください。

  • -s <path> -- .mpy のデバッグ情報に埋め込まれるソースパス文字列。ディスク上のパスが、トレースバックでファイルを表示すべきインポートパスと異なる場合に有用です。

  • -X emit=bytecode|native|viper -- モジュール全体の既定エミッタを選択します(@native / @viper デコレータに対する関数単位の代替手段)。

  • --version -- このバイナリが生成する .mpy フォーマットのバージョンを表示します。この番号はカメラのランタイムがサポートするバージョン(下記のリリース表を参照)と一致している必要があり、そうでなければインポート時に ValueError('incompatible .mpy file') が発生します。

完全なフラグ一覧については mpy-cross --help を実行してください。

pip パッケージには小さな Python モジュール API も用意されており、ビルドスクリプトが手作業でサブプロセスをフォークする代わりにプロセス内でコンパイラを駆動できます:

import mpy_cross

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

mpy_cross.compilempy_cross.runmpy_cross.mpy_version の 3 つがエントリポイントです。mpy_cross.CrossCompileError は何か問題が発生したときにコンパイラの stderr を保持します。アーキテクチャ定数(NATIVE_ARCH_ARMV7EMSPNATIVE_ARCH_ARMV7EMDP など)は -march フラグが受け付ける文字列に対応しています。

.mpy ファイルのバージョン管理と互換性

ある .mpy ファイルは、ある MicroPython システムと互換性がある場合とない場合があります。互換性は次の点に基づきます:

  • .mpy ファイルのバージョン: ファイルのバージョンは、それを読み込むシステムがサポートするバージョンと一致している必要があります。

  • .mpy ファイルのサブバージョン: .mpy ファイルがネイティブマシンコードを含む場合、ファイルのサブバージョンはそれを読み込むシステムがサポートするバージョンと一致している必要があります。それ以外の場合、.mpy ファイルにネイティブマシンコードがなければ、読み込み時にサブバージョンは無視されます。

  • 小整数ビット: .mpy ファイルは small integer に最小限のビット数を要求し、それを読み込むシステムは少なくともこのビット数をサポートしている必要があります。

  • ネイティブアーキテクチャ: .mpy ファイルがネイティブマシンコードを含む場合、そのマシンコードのアーキテクチャを指定し、それを読み込むシステムはそのアーキテクチャのコードの実行をサポートしている必要があります。

MicroPython システムが .mpy ファイルのインポートをサポートしている場合、sys.implementation._mpy フィールドが存在し、バージョン(下位 8 ビット)、機能、ネイティブアーキテクチャをエンコードした整数を返します。

最初の 4 つのテストのいずれかに失敗する .mpy ファイルをインポートしようとすると ValueError('incompatible .mpy file') が発生します。ネイティブアーキテクチャのテストに失敗する(ネイティブマシンコードを含む場合).mpy ファイルをインポートしようとすると ValueError('incompatible .mpy arch') が発生します。

.mpy ファイルのインポートが失敗する場合は、次のことを試してください:

  • お使いの MicroPython システムがサポートする .mpy バージョンとフラグを、次を実行して確認します:

    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()
    
  • .mpy ファイルの先頭 2 バイトを調べて、その有効性を確認します。最初のバイトは大文字の 'M' であるべきで、2 番目のバイトはバージョン番号で、上記のシステムバージョンと一致するはずです。一致しない場合は .mpy ファイルを再ビルドしてください。

  • システムの .mpy バージョンが、その .mpy ファイルのビルドに使われた mpy-cross が生成するバージョン(mpy-cross --version で確認)と一致するか確認します。一致しない場合は、mpy-cross --version が報告するタグ(またはハッシュ)でチェックアウトした Git リポジトリから mpy-cross を再コンパイルしてください。

  • 上記のコードで確認できる、または使用しているポートの MPY_CROSS_FLAGS Makefile 変数を調べることで確認できる、正しい mpy-cross フラグを使用していることを確かめてください。

  • .mpy ファイルの 3 番目のバイトでビット #6 がセットされている場合は、エンコードされたアーキテクチャ固有のフラグビット vuint が、ファイルをインポートする対象と互換性があるかどうか確認してください。

次の表は MicroPython のリリースと .mpy バージョンの対応を示しています。

MicroPython リリース

.mpy バージョン

v1.23.0 以降

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

参考として、次の表は .mpy バージョンが変更された時点のメイン MicroPython リポジトリの Git コミットを示しています。

.mpy バージョンの変更

Git コミット

6.2 から 6.3 へ

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 から 6.2 へ

6967ff3c581a66f73e9f3d78975f47528db39980

6 から 6.1 へ

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 から 6 へ

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 から 5 へ

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 から 4 へ

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 から 3 へ

ff93fd4f50321c6190e1659b19e64fef3045a484

1 から 2 へ

dd11af209d226b7d18d5148b239662e30ed60bad

0 から 1 へ

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

初期バージョン 0

d8c834c95d506db979ec871417de90b7951edc30

.mpy ファイルのバイナリエンコーディング

MicroPython の .mpy ファイルは、コードオブジェクト(バイトコードとネイティブマシンコード)を入れ子の階層構造として内部に格納するバイナリコンテナ形式です。外側のモジュールのコードが最初に格納され、その子が続きます。各子はさらに子を持つことがあります。たとえばメソッドを持つクラスや、ラムダや内包表記を定義する関数の場合です。ファイルを小さく保ちつつ広い範囲の値を扱えるように、多くの箇所で可変長エンコード符号なし整数(vuint)の概念を使用しています。UTF-8 エンコーディングと同様に、このエンコーディングは 1 バイトあたり 7 ビットを格納し、後続のバイトが 1 つ以上ある場合は 8 番目のビット(MSB)をセットします。符号なし整数のビットは vuint に LSB 形式で格納されます。

.mpy ファイルのトップレベルは 3 つの部分から構成されます:

  • ヘッダー。

  • グローバルな qstr テーブルと定数テーブル。

  • モジュールの外側スコープの raw-code。この外側スコープは .mpy ファイルがインポートされたときに実行されます。

mpy-tool.py を使って .mpy ファイルの内容を調べることができます。たとえば次のようにします(メイン MicroPython リポジトリのルートから実行):

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

ヘッダー

.mpy のヘッダーは次のとおりです:

サイズ

フィールド

バイト

値 0x4d(ASCII 'M')

バイト

.mpy メジャーバージョン番号

バイト

機能フラグ、ネイティブアーキテクチャ、マイナーバージョン番号(古いバージョンでは機能フラグだった)

バイト

小整数のビット数

3 番目のバイトは次のように分割されます(MSB が先):

ビット

意味

7

予約済み、0 でなければならない

6

アーキテクチャ固有のフラグ vuint がヘッダーの後に続く

5..2

ネイティブアーキテクチャ番号

1..0

マイナーバージョン番号

アーキテクチャ固有のフラグ

ヘッダーの機能フラグバイトのビット #6 がセットされている場合、任意のアーキテクチャ固有情報を含む vuint がヘッダーの後に続きます。この整数の内容は、ファイルが対象とするネイティブアーキテクチャによって異なります。

これは現在、MPY ファイルが正しく動作するために I、M、C、Zicsr 以外に必要とする RISC-V プロセッサ拡張を格納するために使われています。ArmV7 の各種フレーバーはそのネイティブアーキテクチャ番号で識別されますが、その仕組みを再利用すると RV32 や RV64 では事情が複雑になります。

特定のプロセッサ拡張を必要としない RV32 または RV64 を対象とする MPY ファイルは、フラグ整数を提供する必要はありません(ヘッダーの該当ビットをセットすることも同様です)。RV32 および RV64 の MPY ファイルでフラグ値がないことは、特定の拡張が不要であることを示すために使われ、最終的な出力バイナリで 1 バイト節約します。

MPY ファイル作成時にこの値を設定するには、mpy-tool.pympy-cross の両方にある -march-flags コマンドラインオプション、および mpy_ld.py--arch-flags コマンドラインオプションも参照してください。

グローバルな qstr テーブルと定数テーブル

.mpy ファイルには単一の qstr テーブルと単一の定数オブジェクトテーブルが含まれます。これらは .mpy ファイルに対してグローバルであり、入れ子になったすべての raw-code オブジェクトから参照されます。qstr テーブルは内部 qstr 番号(.mpy ファイル内部のもの)を、.mpy ファイルがインポートされたランタイムの解決済み qstr 番号にマッピングします。これにより .mpy ファイルが実行されるシステムの残りの部分とリンクされます。定数オブジェクトテーブルには、.mpy ファイルが必要とするすべての定数オブジェクトへの参照が格納されます。

サイズ

フィールド

vuint

qstr の数

vuint

定数オブジェクトの数

...

qstr データ

...

エンコードされた定数オブジェクト

Raw code 要素

raw-code 要素にはコード(バイトコードまたはネイティブマシンコード)が含まれます。その内容は次のとおりです:

サイズ

フィールド

vuint

型、サイズ、および下位 raw-code 要素があるかどうか

...

コード(バイトコードまたはマシンコード)

vuint

下位 raw-code 要素の数(非ゼロの場合のみ)

...

下位 raw-code 要素

raw-code 要素の最初の vuint は、この要素に格納されたコードの型(下位 2 ビット)、この raw-code が子を持つかどうか(下位から 3 ビット目)、および後続するコードの長さ(そのために割り当てる RAM の量)をエンコードします。

vuint の後にはコード自体が続きます。コードの型が再配置を伴う viper コードでない限り、このコードは定数データであり変更する必要はありません。

この raw-code が子を持つ場合(最初の vuint のビットで示される)、コードの後に下位 raw-code 要素の数を数える vuint が続きます。

最後に、すべての下位 raw-code 要素が再帰的に格納されます。