きりかノート 3冊め

おあそびプログラミング

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;
   }

で、これを利用してCocoaのクラスではないクラスはRubyCocoa的には存在しないものとして処理している。

今回のエラーの話

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のテストがぜんぶ通るようになった。