As Sloth As Possible

可能な限りナマケモノでありたい

タグ:MacRuby

こんにちは、「それは一体誰得なんだ」でお馴染みのfaultierお兄さんだよ!今日はみんな大好きMacRubyをどれだけ無駄遣いできるかを考えてて例のごとく失敗したので、その顛末を教えてあげるよ!

MacRubyでDTを動かしたい

まぁ冒頭書いた通りなんだけど、「Objective-CからMacRubyを利用する - Watsonのメモ」を読んでなんか変なことできないかなーと考えてて、そういや俺ってば見た目に面白い以外は全く使い道のないものを以前作ってたじゃん、と思い出したんだけど、上手くいかなったという話。あ、全く使い道の無いものってのは、もちろん言うまでもなくあいつのことですね。

esotericは構成としては、ソースコードをパースしてSexpにするParserと、それをRuby2Rubyを使ってRubyのコードにトランスレートしてから実行するRunnerでできているので、MacRuby Frameworkを使ってesotericをObjCから呼び出せば、アプリケーションにDTやてってってーでプラグインを書ける仕組みを比較的容易に導入できるかと思います。導入したところで誰が使うのかわかりませんが。少なくとも俺は絶対に使わない。

まずは小手調べのコンパイルエラー

とりあえずMacRubyをDownloadしてくる。最新版の0.5はSnow Leopardにしかインストールできないけどこないだクリーンインストールしたばっかりだから全然問題ないもんね!と勝ち誇ってみせたけど、一体誰に勝ったのかはよくわからない。ちなみにソースからのビルドも時々試みてるけど大体こけるので今回は無難にバイナリをインストール。macgemは0.4のときはまともに使えたもんじゃなかったのでちょっと不安だったけど、Ruby2Rubyも特に問題なく入った様子。なに、こんな拍子抜けするくらいさらっと入っちゃっていいの?とニヤニヤしながら次のコマンドを実行。

$ pwd
/Users/taro/Projects/esoteric
$ echo $RUBYLIB
lib:
$ macruby -v 
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macruby bin/dt -v
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:13: end pattern with unmatched parenthesis: /((?:\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1\xE3/
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:74: end pattern with unmatched parenthesis: /(\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1/
dt.rb:3:in `<main>': compile error (SyntaxError)
    from dt:4:in `<main>'

オゥフ。言われたところを見てみたら、parser.rbの13行目には/((?:ど|童貞ちゃうわっ!)+)…/という正規表現が書いてあった。念のため試してみたけど、Ruby 1.9.1ではちゃんと動いてる。どうも、()の中にASCII以外の文字が含まれてるとMacRubyさんは閉括弧を見つけられなくて正規表現として不正だと言ってくる様子。ソースコードはutf-8で書いてあって、magic commentにもutf-8って指定してて、文字列リテラルだと問題ないのに、正規表現だと駄目。仕方ないのでベタに日本語書いてたところを全部Unicodeリテラルにしてみた。"ど"だったら"\u3069"とか。とりあえずそれでコンパイルできないというエラーは出なくなった。CRubyの方でももちろんちゃんと動く。なんだよ、やればできるんじゃないか、ツンデレか?などと思いつつhi.dtを実行させてみる。

$ macruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ macruby bin/dt examples/hi.dt
parser.rb:160:in `numeric:': ArgumentError (ArgumentError)
from parser.rb:80:in `process'
from parser.rb:58:in `block'
from parser.rb:51:in `parse'
from parser.rb:11:in `parse:'
from runner.rb:25:in `run:'

ぬぅ。まだツンツンしてやがる。ちょっと勢い込んでしまったけど、どうもまだMacRubyと打ち解けきれてないみたい。ちなみに、CRubyの方でやるとこんな感じになる。

$ ruby -v
ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-darwin10.0.0]
$ ruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ ruby bin/dt examples/hi.dt
$stack = []
$heap = {  }
$stack.push(72)
$stdout.print($stack.pop.chr)
$stack.push(105)
$stdout.print($stack.pop.chr)
$stack.push(33)
$stdout.print($stack.pop.chr)
$stack.push(10)
$stdout.print($stack.pop.chr)
exit(0)
Hi!

うーん、ちゃんと動いてるよなぁ。該当の箇所を調べたら、本来encodingがUTF-8のStringが来てなきゃいけないところで、MacRubyの場合はUS-ASCIIなStringが来てしまっている。あるぇ?その文字列がどっから来てるかを辿って行くとARGF.readしてるとこなんだけど、MacRubyでは既にその時点でUS-ASCIIとして読み込んでしまっている。CRubyでやったらちゃんと動くのだから、$stdin.external_encodingはちゃんとUTF-8になるはずなんだけど、そもそもそこがnilだし、opneとかset_encodingとかで指定しても変化なし。force_encodingとかしても上手くいかない。と、このあたりでもっと色々なことがおかしいということに気付く。

MacRubyでStringが期待した挙動をしてない件

いまいち良くわからないので、試しにこんなことをしてみた。

$ cat test_string.rb
# coding: utf-8
a = "ど"
b = "\u3069"
puts "\"ど\".encoding        #=> #{a.encoding}"
puts "\"\\u3069\".encoding    #=> #{b.encoding}"
puts "\"ど\" == \"\\u3069\"     #=> #{a == b}"
puts "\"\\u3069\" == \"\\u0069\" #=> #{b == "\u0069"}"
puts "\"ど\" =~ /\\u3069/     #=> #{a =~ /\u3069/}"
puts "\"i\" =~ /\\u3069/      #=> #{"i" =~ /\u3069/}"
puts "\"i\" =~ /\\u0069/      #=> #{"i" =~ /\u0069/}"
$ ruby testb_string.rb
"ど".encoding        #=> UTF-8
"\u3069".encoding    #=> UTF-8
"ど" == "\u3069"     #=> true
"\u3069" == "\u0069" #=> false
"ど" =~ /\u3069/     #=> 0
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0
$ macruby test_string.rb
"ど".encoding        #=> UTF-16
"\u3069".encoding    #=> US-ASCII
"ど" == "\u3069"     #=> false
"\u3069" == "\u0069" #=> true
"ど" =~ /\u3069/     #=> 
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0

おぉう…どういうことなの…なんでこんなに違うの…。ちゃんとわかってないんだけど、こんな感じなのかしら。

  • MacRubyはソースコードがUTF-8で書かれているものと想定して、それをUTF-16に変換している?あと、magic commentを見てないようで、試しにeuc-jpで書いてみたらバイト列をそのままUTF-16の文字列だと解釈してStringクラスにしていて、化ける。
  • IOからの読み込みはASCIIとして扱っている。ARGFでもopenでも同じだった。こちらも環境変数、コードのencoding、magic comment、読まれるファイルのencodingに関わらず同じ。
  • String#encodeやString#force_encodingが何もしないでselfを返してるように見える。NSStringのメソッドを使って変換してやれば変わるんだろうか?
  • Unicodeリテラルを解釈するときに、\uXXXXの後ろ二桁しか見てないっぽい。"\u3069" == "\u0069"がtrueって何の冗談かと思った。
  • Unicodeリテラルの扱いが、文字列リテラルの中なのか正規表現リテラルの中なのかで違っている。"i" == "\u3069"はtrueだけど"i" =~ /\u3069/はfalse。そう言えば、"(ど)"は正しくパースできるのに/(ど)/はSyntaxErrorになるところを見ると、Unicodeリテラルに限らずそもそもそこのパースのロジックが微妙に統一されてない感じ。

さてどうしたもんかな…。日本語を正規表現でマッチさせてるところがまずい(ちなみにBrainf*ckは完全に、Whitespaceは不完全ではあるけど一応動いたので、ソースコードと入力にNON-ASCIIな文字列が含まれてなければ問題ないらしい)なら、完全にバイト列だと思って扱ってやるとか、正規表現じゃなくて==しか使わないとか(もちろんバイト単位で比較)、ObjCでまず入力を正規化してやった上でMacRubyに渡すとか(本末転倒!)、そういう風にすれば動かないでもないかもしんないけど、そういう文字列処理みたいなObjCであんまり書きたくないところをRubyでさらっと書けるから良いんであって、それ以外のところはそもそもObjCで書いたって大して難しくない。performSelectorとかランタイムAPIとか使いまくればいいんだよ!というわけでちょっと残念な感じ。

余談

esotericに付属のesocコマンドを使うとDTやBrainf*ckのコードをRubyのコードに変換できるので、出来たコードをmacrubycにかけてやれば最終的にMacOSXで動作するバイナリができます。DTのコードがなんと高速で動作するネイティブのバイナリに!…と思ったけど結構遅かった。なんかこう、色々読み込むののオーバーヘッドが馬鹿にならない感じ。でも、普通にRubyを使うとstack level too deepで動かないような深い再帰のコードでも動いたりする。ていうかexamples/fact.*、macrubycでコンパイルしないと動かないんですけど。何でこんなコード入れてんだ俺。

表題通り。MacRubyとMacFUSEでファイルシステムを作ってみようとしたんだけど、なんかいまいち上手く行かない。

まずHotCocoaがちゃんと機能してない。hotcocoaコマンドでアプリの雛形を作ってくれて、macrakeすればビルドが走って.appの形にパッケージされるんだけど、できたアプリを起動しても起動するだけで落ちる。これだけだとあまりにも分からなすぎるので、アプリの形にしないでスクリプトのまま起動してみる。

$ hotcocoa sample
$ cd sample
$ macruby -d lib/application.rb
core:in `dump': nil is not a symbol (TypeError)
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/menu.rb:12:in `submenu:'
	from core:in `menu:'
	from /Users/taro/Projects/sample/lib/menu.rb:3:in `application_menu'
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/application.rb:17:in `load_application_menu'
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/application.rb:8:in `handle_block:'
	from core:in `application:'
	from lib/application.rb:in `start'
	from lib/application.rb:1:in `
'

一応hotcocoaのソースを読んでったんだけど、どうやらメニューバーを生成してるところでコケてるっぽい。そしたらってんでlib/manu.rbに書いてあるHotCocoa#application_menuの中身を丸ごとコメントアウトしたところ、一応動いた。

まぁ作ろうとしてるのはFUSEのファイルシステムをマウントするアプリなので、別にメニューも要らないしViewも出さないしいっか、と思って、気持ち悪いけどそれは特に原因追わずそのままにして、前回のコードをアプリケーションとして起動できるように直してやる。んで再びmacrubyで起動させてみると、どうやらちゃんとマウントできるようだ。よしよし、と気を良くしてmacrakeを走らせたらNamakeFS.appはできたものの、こいつをダブルクリックしても起動して直後に落ちる。スクリプトのままだと行けるんだけどアプリにするとダメ。くそっ。

そもそもHotCocoaでアプリを作るのが今回の主目的じゃないので、もういいや調べるのは別な機会に、と放置することにして、今度は素直にデーモンとして動くように作り直す。これは概ね上手くいって、namakefsコマンドを叩けばNamakeFSが/Volumesの下にマウントされて、FinderからNamakeFSをアンマウントしてやればnamakefsのプロセス自体も終了してくれるようにできた。よしよし。

じゃあ次は早速AtomPubで取得した情報を出してみよう、と思ってatomutilをインストールしてみたが、どうにもこれが動かない。そもそもatomutilは1.9系でちゃんと動くのかって言われると正規表現回りでコケたりして結構微妙なんだけど、まぁ中身読んだのでごまかす手段は知っている。でも、MacRubyから使おうとしたらどうもその問題の箇所にまで到達していない。しゃいせ。

落ち着け、こういうときは慌てず騒がず冷静に、「おかしも(おさない、かけない、しゃべらない、もどらない)」の精神が大切だ、と自分に言い聞かせて、とりあえず素のnet/httpとrexml/documentでサービス文書を取得してみる。そしたら何てことだ、もうその時点でダメだよ。REXML::Document.new(res.body)とかしてみたら、そこでもうコケる。ちょ。ドキュメントオブジェクトすら作れん。ここでatomutil版の方のスクリプトを動かしてみたら、出てるエラー一緒でやんの。ちなみにCRubyの1.9.1ではもちろん何の問題もなく動くコード。ちゃんと期待通りの動作をするのは確認済み。ふぁっきん。

ええい、もうあれだ、REXMLが使えないとかちょっと正直どうかと思うが、MacRubyにはまだあれがある!そうさ、NSXMLDocument(※Cocoaのクラス)!察しの良い方はお気付きかと思いますが、わざわざMacRubyで書いてるのにも関わらずMacFUSEやらNSXMLDocumentやらを使ってるせいで、段々中身はRubyっぽい文法のObjective-Cのコードと化して来ている。もうなんかそろそろ本末転倒な感は否めないけども、とりあえずnet/httpでWSSE認証かましてGETしてきたatomのデータをNSXMLDocumentに食わせればなんとか必要な情報は取れるところまでできた。よし、これをさっきのコマンドに組み込めば…!

segmentation fault …だと?!

なんだか良くわかりません。動いてるコード + 動いてるコード = セグフォ。もう疲れたよパトラッシュ…。

結論から言うとMacRubyはぶっちゃけバグとか地雷とかが多すぎて涙が出てくるので、まだこれでどうこうしようとかは無理です。流石、RubyKaigiのセッションで開発者自ら”Crazy"と言わしめただけあるぜ、MacRuby0.5。これみたいにすごいナチュラルにObjCとRubyの融合ができちゃうっていう「可能性」には期待しているのだけども、まだ早いみたいね…。

さてどうしよう。MacRubyに深入りするのはそれはそれで楽しそうではあるんだけど、そんなことしてる内にFUSEのこと忘れちゃいそうだしなぁ。とりあえずObjCで書くかなぁ。

こないだAtomPubとWebDAVの話をしてて、あーそうかAPIをWebDAVとして実装してたらファイルシステムとしてマウントできて面白かったかもなぁ、でも実装面倒なんじゃないのかなどうだろやったことないし、でもvimで記事書いて:wで投稿されてmvしたらカテゴリ変わってrmで記事削除されてとか何それ胸膨らむ、じゃなかった夢膨らむし胸踊るね、なんて考えたところでふと気付いた。あるじゃん、ファイルシステムじゃないものをクライアント側でファイルシステムにしちゃう仕組み。

そうだ、FUSEだ。Filesystem in Userspace。前々からそのうち遊んでみようとは思ってたものの特にネタも思い付かなかったので手をつけてなかったのだけど、BlogFSに丁度良さそうだし、ちょっとやってみることにした。

Rubyでいじる

FUSEってファイルシステム作るくらいだからやっぱCとかC++とかで書くもんなんだろう、とぼんやり思ってたんだけど、そう言えば前にハチロクの卒研発表会でRubyで実装したとか言ってたよなぁ、あとhatenafsはそう言えばPerlだったっけなぁ、と思い出した。なんだ普段使ってるので書けるな。軽くググったらRubyでもFUSEのライブラリがあるらしい。よし。

…makeでコケた。んー。Rubyのfusefsに問題があるのかもしれないしMacFUSEだからそのせいかも知れないしなんかfusefsの情報古いのしかでてこないなーあー面倒くさいや。どうしたもんかね。

僕らのMacRubyさんがいるじゃないか

RubyからFUSEを使う方法をもう少し調べるとか、Perlから使う方法を調べるとか、そっちに進んでも良かったんだけど、もっとお手軽な方法に気が付いた。MacFUSEをインストールするとMacFUSE.frameworkが入る。てことは、とりあえずObjCからは簡単に使える。ObjCで書くのはまぁいいけど、正直AtomPubの扱いとか文字列の処理とか面倒くさすぎる。あれ、ちょっと待て。ObjCのフレームワークがあるってことは…MacRubyから使えるじゃないか。

$ ls /Library/Frameworks
. .. MacFUSE.framework MacRuby.framework UIM.framework
$ macruby -v
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macirb
irb(main):001:1 > framework 'MacFUSE'
=> true

素敵!愛してる!

とりあえずHelloFS

MacFUSEのサンプルとか、「雑草ブログ Luaでファイルシステムを実装しよう(MacFUSEで)」あたりを参考にとりあえず実装してみた。

これをmacrubyコマンドで実行する。ちなみにSnow Leopard+MacRuby 0.5beta1+MacFUSE 2.0.3でやってるけど、Leopard+MacRuby 0.4でも動くとは思う(未確認)。

$ macruby namakefs.rb &
$ ls /Volumes/NamakeFS
honne.txt
$ cat /Volumes/NamakeFS/honne.txt
寒いので冬眠したい

OK、OK。上出来。ちなみにこれだけでFinder上にもNamakeFSディスクがマウントされて、ちゃんとCocoaアプリとかからでも開ける。思ったより簡単だった。HotCocoaでアプリ化しちゃえば配布できる形にするのも楽だし、ファイルシステムを作るの自体はObjCの作法に則る必要があるけど、中のロジックはRubyで書けるから、RubyのAtomPubのライブラリも使えるしObjCだと本当に面倒な文字列の処理もさくさく実装できる。

MacRubyでやるってのがまぁ難点と言えば難点だけど。正直開発環境としてはまだアレげだし。RubyCocoaでやりゃいいって話ではあるんだけど、ねぇ…まぁ、いっか。

あんまり関係ないけど、Luaが意外とすんなり読めた件。まぁ中でやってることはまんまObjCだからってのもあるけど。ちょっとLuaもいじってみようかしら。

MacRubyとRubyCocoaの微妙な違い (1)MacRubyとRubyCocoaの微妙な違い (2)の続き。

クラスをいじってみる

またまた抜粋

  # MacRuby版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
  # RubyCocoa版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end

違いは一点、MacRuby版でクラスの定義を書き換えたとき、新しく作られるオブジェクトには影響するけど、既にあるオブジェクトには影響しない。が、RubyCocoa版はクラスの定義を書き換えるとそのクラスの全てのオブジェクトに影響する。面白い。Rubyは全てのオブジェクトがメタクラスのオブジェクトを見てるけど、ObjCのオブジェクトはそれぞれが関数ポインタか何かでメソッドを保持してるってことかな?RubyCocoaにおける「Cocoaのオブジェクト」って結局「(ObjCのオブジェクトを内部に持った)Rubyのオブジェクト」だけど、MacRubyのオブジェクトは全てが「ObjCのオブジェクト」なので、若干CRubyと挙動が違ってくるみたい。

ところで、上のコードはインスタンスメソッドを書き換えてるんだけど、ふと思い立ってこんなコードを書いてみた。

$ cat mrb_class.rb 
framework 'build/Release/Namake.framework'

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ macruby mrb_class.rb 
#<Sloth:0x80063c740>
"string"

MacRubyだと、Sloth::newの実装が差し換えられた。RubyCocoaだとどうだろう。

$ cat rbc_class.rb
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ ruby rbc_class.rb 
#<OSX::Sloth:0x2385ac class='Sloth' id=0x3734a0>
"string"

あれっ。別に普通だ。同じだった。というのも、昔RubyCocoa+RSpecでごにょごよやってたときに、Cocoaのクラスにstubを仕込んだら「それはObjC側の実装だから勝手に変えるな」みたいに怒られた記憶があるんだけど、んー、この感じだと特に問題無さそう?っていうかあれか、RSpecのstub!はもっと中で複雑なことしてるんだよなきっと。こんな単純な話じゃなくて。後で読んでみよう。もしかしたらそこが「RubyCocoaには無理だけどMacRubyならできる」とこかもしれないし、逆にMacRubyだと下手にいじると大変なことになる罠だったりするかもしれないし。

試してないこと

意図的に上の例では抜いてるけど、MacRubyの大きな特徴と言えば、「キーワード付き引き数」。そもそもそんな機能自体Rubyには無いので、これを上手く使えたら大分面白いことになりそう。あと「CocoaのクラスのサブクラスをRuby側で定義する」とか。MacRubyは何度も言うけど「全部がNSObjectのサブクラス」なので、ぶっちゃけ普通のRubyスクリプトを書くだけでもそういう風になってるんだけど、RubyCocoaはそうではないので、なんか違いがあるんじゃないかと思ってみたり。

そう言えば

MacRubyではオブジェクトを「release」することができない。undefined methodで例外が出る。まぁそもそもGC有りなのでオブジェクトにreleaseを送っても無意味なんだけど。だから存在する必要がないし「undefined」で当然か。ちなみに、RubyCocoaだとCocoaのオブジェクトにはちゃんとreleaseメッセージを投げられる。もっともRubyのGCでオブジェクトが開放されるときに自動的に呼ばれてるはずなので、RubyCocoaのコードでreleaseする必要もやっぱりない、はず。むしろ予想外のバグになりそうなのでやらない方がいいのかも[要出展]。

MacRubyとRubyCocoaの微妙な違い (1)の続き。

フレームワークの読み込み

まず最初に違うのは、framework。これはRubyCocoaで書くと多分こんな感じ。

require 'osx/cocoa'

def framework(name)
    OSX.require_framework name
rescue
    bundle = OSX::NSBundle.bundleWithPath(name)
    bundle.load
end

MacRubyのframeworkは名前じゃなくてパスを渡してやるとそのFrameworkを読み込んでくれるけど、OSX::require_frameworkは名前しか受け付けない、つまりパス上にあるFrameworkしか探さない。ので、任意のパスにあるFrameworkを動的に読み込みたいときは、NSBundleのクラスメソッドを使ってバンドルのオブジェクトを作っておいて、それのloadメソッドを呼ぶ必要がある。ちなみにこれはRubyCocoaではなくCocoaの方の機能なのでもちろんMacRubyでも使える。

Stringの扱い

RubyCocoa版の方で、Slothクラスのname、setNameを呼び出してるとこだけ抜粋。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.should.be.== "faultier"
    # @obj.name.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    @obj.name.to_s.should.be.== "ふぁうるてぃあ"
  end

コメントアウトしてるところは、実行するとテストが通らないところ。RubyCocoaではStringとNSStringは全く別のクラスなので、引き数にNSStringが来るはずのCocoaのメソッドを呼ぶときはRubyのString#to_nsを呼んで一度NSStringのオブジェクトに変換してから渡してやる必要がある。また、当然kind_of?(String)はfalseになる。面白いのは、

"string".to_ns == "string" #=> true

になるってこと。==のときは変換かけてから比較するらしい。もう一つ面白いのは、asciiのときにはStringとNSCFStringのeql?がtrueになるのに、日本語のときは何故かfalseになる。$KCODEを指定してやっても駄目。ふぅん。あれ。ていうか、"string".to_ns.eql?("string") ってそもそもtrueじゃないよな。なんでshould.be.eql("string")だと通るんだろう。通っちゃう方がおかしい気がする。

全然そんなことなかった。@obj.name.to_s.should.be.eql("faultier")にしてた。そりゃ通るわ!というわけで訂正。==はNSStringとStringの変換を行なった後比較するけど、eql?およびequal?はもう少し厳密で、NSStringとStringを別のものとして扱う。マルチバイトでも特に違いはない。

ちなみに、MacRubyでは以下が全て成立する。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

理由は簡単。

$ ruby -r'osx/cocoa' -e 'p String.ancestors'
[String, Enumerable, Comparable, Object, Kernel]
$ ruby -r'osx/cocoa' -e 'p OSX::NSString.ancestors'
[OSX::NSString, OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ macruby -e 'p String.ancestors'
[NSMutableString, NSString, Comparable, NSObject, Kernel]
$ macruby -e 'p NSString.ancestors'
[NSString, Comparable, NSObject, Kernel]

見ての通り、MacRubyではRubyのStringはNSStringのサブクラスになってる。てことは、Slothクラスのname/setNameにはRubyのStringオブジェクとがそのまま渡ってそのまま返ってきてるはず。この辺は流石にMacRubyの方がずっと自然に感じられる。

さらに続く。なんか妙に筆が進んでしまった。

MacRubyでBaconを調理する準備ができたので、早速Bacon焼くぜー超焼くぜー、と書き始めたんだけど、その前にRubyCocoaとMacRubyの違いを比べてみたくなった。のでちょっと脱線。

# Namake.framework内にあるSloth.h
#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end
$ cat mrb_bacon.rb
# coding: utf-8
require 'bacon'

framework 'build/Release/Namake.framework'

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
end
$ macbacon mrb_bacon.rb
Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (11 requirements), 0 failures, 0 errors
$ cat rbc_bacon.rb
require 'bacon'
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.to_s.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    # @obj.name.should.be.eql "ふぁうるてぃあ"                                          
    # @obj.name.should.be.eql "ふぁうるてぃあ".to_ns                                    
    # $KCODE = 'u'
    # @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end
end
$ bacon rbc_bacon.rb
OSX::Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (12 requirements), 0 failures, 0 errors

上がMacRuby版、下がRubyCocoa版。ぱっと見は思ったより違いはない。まぁgetter/setterしかないクラスなのでアレだけども。RubyCocoaの方はCocoaのクラスはOSXって名前空間の下にあるけど、MacRubyは全てのRubyのクラスがCocoaのクラスのサブクラスって位置付けなので、Cocoaのクラスに特別な名前空間は無い。もともとCocoaのクラスはNSなんとかとかUIなんとかとかそういうprefixが付いてるので、Rubyの組み込みのクラスとバッティングすることはないのでそれでも困らないしね。まぁ、それはRubyCocoaの方でもinclude OSXしてやれば見ためそんなに変わらないので、そこは然程違和感はないね。

長いので分割して書くことにしよう。次に続く。

以前、「Objective-CのテストやるのにOCUnit使うよりも、RSpec使えたら素敵なんじゃね」と思って、色々試行錯誤してみたことがある。

とりあえずそれっぽいことができるところまでは行ったものの、そのあと特に使う機会もなくそのまま放置していたのだけど、今度は本格的にObjective-Cのフレームワークを作らなきゃならなくなりそうなのでまたRubyCocoaなどいじる。そこでふと思い付いた。せっかくMacRubyがあるんだから、今度はMacRubyで同じことをやってみたらどうか、と。RubyとObjCのブリッジであるRubyCocoaよりも、VMのレベルでObjCと融合してるMacRubyのが色々融通効くんじゃね、もしかしたら結構深くObjCのオブジェクトいじりたくなるかもしんないし。

あれ、読み込めない

というわけでまずはObjCのFrameworkを作ってRubyのスクリプトから読み込ませてみる。NSBundle#loadを呼びだすのでもいいけどMacRubyだったらframeworkって構文があるな、これフルパス指定してあげれば任意のFramework読み込めるみたい。

こういうクラスを含むNamake.frameworkというのを作ったとして、

#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end

それを使うRubyスクリプトはこんなふうに書く。

#!/usr/local/bin/macruby

framework 'build/Release/Namake.framework'
# こっちでもOK
#bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
#bundle.load
# /System/Library/Frameworks以下にあるフレームワークとかだったら、
# フレームワーク名だけで行ける
#framework 'Cocoa'

sloth = Sloth.new # Sloth.alloc.init
sloth.name = 'faultier'
p sloth
puts sloth.name

framework 'hoge'のお陰ですっきりして見える。紛うことなきRubyのコードだ。早速実行。

$ macruby mrb_test.rb
2009-04-18 22:19:09.756 macruby[773:10b] Error loading /Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake:  dlopen(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake, 265): no suitable image found.  Did find:
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture
./mrb_test.rb:3:in `framework': framework at path `build/Debug/Namake.framework' cannot be loaded: Error Domain=NSCocoaErrorDomain Code=3585 UserInfo=0x800621ee0 "The bundle “Namake” could not be loaded because it does not contain a version for the current architecture." (dlopen_preflight(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake): no suitable image found.  Did find: (RuntimeError)
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture)
	from ./mrb_test.rb:3:in `<main>'

おおぅ…。何、何言ってるの。architectureがおかしいってなんだよ。だってこれRubyCocoaや素のObjective-Cからはちゃんと読めるよ…?

で、調べてみたところ、MacRubyはデフォルトでは64-bitのバイナリを読むらしい。なるほど。なので、↑のを実行するときに、archで-i386を指定してやると一応実行できる。

$ arch -i386 macruby mrb_test.rb
#<Sloth:0x12d06d0>
faultier

もうちょいちゃんとした解決策としては、ターゲットにx86_64が含まれるようにビルドすればいい。あと、MacRubyはObjCのGCを使ってるので、GCサポートを有効にしてビルドしてないと勿論読み込めないので注意。

macgemがアレげ

さてフレームワークはちゃんと読み込めた。そしたら今度はRSpecだ。えーと、macruby系のコマンドはmacってprefixが付くのでgemはmacgemか。

$ sudo macgem install rspec
Password:
ERROR:  Error installing rspec:
	rspec requires  (, runtime)

えー…。ここでも詰まるか。一応調べては見たけど、なんかバグレポートにも上がってたし、これはまだ未解決なのかな。そうでなくてもRSpecは案外依存関係が多くて、macrubyみたいな環境で使おうと思うとあっちのパスを直してこっちのライブラリを直して、みたいなことになりそうなのでちょっと心が折れかける。

で、しかたないなーtest/unit使うのかーとかぼやいてたら「そんなあなたにBacon」と言われたので、Baconを入れてみることにした。RSpecっぽい構文だけど最低限の要素に絞ってシンプルにしたものらしい。Ramazeで使われているようだ。mockとかが入ってないのは大分物足りないけども、同じ機能でも見た目の差は考え方の差になる程度には大きい。背に腹はかえられない。

$ sudo macgem install bacon
Password:
Successfully installed bacon-1.1.0
1 gem installed
Installing ri documentation for bacon-1.1.0...
Updating class cache with 17 classes...
Installing RDoc documentation for bacon-1.1.0...

おお、サクっと入った。素敵。ところでbaconコマンドが見あたらないんだけどどこに行ったんだ、と思ってたら、

/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/bin/bacon

こんなところにいらっしゃいました。確かにデフォルトのRubyから入れたコマンドを上書きされても困るし、これはこれでいい。他のコマンドにならってmacbaconって名前でPATHの通ってるとこにシンボリックリンクを張ってやることにする

$ macbacon --version
/opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- bacon (LoadError)
	from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `require'
	from /usr/local/bin/macbacon:88
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `call'
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `catch'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1243:in `order!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1334:in `permute!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1355:in `parse!'
	from /usr/local/bin/macbacon:93
	from /opt/local/lib/ruby/1.8/optparse.rb:787:in `initialize'
	from /usr/local/bin/macbacon:11:in `new'
	from /usr/local/bin/macbacon:11

ありゃ。ああそっか、多分shebangとかLOAD_PATHとか書き換えなきゃなんだな。

と、思ったんだけど、実はそんなことしなくても良かったことが判明。この次に書いてあることは実行せず、追記のところまで読んで下さい。

#!/usr/bin/env ruby
# -*- ruby -*-

require 'optparse'
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib/')
module Bacon; end 

ビンゴ。ってことでこれを、

#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby                    
# vim: filetype=ruby fileencoding=utf-8                                                 

require 'optparse'
$LOAD_PATH.unshift '/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/lib/'
module Bacon; end

こんな風に書き換える。マジックコメントをvimにしたのは単に俺がvim使いだから。MacRubyは1.9.1相当なので、encodingはちゃんと指定しておいた方がいいな。パスがベタ書きなのは気持ち悪いと言えば悪いけど、まぁ移動することも無いので別にいいか。

$ macbacon --version
bacon 1.1

できたできた。ふー。このくらいのとこまではmacgemでやってくれるようになったら嬉しいんだけど、なんつーか面倒臭そうではあるね。とにかくこれでMacRuby+Bacon環境が出来たので少し休憩。

追記

上の作業をする前にデフォルトのRubyの方でもBaconを入れてたんだけど、今度はそっちの挙動が何か変だ、と思ったら…

$ cat /usr/bin/bacon
#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby
#
# This file was generated by RubyGems.
#
# The application 'bacon' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
  version = $1
  ARGV.shift
end

gem 'bacon', version
load 'bacon'

おい。待てコラ。さっき「デフォルトのRubyから入れたコマンドを上書きされても困るし」って書いたけど、しっかり上書きされてました。あうー。まぁでもこれを使えば上に書いたような気持ち悪い書き換えはしないで済むみたいだ。ので、

  1. さっき書き換えたところは全部元に戻し
  2. /usr/bin/baconを/usr/local/bin/macbaconにrenameし
  3. 再度/usr/bin/gemの方でBaconをインストール

した。と言うわけで、デフォルトのRuby(/usr/bin/ruby)を使ってる人はmacgemの実行は気を付けた方がいいみたい。まぁ、macgem自体は今殆どのgemがインストール時にコケるし、デフォルトのRubyはバージョンが若干古いので基本的には自分でbuildして/usr/local/binあたりかMacPortsから/opt/local/binあたりに入れてると思うので、そんなに困ることはないかもしれないけど、念のため。

おまけ

Baconってなんだろう、ってググったら当然ベーコンに関する情報がヒットするんだけど、Google先生がなんか妙なものを拾ってきた。

bacon

右端の変態、あんた何やってんだ。

今週いろいろと忙しかったせいで「あとで書くと言ったまま書かないメソッド」が発動してしまって大分出遅れた感があるんだけど、RubyKaigi 2008に行ってきたの続き。

拡張ライブラリの書き方講座(artonさん)

RubyをCで拡張する方法の解説。朝からマニアックなセッションやるなぁ、と思ったら「朝のうちにマニアックなものをやっておく」という運営側の戦略だったらしい。朝一でつくばまでやって来てる人程マニアックな層なので、この戦略は当たりかもしれない。

Rubyの拡張ライブラリは結構簡単に書けるらしい。面白そうなので今度Cの勉強も兼ねて書いてみようかな。ruby拡張を書いてみるテスツ - 大学6年生のhogelogでサンプルがあがってるけど、これはちょっと面白そうだ。

あと個人的に興味深かったのは、artonさんがサンプルプログラムが上手く動かなくてその場でデバッグしはじめたとき。artonさんがあれこれ書き換えてる後ろで流されてるIRCのログに、みんなで「ここをこうすればいいんじゃないか」「arton、うしろうしろー!」などと協力してて面白かった。ペアプロならぬテラプロって誰が言ったんだっけ。

さらに仕事で使うRuby(ごとけんさん)

仕事で使うツールやその運用方法などの紹介。HikiとかRedMineとか。RedMine今回色んなセッションで紹介されてたし、Ruby本体のissue trackingにも使われてるらしい。今度使ってみようかな。

あとなんとオープンソース版Fastladderを紹介していただいた。日曜、月曜とFastladderのダウンロード数が増えてたのは間違いなくごとけんさんのお陰だろうと思う。

でもその後twitterで「Fastladderってでも最近全然開発してないよね。飽きちゃったのかな」とか言われてるのを見て焦った。いやいやいやいや、本当すいません、飽きてないですちゃんとやります。中の人二名(二人ともRubyKaigiに行ってました)が中々手が空かず放置気味になってしまってたのだけど、俺の今季の個人目標にはちゃんと「Fastladderの開発」が入ってますので!Googleグループの方にも反応できてなくて本当に申し訳ないんですが、バグレポートや要望など挙げてもらえたら頑張って対応します。

The future of Ruby in Mac OS X(Laurentさん)

RubyCocoaとMacRubyの紹介をApple社員のLaurent Sansonettiさんから。大変wktkする内容だったんだけど、それにしてもあの盛り上がりようはRubyistのマカー率の高さを示してるのかな。

前半はRubyCocoaの紹介で、後半はRubyCocoaが抱える問題点をMacRubyというアプローチで解決していこうとしている、という話。具体的なところは以前書いた記事をご覧下さい。要は「ブリッジではなくObjective-CでRuby処理系を実装することで、ラッパーを介する際のオーバーヘッドや複雑さを避ける」というのが要旨。当然ながら処理速度が飛躍的に早くなるよってところで歓声が上がってた。

RubyCocoa使いとしては「NSObjectが基底クラス」ってところが一番の目玉に感じたんだけど、Rubyist視点として興味深いのは、なるべく綺麗にObjective-Cのメッセージパッシングを実現するためにキーワード付き引き数を導入してること。MacRubyは今1.9ベースだけど、この機能は1.9にも無い。これが洗練されてくればMRIにも取り入れられるかもしれないってことで今後に期待。

あと、ちなみにMacRubyはiPhoneで動かすことは当分できないそうだ。メモリ管理にRubyのGCではなくObjC 2.0のGCを使っているので、GCサポートが無いiPhoneのランタイム向けにコンパイルできないらしい。これはちょっと残念。

それから質疑応答の光景が中々面白かった。Laurentさんはフランス語が母語?だったようで、込み入った質問になると「日本語で質問→フランス語で通訳→英語で回答→日本語で通訳」とかになってた。マルチリンガル!

Real-World Enterprise Ruby(大場さん、高井さん)

企業向けの開発でRubyを導入するにあたってのノウハウを伊藤忠テクノソリューションズの二人から。内容的には非常に真面目な話な上、二人ともスーツでステージに立ってたにもかかわらず、IRCでは「スーツがコスプレにしか見えなくなってきた」「漫才が始まった」などと盛り上がっていたのでなんだろうと思って行ってみたら、確かに面白いことやってた。なんでそんなことになったのかは高井さん大場さんのキャラクターから推してしるべし。

ちなみに内容はSI業界の人にはかなり参考になったんじゃないかと思う。yuguiさんの「わかっとらんやつは黙ってろ」とは対照的な、上司や顧客にRuby導入を承認させる方法とその効用についてがよくわかるセッションだった。

最後に

仕事柄大変Perl充な日々を送っているのだけど、久々に2日間丸々Rubyまみれな時間を過させてもらって本当に楽しかった。RubyKaigiスタッフの方々、スピーカーの方々、会場でご一緒させてもらった方々に心からの感謝を。

それから車を出してくれたsotarok、家に泊めてくれたdaftbeats、俺が頼まれてたyuguiさんのサイン入り初めてのRubyを代わりに確保してくれたfrom_kyushu、本当に助かりました。ありがとう!ハチロク世代++。

追記

あと俺何気にRubyConfのTシャツやらJRubyのTシャツやらを着てた。初めてのRuby片手にPHPTシャツを着たsotarokと、Sunと何の繋りもないのにJRubyTシャツを着てた俺が連れだって歩いてる光景は中々妙な感じだったんだけど誰にもつっこまれなかったのは、ツッコミ待ちなのがバレてたんだろうか。

Objective-Cランタイムを使うように書き換えられたMacRubyとやらが出たそうなので試しに入れてみた。目的としては今RubyCocoaが抱えている問題の解消、およびOSXのより深い部分までRubyで扱いやすくする、ってとこなんだろうか。

何が違うの?

  • 実装が1.9のものになっている(Leopard標準搭載のRuby+RubyCocoa環境は1.8.6)
  • メモリ管理をObjective-CランタイムのGCを使うようにしている
  • Cocoaとのブリッジ機能を使うのに特別なライブラリをrequireする必要がない
  • Frameworkの読み込みはKernel#frameworkで
  • 全てのRubyクラスはNSObjectのサブクラス
  • もっといろいろ違うとこあるんだろうけど、まだちゃんと見てないのでこんな感じ。

例えば、現行のRuby+RubyCocoaだと、

require 'osx/cocoa'
OSX.require_framework 'coredata'

class AppController < OSX::NSObject
# ほげほげ
end

とかしてるところを

framework 'coredata'

class AppController
# もともとNSObjectのサブクラス
end

と書けるようになってた。

ObjectがNSObjectのサブクラス

Rubyの全てのクラスはObjectクラスから派生しているのだけど、MacRubyではNSObjectがObjectクラスのスーパークラスになってた。

$ ruby --version
ruby 1.8.6 (2007-09-24 patchlevel 111) [universal-darwin9.0]
$ irb
>> Object.ancestors
=> [Object, Kernel]
>> require 'osx/cocoa'
=> true
>> OSX::NSObject.ancestors
=> [OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ ruby --version
MacRuby version 0.1 (ruby 1.9.0 2008-02-18 revision 0) [i686-darwin9.2.0]
$ irb
>> Object.ancestors
=> [Object, NSObject, Kernel]
>> NSObject.ancestors
=> [NSObject, Kernel]

ということはつまり、MacRubyでは全てのクラスはCocoaのクラスであって、RubyCocoaで言うところのOSX::NSObjectから使えるメソッドが全てのクラスで使えるということか。しかしObjectよりNSObjectのが上位にあるってなんか気持ち悪いなぁ、ちょっと。

実装はあとで見る

Objective-Cってことなんでソースが.mのファイルがいっぱいあるんだと思ったんだけど、流石にそういうわけではなくて(Objective-Cで全面書き直ししたわけではなくて)、大体普通のRuby1.9っぽかった。まぁそりゃそうか、わざわざObjCで全面書き直しする意味もないよなぁ。あとで何がどう違うのかちゃんと見てみよう。読める範囲で。

↑このページのトップヘ