mrubyの拡張モジュールであるmrbgemのテンプレートを自動で生成するmrbgem作った

本エントリはmruby advent calendar 2013 18日目の記事になります。

17日目はtsahara@githubさんによる「mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法」という素晴らしい記事でした。今回の話題はこの話にも少し絡むので、まるで内容を見透かされていた気分です。

はじめに

ところで、僕がadvent calendarにmrubyネタを書くなら何が良いかなぁと考えていたのですが、おそらくmrubyに関する細かいネタは多くの人が書くだろうと思い、自分らしいネタを書こうと思っていました。そこでふと、mrubyが公開後、自分は大量にmrbgemとよばれるmrubyの拡張モジュールを書いてきた事を思い出しました。

そこで、mrbgemを作るためのドキュメントを書くくらいなら、そこで培ったノウハウやおすすめの手順を自動化してやればいいのかと思いつき、自動でmrbgem作成に至るまでに必要となる(あると便利なもの含む)各種ファイル群を自動で作成するmruby-mrbgem-templateを作りました。このmrbgemテンプレート作成ツール自体もmrbgemになっているため、mrubyが動くような開発環境では簡単に動作すると思います。

mruby-mrbgem-templateを使う事で、mrbgem作った事が無い人でも、

  1. mrbgemに必要なディレクトリ構造や設定ファイルを自動生成
  2. テストを記述
  3. CやRubyでmrbgemを実装
  4. ローカルの開発環境でテスト
  5. GitHubに公開
  6. Travis CIでテストを自動化
  7. mgemと呼ばれるmrbgem管理ツールに登録
  8. mrubyにGitHub経由でmrbgemをリンクしてビルド

の一連の開発が簡単にできるようになります。mrbgemを作ってどんどんGitHubに公開してみましょう。

インストール

mrubyをGitHubからcloneし、その中のbuild_config.rbを以下のように変更します。

[program lang=’ruby’ escaped=’true’]

MRuby::Build.new do ¦conf¦
  toolchain :gcc

  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'

  conf.gembox 'default'
end

[/program]

そしてmrubyをビルドしましょう。自動でGitHub経由で上記のmrbgemがリンクされます。

[program lang=’ruby’ escaped=’true’]

rake

[/program]

mrbgemテンプレート生成

ビルドすると./bin/mrubyバイナリが作成されると思います。それを確認後、以下のようなRubyスクリプトを書きます。

[program lang=’ruby’ escaped=’true’]

params = {
  :mrbgem_name    => 'mruby-sample',		# mrbgem名(mruby-***という形式にしましょう)
  :license        => 'MIT',					# ライセンス
  :github_user    => 'matsumoto-r',			# GitHubアカウント名
  :mrbgem_prefix  => '..',					# インストールディレクトリ。この場合../mruby-sampleというディレクトリに必要ファイルが生成される(省略するとカレントディレクトリ)
  :class_name     => 'Sample',				# クラス名(省略するとmruby-***の***がcapitalizeされクラス名になる)
  :author         => 'MATSUMOTO Ryosuke',	# Author名(省略するとmruby-*** developersとなる)
}

c = MrbgemTemplate.new params
c.create

[/program]

そして、このスクリプトを以下のように実行すると、生成情報と共にmrbgemの必要ファイルが生成されます。

[program lang=’bash’ escaped=’true’]

$ ./bin/mruby create.rb
Generate all files of mruby-sample
create dir : ../mruby-sample
create dir : ../mruby-sample/src
create file: ../mruby-sample/src/mrb_sample.c
create file: ../mruby-sample/src/mrb_sample.h
create dir : ../mruby-sample/mrblib
create file: ../mruby-sample/mrblib/mrb_sample.rb
create dir : ../mruby-sample/test
create file: ../mruby-sample/test/mrb_sample.rb
create file: ../mruby-sample/mrbgem.rake
create file: ../mruby-sample/mruby-sample.gem
create file: ../mruby-sample/.travis.yml
create file: ../mruby-sample/.travis_build_config.rb
create file: ../mruby-sample/README.md
create file: ../mruby-sample/LICENSE

  > create matsumoto-r/mruby-sample repository on github.
  > turn on Travis CI https://travis-ci.org/profile of matsumoto-r/mruby-sample repository.
  > edit your mruby-sample code, then run the following command:

  cd ../mruby-sample
  git init
  git add .
  git commit -m "first commit"
  git remote add origin git@github.com:matsumoto-r/mruby-sample.git
  git push -u origin master

  > finally, pull-request mruby-sample.gem to mgem-list https://github.com/bovi/mgem-list

[/program]

上記のように、paramsに渡した情報から適切にファイルを生成します。この段階で、Travis CIに必要なファイルや設定、mgem-listの登録に必要なファイルも全て適切に生成してくれます。

mrbgemの実装

では、生成されたテンプレートを元にmrbgemの実装にとりかかりましょう。

[program lang=’text’ escaped=’true’]

create dir : ../mruby-sample
create dir : ../mruby-sample/src
create file: ../mruby-sample/src/mrb_sample.c
create file: ../mruby-sample/src/mrb_sample.h
create dir : ../mruby-sample/mrblib
create file: ../mruby-sample/mrblib/mrb_sample.rb
create dir : ../mruby-sample/test
create file: ../mruby-sample/test/mrb_sample.rb
create file: ../mruby-sample/mrbgem.rake
create file: ../mruby-sample/mruby-sample.gem
create file: ../mruby-sample/.travis.yml
create file: ../mruby-sample/.travis_build_config.rb
create file: ../mruby-sample/README.md
create file: ../mruby-sample/LICENSE

[/program]

テストの実装

続いて、生成されたテンプレートのtest/mrb_sample.rbを眺めると、以下のような内容で自動にテストが生成され、それと同時にテスト内容を実現するための拡張実装がC言語とRubyで書かれたコードとして生成されます。クラス名等を考慮した適切な実装になっている(はず)と思います。

[program lang=’ruby’ escaped=’true’]

# このメソッドはC言語(src/mrb_sample.c)で実装されている
assert("Sample#hello") do
  t = Sample.new "hello"
  assert_equal("hello", t.hello)
end

# このメソッドはRuby(mrblib/mrb_sample.rb)で実装されている
assert("Sample#bye") do
  t = Sample.new "hello"
  assert_equal("hello bye", t.bye)
end

# このクラスメソッドはC言語(src/mrb_sample.c)で実装されている
assert("Sample.hi") do
  assert_equal("hi!!", Sample.hi)
end

[/program]

この段階で、このテストが全て成功するようにテンプレートが生成されています。

ですので、これらをmruby-sampleレポジトリとしてGitHubに作成して、Travis CIのチェックをOnにしておくと、このデータをpushしただけでもTravis CI上でテストが走りテストが成功する状態になります。

また、同時に生成されているmruby-sample.gemをmgem-listにpull-requestすれば、mgemにも取り込まれるという状態です。

Cによる実装

そういうファイルが生成されている前提で、生成されたテンプレートを見ていきましょう。まずは、Cで実装されているSample#helloとSample.hiです。全体のコードは以下のように生成されています。

[program lang=’c’ escaped=’true’]

/*
** mrb_sample.c - Sample class
**
** Copyright (c) MATSUMOTO Ryosuke 2013
**
** See Copyright Notice in LICENSE
*/

#include "mruby.h"
#include "mruby/data.h"
#include "mrb_sample.h"

#define DONE mrb_gc_arena_restore(mrb, 0);

typedef struct {
  char *str;
  int len;
} mrb_sample_data;

static const struct mrb_data_type mrb_sample_data_type = {
  "mrb_sample_data", mrb_free,
};

static mrb_value mrb_sample_init(mrb_state *mrb, mrb_value self)
{
  mrb_sample_data *data;
  char *str;
  int len;

  data = (mrb_sample_data *)DATA_PTR(self);
  if (data) {
    mrb_free(mrb, data);
  }
  DATA_TYPE(self) = &mrb_sample_data_type;
  DATA_PTR(self) = NULL;

  mrb_get_args(mrb, "s", &str, &len);
  data = (mrb_sample_data *)mrb_malloc(mrb, sizeof(mrb_sample_data));
  data->str = str;
  data->len = len;
  DATA_PTR(self) = data;

  return self;
}

static mrb_value mrb_sample_hello(mrb_state *mrb, mrb_value self)
{
  mrb_sample_data *data = DATA_PTR(self);

  return mrb_str_new(mrb, data->str, data->len);
}

static mrb_value mrb_sample_hi(mrb_state *mrb, mrb_value self)
{
  return mrb_str_new_cstr(mrb, "hi!!");
}

void mrb_mruby_sample_gem_init(mrb_state *mrb)
{
    struct RClass *sample;
    sample = mrb_define_class(mrb, "Sample", mrb->object_class);
    mrb_define_method(mrb, sample, "initialize", mrb_sample_init, MRB_ARGS_REQ(1));
    mrb_define_method(mrb, sample, "hello", mrb_sample_hello, MRB_ARGS_NONE());
    mrb_define_class_method(mrb, sample, "hi", mrb_sample_hi, MRB_ARGS_NONE());
    DONE;
}

void mrb_mruby_sample_gem_final(mrb_state *mrb)
{
}

[/program]

ここで、昨晩のmruby advent calendarネタである「mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法」と絡んできます。

クラスをCで実装する際は、Ruby側でインスタンス生成時に呼び出されるC上の関数内で構造体にデータを色々入れておいて、以降各種メソッドが呼び出された時にその構造体のデータを呼び出して何か処理をする、という実装をしたい状況が多々あると思います。例えば、RedisクライアントのCによるmrbgem実装の場合は、インスタンス生成時にRedisのコネクション情報を含む構造体を作っておいて、各種メソッド実行時にその構造体を呼び出してRedisのコマンドを叩く関数を実行したりします。

そこで、これらを実装するために必要な処理をテンプレートとして既に導入しています。

このCによる拡張実装の場合は、

[program lang=’ruby’ escaped=’true’]

t = Sample.new "hello"

[/program]

とRuby上で書くと、C上ではmrb_sample_init()が呼び出され、引数の文字列”hello”に関するデータを構造体dataに格納しておきます。

[program lang=’c’ escaped=’true’]

static mrb_value mrb_sample_init(mrb_state *mrb, mrb_value self)
{
  mrb_sample_data *data;
  char *str;
  int len;

  data = (mrb_sample_data *)DATA_PTR(self);
  if (data) {
    mrb_free(mrb, data);
  }
  DATA_TYPE(self) = &mrb_sample_data_type;
  DATA_PTR(self) = NULL;

  mrb_get_args(mrb, "s", &str, &len);
  data = (mrb_sample_data *)mrb_malloc(mrb, sizeof(mrb_sample_data));
  data->str = str;
  data->len = len;
  DATA_PTR(self) = data;

  return self;
}

[/program]

そして、それをDATA_PTRというマクロでラップします。この辺りのより詳細な解説が昨日のDATA_PTRの記事となりますので、詳しくは記事を読むのが良いと思います。DATA_PTRによって、

[program lang=’ruby’ escaped=’true’]

t.hello

[/program]

とRuby上で書くと、C上ではmrb_sample_hello()が呼び出され、ラップした構造体をDATA_PTRで取り出し、その構造体のデータを使って処理を書くことができます。

[program lang=’c’ escaped=’true’]

static mrb_value mrb_sample_hello(mrb_state *mrb, mrb_value self)
{
  mrb_sample_data *data = DATA_PTR(self);

  return mrb_str_new(mrb, data->str, data->len);
}

[/program]

そして、Cの関数とRubyのメソッドの紐づけがmrb_mruby_sample_gem_init()になるわけです。

[program lang=’c’ escaped=’true’]

void mrb_mruby_sample_gem_init(mrb_state *mrb)
{
    struct RClass *sample;
    sample = mrb_define_class(mrb, "Sample", mrb->object_class);
    mrb_define_method(mrb, sample, "initialize", mrb_sample_init, MRB_ARGS_REQ(1));
    mrb_define_method(mrb, sample, "hello", mrb_sample_hello, MRB_ARGS_NONE());
    mrb_define_class_method(mrb, sample, "hi", mrb_sample_hi, MRB_ARGS_NONE());
    DONE;
}

[/program]

また、クラスメソッドSample.hiについては、

[program lang=’ruby’ escaped=’true’]

Sample.hi

[/program]

と書くと、mrb_sample_hi()が呼び出されます。

[program lang=’c’ escaped=’true’]

static mrb_value mrb_sample_hi(mrb_state *mrb, mrb_value self)
{
  return mrb_str_new_cstr(mrb, "hi!!");
}

[/program]

違いとしては、mrb_mruby_sample_gem_init()の紐づけの仕方がmrb_define_class_method()になっていますね。

以上が、Cで実装されたmrbgemの拡張になり、これらがテンプレートとして自動で生成されますので、この実装を使いまわして処理を書くとかなり楽に書けると思います。

Rubyによる実装

次に、test/mrb_sample.rbをみると、Rubyで実装されたメソッドSample#byeがあります。これは、mrblib/mrb_sample.rbとして実装されており、中身は以下のようになります。

[program lang=’ruby’ escaped=’true’]

class Sample
  def bye
    self.hello + " bye"
  end
end

[/program]

こちらは簡単ですね。Sampleクラスの中に、Cで定義したメソッドhelloを呼び出すようなメソッドを単に定義しているだけです。mrblib/mrb_***.rbにこう書くことで、Sample#byeがmrbgemとして定義されます。

このように、Cでの実装とRubyでの実装を混在してmrbgemを実装することができるので、それぞれ用途に合わせて使い分けると良いと思います。もちろん、Rubyだけで実装する事も可能で、その場合はsrcディレクトリも必要ありません。Cだけの場合は、mrblibディレクトリは必要ありません。

以上をまとめると、mruby-***としてテンプレートが生成した後は、test/mrb_***.rbのテストを実装したい内容に修正します。続いて、src/mrb_***.cとmrblib/mrb_***.rb、あるいはどちらかのファイルの実装をテストに合わせて修正して、mrubyディレクト内のbuild_config.rbに以下のように作ったmrbgemのパスを書いて

[program lang=’ruby’ escaped=’true’]

MRuby::Build.new do ¦conf¦
  toolchain :gcc

  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'
 conf.gem '../mruby-sample'

  conf.gembox 'default'
end

[/program]

テストを行う、という流れです。

[program lang=’ruby’ escaped=’true’]

rake clean 
rake all test

[/program]

ライセンスやREADME

ライセンスファイルやREADME.mdファイルも自動で生成されているので、それを任意の内容に修正しておきましょう。ライセンスファイルはMITを指定していた場合は、自動でそれに適したライセンスファイルを生成しています。

GitHubでの公開とTravis CIでテスト

開発環境でテストが無事おわったら、GitHubに公開しつつ、Travis CIで自動でテストが走るようにしましょう。
まずは、mruby-sampleというレポジトリをGitHub上に作成します。その後、Travic CIの管理ページからmruby-sampleのテストをOnにします。

そして、作っておいたmruby-sampleディレクトリのデータをレポジトリにpushします。これは、テンプレートを生成したときにpushに関するコマンドが自動で以下のように出力されるので、それをコピペして実行しても良いです。

[program lang=’bash’ escaped=’true’]

  > create matsumoto-r/mruby-sample repository on github.
  > turn on Travis CI https://travis-ci.org/profile of matsumoto-r/mruby-sample repository.
  > edit your mruby-sample code, then run the following command:

  cd ../mruby-sample
  git init
  git add .
  git commit -m "first commit"
  git remote add origin git@github.com:matsumoto-r/mruby-sample.git
  git push -u origin master

  > finally, pull-request mruby-sample.gem to mgem-list https://github.com/bovi/mgem-list

[/program]

push後は、Travis CI上でテストが走っていると思います。今回の例だと、 https://travis-ci.org/matsumoto-r/mruby-sample でテストの状態が見られるはずです。また、README.md上にはテストの状態をアイコンで見られるようにしています。

また、作ったmrbgemにライブラリやその他のmrbgemの依存がある場合は、.travis.yamlと.travis_build_config.rb、mrbgem.rakeをそれに合わせて変更する必要がありますが、今回は省略します。基本的には、.travis.yaml(下記内容)に必要なライブラリをインストールするように追記し、

[program lang=’yaml’ escaped=’true’]

language: c
compiler:
  - gcc
  - clang
before_install:
    - sudo apt-get -qq update
install:
    - sudo apt-get -qq install rake bison git gperf
before_script:
  - cd ../
  - git clone https://github.com/mruby/mruby.git
  - cd mruby
  - cp -fp ../mruby-sample/.travis_build_config.rb build_config.rb
script:
  - rake all test

[/program]

.travis_build_config.rb(下記内容)にはmrubyのbuild_config.rbと同様、依存mrbgemの追記すれば良いです。

[program lang=’ruby’ escaped=’true’]

MRuby::Build.new do ¦conf¦
  toolchain :gcc
  conf.gembox 'default'
  conf.gem '../mruby-sample'
end

[/program]

mrbgem.rake(下記内容)にはspec.add_dependencyによって依存mrbgemを追記すると良いでしょう。

[program lang=’ruby’ escaped=’true’]

MRuby::Gem::Specification.new('mruby-sample') do ¦spec¦
  spec.license = 'MIT'
  spec.authors = 'MATSUMOTO Ryosuke'
end

[/program]

そして、テストが完了したら、上記の出力メッセージ通り、最後にmgem-listにmruby-sample.gem(下記内容)をpull-requestしましょう。

[program lang=’text’ escaped=’true’]

name: mruby-sample
description: Sample class
author: MATSUMOTO Ryosuke
website: https://github.com/matsumoto-r/mruby-sample
protocol: git
repository: https://github.com/matsumoto-r/mruby-sample.git

[/program]

すぐにboviさんが取り込んでくれると思います。これで、mgemコマンドにmrbgemが反映されます。

今後、自分の作ったmrbgemをmrubyに取り込みたい場合は、build_config.rbにGitHubの情報を書けば良いです。今回の場合は以下のようになります。

[program lang=’ruby’ escaped=’true’]

MRuby::Build.new do ¦conf¦
  toolchain :gcc

  conf.gem :git => 'https://github.com/matsumoto-r/mruby-sample.git'

  conf.gembox 'default'
end

[/program]

まとめ

このように、mrbgemを作ったことが無い人でも、簡単にmrbgemの実装及びGitHubに公開できるようになったと思います。

一旦作ってしまうと、どんどん慣れてくるので、まずは生成したテンプレートをそのままGitHubにpushしてみて、Travis CIまでの流れを試してみるのも良いかもしれません。また、Cの実装が苦手な人はRubyで簡単な拡張を書いてみたりとか、Cの勉強がてらCで拡張を書いてみるというのも良いと思います。

以上が、これまでmrbgemを沢山作ってきた自分の経験や知識、mgem-list登録までの流れをある程度自動化した仕組みになります。面倒な所はこのmruby-mrbgem-templateにやらせれば良いので、この仕組みを使ってどんどんmrbgemを作って公開していきましょう。きっと楽しくなると思います。

mruby advent calendar 2013、明日19日はkyab212さんです!