はじめに
この記事は最近僕が作っているRubyで型を使えるライブラリの紹介記事です。
対象読者としては、Rubyで簡単に型のようなものを使ってみたい人、またはC++でのRuby拡張を作ってみたい人を想定しています。
Tataraの紹介をしつつ、簡単なC++でのRuby拡張実装についても触れていきます。
作ったもの
TataraというRubyで簡単な型を使えるようにするRubyExtensionを作りました。RubyGemsにもリリースしており、v0.2.0が現在リリースされている最新バージョンになります。
v0.2.0まではRiceというRubyExtensionを簡単にC++で使用していました。しかし、GCへの管理追加が少しややこしかったり、RiceでC++のコードをRuby向けにラップしているため処理が遅くなったりするという問題がありました。そこで次期リリースではC++のみでの実装に書き直し、GCへの管理追加や処理速度向上を図っています。
ちなみに、次にリリースするv0.3.0は11月中にリリースする予定で実装を進めています。
サンプルコード
例えば、Tataraを使うことで以下のようなコードを実行できます。
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
になります。浮動小数点数値なども代入できますが、整数値に暗黙的に変換されます。
@int_container = Tatara::IntVector.new
@int_container << 4.2
# => Push 4
ちなみに、文字列を保存しようとするとエラーになります。
@int_container = Tatara::IntVector.new
@int_container << "42"
# => Error!
このようにTataraでは簡単な型が使えるようにしてあります。
作ろうと思ったきっかけ
Ruby自体は非常に書きやすく、楽しくコーディングができる素晴らしい言語です。
ですが、元々静的型付け言語(C/C++など)を触ることが多かったため「Rubyにも型のようなものが欲しいなぁ……」と思うこともありました。
そんな折、RiceやextppなどでRubyExtensionを作れそうだとわかり、「面白そうなので作ってみよう
!」となり作ってみました。
やったこと
開発初期のころはRiceでのRubyExtension作成についてドキュメントや記事などを漁りに漁っていました。で、Riceで実装できそうなことがわかったので少しづつ実装を進めていった感じですね。
最初はIntegerやFloatなどを実装し、その後VectorやMapなどを実装していきました。
v0.1.0をリリースした辺りでGCへの管理追加などがややこしいことが判明し、Riceを外す方法を模索し始めました。extppなどでのRubyExtension実装ついても調べたりしました。
結果として、Ruby自体の実装コードなどを読みつつ、以下の記事を参考にRiceを外していくことにしました。そのおかげでRubyの実装なども読むことができたので良い経験が積めたと思います。
Riceでの実装
Riceでは以下のようにC++で作成したクラスをラップしてRuby向けに使用できるようにできます。
#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");
のように作成することができます。
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
でクラスに追加しています。
.define_method("value", &Integer::return_value)
.define_method("value=", &Integer::assignment);
define_method
はメソッド名と実行するメソッドを引数として渡すことで、C++側で定義したメソッドをRuby側で使用することができます。
これにより、以下のようなRubyのコードが実行できます。
@integer = Tatara::Integer.new
@integer.value = 42
puts @integer.value
# => 42
このようにRiceを使うことでC++で定義したクラスをRubyで使用することができます。
ちなみに、テンプレートなどを使う場合は以下のように型を指定する必要があります。
Data_Type<CppArray<int>> rb_cIntArray = define_class_under<CppArray<int>>(rb_mTatara, "IntArray")
.define_constructor(Constructor<CppArray<int>>());
C++への実装書き直し
先ほどRiceで書いていたコードは以下のように書き直すことができます。
#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側から呼び出せるようにしています。
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
というモジュールを作成しています。
VALUE mTatara = rb_define_module("Tatara");
また、以下のコードではInteger
クラスをTatara
モジュール内に作成し、その基底クラスにObject
クラスを使用しています。
VALUE rb_cInteger = rb_define_class_under(mTatara, "Integer", rb_cObject);
Rubyのソースコード内ではVALUE
型を使うことでインスタンスの値などを取得することができています。
getIntegr
ではインスタンス自身からC++のクラスのポインタを取得し、そのポインタを使ってクラスで定義されているメソッドを実行しています。
例えば以下のようにreturn_value
メソッドを呼び出すことができます。
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でメモリが解放される際の処理を実装しています。
static void wrap_int_free(Integer *ptr) {
ptr->~Integer();
ruby_xfree(ptr);
}
wrap_int_alloc
はRubyのコードで新しくnew
でインスタンスを作成した際のアロケーションを実装しています。
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
でアロケーション時に実行する関数を指定できます。
rb_define_alloc_func(rb_cInteger, wrap_int_alloc);
wrap_int_init
はinitialize
を実装しています。
static VALUE wrap_int_init(VALUE self) {
Integer *p = getInteger(self);
p = new Integer;
return Qnil;
}
最後に、以下のようにC++で定義されたクラスのメソッドを実行するラップ関数を作成します。
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++側に渡すことができます。
あとは、作成したラップ関数を以下のようにメソッドとして追加します。
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大会
この記事を読んでRubyの内部に興味を持った方は是非参加してみるといいでしょう。
ちなみに、島根県浜田市ではRuby Hacking Challenge in Hamada.rbをHamada.rb内で開催しています。
僕のほうで内部のコードなど解説を行いますので、興味のある方は是非ご参加ください(なお、わかる範囲になりそうですが……)
Ruby Hacking Challenge in Hamada.rb
宣伝終わり
今後もゆっくりとTatara
の実装を行いつつ、Ruby自体の実装への理解を深めていきたいと思います。