2013年10月26日

XCode 5 で SpiderMonkey 17 をビルドする

Mavericks に更新して XCode も更新してさて UnMHT for QuickLook のために SpiderMonkey 17 をビルド、という所で clang が新しくなったからなのか、goto でエラーが出ました。

error: indirect goto might cross protected scopes
DO_NEXT_OP(len);

このバグ、mozilla-central では既に直ってるヤツっぽいんだけど、SpiderMonkey に単体版は今のところ 17 なので、なんとかしてビルドしないといけないワケです。ちなみに SpiderMonkey 24 は来月出るそうなので、それが出ればそっちを使えば OK でしょう。

さて、問題になるのは DO_NEXT_OP マクロ内で使われる DO_OP マクロの中の goto です。調べてみると、DO_OP マクロは JS_THREADED_INTERP によって2種類が使い分けられているようで、JS_THREADED_INTERP がオフの場合の DO_OP マクロには goto が入っていません。

ということで jsinterp.cpp の 909 行目の

# if JS_VERSION >= 160 && (                                                   \



# if JS_VERSION >= 160 && !defined(__clang_major__) && (                      \

のように変更して clang を使っている場合に JS_THREADED_INTERP をオフにするようにすればビルドできるようになります。

2013年9月19日

harmony:spread (6)

おととい mozilla-central に無事入って、昨日の Firefox Nightly 27.0a1 から使えるようになりましたとさ。
めでたしめでたし。

コードレビュー、テストなどなど手助けをしてくれた jorendorff、jandem、sfink
コードの基礎になる spreadarray を実装してくれた benjamin
ドキュメントの整備に協力してくれた teoli, sheppy、rwaldron
その他かかわった大勢に感謝。

ちなみに仕様との不一致は仕様の方を変更する形で解消するそうですよ。

2013年9月9日

Task.jsm でハマったおはなし

Task.jsm は Firefox 17 から入った JavaScript モジュールで、yield と Promise を使って非同期処理をコールバックなしに書けるという素晴らしいヤツです。

ところが、Task.jsm は Firefox 24 の時点では resource://gre/modules/commonjs/sdk/core/promise.js を使用していて、yield に Promise を渡す度に、then(promise.js:155)、then(promise.js:45)、resolve(promise.js:120)、TaskImpl_run(Task.jsm:207) という感じでスタックを少しずつ食います。
このため、同じジェネレータ内で yield を 1000 回くらい呼んだ時点で too much recursion で落ちてしまいます。

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

let simpleTask = function() {
  return Task.spawn(function() {
  });
};
  
let loopTask = function() {
  Task.spawn(function() {
    for (let i = 0; i < 1000; i ++) {
      yield simpleTask();
    }
    throw new Task.Result("ok");
  }).then(function(result) {
    Cu.reportError("result:" + result);
  }, function(e) {
    Cu.reportError("error:" + e);
  });
};

loopTask();

もちろん、そんな処理を書く方が悪いといえばそうかもしれませんけど。 (UnMHT で保存する時に各 DOM ノードの情報を取得するのに Task を使ってみたところ落ちた)

試験的に Task.jsm の中で Promise.jsm を使うように書き換えてみたところ、こちらは非同期になってスタックを食わないので平気なようです。

さて、そうなると Task.jsm が Promise.jsm を使うようになってくれると嬉しいのですが、Bug 887923Bug 881047 を見る限り、(同期的な処理前提のコードがあるため?) 簡単には行かないようです。

とりあえず、自分が使う範囲においては問題ないと思うので、どうしても Task.jsm を使いたくなったら Promise.jsm と一緒にして自分のアドオンの中に放り込む事にしましょうか。

ちなみに yield に渡すのが Promise じゃなくても TaskImpl_run(Task.jsm:211) の 1 段食いますが、実際のスタックの消費量が少ないので (Firefox 24 からスタックの制限は呼び出しの数ではなくメモリの使用量になったので) 60000 回くらいでようやく落ちました。

2013年8月16日

simple-uri とファイル名

UnMHT ver.6.4.0 で URI を

unmht://ホスト名/http.5/ファイル名.mht/

からシンプルな

unmht:http://ホスト名/ファイル名.mht/

に変更しようと思ってたんだけど、そのために standard-url から simple-uri に変更したところトラブルが2つも起きたのでメモメモ。

その1: 保存する際のファイル名がランダムになる

simple-uri は当然ながら nsIURL を実装してないので fileName が取れないのが原因だと思う。

その2:ウェブフォントが動かない

こっちの問題は結局完全に原因をつきとめる事はできなかったんだけど、とりあえず分かったのは standard-url に戻すだけで直る、という事。
やっぱり nsIURL を実装してないのが原因かもしれない。

とりあえず、この2つの問題を simple-uri のまま解決するのは難しそうなので、Ver.6.4.0 からこの変更を削除してリリースする事に仮決定。

Alpha と Beta の間に使ってた URI が無効になるっていうのはどのくらい問題になるかしら。

2013年7月2日

ロケーションバーの内容

UnMHT は MHT ファイルを開く際に、unmht: で始まる URI に変換して、そのプロトコルハンドラが生成したチャネルを通して展開した内容を返す、という形をとってて、実際 MHT ファイルを開いてみるとロケーションバーには unmht: で始まるアドレスが表示される。

もちろんこのアドレスは UnMHT 内でしか通用しないものなので、他人に渡したりするアドレスとしては不適切。とはいえ、ユーザはそんな内部の事情は気にしないので、昔から時々 unmht: で始まるアドレスをウェブ上で見かけたりしてて、なんとかしないといけないなー、と思ってた。情報パネルから本来のアドレスをコピーできるようにしたけど、そもそも MHT ファイルを開いた時だけそういう手順を踏まないといけないっていう事自体が分かりにくいと思う。

で、今度こそなんとかしよう。

対策その1:nsIStreamConverter を使用して元のアドレスのまま開始パートを返す
もちろんそういう方法もアリだけど、MIME-Type が適切じゃないファイルは開けないので却下

対策その2:ロケーションバーの内部を弄って表示するアドレスを変える
どこにもそういうインターフェースが無いので、無理矢理な感じになっちゃう。当然バージョンアップで壊れる。却下

対策その3:適当なタイミングでロケーションバーの表示内容を変更する
現時点で、tabbrowser の nsIWebProgressListener.onLocationChange イベントでロケーションバーに情報パネル用のアイコンを表示してるので、そのタイミングでやってみよう。

と、やってみたら実はブラウザ側もそのタイミングでロケーションバーの内容を変更してて、イベントハンドラの登録順序次第で表示内容が変わったりする。

イベントハンドラの登録を遅くするっていうのも手だけど、あんまり安定した解決策でもない。
なので、setTimeout を 0ms で呼び出して、イベントハンドラから抜けた後で実行する形で対応してみた。

という感じで Ver.6.4.0b3 を出したので、8月までにはリリース版に入るんじゃないかな。

2013年6月3日

harmony:spread (5)

とりあえずオペコード版のまとめ。

オペコードの選択は Parser.cpp で行われている。具体的には、ノードの情報として格納されている。引数はその後で argumentList 関数でパースされて、ここで spread operator の存在をチェックできる。

[js/src/frontend/Parser.cpp]

template <typename ParseHandler>
typename ParseHandler::Node
Parser<ParseHandler>::memberExpr(TokenKind tt, bool allowCallSyntax)
{
...
        lhs = handler.newList(PNK_NEW, null(), JSOP_NEW);
...
        if (tokenStream.matchToken(TOK_LP) && !argumentList(lhs))
            return null();
...
}

なので、spread operator を見付けたら、ノードの格納されたオペコードを spread 版に置き換えればパーサ部分は完了。

…なんだけど、パーサは文法だけ見て何も生成しない SyntaxParseHandler とツリーを生成する FullParseHandler の両方に対応できるように設計されていて、オペコードを指定してノードを生成する事はできるんだけど、ノードからオペコードを取得する事はできない。なぜなら SyntaxParseHandler のノードは指定された情報を捨てちゃうから。

という事で、argumentList 関数には何も情報を持ってないノードが渡ってきてる可能性があるので、適切にオペコードを置き換える事はできない。フラグだけ返して、生成してる側で置き換える。

[js/src/frontend/Parser.cpp]

 template <typename ParseHandler>
 typename ParseHandler::Node
 Parser<ParseHandler>::memberExpr(TokenKind tt, bool allowCallSyntax)
 {
 ...
+        bool isSpread = false;
-        if (tokenStream.matchToken(TOK_LP) && !argumentList(lhs))
+        if (tokenStream.matchToken(TOK_LP) && !argumentList(lhs, &isSpread))
             return null();
+        if (isSpread)
+            handler.setOp(lhs, JSOP_SPREADNEW);
 ...
 }

[js/src/frontend/Parser.cpp]

 template <typename ParseHandler>
 bool
-Parser<ParseHandler>::argumentList(Node listNode)
+Parser<ParseHandler>::argumentList(Node listNode, bool *isSpread)
 {
 ...
     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;
+            *isSpread = 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 (tokenStream.matchToken(TOK_FOR)) {
+        if (!spread && tokenStream.matchToken(TOK_FOR)) {
 ...
         } else
 #endif
 ...
 }

これで、 BytecodeEmitter.cpp に来る時点で既にオペコードは決まってる状態になって、追加のフラグやら何やらは不要になる。

[js/src/frontend/BytecodeEmitter.cpp]

 static bool
 EmitCallOrNew(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
 {
 ...
+    bool spread = false;
+    if (pn->isOp(JSOP_SPREADCALL) || pn->isOp(JSOP_SPREADNEW) ||
+        pn->isOp(JSOP_SPREADEVAL) || pn->isOp(JSOP_SPREADFUNCALL) ||
+        pn->isOp(JSOP_SPREADFUNAPPLY)) {
+        spread = true;
+    }
     switch (pn2->getKind()) {
       case PNK_NAME:
         if (bce->emitterMode == BytecodeEmitter::SelfHosting &&
-            pn2->name() == cx->names().callFunction)
+            pn2->name() == cx->names().callFunction &&
+            !spread)
         {
 ...
         }
     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;
-        for (ParseNode *pn3 = pn2->pn_next; pn3; pn3 = pn3->pn_next) {
-            if (!EmitTree(cx, bce, pn3))
+                    return false;
-            if (Emit1(cx, bce, JSOP_NOTEARG) < 0)
+                    return false;
+        if (!spread) {
+            for (ParseNode *pn3 = pn2->pn_next; pn3; pn3 = pn3->pn_next) {
+                if (!EmitTree(cx, bce, pn3))
+                    return false;
+                if (Emit1(cx, bce, JSOP_NOTEARG) < 0)
+                    return false;
+            }
+        } else {
+            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) {
+                if (!pn3->isKind(PNK_SPREAD)) {
+                    if (!EmitTree(cx, bce, pn3))
+                        return false;
+                    if (Emit1(cx, bce, JSOP_INITELEM_INC) < 0)
+                        return false;
+                } else {
+                    if (!EmitTree(cx, bce, pn3->pn_kid))
+                        return false;
+                    if (Emit1(cx, bce, JSOP_SPREAD) < 0)
+                        return false;
+                }
+            }
+            if (Emit1(cx, bce, JSOP_POP) < 0)
+                return false;
+            if (Emit1(cx, bce, JSOP_ENDINIT) < 0)
+                return false;
+            argc = 1;
         }
         bce->emittingForInit = oldEmittingForInit;
     }
 
     if (Emit3(cx, bce, pn->getOp(), ARGC_HI(argc), ARGC_LO(argc)) < 0)
         return false;
     CheckTypeSet(cx, bce, pn->getOp());
-    if (pn->isOp(JSOP_EVAL)) {
+    if (pn->isOp(JSOP_EVAL) || pn->isOp(JSOP_SPREADEVAL)) {
 ...
     }
 ...
 }

生成されるバイトコードはこんな感じ。
引数は全部配列に放り込む。spread operator は配列の方で対応する。で、call の代わりに spreadcall になってる。引数は配列 1 個なので argc = 1。

f(x, ...[y, z])

00000:  callgname "f"
00005:  undefined
00006:  notearg

00007:  newarray 1
00011:    zero

00012:    getgname "x"
00017:    initelem_inc

00018:    newarray 2
00022:      getgname "y"
00027:      initelem_array 0

00031:      getgname "z"
00036:      initelem_array 1
00040:    endinit
00041:    spread

00042:    pop
00043:  endinit

00044:  spreadcall 1

で、インタープリタではオペコードで分岐する。

[js/src/jsinterp.cpp]

 BEGIN_CASE(JSOP_EVAL)
+BEGIN_CASE(JSOP_SPREADEVAL)
 {
-    CallArgs args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
+    uint32_t argc = GET_ARGC(regs.pc);
+    JS_ASSERT(regs.stackDepth() >= 2 + argc);
+    if (op == JSOP_SPREADEVAL) {
+        RootedObject &arr = rootObject0;
+        arr = &regs.sp[-1].toObject();
+        regs.sp -= 1;
+        JS_ASSERT(arr->isArray());
+        if (!GetLengthProperty(cx, arr, &argc))
+            goto error;
+        if (!GetElements(cx, arr, argc, regs.sp))
+            goto error;
+        regs.sp += argc;
+    }
+    CallArgs args = CallArgsFromSp(argc, regs.sp);
 ...
 }
 ...
 BEGIN_CASE(JSOP_FUNAPPLY)
+BEGIN_CASE(JSOP_SPREADFUNAPPLY)
     if (!GuardFunApplyArgumentsOptimization(cx))
         goto error;
     /* FALL THROUGH */
 
 BEGIN_CASE(JSOP_NEW)
+BEGIN_CASE(JSOP_SPREADNEW)
 BEGIN_CASE(JSOP_CALL)
+BEGIN_CASE(JSOP_SPREADCALL)
 BEGIN_CASE(JSOP_FUNCALL)
+BEGIN_CASE(JSOP_SPREADFUNCALL)
 {
     if (regs.fp()->hasPushedSPSFrame())
         cx->runtime->spsProfiler.updatePC(script, regs.pc);
-    JS_ASSERT(regs.stackDepth() >= 2 + GET_ARGC(regs.pc));
-    CallArgs args = CallArgsFromSp(GET_ARGC(regs.pc), regs.sp);
-
-    bool construct = (*regs.pc == JSOP_NEW);
+
+    uint32_t argc = GET_ARGC(regs.pc);
+    JS_ASSERT(regs.stackDepth() >= 2 + argc);
+    if (op == JSOP_SPREADNEW || op == JSOP_SPREADCALL ||
+        op == JSOP_SPREADFUNCALL || op == JSOP_SPREADFUNAPPLY) {
+        RootedObject &arr = rootObject0;
+        arr = &regs.sp[-1].toObject();
+        regs.sp -= 1;
+        JS_ASSERT(arr->isArray());
+        if (!GetLengthProperty(cx, arr, &argc))
+            goto error;
+        if (!GetElements(cx, arr, argc, regs.sp))
+            goto error;
+        regs.sp += argc;
+    }
+    CallArgs args = CallArgsFromSp(argc, regs.sp);
+
+    bool construct = (*regs.pc == JSOP_NEW || *regs.pc == JSOP_SPREADNEW);
 ...
 }

そしてオペコードを登録しておく。

[js/src/jsopcode.tbl]

-OPDEF(JSOP_UNUSED41,  41, "unused41",   NULL,         1,  0,  0,  JOF_BYTE)
-OPDEF(JSOP_UNUSED42,  42, "unused42",   NULL,         1,  0,  0,  JOF_BYTE)
-OPDEF(JSOP_UNUSED43,  43, "unused43",   NULL,         1,  0,  0,  JOF_BYTE)
-OPDEF(JSOP_UNUSED44,  44, "unused44",   NULL,         1,  0,  0,  JOF_BYTE)
-OPDEF(JSOP_UNUSED45,  45, "unused45",   NULL,         1,  0,  0,  JOF_BYTE)
+/* spreadcall variant of JSOP_CALL */
+OPDEF(JSOP_SPREADCALL,      41, "spreadcall",      NULL,  3, -1,  1, JOF_UINT16|JOF_INVOKE|JOF_TYPESET)
+/* spreadcall variant of JSOP_NEW */
+OPDEF(JSOP_SPREADNEW,       42, "spreadnew",       NULL,  3, -1,  1, JOF_UINT16|JOF_INVOKE|JOF_TYPESET)
+/* spreadcall variant of JSOP_EVAL */
+OPDEF(JSOP_SPREADEVAL,      43, "spreadeval",      NULL,  3, -1,  1, JOF_UINT16|JOF_INVOKE|JOF_TYPESET)
+/* spreadcall variant of JSOP_FUNCALL */
+OPDEF(JSOP_SPREADFUNCALL,   44, "spreadfuncall",   NULL,  3, -1,  1, JOF_UINT16|JOF_INVOKE|JOF_TYPESET)
+/* spreadcall variant of JSOP_FUNAPPLY */
+OPDEF(JSOP_SPREADFUNAPPLY,  45, "spreadfunapply",  NULL,  3, -1,  1, JOF_UINT16|JOF_INVOKE|JOF_TYPESET)

あとは、spread 版のチェックを各所に追加する。
これは沢山あるので省略。

2013年6月2日

harmony:spread (4)

とりあえずパッチは投げたけど色々調べてると結局 JSVAL_TYPE_MAGIC をフラグに使った方がいいんじゃないかとかオペコード追加する方がいいんじゃないかとか。

で、両方作ってみたところオペコード追加するのが一番パフォーマンスも良いし既存部分を壊す事も少ない。
既存のオペコードの動作をフラグで変えるとなると、そのオペコードの実装全部 (インタープリタ、Baseline、Ion) を気にしないといけないんだけど、新しいオペコードを追加するならとりあえずインタープリタだけ実装しておけば JIT の方は未対応って事でコンパイル中断して無視してくれる。

もうちょっとテストしてからオペコード追加版のパッチも投げてみようかしら。

あと、そもそも関連する箇所周辺しかコードを見てないので、実行の全体像を把握する事も必要。インタープリタ部分は自分でも何度か作ってるから、単純な部分は分かるんだけど、型推論とか色々な最適化とかはよく知らないし、まして JIT は触った事ないからよく調べないと手も足も出ないのだ。

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

うーんなんとも適当。

2013年4月25日

UnMHT Ver.6.3.0 のリリーススケジュール

UnMHT Ver.6.3.0 のリリース予定日が 2013年05月07日 に決定しましたー、ワーパチパチ!さっき一人で5分くらい考えて決めただけじゃん!イェイ!

[リリーススケジュール]

という事でいつもよりちょっと長めの翻訳期間 (どのくらい取ったらいいのかよく分かんない……) を取って、今日 BabelZilla に UnMHT Ver.6.3.0b3 をアップしました。新しい文字列はなんと3つだけ! unmht.properties の方はプロパティの名前を階層的に変更したから diff 取るとヒドいけど中身は何にも変わってないのでした。

[翻訳に関する情報]

今回は新機能がほとんど無いのでマニュアルは2カ所くらいしか変わってナイ。まぁ定期的に大きな区切りを持ってくるってのは開発してる感(?)があっていいんじゃないかしら。

[ユーザーズマニュアル (Ver.6.3.0 Beta)]

予定通りいけば、今回こそ Firefox のリリースとビッタリ被ってアワワワ、なんて事も回避できるハズ。

2013年4月17日

toString と valueOf を自分で定義する

JavaScript の Object は toString メソッドを定義すると暗黙的な文字列変換を制御できたのだー。

……実は JavaScript 1.0 の仕様だったんですね。仕様読め私。

という事でメモメモ。

let a2 = {
  x: 10,
  y: 20,
};
print (a2);

これだと結果は

[object Object]

こうなる。いつもの挙動。
でもこれじゃあ中身が何だかサッパリわからない。
そこで toString メソッドを定義する。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  }
};
print (a);

すると

(10,20)

こうなる。わぁ、なんてベンリ。

ちなみに JavaScript 1.1 の仕様に valueOf ってのもある。文字列変換だけじゃなくて、プリミティブな値が欲しい時に呼ばれるメソッド。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  },
  valueOf: function () {
    return "[" + this.x + "," + this.y + "]";
  }
};
print (a);
print (a + "");

1 個目の print では直接文字列表記を取得するので toString、2 個目の print ではその前に文字列演算があって、プリミティブな値を必要とされるので valueOf が呼ばれる。

(10,20)
[10,20]

たとえ文字列演算でも toString ではなく、valueOf が呼ばれる。

計算可能な値をラップするのに使えたりするかもしれない。でも不安なのでたぶん値を取り出すメソッドが getter を定義すると思う。

toString しか用意してなければプリミティブな値を必要とする場合にも toString が呼ばれる。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  }
};
print (a);
print (a + "");

すると

(10,20)
(10,20)

こんなかんじ。

実用にはどうかなーって気もするけど、デバッグにはいいかもしれない。

2013年4月14日

xpcshell で chrome:// な URI を使う

ようやくユニットテストを作り始めた私です。

さて、UnMHT のコードの中には chrome://unmht/... な URI を使える事を前提としたコードがいくつかあります。例えば StringBundle のあたりとか。
で、これをコマンドラインからテストしたい、となった時に例のごとく xpcshell を使うワケですが、当然アドオンとして動いてないので chrome://unmht/... みたいに書いても例外投げられて終わっちゃう。

chrome のプロトコルハンドラを上書きするとか色々考えたけど、どうにもうまく行かなくて困っていたのですが、ふと bootstrapped extension のコードの中で chrome.manifest を動的に登録する部分があったように思って調べたワケです。

// dir は chrome.manifest があるディレクトリ
Components.manager.addBootstrappedManifestLocation (dir);

ユニットテストは tests ディレクトリの中にあるので、テスト用の chrome.manifest には以下のように最低限のモノだけ書いておきます。

content unmht     ../chrome/content/
locale  unmht en-US ../chrome/locale/en-US/

これで chrome://unmht/... みたいに書いて普通に使えるようになります。

ちなみに resource:// な URI はアドオンと同じ手順で登録できます。

// uri はベースになるディレクトリの URI
Services.io.getProtocolHandler ("resource")
  .QueryInterface (Ci.nsIResProtocolHandler)
  .setSubstitution ("unmht", uri);

っていうか chrome.manifest でパスを上に辿れるなんて思わなかった……

2013年4月11日

JavaScript と正規表現の速度

UnMHT の中では主に MHT ファイルや、内部の HTML ファイルのパースの部分で文字列処理を多用するのですが、ファイルが大きくなってくるとこの処理の速度がけっこう体感として重要になってきます。

今のコードの基礎部分はけっこう昔に作ったもので、その頃は Firefox の JavaScript の実行速度はそんなに早いものではなく、JavaScript でループ回したり複数回比較するよりも正規表現を使って match、replace、split した方が速いという場合があったのですが、ハテ今は事情違うんじゃないの?ってコトで再調査です。

var fstream
= Cc ["@mozilla.org/network/file-input-stream;1"]
  .createInstance (Ci.nsIFileInputStream);

fstream.init (file, -1, 0, 0);

var bstream
= Cc ["@mozilla.org/binaryinputstream;1"]
  .createInstance (Ci.nsIBinaryInputStream);
bstream.setInputStream (fstream);

var data = bstream.readBytes (file.fileSize);

bstream.close ();

var boundary = "----=_Part_73251CB_12DB0E1.1365674809317";

var calcTime = function (name, f, count) {
  var startTime = new Date ().getTime ();
  
  for (var i = 0; i < count; i ++) {
    f ();
  }
  
  var endTime = new Date ().getTime ();
  
  show (name + ": " + (endTime - startTime) + " [ms]\n");
};

var parseBoundaryS = function () {
  var parts = [];
  
  var midBoundary = "\r\n--" + boundary + "\r\n";
  var midBoundaryLen = midBoundary.length;
  
  var endBoundary = "\r\n--" + boundary + "--\r\n";
  var endBoundaryLen = endBoundary.length;
  
  var end = 0;
  
  for (;;) {
    var start = data.indexOf (midBoundary, end);
    if (start == -1) {
      start = data.indexOf (endBoundary, end);
      if (start == -1) {
        parts.push (data.substr (end));
      }
      else {
        parts.push (data.substr (end, start - end));
      }
      break;
    }
    
    parts.push (data.substr (end, start - end));
    end = start + midBoundaryLen;
  }
  
  return parts;
};

var parseBoundaryR = function () {
  var escaped = boundary
  .replace (/([\\\[\]\(\)\^\,\.\{\}\|\?\!\-\*\+\$])/g, "\\$1");
  var rexp = new RegExp ("\r\n--" + escaped + "(?:--)?(?:\r\n)?");
  
  return data.split (rexp);
};

calcTime ("parseBoundaryS", parseBoundaryS, 100);
calcTime ("parseBoundaryR", parseBoundaryR, 100);

parseBoundaryS は単純な文字列処理 (indexOf) によって、データを boundary で分割するもの、parseBoundaryR は正規表現 (split) によって、データを boundary で分割するものです。実際に出来上がってくるデータは多少違いますが、気にせずいきましょう。ちなみに今の UnMHT には parseBoundaryR 相当のコードが使われています。

これを Firefox 21 で実行してみます。するとどうでしょう、

parseBoundaryS: 83 [ms]
parseBoundaryR: 299 [ms]

………全然遅いじゃん! 

こりゃ以降正規表現に頼りすぎるのも考えものかも。
もちろん本格的に書き換えるならボトルネックの調査が先でしょうけど。

ちなみに回数を 100 回にして古いマシンの Firefox 2 でテストした結果 (このために let じゃなくて var で書いた)

parseBoundaryS: 520 [ms]
parseBoundaryR: 1564 [ms]

え、やっぱり遅くね?

私は何の幻覚を見たのかな……それとももっと昔のバージョンだったのかな
確かに当時はかなり色々比較した上で正規表現版使おうって決めたハズだったんだけど、自信無くなってきたゾ

2013年4月10日

ウェブページで使用されているフォントを調べる

ウェブフォントが使われているページが結構増えているようです。日本語圏ではサイズの問題からあんまり実用的ではないのですが、アルファベットの少ない言語では結構便利そうなモノです。

さて、ウェブページを保存するとなると、使われているウェブフォントも保存しなくちゃいけません。しかし今のところ CSS を直接パースするという手段は取っていないので、要素を全部眺めながら使われているフォントを調べて行く必要があります。

そこで使うのが inIDOMUtils です。getUsedFontFaces メソッドに調べたい要素の入った Range を渡すと nsIDOMFontFaceList を返してくれます。

let domUtil = Cc ["@mozilla.org/inspector/dom-utils;1"].getService (Ci.inIDOMUtils);
let range = document.createRange ();
range.selectNode (node);
let fontList = domUtil.getUsedFontFaces (range);
for (let j = 0, length = fontList.length; j < length; j ++) {
  let font = fontList.item (j);
  dump (font.URI);
}

さて、node は調べたい要素なのですが、ここに document.body を入れればその中で使われているフォントを全部調べてくれます……と思って使ってました。実際調べてくれはするんです。

しかし、ページが複雑になってくる等、特定の条件で何故かフリーズするんです。詳細は調べ尽くせなかったので原因は他にあるかもしれませんが。

で、今のところ使っている回避策としてはテキストノード (nsIDOMText) のみに使用するという事。フォントが使われている場所、といえばほぼテキストノードなので、これで用件はだいたい満たせた状態になります。UnMHT では保存時に他のスタイル情報を取得するために DOM ツリーを端から端まで走査するので、そのついでに、という感じであればそんなに処理を追加する事なく書けたのはラッキーだったのかもしれません。