Classクラスを使ったRuby/mrubyスクリプトのバイトコードを読んでみる

TechJapanese, mruby, Ruby

前回のKawasaki.rbでいつものパーフェクトRubyの読書で、下のような感じのコードがサンプルとして出てきた。
定数にClass.newの結果を代入すると、その定数名をクラス名とするクラスが定義される機能を初めて知った。
Rubyのバイトコードをちょっと覗いてみたいのもあり、Rubyとmrubyについて、どんな風にバイトコード上で表現されるのか、確認してみた。

サンプルコード

t="Konnichiwa"
MyClass = Class.new do |klass|
    puts t
    def hello
        puts "Hello"
    end
end
puts t
o = MyClass.new
o.hello

このコードを実行してみると。以下のような出力がされる。

Konnichiwa
Konnichiwa
Hello

Rubyのバイトコード

こちらは、Rubyのバイトコード。
https://qiita.com/nownabe/items/47cc5d95e8b4e01205a8
を参考にして、次のようなコマンドで出力した。

puts RubyVM::InstructionSequence.compile_file(ARGV[0], false).disasm
== disasm: @test.rb>=================
== catch table
| catch type: break  st: 0005 ed: 0010 sp: 0000 cont: 0010
|------------------------------------------------------------------------
local table (size: 3, argc: 0 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 3] t          [ 2] o          
0000 putstring        "Konnichiwa"                                    (   1)
0002 setlocal         t, 0
0005 putnil                                                           (   2)
0006 getconstant      :Class
0008 send             >
0010 putspecialobject 3
0012 setconstant      :MyClass
0014 putself                                                          (   9)
0015 getlocal         t, 0
0018 send             
0020 pop              
0021 putnil                                                           (  11)
0022 getconstant      :MyClass
0024 send             
0026 setlocal         o, 0
0029 getlocal         o, 0                                            (  12)
0032 send             
0034 leave            
== disasm: @test.rb>========
== catch table
| catch type: redo   st: 0000 ed: 0017 sp: 0000 cont: 0000
| catch type: next   st: 0000 ed: 0017 sp: 0000 cont: 0017
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s3)
[ 2] klass 
0000 putself                                                          (   3)
0001 getlocal         t, 1
0004 send             
0006 pop              
0007 putspecialobject 1                                               (   4)
0009 putspecialobject 2
0011 putobject        :hello
0013 putiseq          hello
0015 send             
0017 leave            
== disasm: ==================
0000 putself                                                          (   5)
0001 putstring        "Hello"
0003 send             
0005 leave            
           

mrubyのバイトコード

こちらは、mrubyのバイトコードです。mrbcにverboseオプションをつけると出力できる。

mrbc --verbose a.rb
irep 0x7fe43940d390 nregs=6 nlocals=3 pools=1 syms=5 reps=1
file: test.rb
    1 000 OP_STRING R1  L(0)    ; "Konnichiwa"  ; R1:t
    2 001 OP_GETCONST   R3  :Class
    2 002 OP_LAMBDA R4  I(+1)   block
    2 003 OP_SENDB  R3  :new    0
    2 004 OP_SETCONST   :MyClass    R3  
    9 005 OP_LOADSELF   R3      
    9 006 OP_MOVE   R4  R1      ; R1:t
    9 007 OP_SEND   R3  :puts   1
   11 008 OP_GETCONST   R3  :MyClass
   11 009 OP_SEND   R3  :new    0
   11 010 OP_MOVE   R2  R3      ; R2:o
   12 011 OP_SEND   R3  :hello  0
   12 012 OP_STOP

irep 0x7fe43940d6b0 nregs=6 nlocals=3 pools=0 syms=2 reps=1
file: test.rb
    2 000 OP_ENTER  1:0:0:0:0:0:0
    3 001 OP_LOADSELF   R3      
    3 002 OP_GETUPVAR   R4  1   0
    3 003 OP_SEND   R3  :puts   1
    4 004 OP_TCLASS R3      
    4 005 OP_LAMBDA R4  I(+1)   method
    4 006 OP_METHOD R3  :hello
    4 007 OP_LOADSYM    R3  :hello
    4 008 OP_RETURN R3  normal  

irep 0x7fe43940d9a0 nregs=5 nlocals=2 pools=1 syms=1 reps=0
file: test.rb
    4 000 OP_ENTER  0:0:0:0:0:0:0
    5 001 OP_LOADSELF   R2      
    5 002 OP_STRING R3  L(0)    ; "Hello"
    5 003 OP_SEND   R2  :puts   1
    5 004 OP_RETURN R2  normal  

バイトコード読んでみる

mrubyのバイトコードを参考に、Rubyのバイトコードを読んでみる。自分用のメモなので、わかりにくいのは承知で、だらだらと書き連ねてみる。
Rubyのバイトコードをちゃんと見たのは初めてだけど、mrubyで読んでいたので、なんとなく内容は理解できた。正しく理解するため、Rubyのしくみをちゃんと最後まで読まねば、と思った。
mrubyのVMははレジスタマシンだが、RubyのVMはスタックマシンなので、オブジェクトをスタックに積んだり、取り出したりして、処理が行われる。

Class.newを用いたクラスの定義

getconstantで、:Classというシンボルに対応する定数。すなわちClassクラスのインスタンスをとってきて、それをレシーバとして、send(:new,block)しているようだ。
定数名がクラス名に化ける仕組みは、見てみるとsetconstantで:MyClassというシンボルを名前とする定数を定義して、それに対してClass.newの結果を代入している。
クラスオブジェクトも結局は定数であるので、定数に代入するだけで、クラスとしての名前付けと行わなくても、クラスオブジェクトとして使えていると理解した。
mrubyの場合は、newメソッドの戻り値(R3)を、OP_SETCONSTで設定した定数"MyClass"に代入している。この流れはRubyと同じようだ。

クラスを定義するブロック

ブロックの中身は、通常のクラス定義と同じような処理になっていて、defでは、実際には、iseqを引数にdefine_methodメソッドを呼び出す処理になっている。
対して、mrubyでは、OP_LAMBDAと、OP_METHOD命令でメソッドを定義している。mrubyでirepに相当するのが、iseqと思えばだいたいよさそう。
Rubyでは、クラスの定義といっても、上から順番にコードが実行されているだけなので、途中にputs tとか定義に直接関係ないコードをおいても、クラス定義のタイミングにdef ... endなどといっしょに実行される。

クラスを定義するブロックの中での外部オブジェクト参照

もう一つ気になっていたのは、Class.newにブロックを引数として渡すと、その中でクラスの定義を行えるという処理と、ブロックであるので、クラス定義の途中で外のオブジェクトを参照できるという点。
バイトコード上でどう実装されているか見てみる。
t="Konnichiwa"
としてローカル変数に定義したtは、クラスの定義を行っているブロックの中では、getlocal t, 1としてローカル変数から取得して使用されている。
send(:new)したときに、ローカル変数のスコープが引き継がれている、ということみたい。詳しくはsendの処理を更に理解する必要があると思われる。

mrubyの場合は、OP_GETUPVARで呼び出し元にレジスタをさかのぼってピンポイントで参照している。コンパイラが頑張っていることが伺える。

まとめ

Rubyとmrubyで、Classクラスを使った処理をバイトコード上で、比較してみた。
スタックマシンとレジスタマシンの違いはあるが、内容は結構似ていた。
ただ、ブロックのスコープの扱いについては違いが大きそうなので、もう少し調べてみたい。
ここまで触れていないが、Rubyのバイトコード命令中のspecialobjectが何なのかよくわからなかった。ささだんの過去の連載↓には出てきていないので、あとで追加されたもののようだ。調べてみよう。

「YARV Maniacs 【第 1 回】 『Ruby ソースコード完全解説』不完全解説」
https://magazine.rubyist.net/articles/0006/0006-YarvManiacs.html