2013年5月29日

harmony:spread (3)

一旦寝てから考えたらもっとシンプルな解決策を思いついたので修正。

バイトコードを生成する段階で配列であるかのように生成する、っていうんじゃあ配列のバイトコードの修正があった場合のメンテナンス性が下がるし、同じようなコードが二箇所にあるっていうのもアレ。

という事で、パーサの方で spread operator 付きの引数列を配列に変換して、更に spread operator を付けた 1 引数として返す事にする。

[js/src/frontend/Parser.cpp]

bool
Parser<ParseHandler>::argumentList(Node listNode)
{
...
    bool spreadcall = false;
    Vector<Node, 8, SystemAllocPolicy> args;

    do {
        bool spread = false;
        if (tokenStream.matchToken(TOK_TRIPLEDOT, TSF_OPERAND)) {
            if (tokenStream.matchToken(TOK_RP, TSF_OPERAND)) {
                report(ParseError, false, null(), JSMSG_SYNTAX_ERROR);
                return null();
            }
            spread = true;
            spreadcall = true;
        }

        Node argNode = assignExpr();
        if (!argNode)
            return false;
        if (spread) {
            argNode = handler.newUnary(PNK_SPREAD, argNode);
            if (!argNode)
                return null();
        }
...
#if JS_HAS_GENERATOR_EXPRS
        if (!spread && tokenStream.matchToken(TOK_FOR)) {
...
        } else
#endif
...
        args.append(argNode);
    } while (tokenStream.matchToken(TOK_COMMA));

    if (!spreadcall) {
        for (size_t i = 0, len = args.length(); i < len; i ++) {
            handler.addList(listNode, args[i]);
        }
    } else {
        size_t len = args.length();
        if (len == 1) {
            handler.addList(listNode, args[0]);
        } else {
            Node pn = handler.newList(PNK_ARRAY, args[0], JSOP_NEWINIT);
            if (!pn)
                return null();
            handler.setListFlag(pn, PNX_SPECIALARRAYINIT | PNX_NONCONST);
            for (size_t i = 1; i < len; i ++) {
                handler.addList(pn, args[i]);
            }
            pn = handler.newUnary(PNK_SPREAD, pn);
            if (!pn)
                return null();
            handler.addList(listNode, pn);
        }
    }
...
}

バイトコード生成の段階では、引数が 1 つで、かつ最初の引数に spread operator が付いているなら特別扱いする事にする。具体的には argc を -1 にして、JSOP_NOTEARG を吐かない。

[js/src/frontend/BytecodeEmitter.cpp]

static bool
EmitCallOrNew(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
{
...
    bool spread = false;
    if (pn->pn_count == 2 && pn2->pn_next->isKind(PNK_SPREAD)) {
        spread = true;
        argc = -1;
    }
...
        if (bce->selfHostingMode && pn2->name() == cx->names().callFunction &&
            !spread)
        {
...
        }
...
        for (ParseNode *pn3 = spread ? pn2->pn_next->pn_kid : pn2->pn_next; pn3; pn3 = pn3->pn_next) {
            if (!EmitTree(cx, bce, pn3))
                return false;
            if (spread)
                continue;
            if (Emit1(cx, bce, JSOP_NOTEARG) < 0)
                return false;
        }
...
}

例によって argc が -1 の場合は 1 って事にする。

[js/src/jsopcode.cpp]

unsigned
js::StackUses(JSScript *script, jsbytecode *pc)
{
...
      default:
        /* stack: fun, this, [argc arguments] */
        JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL ||
                  op == JSOP_FUNCALL || op == JSOP_FUNAPPLY);
        if (GET_ARGC(pc) == (uint16_t)-1) {
            return 2 + 1;
        }
        return 2 + GET_ARGC(pc);
...
}


で、実行する時には、argc が -1 の場合スタックに乗っている配列をスタックに展開する。

[js/src/jsinterp.cpp]

BEGIN_CASE(JSOP_NEW)
BEGIN_CASE(JSOP_CALL)
BEGIN_CASE(JSOP_FUNCALL)
{
...
    CallArgs args;
    if (GET_ARGC(regs.pc) != (uint16_t)-1) {
        JS_ASSERT(regs.stackDepth() >= 2 + GET_ARGC(regs.pc));
        args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
    } else {
        JS_ASSERT(regs.stackDepth() >= 3);
        RootedObject &arr = rootObject0;
        arr = &regs.sp[-1].toObject();

        uint32_t argc;
        if (!GetLengthProperty(cx, arr, &argc))
            goto error;
        regs.sp --;

        GetElements(cx, arr, argc, regs.sp);
        regs.sp += argc;

        args = CallArgsFromSp(argc, regs.sp);
    }
...
}

という事で、

js> ((...x) => x)(10, 20, ...[30, 40, 50])
[10, 20, 30, 40, 50]
js> [].concat(...[ [10, 20, 30], [40, 50, 60], [70, 80, 90] ])
[10, 20, 30, 40, 50, 60, 70, 80, 90]

今度こそパッチを投げるのだー。
その前にドキュメント読まなくちゃさん。



…… -1 とかマジックナンバーはダメよね。
という事で定数を定義してついでに ARGC の最大値を 1 減らす修正を加える事に。

harmony:spread (2)

結局 JSOP_SPREADARG を使った可変長のスタック生成、消費の方向は諦めて解決したのでメモメモ。

問題になってたのは、オペコードが生成、消費するスタックの数が定数である必要があるのに、スタックに乗せる引数の数を可変にしたい、という点。
で、スタックに乗せるオペコードと消費するオペコードが別だから可変長の生成と消費になるワケで、生成も消費も JSOP_CALL の中でやっちゃえば外から見れば結果は定数になっちゃう、という案。

で、そのためには JSOP_CALL が固定長の引数を拾って可変長に展開する、という事ができなくちゃいけない。
なので、引数をスタックに順番に乗せるんじゃなくて、配列に全部放り込んで、1 引数であるかのように JSOP_CALL まで持っていく。

こうする事で更に便利なのは、spread の部分が JSOP_SPREAD をそのまま使える。
引数生成の部分をそのまま spread な配列生成のループに置き換えてしまう。

で、あいかわらず引数は特別扱いの -1 を使う。

[js/src/frontend/BytecodeEmitter.cpp]

static bool
EmitCallOrNew(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
{
...
    if (emitArgs) {
        /*
         * Emit code for each argument in order, then emit the JSOP_*CALL or
         * JSOP_NEW bytecode with a two-byte immediate telling how many args
         * were pushed on the operand stack.
         */
        bool oldEmittingForInit = bce->emittingForInit;
        bce->emittingForInit = false;
        if (spread) {
            int32_t nspread = 0;
            for (ParseNode *pn3 = pn2->pn_next; pn3; pn3 = pn3->pn_next) {
                if (pn3->isKind(PNK_SPREAD))
                    nspread++;
            }

            ptrdiff_t off = EmitN(cx, bce, JSOP_NEWARRAY, 3);
            if (off < 0)
                return false;
            CheckTypeSet(cx, bce, JSOP_NEWARRAY);
            jsbytecode *pc = bce->code(off);

            SET_UINT24(pc, argc - nspread);

            if (!EmitNumberOp(cx, 0, bce))
                return false;
        }
        for (ParseNode *pn3 = pn2->pn_next; pn3; pn3 = pn3->pn_next) {
            ParseNode *expr = pn3->isKind(PNK_SPREAD) ? pn3->pn_kid : pn3;
            if (!EmitTree(cx, bce, expr))
                return false;
            if (spread) {
                if (pn3->isKind(PNK_SPREAD)) {
                    if (Emit1(cx, bce, JSOP_SPREAD) < 0)
                        return false;
                }
                else {
                    if (Emit1(cx, bce, JSOP_INITELEM_INC) < 0)
                        return false;
                }
            }
            else {
                if (Emit1(cx, bce, JSOP_NOTEARG) < 0)
                    return false;
            }
        }
        bce->emittingForInit = oldEmittingForInit;
        if (spread) {
            if (Emit1(cx, bce, JSOP_POP) < 0)
                return false;
            if (Emit1(cx, bce, JSOP_ENDINIT) < 0)
                return false;
            argc = -1;
        }
    }
...
}

生成されるバイトコードとしては
THIS.FUNC (ARG1, ARG2, ...ARG3)
↓
{FUNC}               // 関数本体に相当するバイトコード列
{THIS}               // this に相当するバイトコード列
JSOP_NOTEARG
JSOP_NEWARRAY
{ARG1}               // 第 1 引数に相当するバイトコード列
JSOP_INITELEM_INC
{ARG2}               // 第 2 引数に相当するバイトコード列
JSOP_INITELEM_INC
{ARG3}               // 第 3 引数に相当するバイトコード列
JSOP_SPREAD
JSOP_POP
JSOP_ENDINIT
JSOP_CALL(argc=-1)

こんな感じ。
配列生成になったので JSOP_SPREADARG は御役御免。
JSOP_NOTEARG は全部 JSOP_CALL の方に任せる事にする (IonMonkey の方は手をつけてないからどうなるか知らないけど)。

で、今回は引数は全部配列に入ってるから、実質的にスタックに乗ってる引数の数は 1 個って事になる。

[js/src/jsopcode.cpp]

unsigned
js::StackUses(JSScript *script, jsbytecode *pc)
{
...
      default:
        /* stack: fun, this, [argc arguments] */
        JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL ||
                  op == JSOP_FUNCALL || op == JSOP_FUNAPPLY);
        if (GET_ARGC(pc) == (uint16_t)-1) {
            return 2 + 1;
        }
        return 2 + GET_ARGC(pc);
...
}

で、JSOP_CALL の実態の方でスタックに展開する。
なんとも有り難い事に、要素をまとめて展開する関数 GetElements があるので使う。

[js/src/jsinterp.cpp]

BEGIN_CASE(JSOP_NEW)
BEGIN_CASE(JSOP_CALL)
BEGIN_CASE(JSOP_FUNCALL)
{
...
    CallArgs args;
    if (GET_ARGC(regs.pc) == (uint16_t)-1) {
        JS_ASSERT(regs.stackDepth() >= 3);
        RootedObject &arr = rootObject0;
        arr = &regs.sp[-1].toObject();

        uint32_t argc;
        if (!GetLengthProperty(cx, arr, &argc))
            goto error;
        regs.sp --;

        GetElements(cx, arr, argc, regs.sp);
        regs.sp += argc;

        args = CallArgsFromSp(argc, regs.sp);
    }
    else {
        JS_ASSERT(regs.stackDepth() >= 2 + GET_ARGC(regs.pc));
        args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
    }
...
}

という事で、一段落と言っていいところまで来たと思うのでパッチにして投げる予定。

harmony:spread

ES6 で追加される仕様 spread を実装するオハナシ。

http://wiki.ecmascript.org/doku.php?id=harmony:spread

「...」というオペレータで、配列を引数や配列の一部としてその場に展開できるという機能で、配列については既に Firefox16 で実装済。

js> [1, 2, ...[3, 4, 5]]
[1, 2, 3, 4, 5]

でも上のページの Sematics とはやや実装が違って、イテレータを使う事になってる。

しかし引数の方は未実装で、エラーになる。

js> print (1, 2, ...[3, 4, 5])
typein:3: SyntaxError: syntax error:
typein:3: print (1, 2, ...[3, 4, 5])

typein:3: .............^

で、これが欲しくなったのです。

きっかけは UnMHT で以下のような形のリストの変換をしようとした時の事。

[ [ [A, B, C], [M, N] ], [ [D, E, F, G], [O, P, Q], ... ] ]
↓
[ [ [A, B, C], [D, E, F, G], ... ], [ [M, N], [O, P, Q], ...] ]
↓
[ [A, B, C, D, E, F, G, ...], [M, N, O, P, Q, ...] ]

unzip して concat って感じなんだけど、JavaScript の Array の concat って複数の配列をまとめて連結する場合、それぞれ引数として渡さないといけない。

[].concat ([A, B, C], [D, E, F], [G, H, I])

手元に配列の配列があった場合には Array.prototype.concat.apply を使わないといけない。

Array.prototype.concat.apply ([], xs)

これを、

[].concat (...xs)

こう書きたい。

で、いつ頃実装されるのかなー、と Bugzilla の Bug 762363 を見てみたら、バグ登録以降進捗ナシ! (実はあるのかもしれないけど...)
なんでかしら、と思って Firefox のソースを覗いて弄り初めたワケです。

...

で、弄ったものがこちらになります。

js> [].concat (...[[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
[1, 2, 3, 4, 5, 6, 7, 8, 9]
js> ((...x) => x) (1, 2, ...[3, 4, 5])
[1, 2, 3, 4, 5]

動いているかのように見える。
確かに動いているには動いているんだけど、ウソをついてる部分があるので完成とは言い難い。

元々、関数呼び出し (JSOP_CALL) は、

THIS.FUNC (ARG1, ARG2, ARG3)
↓
{FUNC}               // 関数本体に相当するバイトコード列
{THIS}               // this に相当するバイトコード列
JSOP_NOTEARG
{ARG1}               // 第 1 引数に相当するバイトコード列
JSOP_NOTEARG
{ARG2}               // 第 2 引数に相当するバイトコード列
JSOP_NOTEARG
{ARG3}               // 第 3 引数に相当するバイトコード列
JSOP_NOTEARG
JSOP_CALL(argc=3)

みたいなバイトコードで動いてる。

1. FUNC、THIS、ARG1、ARG2、ARG3 で、それぞれ引数をスタックに乗っける
-- ここから JSOP_CALL -- 2. スタックのうち引数の範囲を CallArgs から参照させる
3. 関数の中身を実行する
4. return の部分で FUNC があった場所に返り値を放り込む
5. スタックポインタを返り値の所まで戻す

で、5 の部分でいくつ戻すかっていうのが、

スタックの消費数 - スタックの生成数
= (argc + THIS 1 個 + FUNC 1 個) - (返り値 1 個)
= argc + 1

[js/public/CallArgs.h]
class MOZ_STACK_CLASS CallReceiver
{
...
    Value *spAfterCall() const {
        setUsedRval();
        return argv_ - 1;
    }
...
};

(argv_ は スタックのトップから argc だけ下なので、さらに 1 を引けば良い)

で、その数は実行より前に判明している事が前提になってるみたいなコードがある (このコードはスタックの消費数 = argc + 2)。

[js/src/jsopcode.cpp]
unsigned
js::StackUses(JSScript *script, jsbytecode *pc)
{
...
    switch (op) {
...
      default:
        /* stack: fun, this, [argc arguments] */
        JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL ||
                  op == JSOP_FUNCALL || op == JSOP_FUNAPPLY);
        return 2 + GET_ARGC(pc);
    }
}

スタックの消費数は 2 + GET_ARGC(pc) って事になってて、JSOP_CALL みたいなのは 3 バイトのオペコードで、後ろ 2 バイトに argc を持っている。 (GET_ARGC はオペコードの後ろ 2 バイトの整数を取得する)

static bool
EmitCallOrNew(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
{
...
    uint32_t argc = pn->pn_count - 1;
...
    if (Emit3(cx, bce, pn->getOp(), ARGC_HI(argc), ARGC_LO(argc)) < 0)
        return false;
...
}

spread を使うなら、実行してみないと argc が分からないので、バイトコード生成時に argc を与えるなんて事はできない。

ここが解決できてない所で、今はとりあえず可変 (-1 = 65535) という argc を例外扱いして、どうしても正数が必要な場合はウソ (適当に 3 = 引数は 1 個) を返している。
で、引数が 1 個じゃなければ 3 なんてのはウソなんでクラッシュするかと思ったけど別にクラッシュはしなかった...。
一体何のためにこの数が使われてるか更に解析する必要アリ。

[js/src/jsopcode.cpp]
unsigned
js::StackUses(JSScript *script, jsbytecode *pc)
{
...
    switch (op) {
...
      default:
        /* stack: fun, this, [argc arguments] */
        JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL ||
                  op == JSOP_FUNCALL || op == JSOP_FUNAPPLY);
        if (GET_ARGC(pc) == 65535) {
            return 3;
        }
        return 2 + GET_ARGC(pc);
    }
}

で、バイトコード自体はどんな感じになったかというと

THIS.FUNC (ARG1, ARG2, ...ARG3)
↓
{FUNC}               // 関数本体に相当するバイトコード列
{THIS}               // this に相当するバイトコード列
JSOP_NOTEARG
JSOP_ZERO
{ARG1}               // 第 1 引数に相当するバイトコード列
JSOP_NOTEARG
SWAP
JSOP_ONE
JSOP_ADD
{ARG2}               // 第 2 引数に相当するバイトコード列
JSOP_NOTEARG
SWAP
JSOP_ONE
JSOP_ADD
{ARG3}               // 第 3 引数に相当するバイトコード列
JSOP_SPREADARG       // 勝手に新規追加したオペコード
JSOP_CALL(argc=-1)

まず、JSOP_CALL の argc が -1 の場合、argc をスタックのトップに乗っけた状態で JSOP_CALL を呼ぶ、という約束にする。

引数を乗せる前にまず JSOP_ZERO で 0 をスタックに乗っけておく。
引数を乗せたら上下を入れ替えてから 1 を足す、という事を繰り返す事でスタックには引数が順番に乗った上で、さらに現時点での argc が乗る。
さて spread はどうするかっていうと、引数 ARG3 を乗っけて、JSOP_SPREADARG を実行する。
JSOP_SPREADARG は、対象の引数 (ARG3) と argc をスタックからポップして、引数を展開した結果を順番にスタックに乗っけて、さらに展開した数だけ増やした argc をスタックに乗っける。
JSOP_NOTEARG については JSOP_SPREADARG の中で対応。といっても、何もしないオペコードなので JSOP_SPREADARG の中でも何もしない (IonMonkey の方では使ってるらしい)。

さて、JSOP_CALL の本体はどうなってるかというと、バイトコードから argc を取り出して CallArgs を生成する。

[js/src/jsinterp.cpp]
BEGIN_CASE(JSOP_NEW)
BEGIN_CASE(JSOP_CALL)
BEGIN_CASE(JSOP_FUNCALL)
{
...
    JS_ASSERT(regs.stackDepth() >= 2 + GET_ARGC(regs.pc));
    CallArgs args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
...
}

ここで、argc をバイトコードからじゃなくてスタックから取り出すようにすれば動く。

[js/src/jsinterp.cpp]
BEGIN_CASE(JSOP_NEW)
BEGIN_CASE(JSOP_CALL)
BEGIN_CASE(JSOP_FUNCALL)
{
...
    CallArgs args;
    if (GET_ARGC(regs.pc) == 65535) {
        JS_ASSERT(regs.stackDepth() >= 3);
        int32_t argc = regs.sp[-1].toInt32();
        regs.sp --;
        JS_ASSERT(regs.stackDepth() >= 2 + argc);
        args = CallArgsFromSp(argc, regs.sp);
    }
    else {
        JS_ASSERT(regs.stackDepth() >= 2 + GET_ARGC(regs.pc));
        args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
    }
...
}

という事でとりあえず動きはする状態にはなったんだけど、ヘンなものを渡すとクラッシュする。

js> ((...x) => x) (10, 20, ...{});
Assertion failure: index >= size_t(pcstack.depth()), at ***/mozilla-central/js/src/jsopcode.cpp:1489

あと、argc = -1 ってのがどうしてもダメそうなら、
spread がある場合、引数を全部配列に放り込んで 1 引数扱いにして apply を呼ぶようにするとか、
CallArgs を修正して引数をスタック以外に置けるようにするとか、
そんな感じになるかもしれない。

とりあえずもうしばらく弄ってみようと思う。

2013年5月12日

js-ctypes で Objective-C を使う (5) - double を返すメソッド

自動判別をしようと頑張った結果、やっぱりめんどくさかったので手動にする。
セレクタになり得ないものを最後に渡して、それで返り値の型を指定する。

"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_msgSend_fpret = this.lib.declare ("objc_msgSend_fpret",
                                                ctypes.default_abi,
                                                ctypes.double,
                                                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));
  },
  
  /* 返り値が double なメッセージを送る */
  sendf : function (listener, sel, args) {
    return this.objc_msgSend_fpret.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"
                  || sel == "toSource")) {
            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);
        /* 返り値の型を指定されている場合をチェック */
        if (sels.length && sels [sels.length - 1] == "@f") {
          /* double */
          sels.pop ();
          let sel = sels.join (":");
          if (args.length > 0) {
            sel += ":";
          }
          return objc.sendf (obj, sel, args);
        }
        else if (sels.length && sels [sels.length - 1] == "@i") {
          /* int32_t */
          sels.pop ();
          let sel = sels.join (":");
          if (args.length > 0) {
            sel += ":";
          }
          return ctypes.cast (objc.send (obj, sel, args), ctypes.int32_t).value;
        }
        else {
          /* 指定がないのでオブジェクトという事にする */
          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.double,
                                           [objc.types.id,
                                            objc.types.SEL])
                      .ptr (function (self, sel) {
                          return Math.PI;
                        }),
                      objc.encodeMethod (ctypes.double,
                                         [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 ["@f"] ());
print (test.twice ["@i"] (ctypes.int32_t (123)));
print (test.divide.by ["@i"] (ctypes.int32_t (100), ctypes.int32_t (20)));

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

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

ちなみに自動判断に使おうとした関数は以下のとおり
  • object_getClass
  • class_getClassMethod
  • class_getInstanceMethod
objc_msgSend の前に object_getClass でクラス取得して、class_getClassMethod でメソッド取得して、型をチェック、という手順でいけそうかなーと思ったら、自作のクラスではメソッドが取得できなかった、という辺りで断念。

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 を使えという事だと思う。
そうなると、メソッドの定義によって使う関数を変えないといけない。
とりあえず自動でやる方法がありそうなのでこれから調べてみる。

2013年5月10日

js-ctypes で Objective-C を使う (3)

Function Proxy 使ったらいい感じにできそうなのでしてみた。

"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,
                                           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 ();
  },
  
  /* メッセージを送る */
  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) {
    return Proxy.createFunction ({
        get: function (a, sel) {
          /* セレクタを記録する */
          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 (_, name) {
        return objc.proxy (objc.objc_getClass (name), []);
      }
    })
};

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

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

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

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

/* 喋っていただく */
let NSUTF8StringEncoding = ctypes.int32_t (4);
let text = objc.classes.NSString.alloc ()
  .initWithCString.encoding (ctypes.char.array ()("あ"),
                             NSUTF8StringEncoding)
  .autorelease ()();
synth.startSpeakingString (text);

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

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

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

引数 2 個以上のサンプルとして initWithCString を引っ張り出す。

なんか微妙に気持ち悪い感が無いでもない。
でも VALUE と VALUE_OF が別の形で解決できたのでちょっとマシ。

x.initWithCString.encoding (ctypes.char.array ()("あ"), NSUTF8StringEncoding)

これは、

[x initWithCString: "あ" encoding: NSUTF8StringEncoding]

これの事。
プロパティを並べる事で複数の引数を記述する。


あと、データの扱いを間違えると toString とか valueOf のメッセージがオブジェクトに飛ぶという危険な仕様。

js-ctypes で Objective-C を使う (2)

そういえば Proxy なんてものもあったねー、という事で前回のサンプルを Proxy を使ってもうちょっとイイカンジに書き直す。

"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,
                                           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 ();
  },
  
  /* メッセージを送る */
  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 ().VALUE;
  },
  
  /* 名前に対応するクラスを返すオプジェクト的なアレ */
  classes : new Proxy ({}, {
      get : function (_, name) {
        return new Proxy (objc.objc_getClass (name), objc.proxy);
      }
    }),
  
  /* メソッド呼び出しみたいにするプロキシ */
  proxy : {
    get : function (obj, sel) {
      /* 中身を取り出すための特殊プロパティ */
      if (sel == "VALUE") {
        return obj;
      }
      if (sel == "VALUE_AS") {
        return function (type) {
          return ctypes.cast (obj, type).value;
        };
      }
    
      /* 引数に応じてメッセージを送ってくれる関数 */
      return function () {
        let args = Array.prototype.slice.call (arguments, 0);
        if (args.length > 0) {
          sel += ":";
        }
        let ret = objc.send (obj, sel, args);
        return new Proxy (ret, objc.proxy);
      };
    }
  }
};

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

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

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

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

/* 喋っていただく */
synth.startSpeakingString (objc.str ("a"));

/* 喋り終わるのを待つ */
while (synth.isSpeaking ().VALUE_AS (ctypes.int32_t)) {
}

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

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

メソッドの呼び出しっぽく書けるし、クラス取得もプロパティっぽく取れるし、文字列で書く部分が無くなってかなり自然になった気がする。

ただ Proxy から中身を取り出す VALUE と VALUE_AS がイマイチ。
メッセージと衝突する可能性もあるし。



あとから気付いたけど、この方法だと 2 引数以上のセレクタ書けないジャーン

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 ();

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

2013年5月6日

Accept/Referer フィールドの値と Photobucket の応答

Photobucket が状況によって違うファイルを返すっぽいという事に起因するトラブルがあったので調べてみた。

まずは画像を適当に用意する。

http://i1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg

このファイルを以下のようなリクエストで取得しようとすると

GET /albums/ag159/arai_unmht/x_zps7cd4edca.jpg HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: i1303.photobucket.com
Accept: text/html
Connection: close

HTTP/1.1 302 Moved Temporarily
Content-Length: 210
Content-Type: text/html
Location: http://s1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg
Server: Footprint Distributor V4.8
Date: Mon, 06 May 2013 01:01:32 GMT
Connection: close

おや、何か転送されたよ?

GET /albums/ag159/arai_unmht/x_zps7cd4edca.jpg HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: s1303.photobucket.com
Accept: text/html
Connection: close

HTTP/1.1 302 Found
Date: Mon, 06 May 2013 01:02:15 GMT
Server: Apache
Location: http://s1303.beta.photobucket.com/user/arai_unmht/media/x_zps7cd4edca.jpg.html
Content-Length: 262
Connection: close
Content-Type: text/html; charset=iso-8859-1

まだ転送される

GET /user/arai_unmht/media/x_zps7cd4edca.jpg.html HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: s1303.beta.photobucket.com
Accept: text/html
Connection: close

HTTP/1.1 301 Moved Permanently
Date: Mon, 06 May 2013 01:03:23 GMT
Server: Apache
Location: http://s1303.photobucket.com/user/arai_unmht/media/x_zps7cd4edca.jpg.html
Content-Length: 281
Connection: close
Content-Type: text/html; charset=iso-8859-1

まだまだ転送される

GET /user/arai_unmht/media/x_zps7cd4edca.jpg.html HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: s1303.photobucket.com
Accept: text/html
Connection: close

HTTP/1.1 200 OK
Date: Mon, 06 May 2013 01:04:08 GMT
Server: Apache
Set-Cookie: ***
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: ***
Set-Cookie: ***
Set-Cookie: ***
Cache-Control: no-cache
Vary: User-Agent
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf8

という長いやりとりの末に HTML ファイルが返ってくる。

一方、以下のようなリクエストで取得しようとすると

GET /albums/ag159/arai_unmht/x_zps7cd4edca.jpg HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: i1303.photobucket.com
Accept: */*
Connection: close

HTTP/1.1 200 OK
Cache-Control: max-age=21600
Content-Length: 36143
Content-Type: image/jpeg
ETag: "8d2f-4dc023456ed40"
Expires: Mon, 06 May 2013 07:07:32 GMT
Last-Modified: Mon, 06 May 2013 00:58:53 GMT
Accept-Ranges: bytes
Server: nginx
Access-Control-Allow-Origin: *
X-Cache-Status: HIT
X-Forwarded-Server: phx1ws062
X-PB-Link: TRUE
Date: Mon, 06 May 2013 01:07:32 GMT
Connection: close

あっさり画像が返ってくる。

早い話が、Accept フィールドの値によって返すファイルを変えているって事なんだけど、UnMHT ではそういう点を考慮してなかったので状況によっては間違ったファイルを保存しちゃう。
なので Ver.6.3.0b8 で修正。
その修正に伴ってリリース日も 5 月 10 日に延ばす。

それはそうと、この問題別に UnMHT だけで発生するわけじゃなくて

http://i1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg

っていう URL をどういう形で開くかによって結果が変わっちゃうという問題でもある。
たとえばキャッシュが無い状態で、新しいタブを開いてからこの URL を開くと、以下のアルバムのページが表示される (Accept: text/html みたいのが送られる)。

http://s1303.photobucket.com/user/arai_unmht/media/x_zps7cd4edca.jpg.html

ところが、アルバムのページでは再度

http://i1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg

を画像として表示する (Accept: image/* みたいのが送られる)。
結果、このアドレスに対して画像がキャッシュされる。
次に新しいタブを開いてから

http://i1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg

こっちの URL を開くと画像が表示される。
これはさっきのキャッシュ。
で、このページを、キャッシュを無視して強制リロードすると

http://s1303.photobucket.com/user/arai_unmht/media/x_zps7cd4edca.jpg.html

このページに飛ばされる。


それからもうひとつ応答が変わる要因があって、そっちは Referer フィールド。
Referer が photobucket.com なら、Accept が text/html でも画像が返ってくる。

telnet i1303.photobucket.com 80
GET /albums/ag159/arai_unmht/x_zps7cd4edca.jpg HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: i1303.photobucket.com
Referer: http://photobucket.com/
Accept: text/html
Connection: close

HTTP/1.1 200 OK
Cache-Control: max-age=21600
Content-Length: 36143
Content-Type: image/jpeg
ETag: "8d2f-4dc023456ed40"
Expires: Mon, 06 May 2013 07:38:55 GMT
Last-Modified: Mon, 06 May 2013 00:58:53 GMT
Accept-Ranges: bytes
Server: nginx
Access-Control-Allow-Origin: *
X-Cache-Status: HIT
X-Forwarded-Server: phx1ws062
X-PB-Link: TRUE
Date: Mon, 06 May 2013 01:38:55 GMT
Connection: close

ところが、photobucket.com 以外だとリダイレクトされる。

telnet i1303.photobucket.com 80
GET /albums/ag159/arai_unmht/x_zps7cd4edca.jpg HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:21.0) Gecko/20100101 Firefox/21.0
Host: i1303.photobucket.com
Referer: http://unmht.blogspot.com/
Accept: text/html
Connection: close

HTTP/1.1 302 Moved Temporarily
Content-Length: 210
Content-Type: text/html
Location: http://s1303.photobucket.com/albums/ag159/arai_unmht/x_zps7cd4edca.jpg
Server: Footprint Distributor V4.8
Date: Mon, 06 May 2013 01:39:57 GMT
Connection: close

なので、

http://s1303.photobucket.com/user/arai_unmht/media/x_zps7cd4edca.jpg.html

を開いてから、[画像だけを表示] を選択した場合、Referer が photobucket.com なので強制リロードしても画像のままになる。


仕組みを知らずに試すと何とも不思議な挙動に見える。

2013年5月2日

Emacs のタイ文字環境

タイ文字入力したいなーって時、ありますよね。

まずは基本になるタイ文字用のフォントを指定します。
元になるフォント設定がこちら。Mac の Emacs 24.1.1 用の設定です。

(setq my-font "-apple-Monaco-medium-normal-normal-*-*-*-*-*-m-0-fontset-startup")
(setq fixed-width-use-QuickDraw-for-ascii t)
(setq mac-allow-anti-aliasing t)
(set-default-font my-font)
(add-to-list 'default-frame-alist `(font . ,my-font))
(set-fontset-font
 (frame-parameter nil 'font)
 'japanese-jisx0208
 '("Hiragino Maru Gothic Pro" . "iso10646-1"))
(setq face-font-rescale-alist
      '(("^-apple-hiragino.*" . 1.2)
        (".*osaka-bold.*" . 1.2)
        (".*osaka-medium.*" . 1.2)
        (".*courier-bold-.*-mac-roman" . 1.0)
        (".*monaco cy-bold-.*-mac-cyrillic" . 0.9)
        (".*monaco-bold-.*-mac-roman" . 0.9)
        ("-cdac$" . 1.3)))

やっぱり等幅がいいですね。ayuthaya がソレっぽいので追加しましょう。

(set-fontset-font
 (frame-parameter nil 'font)
 'thai-tis620
 '("ayuthaya" . "iso10646-1"))

これで、どこかからコピペしてくきた文字が読めます。



ところがワタシは小さいタイ文字は読み慣れないので文字を倍のサイズにします。全角扱いにする方法は知らないのでケタ数がちょっとアレ。あと行が縦に伸びる。

(setq face-font-rescale-alist
      '(("^-apple-hiragino.*" . 1.2)
        (".*osaka-bold.*" . 1.2)
        
        (".*ayuthaya.*" . 2.0)
        
        (".*osaka-medium.*" . 1.2)
        (".*courier-bold-.*-mac-roman" . 1.0)
        (".*monaco cy-bold-.*-mac-cyrillic" . 0.9)
        (".*monaco-bold-.*-mac-roman" . 0.9)
        ("-cdac$" . 1.3)))



やったー読みやすい!

さて、ここで問題がヒトツ。๛ と ๚ が表示できません。


ayuthaya に入っていないのか、マッピングがおかしな事になっているのか。とりあえず thonburi に入ってたので、こっちでで代用します。幅はズレちゃうけど。

(set-fontset-font
 nil
 '#x0E5A
 '("thonburi" . "iso10646-1"))
(set-fontset-font
 nil
 '#x0E5B
 '("thonburi" . "iso10646-1"))

(setq face-font-rescale-alist
      '(("^-apple-hiragino.*" . 1.2)
        (".*osaka-bold.*" . 1.2)
        
        (".*ayuthaya.*" . 2.0)
        (".*thonburi.*" . 2.0)
        
        (".*osaka-medium.*" . 1.2)
        (".*courier-bold-.*-mac-roman" . 1.0)
        (".*monaco cy-bold-.*-mac-cyrillic" . 0.9)
        (".*monaco-bold-.*-mac-roman" . 0.9)
        ("-cdac$" . 1.3)))



表示はこれでとりあえず OK。

次に入力です。もちろん Mac の設定で Thai のキーボードを追加してもいいのですが、切り替えが面倒なので Emacs 側で解決する事にします。Emacs 側で解決するなら、最初から入っている thai-kesmanee を使う事もできます。

これで入力できちゃいます。ただ、キーボードが日本語だとマッピングが一部おかしくなっちゃうので困ったカンジ。

手っ取り早く対策しよう、ってコトでマッピングを変えたバージョンの input-method を定義しちゃいましょう。
元にするファイルは Emacs.app/Contents/Resources/leim/quail/thai.el.gz
ここから必要そうな部分だけ抜き出して、パスの通った所に適当なファイル名で保存します。

(require 'quail)

(defmacro thai-jp-generate-quail-map (translation-table)
  (let (map)
     (dotimes (i (length translation-table))
       (let ((trans (aref translation-table i)))
         (when (not (eq trans 0))
           (if (> (length trans) 1)
               (setq trans (vector trans))
             (setq trans (aref trans 0)))
           (setq map (cons (list (char-to-string i) trans) map)))))
     `(quail-define-rules ,@map)))

;; Thai Kesmanee on jp keyboard support.

(quail-define-package
 "thai-jp" "Thai" "กJ>" t
 "Thai Kesmanee input method on JP106 keyboard layout"
 nil t t t t nil nil nil nil nil t)

(thai-jp-generate-quail-map
 [
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0       ; control codes
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0       ; control codes
  0  "+" "๑" "๒" "๓" "๔" "ู" "฿" ; S ! " # $ % & '
  "๕" "๖" "." "ซ" "ม" "ข" "ใ" "ฝ"       ; ( ) * + , - . /
  "จ" "ๅ" "/" "_" "ภ" "ถ" "ุ" "ึ" ; 0 1 2 3 4 5 6 7
  "ค" "ต" "ง" "ว" "ฒ" "๘" "ฬ" "ฦ"       ; 8 9 : ; < = > ?
  "บ" "ฤ" "ฺ" "ฉ" "ฏ" "ฎ" "โ" "ฌ"        ; @ A B C D E F G
  "็" "ณ" "๋" "ษ" "ศ" "?" "์" "ฯ"  ; H I J K L M N O
  "ญ" "๐" "ฑ" "ฆ" "ธ" "๊" "ฮ" "\""       ; P Q R S T U V W
  "\)" "ํ" "\(" "ล" "%" "ฃ" "ช" "๗"      ; X Y Z [ \ ] ^ _
  "ฐ" "ฟ" "ิ" "แ" "ก" "ำ" "ด" "เ"        ; ` a b c d e f g
  "้" "ร" "่" "า" "ส" "ท" "ื" "น"  ; h i j k l m n o
  "ย" "ๆ" "พ" "ห" "ะ" "ี" "อ" "ไ"        ; p q r s t u v w
  "ป" "ั" "ผ" "," "-" "ฅ" "๙" 0  ; x y z { | } ~ D
  ])

(provide 'thai-jp)

保存したらこれを require しておきます。

(require 'thai-jp)

これで、set-input-method 経由で thai-jp が使用できるようになります。

私は日本語入力には SKK を使うので、toggle-input-method はタイ語専用にしちゃいましょう。

(global-set-key "\C-\\" 'toggle-thai-input-method)
(defun toggle-thai-input-method (&optional arg)
  "switch thai-jp input method"
  (interactive "P")
  (if current-input-method
      (inactivate-input-method)
    (set-input-method "thai-jp")))

キーマップはこんな感じ。


Shift+0 が使えないので ๗ は _ のキーで代用。あと % と ฃ も適当な位置に。

それと、๛ と ๚ も適当な位置にマップしておきましょう。

(global-set-key "\M-[" (lambda () (interactive) (insert-thai-jp '#x0E5B)))
(global-set-key "\M-]" (lambda () (interactive) (insert-thai-jp '#x0E5A)))
(defun insert-thai-jp (code)
  "insert character in thai-jp mode"
  (if (equal current-input-method "thai-jp")
      (insert code)))

うーんなんとも適当。