As Sloth As Possible

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

タグ:Bacon

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

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

↑このページのトップヘ