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

# はじめに

これは、RubyKaigi 2018 に参加した筆者が [Exploring Internal Ruby Through C Extensions](http://rubykaigi.org/2018/presentations/Yuryu.html#may31) のセッションに触発されて書いた記事です。

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

## Ruby スクリプト

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

```ruby: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` を作成します。

```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` 関数を使います。

```c: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 に相当する値が設定されています。


```c: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 メソッドを定義した部分を移動します。


```c: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の関数を使えるようにします。

```c:bar/bar.c
#include "ruby.h"
```

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

```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 を作成します。

```ruby:bar/extconf.rb
require 'mkmf'

create_makefile('bar')
```

## Makefile を作成して、make する

Makefile を作成するための bar ディレクトリで extconf.rb を実行します。
bar.so (Mac OS X の場合は、 bar.bundle) ファイルができれば成功です。

```shell-session
$ ruby extconf.rb
creating Makefile
$ make
compiling bar.c
linking shared-object bar.so
```

Mac OS X の場合

```shell-session
$ ruby extconf.rb
creating Makefile
$ make
compiling bar.c
linking shared-object bar.bundle
```

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

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

```ruby: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/)

```

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

```shell-session

$ 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](https://github.com/ruby/ruby/blob/trunk/doc/extension.ja.rdoc) を読んでみると良いでしょう。あとは、Rubyの [ext/ 配下のソース](https://github.com/ruby/ruby/tree/trunk/ext) が参考になると思います。

## おまけ(その1)

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

## おまけ(その2)

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