Raspberry Pi 3 で mruby を動かす

DSC_0193
DSC_0193

Raspberry Pi 3 が届きました!

発売より半年ほど出遅れましたが、ようやく Raspberry Pi 3 Model B を購入したので、組み込み向け軽量 Ruby である mruby を動かしていきたいと思います。

RPi3 は 1.2GHz クアッドコア 64bit ARM と 1GB の RAM を搭載したコンピュータです。私が4年ほど前まで使っていたネットブック LaVie Light PC-BL300 が Intel Atom N280 (1コア、1.66GHz) の 1GB RAM 構成でしたので、単純比較はできないものの35ドルで購入できるのはスゴいですね。

(おっと、mruby が想定している組み込み機器と比較してもはるかに高性能な上、MRI そのものも RPi で動いているので意味ないなんて言わないでください! mruby を ARM でセルフビルド・実行してみたかったんだよー!)

数年前に購入した初代 RPi Model B+ と比べても、Wi-Fi や Bluetooth 搭載であることはありがたいです。

RPi3 は Zero のように価格高騰もしていなかったので、普通に Amazon で購入しました。ソニーの英国工場で生産されているらしい RS 版と、中国で生産されている element14 版がありますが、ちょっとだけ安い element14 版、ケースとヒートシンク2個付きを購入しました。5,980円でした。購入時のレートで35ドルは3,722円。日本の Amazon で購入ができる本体のみは4,200円。差額は1,780円ですが、ケースが1,200円、ヒートシンクが2個で580円と考えると妥当な値段だと思います。

DSC_0184
DSC_0184

上が初代 Model B、下が購入した Pi 3 です。緑色のケースが「アリの巣コロリ」っぽくて気に入っています。 このケースがかもし出す昆虫採集感のおかげで、ラズパイを縁の下に設置しなければならなくなった時も安心です。

環境の確認

Raspbian の大きい方のディスクイメージを入れて、最初から色々とプログラミング環境が入っていました。

pi@raspberrypi:~ $ which ruby python perl node lua
/usr/bin/ruby
/usr/bin/python
/usr/bin/perl
/usr/bin/node
/usr/bin/lua

lua まで入っているんですねー。さすが教育用といったところか、私は使わないけど軽量 IDE の Geany もプリインストール済みです。X 環境が入っていない Raspbian LITE 版もありますが、ここまでは充実してないのかな。

pi@raspberrypi:~ $ ruby --version
ruby 2.1.5p273 (2014-11-13) [arm-linux-gnueabihf]
pi@raspberrypi:~ $ git --version
git version 2.1.4
pi@raspberrypi:~ $ gcc --version
gcc (Raspbian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
pi@raspberrypi:~ $ make --version
GNU Make 4.0
このプログラムは arm-unknown-linux-gnueabihf 用にビルドされました
Copyright (C) 1988-2013 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 以降 <http://gnu.org/licenses/gpl.html>
これはフリーソフトウェアです: 自由に変更および配布できます.
法律の許す限り、 無保証 です.

基本的なものはすべて入っているようです。バージョンは念の為 apt upgrade した後のものです。

mruby の取得→セルフビルド

git で clone します。鍵ファイルを RPi に転送してないので、SSH ではなく HTTPS 経由で clone してます。

pi@raspberrypi:~ $ git clone https://github.com/mruby/mruby.git
Cloning into 'mruby'...
remote: Counting objects: 34141, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 34141 (delta 3), reused 0 (delta 0), pack-reused 34117
Receiving objects: 100% (34141/34141), 7.02 MiB | 951.00 KiB/s, done.
Resolving deltas: 100% (21335/21335), done.
Checking connectivity... done.
pi@raspberrypi:~ $ cd mruby/
pi@raspberrypi:~/mruby $ ls
AUTHORS          Makefile   appveyor.yml        doc       mrbgems               test
CONTRIBUTING.md  NEWS       appveyor_config.rb  examples  mrblib                travis_config.rb
ChangeLog        README.md  benchmark           include   mruby-source.gemspec
LEGAL            Rakefile   bin                 lib       src
MITL             TODO       build_config.rb     minirake  tasks

無事 clone できました。 minirake でビルドしてみます。

pi@raspberrypi:~/mruby $ ./minirake

できるかな…!

....
YACC  mrbgems/mruby-compiler/core/parse.y -> build/host/mrbgems/mruby-compiler/core/y.tab.c
sh: 1: bison: not found
sh: 1: bison: not found
rake aborted!
Command Failed: [bison -o "/home/pi/mruby/build/host/mrbgems/mruby-compiler/core/y.tab.c"
  "/home/pi/mruby/mrbgems/mruby-compiler/core/parse.y"]

bison がないって怒られてしまいました。さすがにないか~、ということは flex もなさそうですね。ささっと入れます。

pi@raspberrypi:~/mruby $ sudo apt install bison flex

私の環境では関連ライブラリも含めて3.7MBのファイルが入りました。さあ、気を取り直して再ビルドに挑戦です。

pi@raspberrypi:~/mruby $ ./minirake

結構時間がかかったものの、エラーなくビルドできました!

pi@raspberrypi:~/mruby $ ls bin/
mirb  mrbc  mrdb  mruby  mruby-strip
pi@raspberrypi:~/mruby $ bin/mruby --version
mruby 1.2.0 (2015-11-17)
pi@raspberrypi:~/mruby $ bin/mruby -e 'puts "hello, mruby on pi!"'
hello, mruby on pi!

わあい。ビルドが想像以上に長かったので、早いマシン上の mruby-cli でクロスビルドした方が良さそうです。

ビルド設定を変更

mruby-cli でのクロスビルドは後日として、とりあえずすぐに手元でできるビルド設定を変更していきます。 build_config.rb を開いて、enable_debug をコメントアウトします。念の為、MRuby::Build.new()host-debug, test, bench のところもブロックまるごとコメントアウト。(ここのところ触ってなかったのでもうちょっと良い方法があるのかもしれません)

mrbgems/default.gembox に次の使いたいライブラリを追加しました。

conf.gem :github => "mattn/mruby-xquote"
conf.gem :github => "matsumoto-r/mruby-sleep"
conf.gem :github => "iij/mruby-io"
conf.gem :github => "iij/mruby-regexp-pcre"
conf.gem :github => "iij/mruby-require"
conf.gem :github => "iij/mruby-dir"
conf.gem :github => "iij/mruby-env"

再度、

pi@raspberrypi:~/mruby $ ./minirake

すると、ビルドすることができました。

需要があるかどうか分かりませんが、上の構成でビルドして strip した mruby, mrib, mrbc, mrdb, mruby-strip のバイナリをこちらに置いておきます。Raspberry Pi と Raspbian があれば、ダウンロードして解凍するだけで使える…かもしれません。

次回は GPIO を制御して L チカをやってみたいと思います。

mruby で有理数を扱う mruby-rational を書きました

本家 CRuby の Rational クラスは、バージョン 1.9 から require せずに使えるようになっていますが、mruby では本体に組み込まれておらず、該当するような mrbgem も見つからなかったので書いてみました。

mruby はかなり基本的な機能以外は徹底的に削ってあり、組み込まれた機能も ISO に準拠するような形で実装されています。

Ruby の ISO を確認したところ、Rational クラスは特に入ってないみたいでした。ということで、mruby-rational では挙動を CRuby 1.9.3 のリファレンスに合わせるように書いています。

mruby-rational を使うと、次のように有理数を用いた計算ができます。

a = Rational(1, 4)
  => (1/4)

b = Rational(2, 3)
  => (2/3)

a + b
  => (11/12)

(a + b).to_f
  => 0.91666666666667

x = Rational(1, 3) * 2
  => (2/3)

y = Rational(1, 2)
  => (1/2)

x * y
  => (1/3)

CRuby では rational.c として C で実装されていますが、mruby-rational は Ruby だけで実装しています。必要となるメソッドが Ruby だけで十分そうだった点、せっかくコンパクトな mruby の実装なのだから、gem のコードも簡潔に書きたかった点、などなどいろいろ理由はありますが、本当は自分の書いたコードにあまり自信を持ってないので、風通しをよくして他の方でもバグを見つけてもらいたいな、といった狙いもあります。

中で行なっている約分では、最大公約数を見つけるのにユークリッドの互除法を用いています。

既知の問題点(2015-12-18時点)

ある程度、CRuby の Rational と区別して切り分けして使う分には使えると思いますが、まだまだ互換性がありません。今日までのところ、計算上の精度まわりもチェックをしていませんのでこれから叩き上げたいところです。

Rational.initialize が外に出てる

Kernel.#Rational からインスタンスを作成する際、Rational.convert 経由で普通に Rational.new しています。 つまり外からも new が叩けます。 CRuby だったらアクセサで隠して send でぶっ叩いたり、caller で呼出元によって例外を吐いたりと色々方法がありそうですが…。 もう少し勉強してなんとかならないか考えます。

ちなみに CRuby の rational.c では、rb_undef_method(CLASS_OF(rb_cRational), "new") しておいて C 関数でインスタンス作っているようです。ずるい!

inline static VALUE
nurat_s_new_internal(VALUE klass, VALUE num, VALUE den)
{
    NEWOBJ(obj, struct RRational);
    OBJSETUP(obj, klass, T_RATIONAL);

    obj->num = num;
    obj->den = den;

    return (VALUE)obj;
}

Rational.convertprivate じゃなく形式的に CRuby と合わせているだけなので、利用者サイドの観点からすると不要なのかもしれません。

初期化時に投げられる引数の種類が少ない

個人的に Rational クラスに求めているものは、分子と分母のペアなのでさほど困りはしないのですが、mruby-rational では今のところ

Rational(2, 3) # => (2/3)
Rational(5)    # => (5/1)

という作り方しかサポートしません。つまり、下記のように文字列を一つ渡す

Rational('1/3')  # => (1/3)
Rational('0.33') # => (33/100)

ではエラーとなります。CRuby では、string_to_r_internal() でガリガリやっているっぽいです。 また、CRuby では

Rational(0.3)
  => (5404319552844595/18014398509481984)

となる Float を一つだけ取る初期化も

Rational(0.3)
  => (5.4043195528446e+15/1.8014398509482e+16)

という具合にBIGNUM周りの違いが出ています。

Complex クラスとの整合性はとらない

CRuby では、Complex 型を取り扱う挙動も rational.c に含まれていますが、これはそのままでいいんじゃないかな、と思っています。

なんとなく方向性は分かってきたので、少しずつ手を入れていきたいと思います。

mruby から Lua を呼び出すための mruby-lua を書きました

mruby は、こちらでも述べられているように組み込み言語として広く使われている Lua を強く意識した実装になっています。

mruby も Lua もそれぞれ、他システムに組み込むのが簡単ということだけあって、多からず相互利用の試みは mruby が登場した当初からあるようです。

Lua から mruby を呼ぶのは、ngx_mruby などでちらほらお名前を見かける松本亮介さんが「Lua上でmrubyを動かすための禁断のLuaライブラリを作った」という記事を3年以上前に 書かれています。

人間とウェブの未来 – Lua上でmrubyを動かすための禁断のLuaライブラリを作った

matsumoto-r/mruby-on-Lua

禁断とされている(勝手な思い)Lua上でmrubyを動かす

と、なんだか言葉を濁しつつ書かれているところを見ると、mruby 界隈に暗に「打倒 Lua」の風潮があるのではないかと邪推しちゃいます…(笑)。

また、

RyanScottLewis/lua-mruby

というのもあるようです。(これも3年以上前)

時間をかけて調べたわけではないですが、mruby 側から Lua を呼び出す形のちゃんとしたものはなかったようなので、試しに書いてみました。

それまで Lua といえば、漠然と「オンラインゲームの AI スクリプトとして使われている軽量言語」というイメージしかありませんでした。昨日、Lua の具体的な構文も知らないまま、とりあえず C と C# から Lua を使うデモコードを書いてみたら、なるほど簡単。自分なんかでも、2つの言語環境のライブラリのダウンロードからコーディング、実行まで数十分でできてしまいました。

言語仕様もコンパクトに抑えられ、API ではスタックを使って(ちょっと煩雑になる部分はあるにしろ、考え方そのものは)シンプルにコーディングできます。組み込みスクリプト環境に求めるものや規模にもよるとは思いますが、大抵の場合 Lua で事足りるのも納得できます。

拡張性が高く、ポータビリティもよく、言語の中身自体も簡単で敷居が低い Lua は魅力的ではありますが、それはあくまでもダイナミックに保持しなければならないリライタブルなコード部分を限定する場合に限るんじゃないかな、とも思います。というのも mruby を使うと、「スクリプト機能」という部分的なものだけではなく「システム全体を Ruby の上で書く」ことも、より容易になると考えているからです。柔軟で高次元な言語基盤を持つ mruby をグルー言語として使い、その上で各種アプリが動作する形がメンテナンスの上でも生産性の高さでも強みになりそうです。

mruby も Lua も Windows や組み込み機器といった「スクリプト環境の砂漠地帯」に持っていくための良いツールになります。 目的の用途にあった形でお互いを上手く利用することができたらいいな、と思いました。

mruby-lua について

Github で公開しています。Lua5.2 を使っています。

dyama/mruby-lua

README.md に書いてあること以上でも以下でもないので、ここであんまり書くこともないんですが、mruby から Lua VM を呼び出して実行することができます。

Lua#dostring は Lua スクリプトを含む文字列として、 Lua#dofile は Lua スクリプトファイルのパスを受けて実行します。 Lua#[]Lua#[]= は、Lua 上のグローバルなオブジェクトを取得・設定することができます。昨日 Lua を触りはじめて mruby-lua を書いてみて今日なので…まだ mruby と Lua 上の型を相互変換している箇所において一部対応していない型があります(2015/12/8現在)。

より高級な mruby が動く環境で Lua スクリプトを呼び出すことにどれだけの意味があるかは分かりませんが、Lua スクリプトをデータ記述言語として使う既存のシステムとのやり取りとか…何かに使えるのかな…。

mruby-regexp-pcre の String#gsub について

IIJ さんが開発している mruby 向けの正規表現ライブラリ iij/mruby-regexp-pcre を使ってたら、本家 CRuby と違う挙動の部分を見つけました。

# 文字列の先頭に空白が含まれている場合、それを取り除く。
"foo".gsub /^\s*/, ""

上の "foo" の場合では、先頭に空白は含まれていないので、そのままの文字列 "foo" が返ってくるはずです。 iij/mruby-regexp-pcre を使うと、これが空の文字列 "" として返ってきてました。 CRuby1.9.3 でチェックしてみたところ、"foo" が返ってきています。

最初は mruby の String#gsub の問題かなと思って mruby/mruby のコードを追っていましたが該当箇所が見つかりません。よく考えてみたら、正規表現ライブラリは mruby の基本実装には組み込まれていないはずなので mruby/mruby に問題箇所があるわけがありませんね。2013年の下記の issue を見てみても、「正規表現ライブラリはちゃんと切り分けて、String#gsub なんかの挙動は拡張側で適宜上書きしようぜ!」という流れが見てとれました。(英語苦手だけど)

Library independent RegExp ・ Issue #841 ・ mruby/mruby

また、一瞬だけ本家 CRuby で使用している正規表現ライブラリは鬼車であって、mruby-regexp-pcre は PCRE である違いも考えましたが、

"foo".gsub /^\s*/, ""

で空文字が返ってくるのは、そもそも使用しているライブラリ以前の問題のような気がしました。

ということで、iij/mruby-regexp-pcre の該当コードを読んでいると String#gsub上書きしているコードがありました。 対象部分をデバッガで追ってみると、位置指定子(^)があるにも関わらず、1文字ずつ正規表現マッチ→マッチしたインデックスだけを用いて(空文字で)置換→元文字列がなくなるまでくりかえし、という挙動をしていたため、恐る恐る issue を投げてみました。 (颯爽と手直ししてプルリクできるくらいの能力がどこかに落ちてないでしょうか!)

2015/12/7 追記

レポートしておいた部分をしっかり修正していただけました!感謝多謝です。

OpenCASCADE 6.9.0 を LMDE でビルド

先日、自宅サーバの Debian GNU/Linux Jessie 32bit 版で OpenCASCADE 6.9.0sirenビルドしました

自宅サーバは、SSH で外部からアクセスして開発ができるので便利である一方、オンボード Atom 搭載の省電力サーバなのでコンパイルにはもの凄く時間がかかります。そこで、Intel Core i7 を搭載した普段使い用のラップトップでもビルドしようと思います。

ラップトップは、LMDE(Linux Mint Debian Edition) の無印、あえて言うなら version 1 です。先日、LMDE 2 がリリースされましたが環境を壊すかもしれないのが怖くてまだ移行していません。なお、こちらは 64 bit です。

OCCT 6.8.0 ベースの siren をビルドして開発に使っていたので、こちらも難なくビルドできると思いきや、VTK を指定しないと Visualization 以下のモジュールが上手くビルドすることができませんでした。

configure の出力では、VTK は optional 扱いになっていたのですが、必須なのかなあ…?たしかに先日の Debian の記事でも明示的に VTK を指定していました。なお、VTK は自分の記憶が正しければ 6.8.0 の時点で既に OCCT に利用されていたようですが、その時は確実に optional だったはずです。(VTK をインストールしなくても Visualization 以下のビルドが通っていました)

ないなら入れてしまえと apt-cache search vtk するも、バージョン 5.8 しか公式リポジトリにはありませんでした。試しに libvtk5.8, libvtk5-dev を install して OCCT 6.9.0 をビルドしてみても、やはりビルドが通りません。 なお、Debian の Jessie には公式リポジトリに VTK 6.1 があったため、APT 経由で入れたらビルドが通りました。

ということで、ソースコードを取得して自分でビルドしましょう。VTK の公式サイトのダウンロードページから、対象となる 6.1 の tar ボールをダウンロードします。

cd /tmp
wget http://www.vtk.org/files/release/6.1/VTK-6.1.0.tar.gz
tar zxvf VTK-6.1.0.tar.gz
cd VTK-6.1.0

README.html にビルド方法が載っているので、それに従ってビルドします。

cmake.
make

README.html では cmake ではなく curses 版の cmake である ccmake と書いてありますが、cmake でも通ります。

ビルドにはもちろん CMake が必要です。また、依存しているライブラリもいくつかあると思いますが、幸運なことに自分の環境では追加ライブラリを指定することなく全てのビルドが成功しました。VTK を Debian で APT 経由からインストールした際、200 MB 前後の依存ライブラリがどっと降ってきたので心配でした。ヨカッタ。

例に漏れず、インストールは次のようにします。

sudo make install

インストールによって、ヘッダファイルは /usr/local/include/vtk-6.1 以下に、ライブラリファイルは /usr/local/lib 以下に設置されました。インストールパスは cmake オプションで指定できるはずです。

さて、これで OCCT 6.9.0 のビルド環境は整ったはずです。前回の手順どおりに、一気にやっちゃいます。

cd /tmp
wget http://files.opencascade.com/OCCT/OCC_6.9.0_release/opencascade-6.9.0.tgz
tar zxvf opencascade-6.9.0.tgz
cd opencascade-6.9.0/
./build_configure
./configure -prefix=/opt/occ690 \
-with-vtk-include=/usr/local/include/vtk-6.1 \
-with-vtk-library=/usr/local/lib

configure の出力で Visualization 以下のパッケージもすべて「Yes」になっていることを確認し、

make -j8 install

で、ビルド&インストール。 ビルド時間を計測すると次のとおりになりました。

real  17m11.411s
user  97m46.908s
sys   5m26.720s

前回、3時間以上かかっていたビルドは17分で終わりました。これは強い。

6.9.0 を用いて siren をビルドし直すとこちらでもちゃんと動いているようです。ひとまず安心です。

mruby の public/private について

最近ちょっとバタバタしていてあまりプライベートに時間を割けませんでしたが、連休に台風接近が重なった事もあり、連休初日だけ出社して残りの二日はお休みを頂きました。ということで、メモ変わりにここのところの事を書き止めておきます。

mruby の public/private について

mrubyではpublic/privateキーワードを言語仕様上で制約しているらしいです。この辺の実装を調べていた時に、Matzさんご本人から教えていただきました。

自分のつぶやきにもあるように、mruby-sirenでShapeクラスのインスタンスをスクリプトサイドから作らせたくなかったので、initializeをprivateにしようとしたんですが、なかなか見付からず…

よくよく調べてみると、既にそういう話が出てきていました。

there a way to create private methods? #1357

機能制限されている詳細な経緯は分からないけれど、コンパクトで軽量なmrubyの方針においてpublic/privateは利用者側の運用に任せても特に差し支えない機能のはずだから入っていないのかもしれません。(間違ってたらご指摘をお願いします…!)

ちなみにmruby-sirenのShapeクラスは、initializeをスクリプト側から呼んだら例外を吐くようにしています。

mirb - Embeddable Interactive Ruby Shell
> s = Shape.new
(mirb):1: private method `new' called for Shape:Class (NoMethodError)
> 

実装はそのままこんな感じです。

mruby: C++のクラスをラッピングしてRubyクラスとして使えるようにしてる途中

sirenの改修を行なっています。これはもともと、Open CASCADE の C# アプリケーションサンプルに含まれていた OCCTViewer クラスを参考にして組んだものがベースで、C# アプリケーションのコードとネイティブな DLL の間を取り継いで、機能を提供する sirenenv が動いています。

sirenenv の内部をさらに細かく分けると、C++/CLI の構文で書かれたインターフェイス部分と C++ 構文のみで書かれたコア部分に分けることができます。C++ 部分も含め、結局 /clr オプションでコンパイルしているので C++/CLI として解釈されるんですが、将来の「脱 .NET」を目指して切り分けを進めています。(厳密に構文が分かれてない部分もまだあると思われます)

C++部分をさらに分けると、Open CASCADE のオブジェクトやビューを扱う描画&幾何演算部と mruby インタプリタを扱うスクリプト演算部の2種類に分かれています。

駆逐艦「島風」のローポリモデル作成

仕事の絡みもあって、Maya で作成した三次元モデルをゲームエンジン「Unity」に取り込む検証をしようと思いました。 手持ちの Maya は 2009 と、ちょっと古いんですが FBX 形式の読み書きもプラグインで対応しているようでひとまず安心。 Battle Station; Paciffic のような艦隊戦デモでも作れないものかと、例のごとく駆逐艦のローポリモデルを作ろうと思いました。

Maya 自体久々だったからキーバインドやマウス操作にも迷いつつ…。自作の簡易 3D CAD の siren で、スクリプトの動作検証用に作っていた駆逐艦「島風」の船型を流用してみました。

  1. siren で島風スクリプト(Ruby)を実行。
  2. 生成されたモデルは自由曲面で構成されたソリッドモデルだが、STL で一旦ファイルに保存。
  3. Maya で STL の船型を読み込み。
  4. 艦橋や主砲などを置いていき、島風完成!
  5. 完成した島風を FBX で出力して Unity に取り込み。

といった手順です。結果、上手くいきました。

自分が作った CAD で生成されたモデルをゲームエンジンの中で動かすのは小気味良くて楽しいですね。 モデル自体も、かなりやっつけ感がしますが、これはこれで楽しいからOKかな。

島風 大きい画像

mruby で自作クラスをごにょごにょする

siren では、mruby を組み込んでコマンドドリブンなインタプリタを実装していたので、クラスをまともに使っていませんでしたが、コマンドが多くなるにつれ管理が煩雑になった事や、Ruby のクラスとして強力な機能を実装する目的もあり、改めて mruby のクラスまわりの実装を調べてみました。

C コード上で Ruby クラスを作成

クラスの新規作成

mrb_define_class() を使います。引数は mrb_state*, クラス名, 親クラス。

RClass* prclass = mrb_define_class(mrb, "Myclass", mrb->object_class);

RClass*が返ってきます。C コード中では、このポインタでクラスを指定します。

クラスにメソッドを突っ込む

新規作成したクラス Myclass の中身は空っぽなので、メソッドを突っ込んでみます。

mrb_define_method(mrb, prclass, "myfunc", myfunc, ARGS_NONE());

引数は mrb_state*, ターゲットとなるクラスの RClass*, メソッド名, 実体のC関数, 引数定義です。 RClass* を mrb->kernel_module にすると、Kernel 名前空間に入ってグローバル関数のように使えます。

引数定義

ARGS_NONE() ... 引数なし
MRB_ARGS_REQ(2) | MRB_ARGS_OPT(1) ... 必須引数が2個、省略可能引数が1個

実体の関数

インターフェイスは、戻り値と引数の型に注意するだけでOKです。

mrb_value myfunc(mrb_state* mrb, mrb_value self)
{ 
    mrb_int a;
    mrb_int b;
    int argc = mrb_get_args(mrb, "ii", &a, &b);

    return mrb_fixnum_value(a + b);
}

引数は mrb_get_args() で取得します。

定義済みのクラスを文字列から取得する

名前が分かっているなら RClass* を保持しておく必要はありません。

RClass* prclass2 = mrb_class_get(mrb, "Myclass");

クラスのインスタンス変数を設定・取得する

mrb_obj_iv_set() と mrb_obj_iv_get() を使用します。

RClass* my_class = mrb_define_class(mrb, "Test", mrb->object_class);
mrb_value obj = mrb_class_new_instance(mrb, 0, NULL, my_class);

RObject* pobj = mrb_obj_ptr(obj);
mrb_sym sym = mrb_intern(mrb, "asdf", strlen("asdf"));
mrb_obj_iv_set(mrb, pobj, sym, mrb_fixnum_value(1244));

return mrb_obj_iv_get(mrb, pobj, sym);

クラスからインスタンスを作成

定義したクラスからインスタンスを作成するには mrb_class_new_instance() を使うようです。 引数は mrb_state*, コンストラクタの引数の数、引数、RClass* です。

mrb_value obj = mrb_class_new_instance(mrb, 0, NULL, prclass2);

引数がない場合、0, NULL で良いみたいです。引数は

mrb_value args[3];
args[0] = mrb_fixnum_value(123);
args[1] = mrb_float_value(mrb, 10.4);
args[2] = mrb_nil_value();

のようにこしらえます。

メソッドの削除

コーディングして試してみていませんが、mruby.h に

void mrb_undef_class_method(mrb_state*, struct RClass*, const char*);

がありました。

メソッドを実行する

mrb_funcall(), mrb_funcall_argv(), mrb_funcall_with_block() の3種類が mruby.h に定義されています。

mrb_funcall(mrb, obj, "myfunc", 0);

Ruby スクリプトとして書いたクラスを C プログラムに組み込む

C コードで動的に Ruby のクラスを作れるのは便利なんですが、動的に生成する必要もないものは、普通の Ruby スクリプトとして書いたりデバッグしたりする方が効率的です。

mruby をビルドすると bin ディレクトリに生成される mrbc を使うと、Ruby のスクリプトファイルをバイトコードに変換し、C の配列として扱える C ソースコードファイルに変換してくれます。

Ruby スクリプトから C ソースコードへ変換

次のような内容の vector.rb を準備します。

class Vector
  attr_accessor :x, :y, :z
end

mrbc を使ってコンパイルします。

mrbc -BVector -ovector.c vector.rb
# -B<バイトコード配列名> -o<出力ファイル名> <入力ファイル名>

次のような内容の vector.c が生成されます。

#include <stdint.h>
const uint8_t Vector[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x32,0xe8,0xe5,0x00,0x00,0x00,0xa7,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x89,0x30,0x30,
0x30,0x30,0x00,0x00,0x00,0x33,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x05,
0x00,0x80,0x00,0x05,0x01,0x00,0x00,0x05,0x00,0x80,0x00,0x43,0x00,0x80,0x00,0x45,
0x00,0x00,0x00,0x4a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0x56,0x65,
0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x4a,0x00,0x01,0x00,0x06,0x00,0x00,0x00,
0x00,0x00,0x06,0x00,0x80,0x00,0x06,0x01,0x00,0x00,0x84,0x01,0x80,0x01,0x04,0x02,
0x00,0x01,0x84,0x00,0x80,0x01,0xa0,0x00,0x80,0x00,0x29,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x04,0x00,0x0d,0x61,0x74,0x74,0x72,0x5f,0x61,0x63,0x63,0x65,0x73,0x73,
0x6f,0x72,0x00,0x00,0x01,0x78,0x00,0x00,0x01,0x79,0x00,0x00,0x01,0x7a,0x00,0x45,
0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

アプリケーションをビルドするたびに変換するのもなんですので、ここまでの手順を Makefile なんかに書いておけば良いでしょう。

バイトコード配列を使う

アプリケーションの Makefile やプロジェクト設定で vector.c をビルド対象にして、このバイトコード配列を使っていきます。vector.c は、スクリプト側に変更をかけるたびに上書きで済ませたいので、使うコードは別のファイルがいいでしょう。

別ファイルにて

extern const uint8_t Vector[];

しておくと安心です。

実際の使い方はかなり簡単で、

mrb_load_irep(mrb, Vector);

だけでバイトコードが mrb_state* 空間にロードされます。 あとは、前述のとおり

RClass* prclass = mrb_class_get(mrb, "Vector");

という具合に、名前文字列から RClass* を取得できます。

オマケ

クラスとは関係ありませんが、随所に仕込んでおきたい例外とエラーの表現。

未実装例外

return mrb_exc_new(mrb, E_NOTIMP_ERROR, NULL, 0);

引数エラー例外(メッセージ付)

static const char m[] = "No such specified object.";
return mrb_exc_new(mrb, E_ARGUMENT_ERROR, m, sizeof(m) - 1);

今のところ分かってないこと

new したインスタンスを mrb_state 空間から動的に取得してくる方法が分かりません。インスタンスを取得するにしても、C コード側からは結局は RObject のポインタとしてしか表現されない(名前文字列みたいな「ラベル」もないですし)ので、後から使いたいオブジェクトはどこかにポインタを覚えさせておく必要があるのかもと予想してます。
あんまりよくコードを読んでませんが mrb_state の宣言に RObject* メンバがぶらさがっているんですが、このあたりが実体なのかも。

mrubyがやってきた!

OpenCASCADE の勉強のために、仕事とは関係ないところで、自分なりの簡易三次元 CAD ビューアを作成しています。
形状をパラメトリックに定義したり、対話的に制御したり、一連の処理をモジュール化する上で必要になってくるのがマクロ環境。マクロと言ってしまうと、使い勝手の悪いバッチ処理的なイメージがそこはかとなくありますが、強力なインタプリタ環境を組み込みたいと思いました。

アプリケーションを組む上でもトライアンドエラーのデバッグ効率は上がりますし、CAD ソフトにおける純粋な三次元幾何演算をハード的な部分であるとすれば、「ある決まった工業製品の組み立てモデルをシミュレーションする」といったより論理的な、ソフト的な役割のライブラリ化にも役立つと思っています。

さて、OpenCASCADE にはこのブログでも何回か書いたとおり、DRAW TEST HARNESS というコマンド・ドリヴンな対話型デバッグシェルが含まれています。単機能評価だけで言うと、Tk による最低限の UI とシェルっていう組み合わせは別に捨てたもんじゃないのですが、ユーザに提供するマクロ環境として Tcl/tk を選択するのはいささか時代遅れな感じは否めません。シェルスクリプトや Perl、AWK に馴染みのある人ならば、あんまり気にせずに使い込なせると思うんですが、本質とは違うところでの労力を強いるのは極力避けたいところでもあります。

対話的にも動作することができ、モジュール化しても軽快に動いてくれるインタプリタ環境を探していました。
最低限、

  • 基本的な計算(四則演算、数学関数の呼び出し)
  • 変数の保持と解釈
  • 条件式、繰り返し構文

を備えているものだったら何でも良いと思っていました。ほぼ全ての言語にあるようなものばかりですね。もちろん、書き易さやメンテナンスのやり易さも条件のひとつです。

さて、最初に思いついたのは Python です。Python のコーディングは、私自身、累計でも1万行も書いていないと思いますが、欧米製の大手 CAD ソフトでは Python をマクロ環境として組み込むものが増えてきているようです。フランスのパリに Open CASADE の勉強会に行った時に開発メンバーが見せてくれたデモでも、OpenCASCADE の C++ コードを Python から呼んでいたようです。また、2011年から OpenCASCADE のコミュニティ・エディションをメンテナンスしている Thomas Paviot 氏も PythonOCC という Python ラッパーのプロジェクトを立ち上げています。

大手 CAD や OpenCASCADE との連結でも実績のある Python ですが、組み込み関連の情報を漁っていると、 mruby というものを見つけました。

mruby とは、Ruby の開発者 Matz 氏が最近精力的に開発を進めている組み込み向けの Ruby 実装らしいです。本家 CRuby に比べて依存関係が少なく、小さな端末でも動きやすいよう設計されているようで、Vim 関連の記事でよく参考にさせていただいている mattn 氏頻繁にコミットされている事がブログからも伺えます。

私はガチガチの関数手続き型の頭と、シェルスクリプトのようなゆるゆるのシェル展開ありきの頭の2ビットでコーディングをしていたので、Ruby のような「ちょうど良い」言語には向いていないんだろうなーと食わず嫌いをしていて手をつけなかったような気がします。いや、嫌っていたワケではないのですが、あれ楽しい!これ楽しい!と目移りしている間に、いつの間にか、ちゃんとやってみる機会を逃したというのが正しいかもしれません。少なくとも、ちゃんとやりたい言語の一つであった事は間違いありません。

# 余談ですが、Lisp もちゃんと時間をとってやりたいです。Emacs の設定ファイルのために elisp をごちょごちょ書いたような記憶しかありせん。
# オライリーの「初めての Ruby(オーム社刊、Yasugi 著、2008年)」で紹介されている Ruby の系図に、Ruby が Lisp の考え方も引き継いでいるような図があったので楽しみ。

これは好機とばかりに、mruby を試してみることにしました。

Hello, mruby!

mruby を make するのは簡単でした。他所でも紹介されているように、git で clone して make するだけです。Ruby の環境をまともに触るのも初めてでしたので、初めて知ることがたくさんありました。

まず、Makefile は存在しているのですが、Makefile の中で呼ばれているのは ruby minirake というコマンド。make から minirake が呼ばれて、最終的に gcc がコンパイルする形になってます。コンパイルの動作を処理している minirake は全て Ruby で実装してあり、Ruby 初心者が最初に見た時は「?」マークの連続でした。(でも、それほど時間をかけずに読んでいけました。Ruby すごい!)

頭に mini と付いているのは、きっと mruby のための小さな仕組みであり、本家 CRuby では Rake という Ruby のための Make 環境があるんだろうな、とも理解できました。

Ruby には gem というモジュール管理機構があるようです。Perl で言う CPAN のようなものだと思いますが、rake ファイルにターゲットを追記しておくだけで新しい gem を作ることができました。本家 Perl と軽量目的の mruby を比べるとアレかもしれませんが、分かりやすさ・透明性と簡単さは驚きです。(これなら自分もできるかも!と思わせてくれるのにはテンションが上がります)

さてはて、自分のアプリケーションに組み込む場合は Windows 環境で動かさなければならないので、Windows 環境、とりわけ Visual Studio でビルドできるかというのは大きな問題でした。そして、結果から言うと杞憂でした。
Windows では TortoiseGit と MSYSGIT を使っていますので、Windows に Ruby を入れて PATH を通したあと、MSYS 環境に clone して make するだけでした。途中、bison がないぞって怒られたので、bison.exe を入れたらすんなりと mruby.exe(インタプリタ本体)、mirb.exe(mruby.exeに対話シェル機能を入れたもの)、mrbc.exe(Rubyコードをバイトコードにコンパイルするやつ) が出来てしまいました。これらバイナリは libmruby.a をスタティックリンクしているようで、スタンドアロンで動作可能っていう嬉しさ。さすが組み込み向けです。

コーディングをほとんどしていないのに、こんな側面から mruby がどんどん好きになってしまいます。

Visual Studio で

自分のアプリケーションを Windows 以外のプラットフォームでも提供するんだったら、Qt なんかでチャキっと組んだ方がいいと思うんですが、私は Qt を使ったことがありません。Qt はすごくやりたいのですが、それはまた別の機会として、当面の目標である Windows でそこそこ動くものを作らねばなりません。

そういう理由で、ユーザ・フロントエンドを C#、OpenCASCADE や mruby のラッパー DLL を C++/CLI で実装しよう(※)と思いました。ラッパー DLL も C++/CLI 依存の部分とネイティブ C++ 準拠の部分を分けてコーディングする事により、最悪、別のプラットフォームになった場合、ネイティブ C++ の部分だけ抜き出してビルドし直せばいいと思いました。MFC を使った COM 実装に比べれば、C++/CLI の制約なんて目をつぶる事ができそうですし、そんな高度な事もする予定がないので、当面はこれで問題がないはずです。

※Microsoft の公式推薦の C++/CLI 入門書籍でも、「C++マネージド拡張の失敗という前例もあるし、特に理由がなければ C++/CLI なんて使うな」と前書きに書いてありましたね。あれは声出して笑いました。

さて、さっそく MSYS環境で make した mruby.dll を Visual Studio で作成した C++/CLI の wmruby.exe (デバッグの為、とりあえずEXEを作成)からリンクしてみて実行しました。コンパイル、リンクとも正常に完了しているものの、実行時には 0xc0000007b というエラーが出て、実行できません。調べてみると、どうもバイナリイメージが CIL とネイティブの間で上手く取りつげていないようです。

ここは mruby ではなく、MSYS と、Windows の目紛しく仕組みが変わるバイナリの取り扱いの不一致だろうって事で、あまり深く調べずに考え方を変えました。

mruby.dll を make するために rake ファイルを読み漁ってた時に vs2010 や vs2012 といった、いかにも Visual Studio で mruby をビルドするオプションっぽいのを発見していました。

openssl とか、gem が他のオープンソースライブラリを使う時、Visual Studio ではなく MSYS 環境のほうが色々と苦労が少なそうだなーと思っていたので、あえて MSYS で libmruby.a を make していましたが、そこが問題になりそうだったら選択肢がありません。

vs2010 を使って、ビルドし直してみると、Visual Studio によって libmruby.a ではなく libmruby.lib が生成されました。加えて、MSYS でビルドしていた自前の mruby.dll も Visual Studio で C++/CLI として libmruby.lib にリンクして再ビルドすると、拍子抜けするほど上手く行きました。

libmruby.lib さえビルドできてしまったら、あとはこっちのもんですね!

IIJ 版

これまで触っていた本家の mruby では、require やシェルコマンド実行のクオートなど、Kernel でサポートしているものもほとんどありませんでした。開発途上という側面と、最低限のものを提供するというスタンスがあると思われます。

本家 mruby のほか、IIJ さんも精力的に mruby の開発に手を出しているようで、本家から folk したリポジトリになっています。mattn 氏のブログを拝読していたら、先程述べた require やクオート、正規表現など「モバイル端末などの局所的な組み込みには要らなかもしれない!でもPCで実行する対話的アプリのモジュールなら要るよね!」的な機能を IIJ 版にどんどん追加されていっている様子が伺えました。記事にもありましたが、CRuby にも遜色がないほどの基本機能を提供できるようになっているようです。本当に凄いし、夢が広がります!

という事で早速、IIJ 版を clone して make してみました。linux では何ごともなくすんなり。
./mirb して

[ruby]
ls.each_line do |item|
puts item
end
[/ruby]

すると、ちゃんと実行できました。楽しい。

さて、Windows 環境ではどうかと言うと、ソケットを扱ってそうな gem があったり、コードを見てみると Windows 向けのコーディングがされていなかったりと、半ば予想はしていましたが、Visual Studio でビルドしようとしても失敗しました。当たり前だよネ。

Visual Studio っていう巨大なイレギュラー環境が立ちふさがって、作業がストップしてしまいましたが、丹念に乗り越えなければならない道だろうなぁと思います。今はビルド方法をまとめるくらいしか能力がないですが、Ruby をガリガリ使えるようになって、実装の事も理解できるようになったら issue 飛ばしたり、コーディング面でも協力できるようになれればなあ、と思いました。