結構Rubyの拡張ライブラリーを書いている方だと思っている須藤です。RubyKaigi 2017で拡張ライブラリー関連の話をする予定です。RubyKaigi 2017で私の話をより理解できるようになるために内容を紹介します。
関連リンク:
背景
たくさんRubyの拡張ライブラリーを書いてきた経験を活かして拡張ライブラリーのC APIをもっとよくできないかについて考えています。バインディングについてはRubyKaigi 2016で紹介したGObject Introspectionベースがよいと思っていますが、バインディングではないただの拡張ライブラリーはC++を活用するのがよさそうだと思っています。なぜC++を活用するのがよいと思うかは私が実現したいことに関わっています。
実現したいこと
私が実現したいことはC/C++のライブラリーを使ってRubyスクリプトを高速化することです。具体的には、xtensorというC++で実装された多次元配列ライブラリーを使ってRubyスクリプトを高速化したいです。
1つ1つの機能に対してバインディングを用意してRubyレベルで組み合わせるやり方もあります。ただ、場合によっては機能を実行する毎にRubyレベルに戻ってくるオーバーヘッドを無視できないことがあります。あると思っています。まだ実際に遭遇したわけではありませんが。
あまりよい例ではありませんが。。。たとえば、GPU上で演算をする機能があって、その機能を実行する毎にGPU上にデータを転送して演算をして演算結果をまた転送しなおすとしたら、オーバーヘッドは無視できません。まぁ、この場合は、拡張ライブラリーで一連の演算をまとめるよりも、必要な間はずっとGPU上にデータを置いておく機能をRubyレベルに用意する方が汎用的でよさそうです。最近、Apache ArrowにGPU上のデータを管理する機能が入ったので、この場合はApache Arrowと連携する機能を用意するのがよさそうです。
C++11を活用するやり方
C/C++で書かれたライブラリーを使った拡張ライブラリーを書くにはRubyが提供するC APIを使います。このC APIは悪くないのですが、Cなので書いているときに書きにくいなぁと感じることがあります。
たとえば、メソッドを定義するときに関数定義とメソッドの登録が離れるのが不便だなぁと感じます。次のようにrb_hello()
の定義とrb_define_method()
の呼び出しが離れています。
#include <ruby.h>
static VALUE
rb_hello(VALUE self)
{
return rb_str_new_cstr("Hello");
}
void
Init_hello(void)
{
VALUE rb_cHello = rb_define_class("Hello", rb_cObject);
rb_define_method(rb_cHello, "hello", rb_hello, 0);
}
あとは、例外が発生したときにキレイにリソースを開放するためにrb_rescue()
やrb_ensure()
を使うときが面倒です。
他には、RubyのオブジェクトをCの値に変換する各種APIに統一感がないのも地味に使い勝手が悪いです。たとえば、Rubyのオブジェクトをbool
に変換するにはRTEST()
を使いますし、int
に変換するにはNUM2INT()
を使います。
C++11以降の最近のC++を使うことで今のC APIをもっと便利にできます。
たとえば、C++11にはラムダ式があります。これを活用することで次のようにdefine_method()
で直接メソッドを定義できます。これはExt++というライブラリーを使っています。
#include <ruby.hpp>
extern "C" void
Init_hello(void)
{
rb::Class("Hello").
define_method("hello",
[](VALUE self) { // ←ラムダ式
return rb_str_new_cstr("Hello");
});
}
Rubyでdefine_method
を使うと次のような書き方になりますが、少し似ていますね。
class Hello
define_method(:hello) do
"Hello"
end
end
C++11を活用するやり方のメリット・デメリット
このようなC++11を活用するやり方のメリットは次の通りです。
-
より完結に書ける
-
ラムダ式:その場で関数を定義できる
-
auto
:型推論を使うことで必要な型だけ書けばすむようになる -
range-based for loop
:従来のfor (int i = 0; i < n; ++i)
だけでなく、Rubyのeach
のように自分でインデックスを回さなくてもfor
を使える
-
-
既存のRubyのC APIも使える
- 拡張ライブラリーを書いたことがある人なら徐々に便利APIに移行できる
-
C/C++のライブラリーをそのまま使える
-
(Ruby用のじゃなくてC++用の)バインディングを用意する必要がない
-
たとえば、Rustを使うならバインディングを用意する必要がある
-
-
デバッグしやすい
- 普通にGDB/LLDBを使える
-
最適化しやすい
- 「Feature #13434 better method definition in C API」関連のAPIの改良にも使えるかも
簡単に言うと、既存の資産を活用しつつ便利になるよ、という感じです。
一方、デメリットは次の通りです。
-
C++には難しい機能がたくさんあるので油断するとメンテナンスしにくくなる
- たとえばテンプレート
-
ビルドが遅い
-
C++の例外とRubyの例外は相性が悪い
- Rubyの例外は
setjmp()
/longjmp()
で実装されているのでRubyの例外が発生すると、スコープを抜けたC++のオブジェクトのデストラクターが呼ばれない
- Rubyの例外は
-
古い環境だとC++11を使うのが大変
- たとえば、CentOS 6の標準パッケージの
g++
では使えない
- たとえば、CentOS 6の標準パッケージの
例外に関してはライブラリーでカバーする方法があるので、基本的にはC++に起因するデメリットになります。
このようなデメリットはあるものの、適切に使えば十分メリットの方が大きくなると思っています。Ruby本体にC++のAPIがあってもいいのではないかと考えていた時期もあったのですが、RubyKaigi 2017の資料をまとめていたら少し落ち着いてきて、今は、もう少し検討してよさそうなら提案しよう、くらいに思っています。
C++11を活用する以外のやり方
以前からもっと便利に拡張ライブラリーを書きたいという人たちがいます。私はC++11を活用するアプローチがよいと思っていますが、他のアプローチも紹介します。
大きく分けて3つのアプローチがあります。
-
Rubyを拡張して拡張ライブラリーも書けるようにする
-
C以外の言語で拡張ライブラリーを書けるようにする
-
C APIを使いつつ便利APIで改良する
最後のアプローチがC++11を活用するアプローチです。
最初の「Rubyを拡張する」アプローチはRubexのアプローチです。Rubyに追加の構文を導入して拡張ライブラリーも書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。
Pythonでは同様のアプローチで成功しているプロダクトがあります。それがCythonです。CythonはPythonでデータ分析をする界隈では広く使われています。(使われているように見えます。)
私はこのアプローチはあまりスジがよくないと感じています。理由は次の通りです。
-
一見使いやすそうだが結局使いにくいAPI
-
Rubyっぽい構文なのでRubyユーザーにも使いやすいような気がするが、実際はところどころに違いがあって、結局Rubyではない言語なので使いにくさにつながる
-
Rubyっぽく書けるのでCの知識は必要なさそうに思えるが、libffiを使うときのように結局Cの知識は必要になる
-
RubyとCだけでなくRubexの知識も必要になり、結局覚えることは結構多い
-
-
メンテナンスが大変
-
Rubyが新しい構文を導入したら追従する必要がある
-
Rubyの構文と衝突しないようにRubexを拡張していく必要がある
-
-
デバッグが大変
- Rubexが生成したCのコードをベースにデバッグする必要がある
ただ、Cythonが成功している事実と、ちょっとした拡張機能を書く分にはRubyの知識と少しのRubexの知識だけでよい(Cのことはあまり知らなくてよい)という事実があるので、もしかしたらそんなにスジは悪くないのかもしれません。数年後も開発が継続していたら再度検討してみたいです。
2番目の「C以外の言語を使う」アプローチはHelixのアプローチです。Rustで拡張ライブラリーを書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。
私はC/C++のライブラリーを使いたいのでこのアプローチは私の要件にはマッチしないのですが、高速化のために処理を全部で自分で実装する(あるいはRustのライブラリーを活用して実装する)場合はマッチしそうな気がします。
このアプローチのメリットは、Rustを知っているならCで書くよりもちゃんとしたプログラムをすばやく書けることです。デメリットはRubyのC APIのフル機能を使えない(使うためにはメンテナンスを頑張る必要がある)ことです。たとえば、Ruby 2.4からrb_gc_adjust_memory_usage()
というAPIが導入されましたが、Rustからこの機能を使うためにはバインディングを用意する必要があります。つまり、RubyのC APIの進化にあわせてメンテナンスしていく必要があります。
C++を活用する方法
最後に現時点でC++を活用する方法を紹介します。
1つがRiceを使う方法です。RiceはC++で拡張ライブラリーを書けるようにするライブラリーです。10年以上前から開発されています。C++でPythonの拡張ライブラリーを書けるようにするBoost.Pythonに似ています。
例外の対応やメソッドのメタデータとして引数のデフォルト値を指定できるなど便利な機能が揃っています。ただし、昔から開発されているライブラリーで現在はメンテナンスモードなため、C++11への対応はそれほど活発ではありません。メンテナーは反応してくれるので自分がコードを書いて開発に参加するのはよいアプローチだと思います。
もう1つがExt++を使う方法です。Ext++もC++で拡張ライブラリーを書けるようにするライブラリーです。私が作り始めました。RiceはRubyのCのオブジェクトをすべてラップしてC++で自然に扱えるようにするようなAPIです。つまり、できるだけRubyのC APIを使わずにすむようにしたいようなAPIです。私は、もっとC APIが透けて見えるような薄いAPIの方が使いやすいのではないかという気がしているので、その実験のためにExt++を作り始めました。薄いAPIの方が使いやすいのか、結局Riceくらいやらないと使いやすくないのかはまだわかっていません。Red Data Toolsのプロダクトで使って試し続けるつもりです。
まとめ
RubyKaigi 2017で拡張ライブラリーを書きやすくするためにC++がいいんじゃない?という話をします。
おしらせ
去年もスポンサーとしてRubyKaigiを応援しましたが、今年もスポンサーとしてRubyKaigiを応援します。去年と違って今年はブースはありません。懇親会などで見かけたら声をかけてください。拡張ライブラリーに興味のある人と使いやすいAPIについて話をしたいです!
あと、RubyKaigi 2017の2日目の午後に通常のセッションと並行して「RubyData Workshop」というワークショップが開かれる予定です。まだRubyKaigi 2017のサイトには情報はありませんが、時期に情報が載るはずです。このワークショップではPyCallとRed Data Toolsの最新情報を手を動かして体験することができます。Rubyでデータ処理したい人はぜひお越しください!