Edited at

【超入門】キミにも作れる! Ruby拡張ライブラリ開発

More than 1 year has passed since last update.


はじめに

これは、RubyKaigi 2018 に参加した筆者が Exploring Internal Ruby Through C Extensions のセッションに触発されて書いた記事です。

RubyはC言語で拡張ライブラリを書きやすいようにデザインされています。

元となるRubyスクリプトをC言語の拡張ライブラリに書き変えていくことで、それを説明してみたいと思います。


Ruby スクリプト

たとえば、以下のスクリプトをC言語を使って、拡張ライブラリの形で書くとどうなるか説明をしてみます。


bar.rb

class Bar

def len(arg)
arg.size
end
end


下準備をする

Cのコードを書き始める前に下準備をします。

まずは、rbenv で ruby をインストールします。逆に言えば、rbenv で ruby をインストールできるようになっていれば、拡張ライブラリを作成する環境は整っているということです。(rbenv 以外をお使いの方、ごめんなさい。)

作業用のディレクトリ bar を作成します。

$ mkdir bar


スクリプトの1行目: class Bar をCで書く

class Bar をCで書くには、Rubyで用意されているCのAPI rb_define_class 関数を使います。bar ディレクトリの中に以下のような bar.c を作成します。


bar/bar.c

/* Bar class を Object クラスの派生クラスとして定義する */

VALUE cBar = rb_define_class("Bar", rb_cObject);


スクリプトの2行目: Bar クラスのメソッドの def len(arg) をC言語で書く

def len(arg) の部分を C で書くには、Rubyで用意されている rb_define_method 関数を使います。


bar/bar.c

/*

* Barクラスのメソッド len を定義する。
* lenメソッドの中身は、C言語のfbar_len 関数で定義する。
* lenメソッドの引数は、1つ。
*/

rb_define_method(cBar, "len", fbar_len, 1);


スクリプトの3行目: メソッド len の中身の arg.size をC言語で書く

Bar#len メソッドの中身 arg.size をC言語で書くには、 fbar_len 関数を定義します。その関数の中で、 rb_funcall を使います。

rb_funcall を使って、 size メソッドを呼び出すために rb_intern 関数を使います。

呼び出した結果を Bar#len の戻り値とするためには、 C言語の return を使います。

今回は、引数の self は使っていませんが、 Ruby スクリプトの self に相当する値が設定されています。


bar/bar.c

static VALUE

fbar_len(VALUE self, VALUE arg)
{
/*
* Bar#len メソッドの中身の arg.size を実行して
* その結果をBar#lenメソッドの戻り値にする
*/

return rb_funcall(arg, rb_intern("size"), 0, 0);
}


require 'bar' した時にBarクラスを使えるようにする

bar.rb で定義されている Bar クラスを他のファイルから使えるようにするためには、 require 'bar' としますが、

C言語で書いた拡張ライブラリの Bar クラスを同じように使えるようにするために、 Init_bar 関数の中に、Bar クラスと Bar#len メソッドを定義した部分を移動します。


bar/bar.c

/* require 'bar' で Bar クラスと Bar#len メソッドが定義されるようにする。*/

void
Init_bar(void)
{
/* Bar class を Object クラスの派生クラスとして定義する */
VALUE cBar = rb_define_class("Bar", rb_cObject);

/*
* Barクラスのメソッド len を定義する。
* lenメソッドの中身は、C言語のfbar_len 関数で定義する。
* lenメソッドの引数は、1つ。
*/

rb_define_method(cBar, "len", fbar_len, 1);
}



rb_xxx 関数を使えるようにする

RubyのC APIの関数を使えるようにします。


bar/bar.c

#include "ruby.h"


bar.c 全体は以下のようになります。


bar/bar.c

#include "ruby.h"


static VALUE
fbar_len(VALUE self, VALUE arg)
{
/*
* Bar#len メソッドの中身の arg.size を実行して
* その結果をBar#lenメソッドの戻り値にする
*/

return rb_funcall(arg, rb_intern("size"), 0, 0); /* -> bar.size */
}

/* require 'bar' で Bar クラスと Bar#len メソッドが定義されるようにする。*/
void
Init_bar(void)
{
/* Bar class を Object クラスの派生クラスとして定義する */
VALUE cBar = rb_define_class("Bar", rb_cObject); /* -> class Bar */

/*
* Barクラスのメソッド len を定義する。
* lenメソッドの中身は、C言語のfbar_len 関数で定義する。
* lenメソッドの引数は、1つ。
*/

rb_define_method(cBar, "len", fbar_len, 1); /* -> def len(arg) */
}



bar.c をコンパイルできるように、extconf.rb を作成する

bar.cをコンパイルするためのMakefileを自動生成するための extconf.rb を作成します。


bar/extconf.rb

require 'mkmf'

create_makefile('bar')



Makefile を作成して、make する

Makefile を作成するための bar ディレクトリで extconf.rb を実行します。

bar.so (Mac OS X の場合は、 bar.bundle) ファイルができれば成功です。

$ ruby extconf.rb

creating Makefile
$ make
compiling bar.c
linking shared-object bar.so

Mac OS X の場合

$ ruby extconf.rb

creating Makefile
$ make
compiling bar.c
linking shared-object bar.bundle


拡張ライブラリBarを使ってみる

拡張ライブラリBarを使ってみます。


bar/sample.rb


require './bar'

p Bar.new.len('abcde')
p Bar.new.len([1, 2, 3, 4, 5, 6])
p Bar.new.len(/regex/)


実行結果はこんな感じになります。



$ ruby sample.rb
5
6
Traceback (most recent call last):
1: from sample.rb:5:in `<main>'
sample.rb:5:in `len': undefined method `size' for /regex/:Regexp (NoMethodError)


まとめ


  • Rubyの拡張ライブラリは簡単に書けることがわかったと思います。(本当か?)

  • でも、拡張ライブラリとして実装するかRubyスクリプトとして実装するか、どちらがいいのか、ちゃんと考えた方が良いです。

  • Rubyが公開されてから25年が経ちました。拡張ライブラリ用のC言語APIも進化してきました。ですが、拡張ライブラリの作り方の基本は、今回、説明したようなものになります。少くとも20年ほど前に私が初めて拡張ライブラリを作った時と変わってないです。これは驚くべきことではないでしょうか?

  • 興味が湧いた人は、 extension.ja.rdoc を読んでみると良いでしょう。あとは、Rubyの ext/ 配下のソース が参考になると思います。


おまけ(その1)

今回は、説明をわかりやすくするために rb_funcall を使いました。ですが、 rb_funcall (とそれに類する関数)は、実行が遅いので、できれば、使わない方が良いです。


おまけ(その2)

C言語とRubyを書いたことがあるけれど、Rubyの拡張ライブラリを書いたことがない人は、 method_missing みたいなのは、拡張ライブラリでどうやって書くのだろうと疑問に思うかも知れませんが今回と同様に rb_define_method を使って method_missing を追加してあげれば実装できるようになってます。