2013年5月8日

js-ctypes で Objective-C を使う

js-ctypes で Objective-C を使って喋らせるメモ。

まず js-ctypes は C の関数しか呼べないので、Objective-C のクラスやメッセージを扱う C の関数が必要。
という事で用意するのは以下の 3 つ。
  • objc_getClass
  • sel_registerName
  • objc_msgSend
objc_getClass は名前を渡すと Objective-C のクラスを返してくれる関数。
sel_registerName は名前を渡すと Objective-C のセレクタを返してくれる関数。
objc_msgSend はオブジェクトにメッセージを送る関数。
全部 libobjc.dylib に入っている。
この 3 つが揃えばとりあえず色々できる。

これを js-ctypes から使うにあたって問題がひとつ。
objc_msgSend は可変長引数なのだー。

js-ctypes に可変長引数なんてあったっけ?と思って調べてみると、バグとして登録されて解決されているという情報は見付けた。
https://bugzilla.mozilla.org/show_bug.cgi?id=554790
declare の引数として "..." を渡すと可変長引数になる、そして可変長部分は CData を渡すとよろしくやってくれる、らしい。

という事で、とりあえず作ったサンプルがこちら。

"use strict"

let Cu = Components.utils;

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

/* ライブラリを開いて */
let lib_objc = ctypes.open (ctypes.libraryName ("objc"));

/* 必要な関数を定義 */
let objc_getClass = lib_objc.declare ("objc_getClass",
                                      ctypes.default_abi,
                                      ctypes.voidptr_t,
                                      ctypes.char.ptr);
let sel_registerName = lib_objc.declare ("sel_registerName",
                                         ctypes.default_abi,
                                         ctypes.voidptr_t,
                                         ctypes.char.ptr);
let objc_msgSend = lib_objc.declare ("objc_msgSend",
                                     ctypes.default_abi,
                                     ctypes.voidptr_t,
                                     ctypes.voidptr_t,
                                     ctypes.voidptr_t,
                                     "...");

/* NSString を作る */
function CreateNSString (str) {
  let NSString = objc_getClass ("NSString");
  let s = objc_msgSend (NSString, sel_registerName ("alloc"));
  s = objc_msgSend (s, sel_registerName ("initWithUTF8String:"),
                    ctypes.char.array ()(str));
  s = objc_msgSend (s, sel_registerName ("autorelease"));
  return s;
}

/* プールを作る */
let NSAutoreleasePool = objc_getClass ("NSAutoreleasePool");
let pool = objc_msgSend (NSAutoreleasePool, sel_registerName ("alloc"));
pool = objc_msgSend (pool, sel_registerName ("init"));

/* スピーチシンセサイザを作る */
let NSSpeechSynthesizer = objc_getClass ("NSSpeechSynthesizer");
let voice = "com.apple.speech.synthesis.voice.kyoko.premium";
let synth = objc_msgSend (NSSpeechSynthesizer, sel_registerName ("alloc"));
synth = objc_msgSend (synth, sel_registerName ("initWithVoice:"),
                      CreateNSString (voice));
synth = objc_msgSend (synth, sel_registerName ("autorelease"));

/* 喋っていただく */
objc_msgSend (synth, sel_registerName ("startSpeakingString:"),
              CreateNSString ("a"));

/* 喋り終わるのを待つ */
while (ctypes.cast (objc_msgSend (synth, sel_registerName ("isSpeaking")),
                    ctypes.int32_t).value) {
}

/* プールを開放 */
objc_msgSend (pool, sel_registerName ("release"));

/* ライブラリを閉じる */
lib_objc.close ();

initWithUTF8String のあたりで ctypes.char.array を使って CData を作って渡している。

それにしても関数を直接呼び出すように書くと非常に冗長。
なので適当にオブジェクトを作って書き易くする。

"use strict"

let Cu = Components.utils;

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

/* メソッドチェインみたいにするためのオブジェクト */
function objcObject (obj) {
  this._obj = obj;
}
objcObject.prototype = {
  /* メッセージを送る */
  send : function (sel) {
    let args = Array.prototype.slice.call (arguments, 1);
    return new objcObject (objc.send.apply (objc,
                                            [this._obj, sel].concat (args)));
  },
  
  /* 中身を取り出す */
  get value () {
    return this._obj;
  }
};

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

    this.objc_getClass = this.lib.declare ("objc_getClass",
                                           ctypes.default_abi,
                                           ctypes.voidptr_t,
                                           ctypes.char.ptr);
    this.sel_registerName = this.lib.declare ("sel_registerName",
                                              ctypes.default_abi,
                                              ctypes.voidptr_t,
                                              ctypes.char.ptr);
    this.objc_msgSend = this.lib.declare ("objc_msgSend",
                                          ctypes.default_abi,
                                          ctypes.voidptr_t,
                                          ctypes.voidptr_t,
                                          ctypes.voidptr_t,
                                          "...");
  },
  
  /* おわる */
  release : function () {
    this.lib.close ();
  },
  
  /* 名前に対応するクラスを返す */
  getClass : function (name) {
    return this.objc_getClass (name);
  },
  
  /* メッセージを送る */
  send : function (listener, sel) {
    let args = Array.prototype.slice.call (arguments, 2);
    return this.objc_msgSend.apply (undefined,
                                    [listener, this.sel_registerName (sel)]
                                    .concat (args));
  },
  
  /* NSString を作る */
  CreateNSString : function (str) {
    return this._(this.getClass ("NSString")).send ("alloc")
    .send ("initWithUTF8String:", ctypes.char.array ()(str))
    .send ("autorelease").value;
  },
  
  /* objcObject を作る */
  _ : function (obj) {
    return new objcObject (obj);
  }
};

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

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

/* プールを作る */
let NSAutoreleasePool = objc.getClass ("NSAutoreleasePool");
let pool = objc._(NSAutoreleasePool).send ("alloc").send ("init");

/* スピーチシンセサイザを作る */
let NSSpeechSynthesizer = objc.getClass ("NSSpeechSynthesizer");
let voice = "com.apple.speech.synthesis.voice.kyoko.premium";
let synth = objc._(NSSpeechSynthesizer).send ("alloc")
  .send ("initWithVoice:", objc.CreateNSString (voice))
  .send ("autorelease");

/* 喋っていただく */
synth.send ("startSpeakingString:", objc.CreateNSString ("a"));

/* 喋り終わるのを待つ */
while (ctypes.cast (synth.send ("isSpeaking").value, ctypes.int32_t).value) {
}

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

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

ちょっとマシになったかもしれない。

0 件のコメント:

コメントを投稿