mrbgemsをCとmrubyで作ってみた話

mrubyのgem、mrbgemsを作って遊んでみました。
環境はMac OS X 10.10.5 Yosemiteです。

# mruby のビルド
まずは mruby のソースコードを落としてきてビルドします。
mruby のビルドには、C コンパイラ等のツールや、Ruby1.8 以上が必要になります。[^1]
Mac の場合、XCode Command Line Tools を入れておけば OK なはずです。Ruby は Mac であれば最初から入ってますので、それを利用出来ます。[^2]

```shell-session
% git clone git@github.com:mruby/mruby.git
% cd mruby
% ruby ./minirake
```

これで以下のファイルが出来上がります。

* ./bin/mirb
* ./bin/mrbc
* ./bin/mrdb
* ./bin/mruby
* ./bin/mruby-strip

./bin/mruby が mruby 本体です。 ./bin/mirb が CRuby でいうところの irb にあたります。
他のファイルは本記事では触れません。（ちゃんと調べてなくてあまり理解出来ていません。すいません）

# mrbgems について
mruby はビルドの設定を、build_config.rb に記述します。
このファイルで、mrbgems を参照しておくとビルド時に mrbgems のコードが取得され一緒にビルドされます。
そして mrbgems の機能が組み込まれた ./bin/mruby などが出来上がるようになっています。
CRuby の gem のように依存関係を自動で解決する機能は無いようです。

```rb:build_config.rb
MRuby::Build.new do |conf|
  # load specific toolchain settings

  # Gets set by the VS command prompts.
  if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
    toolchain :visualcpp
  else
    toolchain :gcc
  end

  enable_debug

  # Use mrbgems
  # conf.gem 'examples/mrbgems/ruby_extension_example'
  # conf.gem 'examples/mrbgems/c_extension_example' do |g|
  #   g.cc.flags << '-g' # append cflags in this gem
  # end
  # conf.gem 'examples/mrbgems/c_and_ruby_extension_example'
  # conf.gem :github => 'masuidrive/mrbgems-example', :checksum_hash => '76518e8aecd131d047378448ac8055fa29d974a9'
  # conf.gem :git => 'git@github.com:masuidrive/mrbgems-example.git', :branch => 'master', :options => '-v'

  # include the default GEMs
  conf.gembox 'default'

#  〜
# (略)
#  〜
```
ビルドの設定で、mrbgems を参照している部分ですが ```config.gem``` に続いて mrbgems のソースコードの場所を指定しています。``` # Use mrbgems ``` の行以下にいくつかあるサンプルの通り、指定方法にはローカルのファイルの場所や、git, github のリポジトリを指定出来ます。
また、gembox で複数の mrbgems のまとまりを一度に指定出来ます。mrbgem は自動で依存関係の解決などを行わないので、gembox を使って依存関係にある gem を扱うようです。

ちなみに mruby コマンドや mirb コマンドも mrbgems として作らています。

# mrbgems を作る
それでは、mrbgems を作っていきます。今回は、mruby の Array クラスを拡張して fizzbuzz メソッドを追加する mrbgems を作ります。せっかくなので、mruby による実装と C 言語による実装の両方でやってみようと思います。

## mruby-mrbgem-template の導入
mrbgems を作るときにテンプレートを作成してくれる mruby-mrbgem-template をインストールします。
mruby-mrbgem-template 自信も mrbgems なので、build_config.rb に設定を書いてインストールできます。

build_config.rb を以下のように書き換えます。

```ruby:build_config.rb
MRuby::Build.new do |conf|
  # load specific toolchain settings

  # Gets set by the VS command prompts.
  if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
    toolchain :visualcpp
  else
    toolchain :gcc
  end

  enable_debug

  # Use mrbgems
  conf.gem :git => 'https://github.com/iij/mruby-io.git'
  conf.gem :git => 'https://github.com/iij/mruby-dir.git'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-mrbgem-template.git'

  # include the default GEMs
  conf.gembox 'default'
end
```

```sh
% ruby ./minirake
```

これで、mruby-mrbgem-template 入りの mruby がビルドされました。

次に以下の様な Ruby スクリプトを書きます。

```ruby:create.rb
params = {
  :mrbgem_name    => 'mruby-fizzbuzz',
  :license        => 'MIT',
  :github_user    => 'ore-public',
  :mrbgem_prefix  => '..',
  :class_name     => 'FizzBuzz',
  :author         => 'Masaya Konishi',
}

c = MrbgemTemplate.new params
c.create
```

```sh
% ./bin/mruby create.rb
% cd ../mruby-fizzbuzz
```

これで、mruby-fizzbuzz というディレクトリの中に mrbgems の雛形が作成されました。
ディレクトリの中の構成は以下のようになっています。

```
.
├── LICENSE
├── README.md
├── mrbgem.rake
├── mrblib
│   └── mrb_fizzbuzz.rb
├── mruby-fizzbuzz.gem
├── src
│   ├── mrb_fizzbuzz.c
│   └── mrb_fizzbuzz.h
└── test
    └── mrb_fizzbuzz.rb
```

```mrblib``` が mruby での実装を書く所。```src```が C 言語での実装を書く所。
```test```は名前の通りテストコード置き場です。

## C による実装
まずは C 言語で実装します。
既存の Array クラスにメソッドを拡張するので、[mruby-array-extの実装](https://github.com/mruby/mruby/blob/master/mrbgems/mruby-array-ext/src/array.c) が参考になります。

以下が実装の全体です。

```c
/*
** mrb_fizzbuzz.c - FizzBuzz class
**
** Copyright (c) Masaya Konishi 2014
**
** See Copyright Notice in LICENSE
*/

#include "mruby.h"
#include "mruby/value.h"
#include "mruby/array.h"
#include "mruby/range.h"
#include "mruby/hash.h"
#include "mruby/data.h"
#include "mrb_fizzbuzz.h"

static mrb_value mrb_fizzbuzz(mrb_state *mrb, mrb_value self)
{
  mrb_int i;
  mrb_value v, value, fizzbuzz;

  value = mrb_ary_new_capa(mrb, RARRAY_LEN(self));
  for (i = 0; i < RARRAY_LEN(self); ++i) {
    v = RARRAY_PTR(self)[i];

    if((mrb_fixnum(v) % 15) == 0){
      fizzbuzz = mrb_str_new_cstr(mrb, "FizzBuzz");
    } else if((mrb_fixnum(v) % 5) == 0) {
      fizzbuzz = mrb_str_new_cstr(mrb, "Buzz");
    } else if((mrb_fixnum(v) % 3) == 0) {
      fizzbuzz = mrb_str_new_cstr(mrb, "Fizz");
    } else {
      fizzbuzz = v;
    }
    mrb_ary_set(mrb, value, i, fizzbuzz);
  }

  return value;
}

void mrb_mruby_fizzbuzz_gem_init(mrb_state *mrb)
{
    struct RClass *a = mrb->array_class;

    mrb_define_method(mrb, a, "fizzbuzz", mrb_fizzbuzz, MRB_ARGS_REQ(1));
}

void mrb_mruby_fizzbuzz_gem_final(mrb_state *mrb)
{
}
```

まずは、以下の部分を解説します。

```c
void mrb_mruby_fizzbuzz_gem_init(mrb_state *mrb)
{
    struct RClass *a = mrb->array_class;

    mrb_define_method(mrb, a, "fizzbuzz", mrb_fizzbuzz, MRB_ARGS_REQ(1));
}
```

```mrb_mruby_fizzbuzz_gem_init``` は、初期化処理です。作成する mrbgems のクラスを定義したりメソッドを定義したりします。今回は Array クラスの拡張なので既存の Array クラスの実態を ```a``` に入れて、そこに ```fizzbuzz``` メソッドを定義しています。
```fizzbuzz``` メソッドの実態は、```mrb_define_method``` メソッドの 4 番目の引数に渡している ```mrb_fizzbuzz``` 関数です。
この関数が以下です。

```c
static mrb_value mrb_fizzbuzz(mrb_state *mrb, mrb_value self)
{
  mrb_int i;
  mrb_value v, value, fizzbuzz;

  value = mrb_ary_new_capa(mrb, RARRAY_LEN(self));
  for (i = 0; i < RARRAY_LEN(self); ++i) {  /* self サイズ分ループ */
    v = RARRAY_PTR(self)[i];

    /* fizzbuzz 判定 */
    if((mrb_fixnum(v) % 15) == 0){
      fizzbuzz = mrb_str_new_cstr(mrb, "FizzBuzz");
    } else if((mrb_fixnum(v) % 5) == 0) {
      fizzbuzz = mrb_str_new_cstr(mrb, "Buzz");
    } else if((mrb_fixnum(v) % 3) == 0) {
      fizzbuzz = mrb_str_new_cstr(mrb, "Fizz");
    } else {
      fizzbuzz = v;
    }
    /* 新しく作った Array インスタンスに fizzbuzzの結果をセット */
    mrb_ary_set(mrb, value, i, fizzbuzz);
  }

  return value;
}
```

この関数の 2 番目の引数の```mrb_value self```が、名前の通り self になります。ここでは Array のインスタンスとなります。
```mrb_ary_new_capa``` で ```self``` と同じサイズの Array インスタンス```value```を作っています。
これが最後の戻り値になっています。（ここで作る Array#fizzbuzz は破壊的な操作はしないメソッドになりますね)
後は self の値を1つずつ取り出して fizzbuzz 判定して、新しく作った ```value``` へセットしていくだけです。

## mruby による実装
次は mruby による実装をします。
既存の Array クラスにメソッドを追加するので以下のようになります。Ruby と同じで、既存のクラスをオープンしてメソッドを定義するだけです。
mruby による実装は fizzbuzz2 というメソッド名で定義しました。

```ruby:mrblib/mrb_fizzbuzz.rb
class Array
  def fizzbuzz2
    self.map do |v|
      if (v % 15) == 0
        'FizzBuzz'
      elsif (v % 3) == 0
        'Fizz'
      elsif (v % 5) == 0
        'Buzz'
      else
        v
      end
    end
  end
end
```

# 動かす
作成した mrbgems を組み込んだ mruby をビルドします。

```ruby:build_config.rb
MRuby::Build.new do |conf|
  # load specific toolchain settings

  # Gets set by the VS command prompts.
  if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
    toolchain :visualcpp
  else
    toolchain :gcc
  end

  enable_debug

  # Use mrbgems
  conf.gem :git => 'https://github.com/iij/mruby-io.git'
  conf.gem :git => 'https://github.com/iij/mruby-dir.git'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-mrbgem-template.git'

  # include the default GEMs
  conf.gembox 'default'

  conf.gem '../mruby-fizzbuzz' #追記
end
```
作成した mruby-fizzbuzz へのPATHを config.gem へ渡します。

```sh
% ruby ./minirake
```

これでビルド出来たので動かしてみましょう。

```sh
% ./bin/mirb                                                                                                                                   [master] [2.2.3-p173]
mirb - Embeddable Interactive Ruby Shell

> (1..16).to_a.fizzbuzz
 => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16]
> (1..16).to_a.fizzbuzz2
 => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16]
>
```

C 言語、mruby どちらで実装したものもちゃんと動いてますね！

# その他
Array クラスのメソッドとして定義したのに、要素が Fixnum しか想定してない実装になっています。
他のオブジェクトが入ってたらどうなるでしょうか。

まずは mruby で実装した fizzbuzz2 の方から。

```sh
> [1,"a",{hoge: 1}].fizzbuzz2
/Users/masaya/projects/mruby-fizzbuzz/mrblib/mrb_fizzbuzz.rb:4: undefined method '%' for {:hoge=>1} (NoMethodError)
```

% メソッドが定義されてないオブジェクトが要素に入っていると落ちます。

次に C 言語で実装した fizzbuzz。

```sh
> [1,"a",{hoge: 1}].fizzbuzz
 => [1, "a", {:hoge=>1}]
```

動いてるとは言えないですが落ちませんね。なんで通ってしまうのか、一応確認します。
以下に実装の一部分を載せます。

```c
mrb_value v, value, fizzbuzz;
/*
         〜
        (略)        
         〜
*/
    if((mrb_fixnum(v) % 15) == 0){
```

ここで変数```v```は Array の要素です。
```v``` の方は ```mrb_value```ですね。```mrb_value```は C 言語レベルで mruby のオブジェクトを扱う変数で、オブジェクトへのポインタだったり、数値が入っていたりします。
定義はこんな風になってます。

```c:mruby/boxing_no.h
typedef struct mrb_value {
  union {
    mrb_float f;
    void *p;
    mrb_int i;
    mrb_sym sym;
  } value;
  enum mrb_vtype tt;
} mrb_value;
```

ざっくり言うと、float や int などは値が入っていて、それ以外のオブジェクトの場合は ```void *p```で実態へのポインタを保持している型です。
本来は mrb_vtype でどんな型かを判定して(mrb_type マクロを使って)、適切なフィールドを参照しなければいけないと思うのですが、今回の実装では問答無用で```mrb_fixnum``` で整数として取得しているためポインタの値も整数として割り切れるかどうかの判定をしてしまい、動いてる（ように見える）わけですね。

ちなみに```mrb_fixnum```はマクロとして下のように定義されています。mrb_value 共用体の mrb_intのフィールドを参照するようになってますね。

```c:mruby/boxing_no.h
#define mrb_fixnum(o)   (o).value.i
```

# まとめ
このように、fizzbuzz をするだけでも mruby の実装を垣間見ることが出来て楽しいのでオススメです。

今回作成した fizzbuzz の mrbgems は https://github.com/ore-public/mruby-fizzbuzz で公開しています。

***
[^1]: 参考 https://github.com/mruby/mruby/blob/master/doc/guides/compile.md
[^2]: この記事では、rbenv でインストールした ruby2.2.3 を使いました