2013年5月12日

js-ctypes で Objective-C を使う (4) - クラスを作る

Objective-C ではクラスを動的に作成して使えるらしい。

使う関数は以下のとおり
  • objc_allocateClassPair
  • objc_registerClassPair
  • class_addMethod
objc_allocateClassPair はクラスオブジェクトを作成する関数。
objc_registerClassPair はクラスを登録する関数。
class_addMethod はクラスにメソッドを追加する関数。
最低限必要なのはこれだけ。

objc_allocateClassPair と objc_registerClassPair は特に難しい事はない関数なので説明は省略。

class_addMethod の定義は以下のとおり
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

ここで IMP はメソッド本体である関数のポインタ、types はメソッドの引数と返り値の型をエンコードした文字列。
js-ctypes では関数ポインタを FunctionType で作る事ができる。

ctypes.FunctionType (ctypes.default_abi,
                     返り値の型, [引数の型, ...])
.ptr (function (引数) {
  ..
  return 返り値;
})

types の詳細は Type Encodins を参照
とりあえず ctypes で使う型からこの文字列へエンコードする関数を作ってしまえば OK。

"use strict"

let Cu = Components.utils;

Cu.import ("resource://gre/modules/ctypes.jsm");

/* なんかイイカンジにアレしてくれるアレ */
let objc = {
  /* はじめる */
  init : function () {
    this.lib = ctypes.open (ctypes.libraryName ("objc"));

    this.objc_getClass = this.lib.declare ("objc_getClass",
                                           ctypes.default_abi,
                                           objc.types.id,
                                           ctypes.char.ptr);
    this.sel_registerName = this.lib.declare ("sel_registerName",
                                              ctypes.default_abi,
                                              objc.types.SEL,
                                              ctypes.char.ptr);
    this.objc_msgSend = this.lib.declare ("objc_msgSend",
                                          ctypes.default_abi,
                                          objc.types.id,
                                          objc.types.id,
                                          objc.types.SEL,
                                          "...");
    
    this.objc_allocateClassPair = this.lib.declare ("objc_allocateClassPair",
                                                    ctypes.default_abi,
                                                    objc.types.id,
                                                    objc.types.id,
                                                    ctypes.char.ptr,
                                                    ctypes.size_t);
    
    this.objc_registerClassPair = this.lib.declare ("objc_registerClassPair",
                                                    ctypes.default_abi,
                                                    ctypes.void_t,
                                                    objc.types.id);
    
    this.class_addMethod = this.lib.declare ("class_addMethod",
                                             ctypes.default_abi,
                                             objc.types.BOOL,
                                             objc.types.id,
                                             objc.types.SEL,
                                             objc.types.IMP,
                                             ctypes.char.ptr);
  },
  
  /* おわる */
  release : function () {
    this.lib.close ();
  },
  
  /* メッセージを送る */
  send : function (listener, sel, args) {
    return this.objc_msgSend.apply (undefined,
                                    [listener, this.sel_registerName (sel)]
                                    .concat (args));
  },
  
  /* NSString を作る */
  str : function (str) {
    return this.classes.NSString.alloc ()
    .initWithUTF8String (ctypes.char.array ()(str))
    .autorelease ()();
  },
  
  /* メソッド呼び出しみたいにするプロキシ */
  proxy : function (obj, sels) {
    if (!sels) {
      sels = [];
    }
    
    return Proxy.createFunction ({
        get : function (a, sel) {
          /* 使い方を間違えると valueOf や toString のメッセージが飛んで
           * クラッシュするので適当に回避 */
          if (sels.length == 0 && (sel == "valueOf" || sel == "toString")) {
            throw new Error (sel + " is called.");
          }
          
          /* セレクタを記録する */
          return objc.proxy (obj, sels.concat (sel));
        }
      },
      function () {
        if (sels.length == 0) {
          /* セレクタが無い場合は中身の取得 */
          if (arguments.length == 0) {
            /* 引数が無い場合はそのまま */
            return obj;
          }
          else {
            /* 引数がある場合はキャスト */
            return ctypes.cast (obj, arguments [0]).value;
          }
        }
        
        /* メッセージを送る */
        let args = Array.prototype.slice.call (arguments, 0);
        let sel = sels.join (":");
        if (args.length > 0) {
          sel += ":";
        }
        let ret = objc.send (obj, sel, args);
        return objc.proxy (ret);
      });
  },
  
  /* 名前に対応するクラスを返すオプジェクト的なアレ */
  classes : new Proxy ({}, {
      get : function (obj, name) {
        return objc.proxy (objc.objc_getClass (name));
      }
    }),
  
  /* Objective-C の型 */
  types : function () {
    let types = {};
    
    types.objc_object = new ctypes.StructType ("objc_object");
    types.id = types.objc_object.ptr;
    
    types.objc_class = new ctypes.StructType ("objc_class");
    types.Class = types.objc_class.ptr;
    
    types.objc_selector = new ctypes.StructType ("objc_selector");
    types.SEL = types.objc_selector.ptr;
    
    types.IMP = ctypes.voidptr_t;
    
    types.objc_method_description
    = new ctypes.StructType ("objc_method_description",
                             [ { "method_name"  : types.SEL }, {
                                 "method_types" : ctypes.char.ptr }, {
                                 "method_imp"   : types.IMP } ]);
    types.Method = types.objc_method_description.ptr;
    
    types.BOOL = ctypes.signed_char;
    
    return types;
  }(),
  
  /* 型をエンコードする */
  encode : function (origType) {
    let type = origType;
    let str = "";
    
    /* ポインタを解決 */
    while ("targetType" in type
           && type.targetType) {
      /* 特殊な型だけ先にチェックする */
      if (type == objc.types.id) {
        str += "@";
        return str;
      }
      if (type == objc.types.Class) {
        str += "#";
        return str;
      }
      if (type == objc.types.SEL) {
        str += ":";
        return str;
      }
      
      str += "^";
      type = type.targetType;
    }
    
    /* 関数 */
    if ("abi" in type) {
      str += "?";
      return str;
    }
    
    /* 構造体 */
    if ("fields" in type) {
      str += "{" + type.name + "="
       + type.fields.map (function (field) {
          return Object.keys (field).map (function (name) {
              return objc.encode (field [name]);
            }).join ("");
         }).join ("") + "}";
      return str;
    }
    
    /* 配列 */
    if ("elementType" in type
        && "length" in type) {
      if (type.length) {
        str += "[" + type.length + objc.encode (type.elementType) + "]";
      }
      else {
        str += "^" + objc.encode (type.elementType);
      }
      return str;
    }
    
    /* プリミティブ */
    switch (type) {
      case ctypes.int8_t:     str += "c"; break;
      case ctypes.uint8_t:    str += "C"; break;
      case ctypes.int16_t:    str += "s"; break;
      case ctypes.uint16_t:   str += "S"; break;
      case ctypes.int32_t:    str += "i"; break;
      case ctypes.uint32_t:   str += "I"; break;
      case ctypes.int64_t:    str += "l"; break;
      case ctypes.uint64_t:   str += "L"; break;

      case ctypes.float32_t:  str += "f"; break;
      case ctypes.float64_t:  str += "d"; break;

      case ctypes.bool:                str += "B"; break;
      case ctypes.short:               str += "s"; break;
      case ctypes.unsigned_short:      str += "S"; break;
      case ctypes.int:                 str += "i"; break;
      case ctypes.unsigned_int:        str += "I"; break;
      case ctypes.long:                str += "l"; break;
      case ctypes.unsigned_long:       str += "L"; break;
      case ctypes.long_long:           str += "q"; break;
      case ctypes.unsigned_long_long:  str += "Q"; break;
      
      case ctypes.float:   str += "f"; break;
      case ctypes.double:  str += "d"; break;

      case ctypes.char:           str += "c"; break;
      case ctypes.signed_char:    str += "c"; break;
      case ctypes.unsigned_char:  str += "C"; break;

      case ctypes.size_t:     str += "L"; break;
      case ctypes.ssize_t:    str += "l"; break;
      case ctypes.intptr_t:   str += "l"; break;
      case ctypes.uintptr_t:  str += "L"; break;

      case ctypes.jschar:  str += "s"; break;

      case ctypes.void_t:  str += "v"; break;

      case ctypes.Int64:   str += "q"; break;
      case ctypes.UInt64:  str += "Q"; break;
      
      default: throw new Error ("Unsupported type: " + origType);
    }
    
    return str;
  },
  
  /* メソッドの引数と返り値の型をエンコードする */
  encodeMethod : function (ret, args) {
    return [ret].concat (args).map (function (t) objc.encode (t)).join ("");
  }
};

/* ---- ここから本体 ---- */

/* はじめる */
objc.init ();

/* プールを作る */
let pool = objc.classes.NSAutoreleasePool.alloc ().init ();

/* クラスを作る */
let testJSClass = objc.objc_allocateClassPair (objc.classes.NSObject (),
                                               "TestJSClass", 0);

/* メソッドを作る */
objc.class_addMethod (testJSClass,
                      objc.sel_registerName ("pi"),
                      ctypes.FunctionType (ctypes.default_abi,
                                           ctypes.int32_t,
                                           [objc.types.id,
                                            objc.types.SEL])
                      .ptr (function (self, sel) {
                          return Math.floor (Math.PI * 10000);
                        }),
                      objc.encodeMethod (ctypes.int32_t,
                                         [objc.types.id,
                                          objc.types.SEL]));
objc.class_addMethod (testJSClass,
                      objc.sel_registerName ("twice:"),
                      ctypes.FunctionType (ctypes.default_abi,
                                           ctypes.int32_t,
                                           [objc.types.id,
                                            objc.types.SEL,
                                            ctypes.int32_t])
                      .ptr (function (self, sel, a) {
                          return a * 2;
                        }),
                      objc.encodeMethod (ctypes.int32_t,
                                         [objc.types.id,
                                          objc.types.SEL,
                                          ctypes.int32_t]));
objc.class_addMethod (testJSClass,
                      objc.sel_registerName ("divide:by:"),
                      ctypes.FunctionType (ctypes.default_abi,
                                           ctypes.int32_t,
                                           [objc.types.id,
                                            objc.types.SEL,
                                            ctypes.int32_t,
                                            ctypes.int32_t])
                      .ptr (function (self, sel, a, b) {
                          return Math.floor (a / b);
                        }),
                      objc.encodeMethod (ctypes.int32_t,
                                         [objc.types.id,
                                          objc.types.SEL,
                                          ctypes.int32_t,
                                          ctypes.int32_t]));
/* クラスを登録する */
objc.objc_registerClassPair (testJSClass);

/* オブジェクトを作る */
let test = objc.classes.TestJSClass.alloc ().init ().autorelease ();

/* メソッドを呼ぶ */
print (test.pi ()(ctypes.int32_t));
print (test.twice (ctypes.int32_t (123))(ctypes.int32_t));
print (test.divide.by (ctypes.int32_t (100), ctypes.int32_t (20))(ctypes.int32_t));

/* プールを開放 */
pool.release ();

/* おわる */
objc.release ();

NSObject を親にすると色々楽ができる。

あとついでに toString と valueOf でエラーが起きるようにしておく。
作ってる途中で何度もクラッシュさせてしまった。
toString とか valueOf を実際にメッセージとして使うクラスを見付けるまでこのまま放置。


ところでこの実装、実はバグがあって、メソッドの返り値が整数じゃないと動かない。
(なので pi の返り値が適当な整数になってる)
メッセージを送信する関数は返り値によって使い分けないといけない事になっていて
  • objc_msgSend
  • objc_msgSend_fpret
  • objc_msgSend_stret
の 3 つが用意されている。それぞれ整数、浮動小数点数、構造体らしい。
ポインタと整数の場合は objc_msgSend で済んでたけど、メソッドの返り値を小数にしょうとすると正しい値が返ってこない。たぶん objc_msgSend_fpret を使えという事だと思う。
そうなると、メソッドの定義によって使う関数を変えないといけない。
とりあえず自動でやる方法がありそうなのでこれから調べてみる。

0 件のコメント:

コメントを投稿