きりかノート 3冊め

おあそびプログラミング

RubyCocoaのLion?での問題を修正 - (4)

つづき。これはSnow Leopardのときからありました。まちがいなく。

ていうか64bitのlibffiとの問題です(わかってみれば)。

NSDecimalや配列を含む値が返ってくるメソッドで落ちる

2009年の9月から積み残したままの問題。64bitだと落ちるテストがあった。

 struct ttype2 {float a[2];};

 @implementation NSObject (FooTests)
 - (struct ttype2)test2 {
  struct ttype2 r;
  r.a[0] = 1.;
  r.a[1] = 2.;
  return r;
 }

Ruby側からtest2メソッドを呼び出すと落ちる。

 Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
 0   libsystem_kernel.dylib        	0x00007fff8c1c6ce2 __pthread_kill + 10
 1   libsystem_c.dylib             	0x00007fff8738b7d2 pthread_kill + 95
 2   libsystem_c.dylib             	0x00007fff8737ca7a abort + 143
 3   libruby.dylib                 	0x0000000108ff99e1 rb_bug + 241
 4   libruby.dylib                 	0x0000000109066b2f sigsegv + 63
 5   libsystem_c.dylib             	0x00007fff873ddcfa _sigtramp + 26
 6   libffi.5.dylib                	0x0000000109532b03 classify_argument + 307
 7   libffi.5.dylib                	0x0000000109532b21 classify_argument + 337
 8   libffi.5.dylib                	0x00000001095327f5 ffi_prep_cif_machdep + 53
 9   com.apple.rubycocoa           	0x00000001093a8529 rb_ffi_dispatch + 7001
 10  com.apple.rubycocoa           	0x000000010938c619 ocm_send + 4409 (mdl_objwrapper.m:459)
 11  com.apple.rubycocoa           	0x000000010938ca54 wrapper_ocm_send + 52 (mdl_objwrapper.m:521)

libffiのffi_prep_cif_machdep -> classify_argumentの中で落ちてる感じ。libffiのコードを読んでみると、これは呼び出し前に返り値とパラメータの情報を準備する処理のようだ。ということは呼び出しの実行そのものとは関係なく、単純に渡してるパラメータに問題があるようだ。

ということでclassify_argumentのコードを見てみる。この処理はx86_64にしかない(src/x86/ffi64.c)。渡した型が構造体のとき、

   case FFI_TYPE_STRUCT:
     {
       // 略

       /* If the struct is larger than 32 bytes, pass it on the stack.  */
       if (type->size > 32)
         return 0;

       // 略

       /* Merge the fields of structure.  */
       for (ptr = type->elements; *ptr != NULL; ptr++)
         {
           int num;

           byte_offset = ALIGN (byte_offset, (*ptr)->alignment);

           num = classify_argument (*ptr, subclasses, byte_offset % 8);
           if (num == 0)
             return 0;
           for (i = 0; i < num; i++)
             {
               int pos = byte_offset / 8;
               classes[i + pos] =
                 merge_classes (subclasses[i], classes[i + pos]);
             }

           byte_offset += (*ptr)->size;
         }

32byte以下のときは構造体の要素をひとつずつ見てって処理すると。とりあえずttype2の配列のサイズを大きくしてみるとテストは通るようになるので、後続の処理っぽい。

RubyCocoaでは、上述のttype2のように構造体の中に配列があるとき、libffiに渡す型情報(FFI_TYPE_*)には配列の型がないので、てきとうに同サイズの構造体を用意して渡すようにしている。

 static ffi_type *
 fake_ary_ffi_type (unsigned bytes, unsigned align)
 {
   // 略
   type->size = bytes;
   type->alignment = align;
   type->type = FFI_TYPE_STRUCT;
   type->elements = malloc(bytes * sizeof(ffi_type *));
   ASSERT_ALLOC(type->elements);
   for (i = 0; i < bytes; i++)
     type->elements[i] = &ffi_type_uchar;

   st_insert(ary_ffi_types, (st_data_t)bytes, (st_data_t)type);

   // 略

ん、あれ?

最後の要素がNULLになってないじゃん。そらEXEC_BAD_ACCESSですわ。

  • x86_64
  • 構造体の中に配列がある
  • 構造体のサイズが32バイト以下

のときにSEGVする、ということのようだ。

で、個数+1の領域をとって最後にNULLを入れるようにして修正(r2314)。とりあえずテストぜんぶ流してもSEGVしなくなった。

気がついてみれば「あー」て感じですが、libffiまわりとか今回ほとんど初めてみたようなもんだし(基本的にここは他のメンテナまかせにしてた)。

返ってきた配列から値を適切に取り出せない

だいたい似たような問題。

   6) Failure:
 test_cary_struct(TC_Types) [./tc_types.rb:293]:
 <1.0> expected but was
 <2.80259692864963e-45>.

たぶんずれてますねー。構造体のサイズを32バイト超にすると通るので、前述のと同様にffi_typeの設定値の問題だろう、きっと。

x86-64.orgのABIのドキュメント(PDF)をなんとなく読んだ感じだと、配列のときそのまんま値がならんでるイメージでよさそう。ということでなんでもucharで詰めるのではなく、もともとの値を用意してlibffiに渡してやるようにしたら直った(r2315)。

配列の型情報はbsBoxedから取るのがいちばん簡単そうだったけれど、そのためには値をいろんなとこで渡して保持するようにしないといけない。それはあまりいいコードではなさそう。sscanf()とか使いたくなかったんだけどやむなく。よさげな別の方法あったら教えてください。

残っている問題

テストがぜんぶ通るわけじゃないんだけど、まーあとはささいなこと。クラス名がちがうとかね。

Snow Leopardのころから話をきいている、スクロールしていると落ちてしまう問題を調べる予定。状況にもよるけど、とりあえず今月中には一度リリースをするつもり。