mrubyファミリーの歩き方(を装ったビルドシステムの話)

hasumikin is a programmer.


これはmrubyファミリー Advent Calendar 2023の最初の記事です。

最初なので、mrubyファミリーの全体像を解説します。 ファミリーにはmruby、mruby/c、PicoRubyの3種類あると言われています(他にもあれば教えてください!)。 これらの違いをmruby/c開発チームメンバでありPicoRuby作者であるわたくしの偏った視点から説明いたします。

mruby――汎用組み込みRuby

mrubyはまつもとゆきひろさんが2012年につくり始めた「組み込み向けのRuby」実装です。 「mruby(軽量Ruby)」と表記されることが多く、これはmrubyの開発が始まったころの表現としては適切だったのですが、2023年時点ではもっと軽量な実装が存在する(後述)ので、いまでは「汎用の組み込みRuby」と表現するほうが適切ではないかと思います。 「組み込みソフトウェア」とは一般に、特定のハードウェアに最適化されたアプリケーションとデバイスドライバが一体になったものを指します。 mrubyが組み込まれたハードウェアの実例として、ネットワーク機器や人工衛星などがあります。

mruby活用の事例にはハードウェアへの組み込みのほか、ngx_mrubyのようにNginxというWebサーバ(リバースプロキシ)のモジュールとして動作するものがあり、これは「ソフトウェアへの組み込み」と言えます。 以上より、mrubyは「汎用の組み込みRuby」だと考えてよいでしょう。

mrubyの言語仕様の特徴に、RubyコンパイラとRubyバーチャルマシンを切り離せる、という点があります。 mrubyコンパイラは、Rubyプログラムをmrubyバーチャルマシン用のVMコードへとコンパイルします。 mrubyバーチャルマシンは、そのVMコードを実行します。 そのため、コンパイル済みのVMコードとバーチャルマシンだけを組み込むことによってコードサイズを削減することが可能です。

mrubyは両方の構成が可能:

  • Rubyスクリプト + mrubyコンパイラ + mrubyバーチャルマシン → コードサイズ大
  • コンパイル済みVMコード + mrubyバーチャルマシン → コードサイズ小

CRuby(Ruby on Railsに使用される「ふつうのRuby」)も内部実装はコンパイラとバーチャルマシンに分かれていますが、mrubyのように切り離して走らせることはできません。

CRubyと比較した場合のmrubyの特徴について、わかりやすい例を挙げましょう。 ミニマム構成のmrubyには、puts メソッドがありません。 これがまさに「組み込み用」であることを示しています。 CRubyは通常、POSIXあるいはWin32 APIに準拠したコンピュータで使用するものですから、標準入出力が存在するのは当然の前提です。 他方、mrubyが組み込まれる環境にはそのようなIOがないこともあります。 したがってmrubyでputsを使用したいならば、mruby-print というmrbgemライブラリ(CRubyで言うところのRubygem)を使用(いっしょにビルド)する必要があります。

mruby/c――Rubyコンパイラを含まない軽量VM

mruby/cは、mrubyがターゲットとするよりももっと小さなマイクロコントローラ上で動作するRubyとして、2015年にITOC(しまねソフト研究開発センター)と九州工業大学・田中和明研究室が共同で開発を始めました。 mruby/cプロジェクトはバーチャルマシン部分だけを開発しており、その入力つまりVMコードは、上述のmrubyコンパイラに生成してもらうことを前提とします。 VMコードの仕様が共通なので、このようなことが可能です。 mrubyでは組み込み先のリソースに余裕があるならコンパイラもいっしょに組み込みますが、mruby/cアプリはVMコードとバーチャルマシンだけを組み込みます。

mruby/cはmrubyよりも軽量であることと引き換えに、制限もあります。 mrubyがCRubyとほぼ遜色ない標準ライブラリを備えている(上述のとおりビルド時に選択可能)一方、mruby/cは組み込みクラス数やメソッド数がかなり少なく実装されています。 2023年時点では、module機能がない、C関数内からRubyメソッドを実行することができない、などの制限もあります(制限の解消に向けた検討中です)。 他方、OSのない環境(ベアメタル)でのマルチタスク(マルチスレッド)実行をサポートするという独自の機能を備えています。

このようにmruby/cは、ワンチップマイコンを制御するための小さな実装に抑えつつ、同時にRubyでアプリケーションを書くことのメリットを享受できるようバランスを取ったRubyバーチャルマシンを目指しています。

PicoRuby ――世界最軽量(?)Rubyコンパイラとmruby/cの統合

PicoRubyは、独自実装のmruby互換コンパイラ(PicoRubyコンパイラ)とmruby/cを組み合わせたRubyです。 バーチャルマシンは、mruby/cがそのまま使用されています。 それからPicogemというライブラリ管理システムにより、クラスやメソッドを選択的に追加できるようになっています。

mrubyコンパイラとmruby/cバーチャルマシンの組み合わせでは、mruby/cの省メモリ性能を完全にスポイルしてしまいます。 そこで、mruby/cといっしょに組み込める省メモリなRubyコンパイラを筆者が開発しました。 それがPicoRubyコンパイラです。 これにより、マイコンアプリケーションの開発者がホストコンピュータ上でRubyスクリプトをコンパイルする必要はなくなりました。 代わりにPicoRubyが、Rubyスクリプトをマイコン上でコンパイルして即座に実行します。

ビルドスタイルの違い――MakeかRakeか

さて、ここからが本題です。 mruby、mruby/c、PicoRubyそれぞれのアプリケーション開発スタイルの違いについてです。 mrubyは、「伝統的?な組み込みエンジニア」からは敬遠される傾向がある、という話題でもあります。

mrubyはビルドの仕組みにCRuby製のRakeタスクを使用します。 これが、わかっている人には非常に便利な一方、わからない人には徹底的にわからないものに見えている(と筆者は思う)のです。 言い方を換えると、mrubyアプリケーションのビルドには、CRubyが必要です。 そしてそれがRake(Ruby版Make)のタスクとして書かれています。 これをCRubyに慣れていない人が使ってつまずくと、途端に何もわかりません。

他方、mruby/cは、アプリケーション(mrubyコンパイラが生成したVMコード)とmruby/c自体のソースコード(もちろん複数のファイルに分かれています)とターゲットマイコンのSDKなりlibcなりを「まとめてコンパイルしてリンクすれば動く」、という思想です。 Makefileでビルドタスクを書けるし、各種マイコン特有のIDEやSDKのワークフローに(最初は)載せやすいので、「伝統的な組み込み分野からやってきた、Rubyにちょっと興味があるプログラマ」には敷居が低いのです。

PicoRubyはどうかと言うと、mrubyと同じスタイルです。 mrubyのビルドシステムをフォークして、ほぼそのまま使用しています。 mruby/cVM自体も、ひとつのPicogemという位置づけです。 さらにPicoRuby独自のrequire機能を実装しており、動的にライブラリをロードできます。 mrubyと同様、CRubyに疎い人からは敬遠されるかもしれません。

以上をまとめると、以下のようにに分類できると筆者は考えています。

  • mrubyとPicoRubyはRake派
  • mruby/cはMake派

それぞれのメリットとデメリットをまとめます:

Rake派(mrubyとPicoRuby)

  • メリット
    • ビルドシステムが、ライブラリと言語処理系の依存関係を自動的に解決するので、それらのバージョンアップが簡単にできる(というか、うまく動かないときの切り戻しが簡単でべんり)
  • デメリット
    • 使い始めるためにrubyコマンド(CRuby)が必要
    • ビルドプロセスにトラブルが発生したとき、Rakeへの理解がないとデバッグが難しい

Make派(mruby/c)

  • メリット
    • Makeという枯れた仕組みでビルドタスクを書くため、多くの組み込みプログラマにとってわかりやすい
  • デメリット
    • 言語実装(mruby/c)やライブラリのバージョンアップ時に、手作業でソースファイルをコピーする必要がある。あるいはそういうMakeタスクを書く必要がある

筆者はRubyが(少しだけ)わかるため、Make派の抱えるデメリットを我慢する気になれません(個人の感想です)。 オレオレMakeタスクを前提としてライブラリのバージョンアップや各マイコンへのポーティングなどをメンテナンスするのも、再利用性の低さが問題になりそうです。 以下のリンクでもわかるとおり、PicoRubyにはすでに多くのライブラリが書かれていますが、これらの依存関係を(マイコンごとに、あるいはプロジェクトごとに)メンテし続けるのは辛いでしょう。

https://github.com/picoruby/picoruby/tree/master/mrbgems

C言語関連ツールの知識だけで戦えるほうがよい、そのほうが俺には理解しやすいのだ、と考える人がいることはもちろん知っていますし、ある程度は共感します。mrubyも当初はMakeでビルドしていたような気がします。最終的には好みの問題だと思いますが、Rubyで組み込みをやるのだから、Rakeを知っておくのは悪くないです。Rakeべんりですよ。

まつもとさんがよくこう言います:「言語がシンプル(たとえばGo)ならアプリの中身は非シンプルになる傾向がある。他方、言語が非シンプル(たとえばRuby)ならアプリはシンプルに書ける(書く人の腕しだいだとは思います)。つまり、複雑さの総和は同じ」――ってことがビルドシステムにも言えそうです。 単発のアプリ開発だけでなく、ライブラリのメンテナンスや開発コミュニティの成長を含めた、全体としてのビルドシステムの複雑さをどこかで解決しなければなりません(あるいは何かを切り捨てるという選択も可能です)。

次回予告(次回?)

mrubyとPicoRubyはRakeビルドによって、 libmruby.a というアーカイブファイルを生成します。 アーカイブファイルにmruby本体やmrbgem(Picogem)やアプリケーションプログラムを1ファイルにパッケージングできます。 種々の作法に彩られた各種のマイコンIDEやSDKによるELFビルド時に、このアーカイブファイルをリンクすればよい、という汎用性の高さがmrubyとPicoRubyのビルド方式の売りです(まつもとさんが同じように考えているかは知りませんが)。 なのですが、あれ? もしかしたら .a ファイルへの親近感が(ふつうのCRuby勢はもちろん伝統的組み込み勢も?)ないのかな? 原因はRakeだけじゃない?

というわけなので、mruby(PicoRuby)のビルド方式に拒絶反応を示す人々をこちらに向かせるための記事を、このアドベントカレンダーの別の日に(気が向いたら)書いてみようと思います。


あしたのmrubyファミリーアドベントカレンダーは @ima1zumi がUTF-8について書くみたいです!