MicroPython 文字列の隔離化

MicroPython は 文字列の隔離化 を使って RAM と ROM の両方に保存します。これにより、同じ文字列の重複コピーを保存する必要がなくなります。関数または変数名のようなものがコードの複数の場所に現れる可能性が非常に高いため、これは主にコードの識別子に適用されます。MicroPython では、インターンされた文字列は QSTR (uniQue STRing)と呼ばれます。

QSTR 値(qstr 型)は、QSTR プールのリンクリストへのインデックスです。QSTRは重複排除プロセス中の高速比較のために、長さとコンテンツのハッシュを保存します。文字列を操作するすべてのバイトコード操作は、QSTR 引数を使います。

コンパイル時の QSTR 生成

MicroPython の C ソースコードでは、ビルド後のファームウェアに隔離する必要のある文字列はすべて MP_QSTR_Foo として書いてあります。これはコンパイル時に、QSTR プール内の "Foo" を指すインデックスである qstr 値に評価されます。

Makefile 中の多段階の処理がこの機能を実現しています。この処理は大きく次の3つの段階があります:

  1. ソースコード中のすべての MP_QSTR_Foo トークンを検出
  2. すべての文字列データ(長さとハッシュを含む)を持つ静的な QSTR プールを生成
  3. MP_QSTR_Foo すべてを(プリプロセッサを使って)対応するインデックスに置換

MP_QSTR_Foo トークンは次の2つのソースコードから検索されます:

  1. $(SRC_QSTR) で参照されるすべてのソースファイル。これは、すべてのCコード(すなわち py, extmod, ports/stm32 など)ですが、 lib のようなサードパーティのコードは含みません。
  2. $(QSTR_GLOBAL_DEPENDENCIES) で追加されるもの(mpconfig*.h を含む)。

注記: (mpy-tool.py によって生成される) frozen_mpy.c には独自の QSTR 生成とプールがあります。

MP_QSTR_Foo 構文を使って表現できない追加の文字列(たとえば、英数字以外の文字を含んだもの)は、 $(QSTR_DEFS) に指定する qstrdefs.hqstrdefsport.h で明示的に与えます。

処理は次の段階を踏みます:

  1. qstr.i.last は、C プリプロセッサをとおした各入力ファイルを結合したものです。これは、条件付きで無効化されたコードが削除され、マクロが展開されていることを意味します。これは、ビルドしたファームウェアで使われていない文字列をプールに追加しないことを意味します。この段階では(QSTR_GEN_CFLAGS によって追加された NO_QSTR マクロのおかげで) MP_QSTR_Foo のための定義がないため、何の作用もなくこの段階を通過します。このファイルには、行番号情報を含むプリプロセッサからのコメントも含まれます。この段階では、変更されたファイルのみを使うことに注意してください。つまり qstr.i.last には直前のコンパイル以降に変更されたファイルのデータのみが含まれます。
  2. qstr.split は qstr.i.last に対して makeqstrdefs.py split を実行して作成される空のファイルです。このファイルは、この段階が実行されたことを示すための依存関係としてのみ使われます。このスクリプトは、元の入力 C ソースファイルごとに1つのファイルを出力します。出力ファイルは genhdr/qstr/...file.c.qstr のようなファイル名であり、ファイルの中身は抽出した QSTR のみとなります。各 QSTR は Q(Foo) の形式で出力されます。この段階は、qstr.i.last にある増分更新から生成された新しいデータを既存ファイルにマージするために必要です。
  3. qstrdefs.collected.h は、 makeqstrdefs.py cat を使って genhdr/qstr/* を全て結合した出力です。これは、ソースコード内に見つかった MP_QSTR_Foo の完全なセットです。フォーマットは Q(Foo) のまま、1行に1つずつ、重複ありとなっています。このファイルは、qstr のセットが変更された場合にのみ更新されます。QSTR データのハッシュは別のファイル(qstrdefs.collected.h.hash)に書き込まれ、ビルド全体の変更を追跡できます。
  4. Generate an enumeration, each entry of which maps a MP_QSTR_Foo to it's corresponding index. It concatenates qstrdefs.collected.h with qstrdefs*.h, then it transforms each line from Q(Foo) to "Q(Foo)" so they pass through the preprocessor unchanged. Then the preprocessor is used to deal with any conditional compilation in qstrdefs*.h. Then the transformation is undone back to Q(Foo), and saved as qstrdefs.preprocessed.h.
  5. qstrdefs.generated.hmakeqstrdata.py の出力です。qstrdefs.preprocessed.h (と、追加でハードコーディングされたもの)中の Q(Foo) それぞれについて QDEF(MP_QSTR_Foo, (const byte*)"hash" "Foo") を出力します。

次に、メインのコンパイルで、 qstrdefs.generated.h について次の2つのことが起きます:

  1. qstr.h では、各 QDEF は列挙型のエントリになり、ソースコード中で MP_QSTR_Foo を使えるようになります。これは QSTR テーブル内の文字列のインデックスと同等です。
  2. qstr.c では、実際の QSTR データテーブルが mp_qstr_const_pool->qstrs の要素として生成されます。

実行時のQSTR生成

実行時にも追加の QSTR プールを作成して、文字列を追加できるようになっています。たとえば、次のようなコードがあるものとします:

foo[x] = 3

x の値の QSTR を作成されれば、それを "load attr" バイトコードで使えるようになります。

また、Python コードをコンパイルするときは、識別子とリテラルの QSTR が作成される必要があります。注記: 10文字より短いリテラルのみが QSTR になります。これは、ヒープ上の通常の文字列が常に最小16バイト(1 GCブロック)を占有するのに対し、QSTR を使った場合はより効率的にプールにパックできるためです。

QSTRプール(および文字列データを格納する基盤となる「チャンク」)は、最小サイズでオンデマンドでヒープに割り当てられます。