LoginSignup
0
0

More than 1 year has passed since last update.

mruby で特殊変数 `$!` を使えるようにするには? (脳内妄想・アイディア段階)

Last updated at Posted at 2021-12-19

mruby には特殊変数である $! が実装されていません。
また rescue / ensure 中に C 空間からエラーを直接取得する、CRuby にある rb_errinfo() 関数のようなものもありません。

mrb->exc が利用できるだろうと考えたこともありますが、rescue / ensure ブロックの中では仮想機械のスタックやマシンスタックに一時保存されて mrb->exc は空っぽになってしまいます。

どのようにしたら実装できるかについて頭の中で考えていることを書き留めてみようと思います。
なおあくまでも脳内妄想であり、実際に試したわけでも現在取り組んでいるわけでもないことを明言しておきます。

やること① 特殊変数の仕組みを使えるようにする

まず、mruby には特殊変数のための仕組みそのものが実装されていません。
どういう事かと言うと、$! どころか $0 だったり $~ だったりがまるっきり機能しません。

bin/mruby でも $0 が使えるでしょと思ったそこのあなた、mruby だとそれはただの大域変数です。
このことは $0 に文字列を代入してみても、ps コマンドでの表示が変更されないことを意味しています。

mruby-3.0.0 + mruby-process で実行してみるとこんな感じ。

% bin/mruby -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
  PID TT  STAT    TIME COMMAND
93979 23  S+   0:00.01 bin/mruby -e $0 = "進行状況: 永遠の99%"; system("ps #{$$}")

Ruby-3.1 preview1 で試すと表示が変更されているのが確認できます。

% ruby31 -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
  PID TT  STAT    TIME COMMAND
94872 23  S+   0:00.15 ruby31: 進行状況: 永遠の99% (ruby31)

mruby では alias も出来ません。

% bin/mruby -e 'alias $ERROR_INFO $!'
-e:1:17: syntax error, unexpected global variable

なのでこのあたりの仕組みを丸ごと追加する必要があります。

ちなみに仮想機械の命令は存在こそしていますが、その先の実装がないため機能することはありません。
もっとも、構文解析で大域変数として処理されるので、命令自体が生成されることもありませぬ。

やること② エラー情報を保存・取得出来るようにする

すでに示してきましたが CRuby であれば、発生した例外は rescueensure 内部で $! を使うと取得できるようになっているし、C 空間からは直接 rb_errinfo() で取得可能です。

mruby (3.0.0) では仮想機械のレジスタ上に例外オブジェクトが保存されますが、実行中にこれを取り出せません。
理由はどのレジスタに例外オブジェクトが置かれているかの情報がないためです。
なので取り出せるようにしなければなりません。

rescue/ensure ブロックごとに例外オブジェクトを保存するレジスタはコンパイル時に決定されます。
この時の mruby-compiler はレジスタ番号と、rescue/ensure ブロックの区間を把握しています。
そのため、このあたりの情報を irep に保存しておくことで解決できそうです。

それでどのように保存させるかについての問題が出てきます。
これについては例外ハンドラを利用できるだろうと考えています。
mruby-3.0.0 では例外・大域ジャンプが起きた場合の例外ハンドラ (rescue/ensure ブロック) をコンパイル時に irep へと組み込んでおくようになっているので、これを拡張しましょう。

 0 +1: catch handler type
 1 +4: catch handler begin
 5 +4: catch handler end
 9 +4: catch handler target           (rescue/ensureブロックの開始位置)
13 +4: catch handler finish  <<< NEW! (rescue/ensureブロックの終了位置)
17 +2: error registor index  <<< NEW!
19 ..: <END>

まあ見て分かるようにハンドラ一つあたり6バイト増えることになります。
RAM どころか ROM も少しでも減らしたい場合、これを削減するために mrb_irep::flags |= MRB_IREP_NO_ERRINFO みたいなものを追加してもいいかもしれません。

ここまで見てきたのは Ruby 空間での rescue/ensure ブロック内でのことです。
C 空間のことを考えた場合、C での mrb_rescue()mrb_ensure() 経由で使うためにこの例外ハンドラテーブルを利用できないことに気がつくでしょう。

ある時、構造体 mrb_callinfo を使う方法を思いつきました。CINFO_ERRINFO (仮) を導入します。

mrb_callinfo *ci = mrb->c->ci;
ci->cci = CINFO_ERRINFO;        // <<< NEW!
ci->stack[0] = exception_object;

mrb_exc_keep(mrb, exc, userfunc, userdata) 関数 (仮) を追加して使えるようにすれば良さそうです。
userfunc() が呼ばれた内部では、この時に保存された例外オブジェクトが見えるはずです。

副作用は、mrb_get_args() 関数は ci->cci == CINFO_ERRINFO なフレームを上へ辿るようにする必要が出てくるし、他にも影響が出てくるでしょう。

何はともあれ、これでとりあえずはエラー情報の保存と取得ができそうです。

やること③ エラー情報を探せるようにする

実際にエラー情報を探索する場合、呼び出し情報 (callinfo; ci) を呼び出し元に辿っていきながら確認することになります。

ci->cci == CINFO_NONE あるいは ci->cci == CINFO_SKIP であれば、現在実行中の pc が例外ハンドラテーブルの rescue/ensure ブロックの区間と一致しているかどうかを見ていきます。
一致すれば示されたレジスタの値が探し求めたエラー情報です。

ci->cci == CINFO_ERRINFO であれば、ci->stack[0] がエラー情報です。

ci を一番大本まで辿って何もなければエラー情報は nil となります。

探索処理は結構重そうですが、そもそも頻繁に使うものではないので許容できると考えています。

最後に

これが実現できると、お試しコードで役に立つ rescue 修飾子で p $! と書けます。

内部でエラーが起きるメソッド rescue p $!

また、引数なしの raise で例外の再送が出来るようになります。

begin
  ほげ
rescue
  ふが
  raise
end

ただし最初に述べたように、僕は実際の作業を行っていません。
すぐに使いたい人は是非とも挑戦して下さい。
別の方法だとしても歓迎します! 僕もたまに使いたくなるので!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0