mrubyを改造したり、mrubyを何かアプリケーションとかに組み込むときに必ず必要になる物に、mrb_state型の構造体へのポインタ(多くの場合はmrbという名前で参照されている)があります。これは、mrubyで使う大域変数や状態を1つの構造体にまとめた物です。
mrubyのインタプリタを使うときにはmrb_state型の構造体のインスタンスを用意してそれをインタプリタに渡します。複数のインタプリタが欲しいときには複数のインスタンスを用意します。
こうすることで、構造体のインスタンスを複数用意することで複数のmrubyインタプリターをプログラムコードは共有して使えます。これは、並列性は息を吸うのと同じくらい普通で、メモリの一滴は血の一滴くらいメモリが貴重な1組み込み界隈ではとっても嬉しい仕様なわけです。
mrb_stateはmrubyの大域変数や状態が全て入っているので、mrb_stateを理解することはmrubyの内部を理解することに直結しますし、うまく(悪用?)使うといろいろ面白いことが出来ます。
なぜかmrb_stateの詳しい解説って見たことがないです。そこで、解説を書いてみたいと思います。私が知らないだけで、実は優れた解説があるのかもしれません。でも、解説が2つや3つあってもおかしくないくらい(Cの解説がいくつあるか考えるとよい)重要なので、大丈夫です。
mrb_stateはinclude/mruby.hで定義されています。基本的には解説は構造体の定義に沿いますが、たまに順番を変えることがあります。
# メモリ確保に関するメンバー
mrubyは組み込み向けですからね。貴重なメモリを無駄なく使えるようにメモリ確保のためのやり方をカスタマイズできるようになっています。
allocf
メモリを確保する関数(アロケータ)へのポインタを格納します。デフォルトではmrb_default_allocfを使います
allocf_ud
allocfに渡すデータです。デフォルトでは常にNULLです。これを実際に活用した事例は知らないのですが、アロケータが確保するのに使ってほしいメモリ領域を指定するとか、アロケーションの目的を教えてフラグメンテーションを軽減するとか考えられます。
コンテキストに関するメンバー
mrubyのVMを動かすのに必要なデータが入っています。メンバーは2つだけ(しかも1つはほとんど使われない)ですが、非常に重要です。
c
VMを動かすためのcontext構造体へのポインタ。文字通りVMのコンテクストでスタックやcallinfoからなります。スタックはローカル変数や引数渡しのために使用するための領域、callinfoはメソッドやブロックの呼び出し履歴になります。
context構造体は構造体なので複数インスタンスを作ることができます。このインスタンスを切り替えることで見かけ上たくさんのVMを動かすことが出来ます。こうやって実現した物が、みんな大好き(ほんとなんでだろう?) Fiberです。
context構造体とその下のcallinfo構造体は非常に奥が深くてちゃんと説明すると、この記事の量が倍くらいになっちゃいますのでまた機会があれば、別に書きます。
root_c
起動時のFiberのcontext構造体。Fiberを全く使わないとcとroot_cは常に同じになります。root_cのコンテクストのFiberが終了するとmruby全体を終了しないといけないけど、そうじゃないFiberはそれだけを終了させないといけないからチェック用に持っています。
例外処理に関するメンバー
このカテゴリーについてはメンバーの定義がいろいろな場所に分散しているのでmrb_statusの定義の順番とは異なりますので注意してください
jmp
mrubyで例外が発生した場合にCのスタックを巻き戻すための巻き戻し先を格納するメンバー。何かに使えないか考えたけど思いつかなかった。実際に入っているのはsetjmpのjmp_buf構造体かC++の例外オブジェクト。コンパイルオプションによって変わります。
exc
例外が起ったらここに例外オブジェクトを入れておくと、VMに戻った時によきにはからった、例外処理をしてくれます。でも、このメンバーを直接使うよりはmrb_raiseとかのAPIを使った方がメリット多いでしょうね。
eException_class
例外クラスへのポインタ。例外処理というより後述するようにクラスへのポインタを色々mrb_stateは持っているのでその一環って感じ。
eStandardError_class
StandardError例外クラスへのポインタ。そうなんだけど、なぜこれがメンバーである必要があるのかよくわからないです。mrb_init_exceptionの関数内でしか使われていないみたい。
nomem_err
メモリーが足りなくなったときに使う例外オブジェクト。これはクラスじゃないのに注意。実体は文字列です。そりゃーメモリが無いときに例外オブジェクトアロケーションしていたら絶対失敗するよな。
個人的には、メモリが足りない場合のmrubyのデフォルトの動作、組み込みをなめんとんのかって思うのですけど、ちゃんと使えるくらいにカスタマイズするだけのポテンシャルも持っているような気がします。allocf_udとかいいよね。
グローバル変数関係
globals
グローバル変数のテーブルです。インスタンス変数と全く同じ方式(ハッシュテーブルか線形リスト)で実装されています。これで、すべてのグローバル変数のリストが欲しくなっても安心ですね。
クラスオブジェクトへのポインタ関係
Rubyもクラスもオブジェクトなのでインタープリタを起動してから動的にクラスをアロケートする必要があります。つまり、クラスはいろんなところから参照されるのに定数として定義できないわけです。mrubyでは大域変数はご法度なのでmrb_stateにクラスオブジェクトへのポインタをもたせる必要があるわけです。
このメンバーになっているクラスオブジェクトを書き換えていろんな変態的なmruby(たとえば分散対応とか)にすることも可能かもしれませんが、まあ夢物語でしょう。
top_self
object_class
class_class
module_class
proc_class
string_class
array_class
hash_class
fixnum_class
true_class
false_class
nil_class
symbol_class
kernel_module
# メモリ管理関係
mems
現状何に使っているのか非常に謎のmrb_allocaで使います。memsの型のalloca_headerってのがリストでmrb_allocaでアロケートしたメモリ領域をすべてつないでいます。そして、mrb_freeで一気に解放するって寸法です。いろいろ、特に組み込み方面で面白い使い方が思いつきますが、現状何に使われているのか、なぜ入っているのかは謎です。
gc
mrb_gcというGC関係のパラメータとか変数とかが詰まった構造体です。たくさんあるし、GCの仕組みの詳細に関わっている(私はよくわかっていない)のですべては説明できませんが分かる範囲で説明します。パラメータについては、gc.cの冒頭のコメントが参考になりそうです(でも英語なのでよく分かっていない)。
mrb_gcのメンバーはこんな感じです
heaps
ヒープ全体だと思うのですが、良くわからないです。ヒープは複数のページというメモリ領域のリストとして管理するようです。
sweeps
スイープするページを示すようです。
free_heaps
未使用のページのようです。
lives
現在生きているオブジェクトの数です。アロケートすると1増えて、GCされると1減ります。
arena
よく話題になるアリーナです。コンパイルオプションで可変長と固定長の選択ができます。アリーナについてはMatzにっきの解説を読むといいでしょう。直接メンバーにアクセスするメリットは多分ないです。
arena_capa
可変長の場合の現在のアリーナの大きさです
arena_idx
現在、使用中のアリーナの数を示します。
state
GCの状態。インクリメントGCをサポートしているので、GCの途中で中断する必要があります。その場合、いまGCのどのフェーズをやっているか覚えておく必要があります。そういうことでこれが必要です。状態は以下のようなものがあります。
- MRB_GC_STATE_ROOT
ルートをスキャンする状態 - MRB_GC_STATE_MARK
マークする状態 - MRB_GC_STATE_SWEEP
スイープする状態
current_while_part
よくわからない・・・
gray_list
よくわからない・・・
atomic_gray_list
ライトバリアで登録されたオブジェクトを格納する物ですが、gray_listとの違いがよくわかりませんでした・・・・
live_after_mark
前回のGCでmarkした直後のオブジェクトの数です。
threshold
livesがこの値を超えるとインクリメンタルGCが始まります。この値の計算にはinterval_ratioが使われます。前回のGCでmarkした直後のオブジェクトの数(live_after_mark)のinterval_ratio%が設定されます。
interval_ratio
interval_ratioメソッドの設定値を格納しています。これは、thresholdの値の計算に使います。単位は%で、ディフォルトは200%です。
step_ratio
step_ratioメソッドの設定値を格納しています。これはインクリメンタルGCの1ステップをどのくらいがんばるかの値で、GC_STEP_SIZE(1024)の何%かで示します。GC_STEP_SIZE * step_ratioだけの数のオブジェクトをマークするかスイープするだけインクリメンタルGCをがんばります。
disabled
(なぜか)みんな大好き、gc_disableを設定したら、これがtrueになります。これとかビットフィールドにしない方がいいんじゃないかと個人的には思います。
full
full GCを行っているときtrueになります
generational
generational_modeメソッドの設定値を格納します。trueだと世代別GCになります。
out_of_memory
GCしてもメモリが無いときにtrueになります。これがtrueでGCが起動すると、パニックします。
majorgc_old_threshold
メジャーGCを始めるためのlivesの数の閾値を格納します。
シンボルに関するメンバー
シンボルは同じ名前の物には同じ値を割り当てる必要があります。これを実現するためにmrubyではハッシュテーブルを使っています。このハッシュテーブルは動的にアロケートされますので定数ではなく変数にする必要がありmrb_stateに入れる必要があります。
また、シンボルの値を名前に変換したい場合もあります。たとえば、シンボルを表示する場合です。こういう場合のためにシンボルの値から名前(文字列)を変換するための配列もあります。
symidx
現在割り当てられている最大のシンボルの番号。これを見れば今何個シンボルが定義されているか分かりますね。
name2sym
シンボルの名前(文字列)からシンボルの値を返すハッシュテーブル
symtbl
シンボルの値からシンボルの名前を返す配列
symcapa
symtblの大きさ。これよりシンボルの数が多くなったら取り直します。
デバッグ用フックに関するメンバー
コンパイル時に設定することでデバッグ用のフックをかけることができます。フックはあるタイミングで関数を呼び出すもので、その関数へのポインタを格納するメンバーが定義されています。コンパイル時にこれを無効にするとメンバーそのものが無くなります。
code_fetch_hook
命令をフェッチしたタイミング(新しい命令をプログラムカウンタを指しているが、その命令は実行される前)にここに入っている関数を呼び出します。呼び出す際に、mrb_state構造体(今説明しているものですね), 現在実行中の命令表現(mrb_irep), プログラムカウンタ, レジスタ配列が渡されます。
デバッグだけではなく色々応用できますが、1つしか登録できないのが痛いところです。
debug_op_hook
VMのOP_DEBUG命令を実行すると実行されます。渡される引数はcode_fetch_hookと同じです。今の所なにも使っていないようです。デバッガのブレークポイントで使えないかなと思います。今は、code_fetch_hookで呼ばれる関数でブレークポイントに差し掛かっているかチェックしているようです。
ユーザ定義データ
ud
ユーザ定義のデータです。なにかmrubyを組み込んだ先で大域的にデータを参照したいときここにぶち込んでおくと便利です。
ユーザ定義終了関数
VMの実行が終わってmrb_stateの構造体を解放するときに呼ばれます。今の所使われている様子は無いですが、なぜこれが出来たかは分かります。この議論からです。日本語が併記しているコメントがあって大変助かります。
atexit_stack
終了時に呼ばれる関数の配列。コンパイルオプションで固定長か可変長か選べます。
atexit_stack_len
いくつ関数が入っているか示しています。
最後に
長かったー、以上ですべてのメンバーの紹介でした(手抜きが結構あるけど)。このような記事を書いたのは、現状のmrubyに問題がある気がしてそれを直したくて調査の一環だったのです。
その問題とは、次の2つです。
- mrubyを並列化する場合に関して
- 例外処理を組み込みで満足に使えるレベルにする
並列化に関して
現状のmrb_stateは実行に必要な大域データをすべて持っているためmrb_stateを分けて全く別のVMとして実行するか、せいぜいFiberのレベルで協調的にスレッドを切り替えるかの2つのレベルしか選択できません。
その中間のオブジェクトやシンボルを共有したまま、コンテクストやGCのパラメータなどは分離するといったきめ細かい並列化の対応は不可能だと思います。そういうわけで、mrb_stateをグループ化してきめ細かく共有・分離が制御できるようにしたいと思います。
満足のいく例外処理
現状のmrubyではアロケーションを行って、メモリが足りない場合はGCをしますが、それでもメモリが足りない場合、アボートしてしまいます。しかし、商品に組み込まれている場合、そんなことで落ちてしまって許されるでしょうか?少なくとも、緊急用メモリ領域を前もって確保してそれを利用して、保持しているデータを外部記憶に書き出すとか、圧縮するとか何かあがくことができるチャンスが欲しいのではないかと思います。
または、mrubyで実行するタスクを1つのトランザクションとして見て、メモリが足りない場合はそのトランザクション全体を失敗させてそのトランザクション前の状態(これならメモリが足りることを保証できる)に戻すというアプローチも可能かもしれません。(これにはmemsやライトバリア関係のメンバーが活躍することでしょう)
いずれにしても、組み込みではメモリ環境も使用目的もパニック時の行うべき動作もまちまちです。私だけでは何もできません。でも、たたき台くらいは示せるのではないかと思います。この作業において、重要な部品になるのはmrb_stateのメンバーです。部品は揃っていると思いますが、新たに追加する必要があるかもしれません。
本当に最後に
完成の暁にはプルリクエストしたいと思いますので、その際にはよろしくお願いします。
-
今はメモリは安いのではという反論があるかもれませんが、ただではないですからねー。量差前提なら原価安くするためにメモリ減らすのは充分考えられます。 ↩