はじめに
この記事はRuby Advent Calendar 2019の8日目の記事です。
昨日の記事は @Nymphium さんのRubyでもalgebraic effectsがしたい!でした。
この記事は、C++でのRuby拡張実装について、つらつらと書いている記事になります。
内容としてはTataraというRubyで型を使えるライブラリを作ってみたで紹介した自作Ruby拡張を作るにあたって得たC++でのRuby拡張実装知見の記事になります。
Ruby拡張って?
皆さんが普段使っているRuby(ここではCRubyのことです)はCによって実装されています。ですので、Cを使ってRubyの拡張機能を作成することもできます。
つまり、Cで既に作成されているライブラリなどをRuby拡張として作成することができるというメリットがあります。CでRuby拡張を実装した場合、Rubyで実装するよりも高速に処理できるケースもあるようです。
TataraというRubyで型を使えるライブラリを作ってみたで紹介しているTataraでベンチマークなどを試してみた感じでは、「現在のCRubyはかなり速い」という印象ですね。
RubyKaigi 2018のExploring Internal Ruby Through C Extensionsでも「Rubyは速い」という話も出ています。
Rubyで実装できる内容のものであればRubyのほうが処理が速く、CやC++でわざわざ実装するほどでもないのかもしれません。
実際にCで拡張機能が実装されているgemとしてはsqlite3やmysql2などがあります。
またRustやC++でRubyの拡張機能を作成するケースもあります。
例えば最近面白いなぁと思ったのはRustでのRuby拡張を実装できるHelixですね。Rustを使うことでCやC++よりも安全にRuby拡張を書くことができます。
また実装コード自体もかなり読みやすく以下のようなコードでクラスとメソッドを実装できます(※ HelixのREADMEより引用)。
ruby! {
class Console {
def log(string: String) {
println!("LOG: {}", string);
}
}
}
ただHelix公式のチュートリアルではRails向けに拡張機能を実装する内容になっています。そのためRuby向けの拡張を作成する際のドキュメントがあまりなく、少し辛いところがあります。
実際にHelixでRuby拡張を作成しているものとしては以下の記事などがあります。
ref: RubyからRustを呼び出すいくつかの方法のまとめ - Qiita
ref: Rustでgemを書く際のハマりどころ in 2017
ref: Writing Ruby gems with Rust and Helix
またC++ではRiceやExt++などのRuby拡張を実装できるライブラリも存在しています。
RubyKaigi 2017ではImprove extension API: C++ as better language for extensionにてC++でのRuby拡張実装について紹介されています。
興味のある方はそちらも確認してみると良いでしょう。
今回はC++でのRuby拡張の実装方法について解説します。具体的にはRiceやExt++、C++のみでの実装方法などを解説していきます。
つくるもの
今回は、Hello
というクラスを作成し、Hello Ruby Extension!
と画面に表示するsay
というメソッドを実装します。
具体的には以下のようなコードが実行できるRuby拡張を実装していきます。
require 'hello'
Hello.new.say
# => "Hello Ruby Extension!"
今回はRice、Ext++、C++でそれぞれ実装していきます。
今回の記事作成にあたって各ライブラリでの実装サンプルをGitHubに上げておきました。興味のある方はこちらも見ると良いかも。
S-H-GAMELINKS/RubyAdventCalendarExtensionSample
実装
Riceでの実装
Riceとは?
Riceとは、C++を使ってRuby拡張を簡単に作成できるライブラリになります。
RiceはgemとしてRubyGemsからインストールすることができます。
gem install rice
これでRiceが使えるようになります!
ちなみに、実際にRiceを使ったサンプルコードは以下のようになります。
#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>
using namespace Rice;
class Hello {
public:
Hello() {};
void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};
extern "C" {
void Init_hello() {
Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
.define_constructor(Constructor<Hello>())
.define_method("say", &Hello::say);
}
}
このようにRiceを使う場合、非常に簡単にRuby拡張を作ることができます。
またC++のテンプレートなどを使って以下のようなコードを書くこともできます。
template <class T>
class CppArray {
public:
CppArray<T>() {};
};
Data_Type<CppArray<int>> rb_cIntArray = define_class<CppArray<int>>("IntArray")
.define_constructor(Constructor<CppArray<int>>());
Riceを使うメリットととしては、非常に簡単にC++でのRuby拡張を作ることができる点ですね。C++のライブラリなどをRubyで使えるようにするラッパーなどは、Riceを使って実装するといいかもしれません。
デメリットとしては、日本語のドキュメントもあまりないことと、開発自体があまり活発でない印象があることですね。
日本語で書かれた記事はあまり(Rice以外での実装とかはあったりする)なく、IBMのRice を使用して Ruby の拡張機能を C++ で作成するが日本語で唯一詳しく書かれたRiceのチュートリアルになりそうです。
英語が読める方であれば、こちらのドキュメントを読み解けばよいかと思います。
GitHubのリポジトリでのコミットログなどを見るた印象ではあまり開発が活発な印象はないです。最近、いくつかPull Requestが取り込まれてはいるようですが……。
そのため、Rice側の開発が打ち切られると辛いことになりそうな気配がありますね……。
とはいえ、大きな変更が入る可能性は少ないのでとりあえずC++でのRuby拡張を作る分には良いライブラリだと思います。
実装
それでは、Riceを使ってRuby拡張を実装してみましょう。
なにはともあれ、Riceをインストールしましょう。
gem install rice
インストールが無事終了した後は、extconf.rb
というファイルを作成します。これはC++のコードをビルドするMakefile
を自動生成するためのファイルになります。CでRuby拡張を作る場合も同様にextconf.rb
を作成します。
require 'mkmf-rice'
create_makefile('hello')
mkmf-rice
はRiceを使ってかかれたC++のソースをもとにMakefile
を作成するためのライブラリになります。ちなみに、Cで拡張機能を実装する場合はmkmf
というライブラリを読み込んでMakefile
を自動生成していますね。
またcreate_makefile
に渡している文字列がビルドされた拡張ライブラリの名前になります。
次に、hello.cpp
をextconf.rb
と同じ階層に作成します。
#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>
using namespace Rice;
class Hello {
public:
Hello() {};
void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};
extern "C" {
void Init_hello() {
Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
.define_constructor(Constructor<Hello>())
.define_method("say", &Hello::say);
}
}
軽くコードの解説をすると、以下の二行でRiceのヘッダーを読み込んでいます。
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>
RiceではData_Type
を使い、既存のクラスをもとにRuby向けにコンバートしています。
Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
上記のコードではC++で定義したHello
クラスをRubyで呼び出すHello
というクラスに変換しています。
.define_constructor(Constructor<Hello>())
.define_constructor(Constructor<Hello>())
ではC++で定義したHello
クラスのコンストラクタ(Rubyでいうところのinitializeのようなもの)を使って、RubyでHello
クラスのインスタンスを作成できるようにしています。
つまり、Rubyのinitializeを実装しています。
最後に.define_method("say", &Hello::say);
でsay
というメソッドをHello
クラスに追加しています。
.define_method("say", &Hello::say);
これでC++側での実装は完了です。
次に、extconf.rb
を実行してMakefile
を生成します。
ruby extconf.rb
# => Makefileを自動生成
あとは、make
コマンドでビルドすればhello.o
とhello.so
が生成されていると思います。
make
# => hello.o と hello.so が生成される
最後に、作成したRuby拡張を実際に動かしてみましょう。hello.rb
を以下のように作成して実行してみましょう。
require './hello.so'
Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!
Hello Ruby Extension!
と表示されていればOKです!
Ext++での実装
Ext++とは?
Ext++はRice同様にC++を使って、Ruby拡張を作成できるライブラリです。
Ext++もRubyGemsで配布されているのでgemとしてインストールできます。
gem install extpp
Ext++での実装は以下のようになります。
#include <iostream>
#include <ruby.hpp>
RB_BEGIN_DECLS
void Init_hello() {
rb::Class klass("Hello");
klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
return Qnil;
});
klass.define_method("say", [](VALUE rb_self) {
std::cout << "Hello Ruby Extension!" << std::endl;
return Qnil;
});
}
RB_END_DECLS
Ext++ではC++のラムダ式を引数に渡して実装することができる点が特徴的です。そのためラムダ式をうまく使うことでRubyのメソッドとC++の実装を一度に書くことができます。
また、Ext++ではruby.hpp
をインクルードするだけで良いところも便利です。Riceの場合、必要なヘッダーを個別に読み込まなければならず
Riceではラムダ式を使ってメソッドの定義などはできないため、ラムダ式でメソッドを定義したい人はExt++を使うと良いかもしれません
Ext++を使うメリットとしては、実装が一か所で済む点かなと思います。また開発者が日本の方(というか @ktou さん)ですので開発者本人にあって話が聴けるという点もメリットかもしれません。
デメリットとしては、サンプルのコードが一つしかなく、人によってはどのように実装を進めていけばいいのかが分かりにくい時がある点でしょうか?その点に関しては今後Pull Requestなどでサンプルコードを投げれたらと思っていますね。
また開発バージョンであり、今後のバージョンアップでは大きな変更も入る可能性もありそうです。
しかしながら、開発者本人に直接話を聞くことができそう(日本人からすると)なので採用するメリットはかなり大きいと思います。
またRiceと違い、Rubyの実装自体に近い実装コードを書くのでCRubyの実装を学んでみたいという人にもオススメかもしれませんね。
実装
それでは、Ext++を使ってRuby拡張を実装していきましょう。
まずはExt++をインストールします。
gem install extpp
インストール完了後、Riceでの実装の時と同じようにextconf.rb
を作成します。
require 'extpp'
create_makefile('hello')
Riceの時とおおよそ同じコードですね。違う点としてはmkmf-rice
ではなく、extpp
を読み込んでいます。
次に、hello.cpp
をextconf.rb
と同じ階層に作成します。
#include <iostream>
#include <ruby.hpp>
RB_BEGIN_DECLS
void Init_hello() {
rb::Class klass("Hello");
klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
return Qnil;
});
klass.define_method("say", [](VALUE rb_self) {
std::cout << "Hello Ruby Extension!" << std::endl;
return Qnil;
});
}
RB_END_DECLS
Ext++ではrb::Class
で新しいクラスを作成します。また、作成したklass
とdefine_method
を使うことで必要なメソッドを新しく定義しています。
Qnil
はRubyでのnil
を返しています。CRubyのメソッドなどでnil
が返ってきているメソッドでは、子のようにreturn Qnil;
と書かれています。興味のある方はRuby Hack Challenge Holidayに参加したり、GitHubのruby/rubyのコードを読んでみると良いかもしれません。
あとは。extconf.rb
を実行し、Makefile
を生成します。
ruby extconf.rb
# => Makefileが生成される
その後、make
で作成したRuby拡張をビルドします。
make
# => hello.o と hello.soが生成される
最後にhello.rb
を以下のように作成し、実行してみましょう。
require './hello.so'
Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!
Hello Ruby Extension!
と表示されていればOKです!
C++での実装
実装
最後にC++でのみで作成するRuby拡張について紹介します。
まずはextconf.rb
を作成します。
require "mkmf"
create_makefile("hello")
mkmf
はCRubyに添付されているRuby拡張のためのMakefile作成ライブラリですね。
次に、hello.cpp
を以下のように作成します。
#include <ruby.h>
#include <iostream>
class Hello {
public:
Hello() {};
~Hello() {};
void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};
struct WrapHello {
Hello* hello;
};
static Hello* get_hello(VALUE self) {
WrapHello *ptr;
Data_Get_Struct(self, WrapHello, ptr);
return ptr->hello;
}
static void wrap_hello_free(WrapHello *ptr) {
delete ptr->hello;
ruby_xfree(ptr);
}
static VALUE wrap_hello_alloc(VALUE klass) {
auto *ptr = RB_ALLOC(WrapHello);
ptr->hello = new(Hello);
return Data_Wrap_Struct(klass, NULL, wrap_hello_free, ptr);
}
static VALUE wrap_hello_init(VALUE self) {
return Qnil;
}
static VALUE wrap_hello_say(VALUE self) {
get_hello(self)->say();
return Qnil;
}
extern "C" {
void Init_hello() {
VALUE rb_cHello = rb_define_class("Hello", rb_cObject);
rb_define_alloc_func(rb_cHello, wrap_hello_alloc);
rb_define_private_method(rb_cHello, "initialize", RUBY_METHOD_FUNC(wrap_hello_init), 0);
rb_define_method(rb_cHello, "say", RUBY_METHOD_FUNC(wrap_hello_say), 0);
}
}
ポイントとしては#include <ruby.h>
でRuby拡張の実装で使用するマクロや関数などを呼び出している点ですね。これがないとRuby拡張を実装することができません。
またget_hello
関数はRubyのインスタンスを引数に受け取って、C++のインスタンスのポインタを返しています。この関数を使うことでC++のクラスのメソッドをラップ関数から呼び出して使うことができるようになります。
ちなみに、このコードではData_Get_Struct
というマクロを使用していますが、TypedData_Get_Struct
を使う方が良いようです(@ktou さんからのコメント参照)。
Ruby 2.1からは世代別GCが実装されており、古いオブジェクトから新しいオブジェクトへの参照を検知するためのライトバリアという仕組みが導入されています。このライトバリアに対応できているのがTyped_Get_Struct
などになるようです。
ですので、新しくC++でRuby拡張を作成する場合はTypedData_*
ではじまるマクロを使うと良いでしょう。
wrap_hello_free
関数はRubyのGCが呼び出された際にメモリから解放する際の処理がかかれた関数になります。
wrap_hello_alloc
はインスタンスを作成する際のアロケータになります。wrap_hello_init
はRubyでのinitialize
になりますね。
あとは、extconf.rb
を実行し、make
を実行してビルドしてみましょう
ruby extconf.rb
# => Makfileが生成される
make
# => hello.o と hello.so が生成される
最後に、hello.rb
を以下のように作成して実行しましょう。
require './hello.so'
Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!
Hello Ruby Extension!
と表示されていればOKです!
おわりに
C++でのRuby拡張についてRice、Ext++、C++それぞれでのでの実装を紹介しました。意外と簡単そうと思っていただければ幸いです。
あと今回の記事ではC++をベースに紹介しましたが、もちろんCでの実装を行う方法もあります。むしろ、そちらのほうが参考になる記事が多いので、Ruby拡張を作る際にはCで作ると良いかもしれません。
あと、この記事でRubyの実装に興味を持たれた方はRuby Hack Challenge Holidayなどに参加してみると良いかもしれません。
以前まではGitterにてRubyの実装の話などを訊くことができていましたが、ruby-jpが出来てからは#rhc
チャンネルでやり取りされているようです。ですので、Rubyの実装に興味が湧いたらruby-jpの#rhc
チャンネルへ行きましょう!
意外と簡単にC++でもRubyの拡張機能を作ることができるので、今後もC++の良さげなライブラリなどをRuby向けに実装していきたいと思います。
参考記事
ref: Rice
ref: Ext++
ref: Improve extension API: C++ as better language for extension
ref: Exploring Internal Ruby Through C Extensions
ref: Rice を使用して Ruby の拡張機能を C++ で作成する
ref: Rice - Ruby Interface for C++ Extensions
ref: ko1/rubyhackchallenge
ref: ruby/ruby
ref: C++言語で簡単なRuby拡張ライブラリを書いてみた
ref: Rubyの拡張ライブラリの作り方
ref: Rubyソースコード完全解説
ref: TataraというRubyで型を使えるライブラリを作ってみた
ref: Tatara
ref: Rubyコミッター・笹田耕一に世代別インクリメンタルGCを発想したプロセスを聞いてみた
ref: YARV Maniacs 【第 12 回】 インクリメンタル GC の導入
ref: Rubyにおけるライトバリアのないオブジェクトを考慮した世代別インクリメンタルGCの実装