Ruby(とC)でRubyを実装してみた（builtinで遊んでみた）

# はじめに

タイトルにもあるようにRubyのbuiltin（正式名称を知らないので呼び出し方法から拝借）というものを使ってRuby自体をRuby(とC)で実装してみた話です。

内容としてはRuby自体の実装に興味のある方向けの話になります。

# builtinって？

builtinとはRuby(とC)でRuby自体を実装するというものです(正式な名前は今のところないみたい？)。以下のように`__builtin_<Cで定義した関数名>`をRubyのコードから呼び出すことでRubyとCを使い、より簡単にRubyの実装を行うことができます。

たとえば、`Hash#delete`はCで以下のように実装されています。

```c
static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
	return val;
    }
    else {
	if (rb_block_given_p()) {
	    return rb_yield(key);
	}
	else {
	    return Qnil;
	}
    }
}
```

第一引数の`hash`はハッシュ自体を引数に受け取り、第二引数の`key`は`Hash#delete`で渡しているキーを受け取っています。ちなみに、Ruby側の変数などの値は`VALUE`型で受け取り、Cの関数で処理されています。

```c
    rb_hash_modify_check(hash);
```

`rb_hash_modify_check`関数は内部で`rb_check_frozen`関数を実行し、ハッシュが凍結されているかを確認しています。

```c
static void
rb_hash_modify_check(VALUE hash)
{
    rb_check_frozen(hash); // オブジェクトが凍結されているか確認
}
```

`val = rb_hash_delete_entry(hash, key);`では引数に受け取ったキーをもとに削除する値を取得し、同時に削除を行っています。キーと対になる値がない場合は`Qundef`というCで使用する未定義の値が入ります。

```c
    if (val != Qundef) {
	return val;
    }
    else {
	if (rb_block_given_p()) {
	    return rb_yield(key);
	}
	else {
	    return Qnil;
	}
    }
```

`val`の値で処理を分岐させ、`Qundef`ではない場合（つまりキーを使って値が取れ、削除できた場合）は削除された値を返します。
`Qundef`だった場合は`Qnil`(Rubyでの`nil`)を返します。ブロックが渡されている場合は`rb_yield(key)`を実行し、その結果を返しています。

このように皆さんが普段使っているRubyは、Cを使って実装されています。

builtin機能を使うことで先ほどのコードが以下のようになります。

```ruby
class Hash
    def delete(key)
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end
```

```c
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
	    return val;
    }
    else {
	    return Qnil;
    }
}
```

Ruby側でブロックの実行などを処理させているため。Cでの実装はよりシンプルで読みやすくなったと思います。
また

このようにbuiltin機能を使うことでRubyと少しのCのコードでRubyを実装することができます。

またCで実装するよりもRubyで実装した場合、パフォーマンスが向上するケースもあるようです。
より具体的な話は笹田さんがRubyKaigi 2019にて話されていますのでそちらを参照して頂ければと思います。

[Write a Ruby interpreter in Ruby for Ruby 3](https://rubykaigi.org/2019/presentations/ko1.html#apr18)

# やってみた

builtinを使うことでRubyのコードを使い、メソッドを実装できることがわかったので実際にやってみました。

### 開発環境構築

まずはRubyの開発環境を作成するところからはじめました。僕の環境としてはWSL+Ubuntu 18.04を使い、開発環境を構築しました。
基本的な手順としてはRuby Hack Challengeの[(2) MRI ソースコードの構造](https://github.com/ko1/rubyhackchallenge/blob/master/JA/2_mri_structure.md)を参考に進めました。

まずは使用するライブラリなどをインストールしていきます。

```bash
sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev
```

次に、作業用のディレクトリを作成し、そこへ移動します。。

```bash
mkdir workdir
cd workdir
```

作業用のディレクトリに移動後、Rubyのソースコードをcloneします。結構時間がかかるのでこの間にコーヒーでも入れておくといいでしょう。

```bash
git clone https://github.com/ruby/ruby.git
```

ソースコードのcloneが終わったら、`ruby`ディレクトリへと移動し、`autoconf`を実行します。あとで実行する`configure`スクリプトを生成するためですね。実行後、`workdir`まで戻ります。

```bash
cd ruby
autoconf
cd ..
```

次に、ビルド用のディレクトリを作成し、そこへ移動します。

```bash
mkdir build
cd build
```

`../ruby/configure --prefix=$PWD/../install --enable-shared`を実行してビルドするためのMakefileを作成します。また`--prefix=$PWD/../install`ではRubyをインストールする先を指定しています

```bash
../ruby/configure --prefix=$PWD/../install --enable-shared
```

その後、`make -j`を実行してビルドします。`-j`は並列にコンパイルを実行するためのオプションです。特に急ぐわけでもない場合は`make`だけでも良いでしょう。

```bash
make -j
```

最後に`make install`を実行すると`workdir`ディレクトリ内に`install`ディレクトリが作成され、Rubyがインストールされます。

```bash
make install
```

これで最新のRubyが`workdir/install`にインストールされています。

ちなみに、本当にインストールされているか気になる方は`../install/bin/ruby -v`を実行してみましょう。`ruby 2.8.0dev`とRubyのバージョンが表示されていればRubyは正しくインストールされています。

## builtinでメソッドを再定義してみる

開発環境が整ったのでbuiltinを使い、メソッドを再定義していきます。先ほど例にも挙げた`Hash#delete`を再実装していきます。

### common.mkの修正

まずは、ビルドの際にRubyのソースコードを使用するための諸設定を`common.mk`に追加します。
`common.mk`の1000行目辺りに、`BUILTIN_RB_SRCS`という記述があります。この`BUILTIN_RB_SRCS`で読み込むRubyのコードが記述されているファイルを追加します。

```makefile:common.mk
BUILTIN_RB_SRCS = \
		$(srcdir)/ast.rb \
		$(srcdir)/gc.rb \
		$(srcdir)/io.rb \
		$(srcdir)/pack.rb \
		$(srcdir)/trace_point.rb \
		$(srcdir)/warning.rb \
		$(srcdir)/array.rb \
		$(srcdir)/prelude.rb \
		$(srcdir)/gem_prelude.rb \
		$(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)
```

今回は、Hashの実装を行うため`hash.rb`を以下のように追加します。

```diff
BUILTIN_RB_SRCS = \
		$(srcdir)/ast.rb \
		$(srcdir)/gc.rb \
		$(srcdir)/io.rb \
		$(srcdir)/pack.rb \
		$(srcdir)/trace_point.rb \
		$(srcdir)/warning.rb \
		$(srcdir)/array.rb \
		$(srcdir)/prelude.rb \
		$(srcdir)/gem_prelude.rb \
+		$(srcdir)/hash.rb \
		$(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)
```

次に、2520行目辺りにあるHashのビルドで読み込むファイルを指定している部分を修正します。
このように`hash.c`など読み込むファイルが指定されています。

```makefile:common.mk
hash.$(OBJEXT): {$(VPATH)}hash.c
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h
```

ここに、`hash.rbinc`と`builtin.h`を追加します。

```diff
hash.$(OBJEXT): {$(VPATH)}hash.c
+hash.$(OBJEXT): {$(VPATH)}hash.rbinc
+hash.$(OBJEXT): {$(VPATH)}builtin.h
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h
```

`hash.rbinc`は`make`実行時に自動的に生成されるファイルで、`hash.rb`内の`__builtin_<呼び出すCの関数名>`をチェックした内容をもとに生成されています。また`builtin.h`はbuiltinを使うための実装などがかかれたヘッダーファイルです。

これで`common.mk`での修正は完了です。

### inits.cの修正

次に、`inits.c`を修正します。といっても非常に修正は簡単なものです。

```c:inits.c
#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
    Init_builtin_prelude();
}
```

`inits.c`では上記のようにbuiltinを使用しているRubyのソースファイルを追加しています。ここに同じように`BUILTIN(hash);`を追加します。

```diff
#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
+    BUILTIN(hash);
    Init_builtin_prelude();
```

`inits.c`の修正はこれでOKです。

### hash.cの修正

いよいよ、`hash.c`のコードを修正していきます。

#### builtin.hの読み込み

まずは、40行目辺りのヘッダー読み込み部分に`#include "builtin.h"`を追加します。

```diff
  #include "ruby/st.h"
  #include "ruby/util.h"
  #include "ruby_assert.h"
  #include "symbol.h"
  #include "transient_heap.h"
+ #include "builtin.h"
```

これでbuiltinに必要な構造体などを`hash.c`で使用することができます。

#### Hash#deleteの定義を削除

次に、`Hash#delete`を定義している部分を取り除きます。

`hash.c`の下部に`Init_Hash(void)`という関数が定義されていると思います。

```c
void
Init_Hash(void)
{
 /// Hashの実装コードなどが記述されています。
}
```

Rubyの各クラスのメソッドはこの関数内で以下のように定義されています。

```c
rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
```

`rb_define_method`はRubyでいうところのメソッドの定義と同じと考えてください。第一引数にメソッドを定義するクラスの`VALUE`を渡し、第二引数がメソッド名となっています。
　第三引数がCで定義された関数（メソッドで実行される処理）で、第四引数がメソッドが受け取る引数の数となっています。

builtinでRubyのメソッドを定義する場合はこの定義部分を削除する必要があります。今回は`Hash#delete`を再実装しますので、`delete`が定義されている部分を削除します。

```diff
    rb_define_method(rb_cHash, "shift", rb_hash_shift, 0);
-   rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
    rb_define_method(rb_cHash, "delete_if", rb_hash_delete_if, 0);
```

#### rb_hash_delete_mをbuiltinから使用できるように修正

先ほど削除した`rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);`で呼び出されている`rb_hash_delete_m`をbuiltinで使用できるように修正します。

2380行辺りに`rb_hash_delete_m`の実装があります。

```c
static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
	return val;
    }
    else {
	if (rb_block_given_p()) {
	    return rb_yield(key);
	}
	else {
	    return Qnil;
	}
    }
}
```

これを以下のように修正します。

```c
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef)
    {
        return val;
    }
    else
    {
        return Qnil;
    }
}
```

builtin対応のために第一借り引数に`rb_execution_context_t *ec`を渡しているところが実装の肝ですね。

これでRubyからCで定義した関数を呼び出すことができるようになります。

#### hash.rbincの読み込み

最後に、自動生成される`hash.rbinc`を読み込むようにします。
`#include "hash.rbinc"`を`hash.c`の一番下に追加します。

```c
#include "hash.rbinc"
```
これでCのコード側での修正は完了しました。

### hash.rbの作成

それではRubyで`Hash#delete`を実装してみましょう。`hash.c`と同じ階層に`hash.rb`を作成します。
作成後、以下のようにコードを追加します。

```ruby
class Hash
    def delete(key)
        puts "impl by Ruby(& C)!"
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end
```

先ほどbuiltinで呼び出せるようにした`__builtin_rb_hash_delete_m`に受け取ってきた引数を渡し、その結果を`value`に代入しています。

あとは、`value`の値が`nil`か同課で処理を分岐させています。`nil`の場合かるブロックが渡されている場合は`key`を引数にブロックを実行しています。

`puts "impl by Ruby(& C)!"`は実際に試す際に確認するためのメッセージになりますね。

これでbuiltinでの実装はすべて完了しました！

### ビルドしてみる

それでは開発環境を構築した時同様にビルドしてみましょう。

```bash
make -j && make install
```

ビルドが成功すればOKです！もしビルドが失敗した場合はtypoなどが無いか確認してみましょう。

### 実際にirbで試してみる

それでは`irb`を使ってbuiltinで実装した`Hash#delete`を試してみましょう！

```bash
../install/bin/irb
```

後は以下のコードを貼り付けてみましょう！

```ruby
hash = {:key => "value"}
hash.delete(:k)
hash.delete(:key)
```

以下のように結果が表示されていればbuiltinでの実装は完了です！

```bash
irb(main):001:0> hash = {:key => "value"}
irb(main):002:0> hash.delete(:k)
impl by Ruby(& C)!
=> nil
irb(main):003:0> hash.delete(:key)
impl by Ruby(& C)!
=> "value"
irb(main):004:0>
```

`impl by Ruby(& C)!`と表示されているのでRubyで定義した`Hash#delete`が実行されていることが分かりますね。

これでRuby(とC)でRubyを実装できました！

# 終わりに

このようにbuiltinを使うことでRubyと(少しのC）のコードを使ってRuby自体を実装することができます。
そのため普段Rubyを書いている人でも気軽にメソッドの修正などのパッチを送ることができるようになるのではないかと思います。

あとやってみてRuby側で処理が書けたりするので意外と書きやすいというのはうれしいですね。

個人的にはExtensionなどでも使用できるようになるとC/C++でのRuby拡張がより書きやすくなるのではないかと思うので、今後の展望が非常に楽しみですね。

# 参考

[Write a Ruby interpreter in Ruby for Ruby 3](https://rubykaigi.org/2019/presentations/ko1.html#apr18)

[Rubyソースコード完全解説](http://i.loveruby.net/ja/rhg/book/)

[Rubyのしくみ](https://magazine.rubyist.net/articles/0049/0049-BookRUM_ja.html)