RubyCocoa 今週のコミット ..2014-07-26 10.6/10.7での初期化エラーを修正
細かいとこではドキュメント更新したり、コンパイル時の警告つぶしたりしてた。
大きい作業としては、Snow LeopardやLion環境では"trunkでrequire 'osx/cocoa'しただけで落ちる"という致命的な問題があってその対応をしていた。
エラーメッセージはこんなの。
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby" -I../e xt/rubycocoa -I../lib testall.rb 2014-07-25 05:48:43.038 ruby[4778:60f] *** NSInvocation: warning: object 0x7 fff708db850 of class 'Object' does not implement methodSignatureForSelector: -- trouble ahead 2014-07-25 05:48:43.041 ruby[4778:60f] *** NSInvocation: warning: object 0x7 fff708db850 of class 'Object' does not implement doesNotRecognizeSelector: - - abort test failed
ようするに(Objective-Cの)Objectクラスを処理しようとしてエラーになってるわけだ。
Objective-Cのクラスの話
前提として説明すると、Objective-CのランタイムにあるすべてのクラスがCocoaのクラスというわけではない。Cocoaでは、NSObjectとNSProxyの2つのルートクラスがあり、ほとんどはNSObjectの派生クラスとしてFoundation等のフレームワークは構成されている。
で、それらとは別系統のクラスもあって、たぶんObjective-C組み込みと思われる"Object"クラスなんてのもある。
// OS XのObjective-Cのクラス階層 + NSObject + NSArray | + NSMutableArray + NSResponder + NSView + NSText | + NSTextView + NSTableView + NSProxy + Object # <= NOT a Cocoa class
ちなみに、今までほとんど表にでてこなかったObjectクラスなんだけど、SwiftのクラスはこのObjectクラスの派生クラスになってるという話もあるみたいね。
話を戻そう。(最近の)RubyCocoaでは、この「Cocoaのクラスかどうか」を
- 自クラスまたはその親のクラスがNSObjectプロトコルに適合しているかどうか
で判定している。実装はこんな感じ。
// framework/src/objc/mdl_osxobjc.m /* * Detects Cocoa Objective-C class or not with protocol "NSObject". * - NSObject => YES * - NSProxy => YES * - Object => NO */ static BOOL class_is_cocoa_class_p(Class klass) { Protocol *proto = objc_getProtocol("NSObject"); Class klass_sup = klass; while (klass_sup) { if (class_conformsToProtocol(klass_sup, proto)) { return YES; } klass_sup = class_getSuperclass(klass_sup); } return NO; }
今回のエラーの話
RubyCocoaでは、できるだけObjective-Cのクラス階層をRuby側でも再現するために、あるクラスがRuby側に現れたとき、そのスーパークラスもRuby側に呼び出すようにしている。今回はその機能でひっかかるとこができてた。
RubyCocoaの初期化時にOSX.ns_import_allでぜんぶのクラスをRuby側に持ってくるようにしているんだけど、そのとき処理対象となるProtocolクラスのスーパークラスがSnow Leopard環境などではObjectになってる。でObjectクラスをRuby側で扱おうとしてもCocoaなクラスじゃないのでRubyCocoaを通した操作ができない。でエラーになるというわけ。
RuntimeBrowserで実装を見てみると、ProtocolクラスのスーパークラスがMavericksではNSObjectに、Snow LeopardではObjectになっていることがわかる。
// OSX 10.9 Mavericks @interface Protocol : NSObject { // OSX 10.6 Snow Leopard @interface Protocol : Object <NSObject>
これを解消するためには、OSX::Protocol.oc_superclassがnilを返す(スーパークラスなし)ようにすればよい。つまり、Objective-Cのメソッド呼び出しの結果がClassで、そのクラスがCocoaのクラスじゃないときにはnilを返すように変更することになる。
関連するコミット。
- Cocoaのクラスかどうかの判定基準をisKindOfClass:でなく、NSObjectプロトコルに適合しているかどうかに変更。10.8以前では+[NSProxy isKindOfClass:]を呼び出すとエラーになるため。(r2618)
- Cocoaのクラスかどうかの判定を関数class_is_cocoa_class_p()に切り出し。(r2620, r2622)
- メソッドの返り値がクラスで非Cocoaのクラスのときnilを返すようにする。(r2626)
これでOS X 10.6 - 10.9の環境でtrunkのテストがぜんぶ通るようになった。