TataraというRubyで型を使えるライブラリを作ってみた

### はじめに

この記事は最近僕が作っているRubyで型を使えるライブラリの紹介記事です。

対象読者としては、Rubyで簡単に型のようなものを使ってみたい人、またはC++でのRuby拡張を作ってみたい人を想定しています。

Tataraの紹介をしつつ、簡単なC++でのRuby拡張実装についても触れていきます。

### 作ったもの

[Tatara](https://github.com/S-H-GAMELINKS/tatara)というRubyで簡単な型を使えるようにするRubyExtensionを作りました。RubyGemsにもリリースしており、[v0.2.0](https://rubygems.org/gems/tatara)が現在リリースされている最新バージョンになります。

v0.2.0までは[Rice](https://github.com/jasonroelofs/rice)というRubyExtensionを簡単にC++で使用していました。しかし、GCへの管理追加が少しややこしかったり、RiceでC++のコードをRuby向けにラップしているため処理が遅くなったりするという問題がありました。そこで次期リリースではC++のみでの実装に書き直し、GCへの管理追加や処理速度向上を図っています。

ちなみに、次にリリースするv0.3.0は11月中にリリースする予定で実装を進めています。

#### サンプルコード

例えば、Tataraを使うことで以下のようなコードを実行できます。

```ruby
require 'tatara'

@int_container = Tatara::IntVector.new

(1..10).each{|i| @int_container << i}
# Insert 1..10 to @int_container

@int_container.map{|i|
    puts i
    # 1..10 value is shown
}
```

`@int_container`は整数値のみが保存できる`Vector`になります。浮動小数点数値なども代入できますが、整数値に暗黙的に変換されます。

```ruby
@int_container = Tatara::IntVector.new
@int_container << 4.2
# => Push 4
```

ちなみに、文字列を保存しようとするとエラーになります。

```ruby
@int_container = Tatara::IntVector.new
@int_container << "42"
# => Error!
```

このようにTataraでは簡単な型が使えるようにしてあります。

### 作ろうと思ったきっかけ

Ruby自体は非常に書きやすく、楽しくコーディングができる素晴らしい言語です。

ですが、元々静的型付け言語(C/C++など)を触ることが多かったため「Rubyにも型のようなものが欲しいなぁ……」と思うこともありました。

そんな折、[Rice](https://github.com/jasonroelofs/rice)や[extpp](https://github.com/red-data-tools/extpp)などでRubyExtensionを作れそうだとわかり、「面白そうなので作ってみよう
！」となり作ってみました。

### やったこと

開発初期のころは[Rice](https://github.com/jasonroelofs/rice)でのRubyExtension作成についてドキュメントや記事などを漁りに漁っていました。で、Riceで実装できそうなことがわかったので少しづつ実装を進めていった感じですね。

最初はIntegerやFloatなどを実装し、その後VectorやMapなどを実装していきました。

v0.1.0をリリースした辺りでGCへの管理追加などがややこしいことが判明し、Riceを外す方法を模索し始めました。[extpp](https://github.com/red-data-tools/extpp)などでのRubyExtension実装ついても調べたりしました。

結果として、Ruby自体の実装コードなどを読みつつ、以下の記事を参考にRiceを外していくことにしました。そのおかげでRubyの実装なども読むことができたので良い経験が積めたと思います。

#### Riceでの実装

Riceでは以下のようにC++で作成したクラスをラップしてRuby向けに使用できるようにできます。

```cpp
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Integer {
    int value = 0;

    public:
        Integer();
        ~Integer();
        int assignment(const int var);
        int return_value();
};

Integer::Integer() {}

Integer::~Integer(){}

int Integer::assignment(const int var) {
    return this->value = var;
}

int Integer::return_value() {
    return this->value;
}

extern "C" {
    void Init_tatara() {
        Module rb_mTatara = define_module("Tatara");

        Data_Type<Integer> rbcInteger = define_class_under<Integer>(rb_mTatara, "Integer")
            .define_constructor(Constructor<Integer>())
            .define_method("value", &Integer::return_value)
            .define_method("value=", &Integer::assignment);
    }
}
```

RiceではC++で定義したクラスを`Data_Type`でRubyのクラスへとラップしてくれます。またモジュール名などは`Module rb_mTatara = define_module("Tatara");`のように作成することができます。

```ruby
Module rb_mTatara = define_module("Tatara");

Data_Type<Integer> rbcInteger = define_class_under<Integer>(rb_mTatara, "Integer")
```

上記のコードでは`Tatara`モジュール内に`Integer`クラスを作成しています。

```
.define_constructor(Constructor<Integer>())
```

またこのコードではC++の`Integer`クラスのコンストラクタを`new`として利用しています。

あとは、以下のように必要なメソッドを`define_method`でクラスに追加しています。

```ruby
.define_method("value", &Integer::return_value)
.define_method("value=", &Integer::assignment);
```

`define_method`はメソッド名と実行するメソッドを引数として渡すことで、C++側で定義したメソッドをRuby側で使用することができます。

これにより、以下のようなRubyのコードが実行できます。

```ruby
@integer = Tatara::Integer.new
@integer.value = 42
puts @integer.value
# => 42
```

このようにRiceを使うことでC++で定義したクラスをRubyで使用することができます。

ちなみに、テンプレートなどを使う場合は以下のように型を指定する必要があります。

```cpp
Data_Type<CppArray<int>> rb_cIntArray = define_class_under<CppArray<int>>(rb_mTatara, "IntArray")
    .define_constructor(Constructor<CppArray<int>>());
```

#### C++への実装書き直し

先ほどRiceで書いていたコードは以下のように書き直すことができます。

```ruby
#include <ruby.h>

class Integer {
    int value = 0;

    public:
        Integer();
        ~Integer();
        int assignment(const int var);
        int return_value();
};

Integer::Integer() {}

Integer::~Integer(){}

int Integer::assignment(const int var) {
    return this->value = var;
}

int Integer::return_value() {
    return this->value;
}

static Integer *getInteger(VALUE self) {
    Integer *ptr;
    Data_Get_Struct(self, Integer, ptr);
    return ptr;
}

static void wrap_int_free(Integer *ptr) {
    ptr->~Integer();
    ruby_xfree(ptr);
}

static VALUE wrap_int_alloc(VALUE klass) {
    void *p = ruby_xmalloc(sizeof(Integer));
    p = new Integer;
    return Data_Wrap_Struct(klass, NULL, wrap_int_free, p);
}

static VALUE wrap_int_init(VALUE self) {
    Integer *p = getInteger(self);
    p = new Integer;
    return Qnil;
}


static VALUE wrap_int_return_value(VALUE self) {
    const int value = getInteger(self)->return_value();
    VALUE result = INT2NUM(value);
    return result;
}

static VALUE wrap_int_assignment(VALUE self, VALUE value) {
    const int v = NUM2INT(value);
    const int r = getInteger(self)->assignment(v);
    VALUE result = INT2NUM(r);
    return result;
}

extern "C" {
    void Init_tatara() {
        VALUE mTatara = rb_define_module("Tatara");

        VALUE rb_cInteger = rb_define_class_under(mTatara, "Integer", rb_cObject);

        rb_define_alloc_func(rb_cInteger, wrap_int_alloc);
        rb_define_private_method(rb_cInteger, "initialize", RUBY_METHOD_FUNC(wrap_int_init), 0);
        rb_define_method(rb_cInteger, "value", RUBY_METHOD_FUNC(wrap_int_return_value), 0);
        rb_define_method(rb_cInteger, "value=", RUBY_METHOD_FUNC(wrap_int_assignment), 1);
    }
}
```

C++のクラスをそのままRuby側で呼び出すことはできないので、以下のような関数を作成し、Ruby側から呼び出せるようにしています。

```cpp
static Integer *getInteger(VALUE self) {
    Integer *ptr;
    Data_Get_Struct(self, Integer, ptr);
    return ptr;
}

static void wrap_int_free(Integer *ptr) {
    ptr->~Integer();
    ruby_xfree(ptr);
}

static VALUE wrap_int_alloc(VALUE klass) {
    void *p = ruby_xmalloc(sizeof(Integer));
    p = new Integer;
    return Data_Wrap_Struct(klass, NULL, wrap_int_free, p);
}

static VALUE wrap_int_init(VALUE self) {
    Integer *p = getInteger(self);
    p = new Integer;
    return Qnil;
}
```

モジュールは`rb_define_module`で作成でき、以下のように`Tatara`というモジュールを作成しています。

```cpp
VALUE mTatara = rb_define_module("Tatara");
```

また、以下のコードでは`Integer`クラスを`Tatara`モジュール内に作成し、その基底クラスに`Object`クラスを使用しています。

```cpp
VALUE rb_cInteger = rb_define_class_under(mTatara, "Integer", rb_cObject);
```

Rubyのソースコード内では`VALUE`型を使うことでインスタンスの値などを取得することができています。

`getIntegr`ではインスタンス自身からC++のクラスのポインタを取得し、そのポインタを使ってクラスで定義されているメソッドを実行しています。
例えば以下のように`return_value`メソッドを呼び出すことができます。

```cpp
static VALUE wrap_int_return_value(VALUE self) {
    const int value = getInteger(self)->return_value();
    VALUE result = INT2NUM(value);
    return result;
}
```

また`wrap_int_free`ではRubyのGCでメモリが解放される際の処理を実装しています。

```cpp
static void wrap_int_free(Integer *ptr) {
    ptr->~Integer();
    ruby_xfree(ptr);
}
```

`wrap_int_alloc`はRubyのコードで新しく`new`でインスタンスを作成した際のアロケーションを実装しています。

```cpp
static VALUE wrap_int_alloc(VALUE klass) {
    void *p = ruby_xmalloc(sizeof(Integer));
    p = new Integer;
    return Data_Wrap_Struct(klass, NULL, wrap_int_free, p);
}
```

`rb_define_alloc_func`でアロケーション時に実行する関数を指定できます。

```cpp
rb_define_alloc_func(rb_cInteger, wrap_int_alloc);
```

`wrap_int_init`は`initialize`を実装しています。

```cpp
static VALUE wrap_int_init(VALUE self) {
    Integer *p = getInteger(self);
    p = new Integer;
    return Qnil;
}
```

最後に、以下のようにC++で定義されたクラスのメソッドを実行するラップ関数を作成します。

```cpp
static VALUE wrap_int_return_value(VALUE self) {
    const int value = getInteger(self)->return_value();
    VALUE result = INT2NUM(value);
    return result;
}

static VALUE wrap_int_assignment(VALUE self, VALUE value) {
    const int v = NUM2INT(value);
    const int r = getInteger(self)->assignment(v);
    VALUE result = INT2NUM(r);
    return result;
}
```

`INT2NUM()`や`NUM2INT()`はCとRubyとの間でデータの変換を行う処理になります。この処理を間に挟むことでRuby内の変数の値などをC++側に渡すことができます。

あとは、作成したラップ関数を以下のようにメソッドとして追加します。

```cpp
rb_define_method(rb_cInteger, "value", RUBY_METHOD_FUNC(wrap_int_return_value), 0);
rb_define_method(rb_cInteger, "value=", RUBY_METHOD_FUNC(wrap_int_assignment), 1);
```

`rb_define_method`は第一引数にメソッドを追加するクラスの`VALUE`を渡します。第二引数にはメソッド名、第三引数には実行する関数を渡します。  
`RUBY_METHOD_FUNC`では渡している関数ポインタをRubyのメソッドとしてキャストしています。  
第四引数は、そのメソッドが受け取る引数の数になります。`0`の場合は引数はなく、`1`などの場合はその数だけ引数を受け取ります。なお、`-1`の場合は可変長引数として受け取っているようです(RubyのArrayクラスの実装などで見る限り）。

このようにしてC++でRubyのクラスを作成することができます！

### おわりに

今回作成した`Tatara`の紹介をしつつ、C++でのRuby拡張の実装などについても触れてみました。普段使っているRubyの内部コードについても少し触れましたので、興味が湧いた方もおられるかもしれません。

#### ここから宣伝。

なお、東京近隣の方であればRuby Hacking Challenge HolidayというRubyの実装などをRubyコミッターから直々に聴けるというイベントがあります！  
ちなみに、次回は「Ruby 2.7新機能紹介」や「Rails Girls Tokyo, More! 参加者の皆様と合同LT大会」を行うようです。

[Ruby Hack Challenge Holiday #9 Ruby 2.7 + 年末LT大会](https://rhc.connpass.com/event/155899/)

この記事を読んでRubyの内部に興味を持った方は是非参加してみるといいでしょう。

ちなみに、島根県浜田市ではRuby Hacking Challenge in Hamada.rbをHamada.rb内で開催しています。  
僕のほうで内部のコードなど解説を行いますので、興味のある方は是非ご参加ください（なお、わかる範囲になりそうですが……）

[Ruby Hacking Challenge in Hamada.rb](https://hamadarb.connpass.com/event/155642/)


#### 宣伝終わり

今後もゆっくりと`Tatara`の実装を行いつつ、Ruby自体の実装への理解を深めていきたいと思います。

### 参考資料

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

[Rubyの拡張ライブラリの作り方](https://github.com/ruby/ruby/blob/master/doc/extension.ja.rdoc)

[ko1/rubyhackchallenge](https://github.com/ko1/rubyhackchallenge)

[ruby/ruby](https://github.com/ruby/ruby)