mruby-io のつくりかた
本稿では、C言語とRubyの両方を使ってmrbgemを実装した例として、 iij/mruby-io の実装を紹介します。
12/17の mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法 の中で、 FILE構造をmrubyで扱う上での注意すべき点が説明されているので、合わせてお読みください。
before iij/mruby-io
iij/mruby-io の実装に着手したのは 2013年3月頃です。
それまでは、CRubyのIO実装をmrubyへ移植したものを利用していました (当時はmrbgemの機構が無かったので、 src/ext などのディレクトリに .c ファイルを配置していました)。
当時の面影が iij/mruby の s1ブランチ に残っています。
IO や File のコード量をまとめてみると以下のようになりました。
Filename | lines | sloc |
---|---|---|
src/ext/io/file.c | 338 | 284 |
src/ext/io/io.c | 1896 | 1635 |
mrblib/ext/file.rb | 63 | 56 |
mrblib/ext/io.rb | 104 | 85 |
(.c -- total) | 2234 | 1919 |
(.rb -- total) | 167 | 141 |
コードの大部分をC言語で記述していることが見て取れます。
また、CRubyのIO実装を移植しているため、各種環境への移植のための多数の ifdef が混じったコードとなっていたり、mrubyの想定利用場面では実装されていなくとも不自由しない機能までコードとして存在するために見通しが悪い、といった課題がある状況でした。
Note: io.c などを見て頂ければ分かりますが、CRubyの実装では FILE 構造体の中にアクセスする必要があるため、OSやライブラリ、コンパイラに合わせた拡張が必要になっています。
mruby本体は C99 の範囲内で移植性を維持することを目指しています。その拡張ライブラリであるmruby-io も、可能な範囲で移植性を保つべきでしょう。
iij/mruby-io の実装コンセプト
mrubyのIO実装を行った時期には、mrubyのモジュール機構であるmrbgemは未だ登場していませんでした。
そして、CRuby実装を参考にmrubyのIOを作ったために、本家mrubyでは無効化されている MRB_TT_FILE をデータ構造として使用していました。
その後、mrbgemの仕組みが作られ、IO実装のモジュール化を実現するために、iij/mruby-ioの実装に着手しました (2013年3月頃)。
iij/mruby-ioは、以下のようなコンセプトで実装を行いました。
- データ構造として MRB_TT_FILE の使用をやめて、 MRB_TT_DATA を使う
- libc依存を前提とする (Unix, BSD系の環境で動作すること)
- FILE構造体の内部状態を利用しないコードにする
- メソッドの記述は、出来る限りRubyで記述する
この方針で再実装した iij/mruby-io のコード量は以下のようになりました。
Filename | lines | sloc |
---|---|---|
src/file.c | 318 | 277 |
src/file_test.c | 324 | 262 |
src/io.c | 767 | 655 |
mrblib/file.rb | 166 | 141 |
mrblib/file_constants.rb | 32 | 26 |
mrblib/io.rb | 312 | 260 |
mrblib/kernel.rb | 16 | 13 |
(.c -- total) | 1409 | 1194 |
(.rb -- total) | 526 | 440 |
slocベースではC実装の行数が半分近くに減り、Ruby行数が3倍に増えています。
実装されているメソッドの数は、厳密に数えていませんが1.5倍ほどに増えていることと合わせても、Rubyの記述性の高さを再認識することになりました。
iij/mruby-io の実装
ここでは、mruby-ioの実装ポリシーを踏まえて、どのように実装を進めていったのか、簡単に紹介したいと思います。
Output編
Ruby 1.9.3 リファレンスマニュアル には、syswriteを除く全ての出力メソッドは、最終的にwriteが呼び出されるとの記述があります。
このことから、 writeとsyswriteを用意しておけば、IOの他の出力メソッドをRubyで実装することが出来そう です。
mruby-ioでは、 write メソッドは syswrite を使ってRubyで実装しているので (mrblib/io.rb#L44-L55)、実質的には syswrite が最もプリミティブな出力メソッドということになります。
現在のmruby-ioでは、write bufferを実装していませんが、必要になった場合には write メソッドの実装を変更することで、バッファリング処理をRubyで記述することが出来るでしょう。
class IO
def write(string)
str = string.is_a?(String) ? string : string.to_s
return str.size unless str.size > 0
len = syswrite(str)
if str.size == len
@pos += len
return len
end
raise IOError
end
end
Input編
IOから読み込むメソッドについては、書き込むメソッドと比べて若干複雑です。
以下に、IOの読み込みメソッドの特徴を整理しました。
メソッド | 動作 | EOFに達していた時 |
---|---|---|
IO#gets | 区切り文字が出現するまで読み込む | nil |
IO#readline | 区切り文字が出現するまで読み込む | EOFError |
IO#getc | 1文字読み込む | nil |
IO#readchar | 1文字読み込む | EOFError |
IO#read(length = nil) | EOFに達するまでファイルを読み込む | "" (空文字列を返す) |
IO#read(length = size) | sizeに達するまで待つ(ブロックする) | nil |
ここで、getsやreadlineのような、 区切り文字まで読むメソッドを実装する方法を考える必要があります。
単純には以下の2つのアプローチがあるかと思いますが、mruby-ioでは後者のアプローチを選択しました。
- 1バイトずつファイルを読み、区切り文字が出現した時点までに読んだ文字列を返す
- 一定の長さずつファイルを読み、その中に区切り文字が含まれていれば、その区切り文字までの部分文字列を返す
- 残りの部分文字列は、次回のファイル読み込みメソッドの実行時に処理される
この、 一定の長さずつファイルを読み込む部分は、IO#_read_buf
として実装しています。
class IO
def _read_buf
return @buf if @buf && @buf.size > 0
@buf = sysread(BUF_SIZE)
end
end
@buf
は IOクラスのインスタンス変数で、read bufferの実体となります。
mruby-ioでは、@bufの状態とIO#sysreadの実行を組み合わせることで、CRubyのIOに準拠した機能を実現しています。
まとめにならないまとめ
本稿では、mruby-ioの実装について紹介しました。
mrbgemの実装を行う際に、Cとrubyを組み合わせて機能を実現する歳の参考になれば幸いです。