Category Archives: javascript

javascript

javascript vs PHP 速度比較

はじめに

WebKit, Chrome, Firefox, Operaでjavascriptのスピードアップ合戦を繰り広げている結果そのへんのLightweight programing languageより速くなっているのではないか、という予測におおまかな数字を与えるために測ったものです。本当にどちらが速いのかはHTTPサーバ・データベース・アプリケーションによるので、これはあくまでどの程度なのかを知るためのものです。

ユースケース

友だちが100人ずついるAさんとBさんの共通の友だちを探します。データベースから持ってきたレコードを手動でjoinする処理です。今回は言語の実行速度のみを知りたいのでデータベースから持ってくる部分は測る時間に含めていません。

速度を意識した実装はしないで、普段書いているような記述にしています。評価環境はmacBookPro”15 2.4GHz Core i5 です。

javascript

node.js 0.4.11を使いました。だいたい2.9秒。

function virtual_friends() {
	var friends = [];
	while (friends.length < 100) {
		var n = Math.floor(Math.random()*10000);
		if (!friends.some(function (v) {return v === n})) {
			friends.push({user_id:n});
		}
	}
	return friends;
}
var a = virtual_friends();
var b = virtual_friends();

var starts = Date.now();
	for (var i = 0; i < 100 * 1000; i++) {
		var user_id_key_table_a = {};
		a.forEach(function (u) {
			user_id_key_table_a[u.user_id] = true;
		});

		var friends_in_common = [];
		b.forEach(function (u) {
			if (u.user_id in user_id_key_table_a) {
				friends_in_common.push(u);
			}
		});
	}
var ends = Date.now();
console.log((ends - starts) / 1000);

PHP

PHP 5.3.4を使いました。だいたい3.25秒。

<?php
function virtual_friends()
{
	$friends = array();
	while (count($friends) <  100) {
		$n = rand(1, 10000);
		$m = array_search($n, $friends);
		if ($m === false) {
			$friends[] = array(
				'user_id' => $n
			);
		}
	}
	return $friends;
}
$a = virtual_friends();
$b = virtual_friends();

$starts = microtime(true);
	for ($i = 0; $i < 100 * 1000; $i++) {
		$user_id_key_table_a = array();
		foreach ($a as $u) {
			$user_id_key_table_a[$u['user_id']] = true;
		}

		$friends_in_common = array();
		foreach ($b as $u) {
			if (isset($user_id_key_table_a[$u['user_id']])) {
				$friends_in_common[] = $u;
			}
		}
	}
$ends = microtime(true);

print $ends - $starts;

考察

というわけで、これだけではなんとも言えませんが、この例ではjavascript(node.js/V8)のほうが速かったです。この例はV8のJITが効きまくるのが明らかなのでV8に有利すぎる設定なのかもしれません。

この共通の友だちを探すのは一般的なウェブアプリケーションのロジックの中では例外的にCPU intensiveな処理で、実際のアプリケーションでコードを見てみたところ大半はDBから持ってきた変数をテンプレートエンジンにセットするだとか、JITの効かなそうな処理が多かったです。それでもnode.jsインスタンスがずっと動いているなら二度目のリクエストからはJITの恩恵に預かれるのかもとか、そんなことを考え始めるときりがないのでこのへんで。

Freebaseをサーバサイドjavascript実行プラットホームとして使う

たぶん前(ちょうどappjetが潰れることになった後くらい)に焼き肉に行った時にid:koyachiucnvswdyhyoupyか誰かにFreebaseでappjetと同じようにjavasriptのコードをサーバで実行できる、というのを教えてもらいました。

今日いじってみたらだいたいappjetみたいなかんじでjavascriptでコードを書いたら実行してくれます。必要な範囲でしかいじってないけどappjetより完成度は高い印象。はじめ使い方が全然わからなかったのでメモ。

Getting Started

はじめにFreebaseのアカウントを作ってログインしたらFreebase App Editorにアクセス。

Acre.Freebase.Com

こういう画面が出てくるのでてきとうに名前をつける。
そうするとコードが書けるようになるので(Greasemonkeyと相性が良くないみたいなので、この画面が出なかったらGreasemonkeyをoffにしてみてください)

デフォルトではPHPみたいなテンプレートエンジンモードになっていて、scriptで囲まないとjsのコードとして認識されないのでFileからAcre typeScriptに変えましょう。そうするとベタにjsが書けます。

設定を変えたら

acre.write("hello")

と書いて右上のView with Consoleを押せばhelloとだけ書かれたページが表示されます。

あとはHelpにAPI Referenceがあるのでそれを参考にコードを書きましょう。

secrets

Freebaseはning, appjetの影響でconle this appボタンがついていて、ボタンを押すだけでほかのアプリを自分のところにコピーしてきて編集できるようになっています。でもそういう時に困るのが秘密にしておかないといけないAPI keyで、ningもappjetもそれぞれ秘密のAPI keyまでもがコピーされないようにする仕組みがありました。Freebaseではacre.keystoreというのを使って秘密のコピーされないデータにアクセスできるようになっています。
秘密のデータを設定するのはGUIからしかできません。
左上のアプリケーション名が書かれているボタンを押してWeb Servicesのタブを押すと設定できます。

雑感

  • appjetよりも自由度の高いHTTP requestが出せるので便利
  • cronはないっぽい
  • Content-Typeを設定しても反映されない text/plain固定なのかも

ちなみにこのacre本来はウェブ上のデータを持ってきて編集してfreebaseに格納するのに使うためのものみたいです。
まじめにつくればちゃんとしたウェブアプリも作れます。

spidermonkeyの作る構文木をjsonで出力する

tomblooをchromiumで動かそうとするとspidermonkeyだけにあるE4Xなどの便利機能が壁になりました。E4XはXMLデータにアクセスするコードを簡潔に書けるほかHTMLテンプレートエンジンとしても使えるので便利なのですが、それくらいにしか便利でないのでWebKitには実装されそうにありません(ref. [webkit-dev] Support for E4X)。ほかにも地味に便利なシンタックスシュガーの分割代入など、V8に読み込ませるとシンタックスエラーになるものがいくつかあります(識別子の扱い変更とか)。

E4Xはともかく、ほかのものは自動で容易に書き換えられそうなものばかりです。spidermonkeyをベースにしたDehydra steps to phantasien(2008-10-12)のようにjavascriptの構文木を出力するものがあるだろうと探したら目的のものそのものは見つかりませんでしたがspidermoneyでどう書けば構文木を出力できるかが書いてあるページがありました。Parsing JavaScript with SpiderMonkey

わざわざCで書かれたspidermonkeyをベースにしたのはE4Xをサポートしているようなjavascriptのパーサがそれしかないからです。

jsparsenodetree

jsonで構文木を出力するものを作りました。

もとのParsing JavaScript with SpiderMonkeyのコードはspidermonkey1.7をベースにしており、spidermonkey1.8ではjs_NewFileTokenStreamなど存在しない関数があったのでspidermonkey1.8で動作するように構文木を生成する部分を以下のように書き換えました。


JSParseContext pc;

fseek(fp, 0L, SEEK_END);
long n = ftell(fp);
fseek(fp, 0L, SEEK_SET);
char* chars = (char*)malloc(n);
fread(chars, 1, n, fp);

size_t length = n;
jschar* jschars = js_InflateString(context, chars, &length);

if (js_InitParseContext(context, &pc, NULL, jschars, length, NULL, NULL, 1)) {
  node = js_ParseScript(context, global, &pc);
  JS_free(context, jschars);
} else {
  exit(EXIT_FAILURE);
}

コード

現状でtomblooのlibraryにあるjsファイルはすべてパースできるようになっています(元のjavascriptコードを復元するのに十分な情報が出力されているかは確認していません)。

ku’s jsparsenodetree at master – GitHub

実際に生成される構文木

で、実際にいくつかコードを書いてみて生成される構文木を見ていたら、最適化されていたりしてちょっとおもしろかったです。

定数同士の演算


var a = 4 * 4;

こういうのは


{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        {
          "node":"NAME",
          "token":"NAME",
          "identifier":"a",
          "assign": { "node":"NULLARY", "token":"NUMBER", "value":16.000000 }
        }
      ]
    }
  ]
}

あらかじめ計算されてvar a = 16になります。が、


var a = 4 * 4 * n;

にすると


{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        {
          "node":"NAME",
          "token":"NAME",
          "identifier":"a",
          "assign": {
            "node":  "LIST",
            "token": "STAR",
            "list": [
              { "node":"NULLARY", "token":"NUMBER", "value":4.000000},
              { "node":"NULLARY", "token":"NUMBER", "value":4.000000},
              { "node":"NAME", "token":"NAME", "identifier":"n"}
            ]
          }
        }
      ]
    }
  ]
}

こうなるので、式の中に定数でないものがひとつでも混じると、式の一部があらかじめ計算できてもしてくれなくなるようです。でもよく見ると4 * 4したものとnを掛ける、という構文木ではなく4と4とnを掛ける、という構文木になっており、ノードが減っている分速く動作しそうです(実際にはこのこの構文木を元にTraceMonkeyがバイナリコードを生成してそれが実行されるので構文木だけでは実行速度を判断できません)。

定数文字列添字

var t = document['body']var t = document.bodyに変換されていました。


{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        {
          "node":"NAME", "token":"NAME", "identifier":"t", "assign": {
            "node":"NAME", "token":"DOT", "identifier":"body", "left": {
              "node":"NAME", "token":"NAME", "identifier":"document"
            }
          }
        }
      ]
    }
  ]
}

ちなみにvar t = a[0]のような数値定数添字のときはTOK_DOTではなくTOK_LB(left brace)でgetelemというopcodeが生成されました。


{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        {
          "node":"NAME", "token":"NAME", "identifier":"t", "assign": {
            "node":  "BINARY",
            "token": "LB",
            "opcode": "getelem",
            "left": { "node":"NAME", "token":"NAME", "identifier":"a"},
            "right": { "node":"NULLARY", "token":"NUMBER", "value":0.000000}
          }
        }
      ]
    }
  ]
}

E4X

懸案のE4Xの場合。

var xml = <a href="#firefox">firefox</a>;

単純なものは単なる文字列として解釈されています。


{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        { "node":"NAME", "token":"NAME", "identifier":"xml", "assign": {
            "node":  "LIST",
            "token": "XMLELEM",
            "list": [
              { "node":"NULLARY", "token":"XMLTEXT", "opcode":"string", "text": "<a href="#firefox\">firefox\"</a>"}
            ]
          }
        }
      ]
    }
  ]
}

変数展開を含むもの。

var xml = <a href={url}>firefox</a>;

{
  "node":  "LIST",
  "token": "LC",
  "list": [
    {
      "node":  "LIST",
      "token": "VAR",
      "list": [
        {
          "node":"NAME", "token":"NAME", "identifier":"xml", "assign": {
            "node":  "LIST",
            "token": "XMLELEM",
            "list": [
              {
                "node":  "LIST",
                "token": "XMLSTAGO",
                "list": [
                  { "node":"NULLARY", "token":"XMLNAME", "opcode":"string", "text": "a"},
                  { "node":"NULLARY", "token":"XMLNAME", "opcode":"string", "text": "href"},
                  {
                    "node":  "UNARY",
                    "token": "LC",
                    "kid": { "node":"NAME", "token":"NAME", "identifier":"url"}
                  }
                ]
              },
              { "node":"NULLARY", "token":"XMLTEXT", "opcode":"string", "text": "firefox</a>"}
            ]
          }
        }
      ]
    }
  ]
}

どうも構文木を作る段階では極力ただのテキストとして解釈する仕組みのようで、href属性よりも後の部分はfirefox</a>がひとまとまりのテキストとして扱われていました。

これで構文木をJSONで取り出せるようになったのでここからV8がサポートしていない構文を書き換えてjavascriptで書き出しなおせば自動でFirefox->chrome変換ができます。

XMLHttpRequestのリダイレクトを検出する(chromium extension ハックス#1)

FirefoxのextensionはC++実装のクラスでちょっとでも有用そうなものはjsからも使えるようにしてるんじゃないかというくらいに何でもやりたい放題なのに比べるとchromiumは何もできないに等しいのでFirefoxならふつうにできることができなくて困ります。

今回はFirefox extensionならばchannelを参照するだけですむXMLHttpReqeustのリダイレクトを検出する方法です。

Content-Lengthをチェックするアプローチをとります。XMLHttpRequestオブジェクトでRedirectをハンドリングするには?(リダイレクトを拾う方法) – on the center line.を読んで初めて知ったのですが、なぜかリクエストがリダイレクトされたときにはgetResponseHeader()で得られるヘッダの値が30xを返したURIのページのものになっています。kenpocoさんの記事ではContent-Typeを見て判別されていますが、より正しく判別できるようにonprogressContent-Lengthのセットで検出するとより安全です。
onprogressに指定したコールバック関数の引数に渡される値はリダイレクト後のページを元にしているのに対し、getResponseHeader('Content-Length')はリダイレクト前のページの値を返すので両者が異なっていたときはリダイレクトされていると判断します。
ちなみにタイトルには”chromium extension ハックス”とありますがextensionでなくても使えます。

    var req = new XMLHttpRequest();
    req.position = -1;
    req.onprogress = function (e) {
        req.position =  e.position ;
    }
    req.onreadystatechange = function () {
        if ( req.readyState == 4 ) {
                var length = 0;
                try {
                    length = parseInt ( req.getResponseHeader("Content-Length"), 10);
                } catch (e) {
                    // content-length header not available.
                }
                if ( length != req.position )
                    // request is redirected.
        }

昔はけっこうContent-Lengthと実際にかえってくるバイト数とが数バイトずれてるサーバがあった(たしかgooのサーバはずれてた。IIS時代かも)けどいまは直ってるのだろうか。今のところ問題ないので実用上は問題ないんだと思います。

JSON.stringifyの第2引数、第3引数

JSON.stringify/parseは困ったもんだ。 – IT-Walker on hatena

このはなしを追ってECMAScript Fifth Edition Candidate Specification(PDF)をみてたらJSON.stringifyに第2引数と第3引数が定義されているのを知りました。

JSON.stringify ( value [ , replacer [ , space ] ] )

Firefoxは現行の3.5.3には実装されておらず、開発版の3.7.2alphaでも実装されていませんでしたが、chromium(25003), Safari(4.0.3)には既に実装されています。

第2引数のreplacerは組み込みのシリアライザのかわりに自分でシリアライズしたいときに使う関数を渡します。第1引数にJSON.stringifyに渡した第1引数がくるので自分でシリアライズするコードを書いてreturnするとそれがJSON.stringifyの値として返されます。そんなんいつ使うん?

それに対して第3引数は極めて有用。ふつうにstringifyするとムダなスペースのいっさい入っていない人間には読みづらいJSONが出てきますが第2引数を指定するとpretty-printedで読みやすいJSONになって出てきます。

> JSON.stringify( {a:"a", b:"b", c:[1,2,3]})
{"a":"a","b":"b","c":[1,2,3]}

これが第3引数を指定すると適切に改行が挿入されて読みやすくなります。

> JSON.stringify( {a:"a", b:"b", c:[1,2,3]}, null, " ")
{
 "a": "a",
 "b": "b",
 "c": [
  1,
  2,
  3
 ]
}

WebKit(safari/chrome)のFirebug互換API

WebKitというかChromiumのことでいろいろ調べていたら偶然Surfin’ Safari – Blog Archive » Web Inspector RedesignでWebKitについてるinspectorにもFirebugとおなじAPIが実装されているのを知りました。このコードはもうSafariにもchromiumにも入っていて実際に使うことができます。

Our compatibility with Firebug’s command line and window.console APIs has also been greatly improved by Keishi Hattori (服部慶士), a student at The University of Tokyo (東京大学) who tackled this area as a summer project.
Surfin’ Safari – Blog Archive » Web Inspector Redesign

すばらしい夏休みの自由研究!

Dollarx

firebugにはHTMLタブで選択しているノードが$0に、そのまえに選択していたノードが$1に入るようになっていて、自分はこの機能で選んだノードをコンテキストノードにしてXPathをためしに書いたりするときに頻繁に使っていたのでとても嬉しいです。Firebugは2つしか覚えていてくれませんが、WebKitのAPIではさらに$2, $3, $4まで覚えててくれます。$4までなのは全部で5つってことなんでしょうか。Safari4にはまだ載っていませんがchromiumではもう使えます。

そしてこっちは今まで知らなかっただけで前からなのかもしれませんが、WebKitのinspectorについてる$x()は第2引数にコンテキストノードを渡せるようになっていてfirebugのよりもべんりです。

        $x: function(xpath, context) {
            var nodes = [];
            try {
                var doc = context || document;
                var results = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null);
                var node;
                while (node = results.iterateNext()) nodes.push(node);
            } catch (e) {}
            return nodes;
        }

PubSubHubbubとWebSocketsとクライアントサイドのjavascript

ここのところwebのことからはさっぱりはなれてたのでTwitter / Ken Nishimura: PubSubHubbubがWebhookよりいい点って …をきっかけにクライアントサイドのjavascriptでなにかするという観点からPubSubHubbubとWebhookとWeb Socketについて調べた。

PubSubHubbub+ReverseHttp

Twitter / mala: @knsmr webhookは概念でpubsubhubbubは具体的なプロトコル仕様。

HTTP PubSub: Webhooks & PubSubHubbub – igvita.comの下の方にあるgoogle docsの資料と(関係ないけどこれflashだとおもったらiframe+HTMLでできてて驚いた)、仕様そのものDraft: PubSubHubbub Core 0.1 — Working Draftをざっと見るのがわかりやすかった。discovery, subscribe/unsubscribe, notification, distributionのプロトコルが規定されている。

subscriberになるにはpublisherからHTTPのリクエストを受け取ることがでる必要がある。通常firewallの中にいるブラウザは受け取ることができない。そこで出てくるのがReverseHttpでグローバルにあるサーバをsubscriberとして置き、通常firewallの裏にあるブラウザからそのサーバにcometなりweb socketなりで接続しておいてリアルタイムにnotificationを読み出せるようにしておく。

アホみたいな感じもするけどOpera Uniteも似たようなものでMSもTeredo serviceとしてやっている(ってSmart Clients: ReverseHTTP & WebSockets – igvita.comに書いてある)。

Opera Uniteの中身は全然知らなかったので調べた。通常firewallの裏側にあるPC上で動いているOperaをHTTPサーバとして機能させるために*.*.operaunite.comというサーバをproxyとしてリクエストを受け取る(ref. Opera、Webブラウザをサーバー化する「Opera Unite」を公開 -INTERNET Watch)。operaunite.comとoperaの間の通信方法は調べてないです。ここでTCPSocketなのかな。結果opera uniteのwidget(jsで記述)でHTTPリクエストをハンドルできる(ref.Unite API Overview)。
nokia端末向けのmobile web server*.mymobilesite.net経由で繋いでるのも似たようなもの。たしかにクレイジーな気がするだけで実績のあるやり方。

Web Socket

The Web Sockets APIがブラウザのjavascriptエンジンがサポートすべきWeb Socketの仕様で、Web Socketサーバとの通信プロトコル(少なくとも現状では素のApacheにはWeb Socketのリクエストをうまくハンドルできない)の詳細はIETF draftのThe Web Socket protocolに書かれている。

プロトコルの概要をつかむにはComet Daily » Blog Archive » Independence Day: HTML5 WebSocket Liberates Comet From Hacksが一番わかりやすかった。

できることを挙げると以下のようなことができる。

  • コネクションを閉じないことを除いて基本的にHTTPと同じ
  • 平文で通信するデフォルトポート81のws://とSSLで通信するデフォルトポート816のwss://とがある
  • firewall/proxyを通過できる
  • サーバで許可されていればクロスオリジン通信が可能(HTTP access controlではなくWeb Scoketの仕様に同等のものが含まれている)
  • HTTPのcookieを利用可能
  • バイナリデータも扱える

逆にできないことはThe Web Sockets API

This interface does not allow for raw access to the underlying network. For example, this interface could not be used to implement an IRC client without proxying messages through a custom server.

と書かれているようにIRCのクライアントのようなもの(というよりかはWeb Scoketで利用することを前提としていない全てのプロトコル)はWeb Socketでは実現できない。

それはなぜか。Web Socketは基本的にはHTTPと同じようなやり取りをする。つまりはじめにクライアントからリクエストを送りサーバがそれに対してレスポンスを返す。HTTPとの違いはレスポンスを返した後コネクションが切断されるのに対してWebSocketでは切断されずに全二重双方向通信に使うことができるところ。このはじめのリクエスト/レスポンスのやり取りはhandshakeと呼ばれていてる。

draftによれば、このhandshakeはクライアントが


GET /demo HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com:81
Origin: http://example.com
WebSocket-Protocol: sample

というリクエストを送り、それを受け取ったWebScoketサーバが


HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://example.com
WebSocket-Location: ws://example.com/demo
WebSocket-Protocol: sample

というレスポンスを返すことで行われる。

このhandshakeは必ず必要なので、このWeb Socketのhandshakeを理解できないサーバとはWeb Socketで通信することはできない。つまり既存のプロトコルと自由にソケット通信することはできない(ただ既存のサーバの側でWeb Socketに対応することは可能だと思う)。

TCPSocket(Network Communication API)であれば可能だがOperaでしか実装されていない。

サーバ側の対応状況

現状ではApacheでWeb Socketのリクエストをハンドルすることができない(理由は追ってません)。
mozillaのbugzillaではBug 472529 ? Support for Web sockets’ HTML5 Draft Recommendation (websocket)にだめみたいと書かれており、WebKitのbugzillaでもBug 27490 – Web Sockets Test Infrastructure Part 1/3: Serverにmod_pythonを使ったweb socketサーバを作ってテストを動かすと書いてある。

apacheのbugzillaでもBug 47485 ? HTML5 Websocket implementationにモジュールとして作ればいいんじゃないの、と書かれているので(ApacheでWeb Socketをサポートすることについて微妙にもめてて野次馬的に面白かったんですけどそこは割愛)Web Socketをサポートしたければそういうサーバを用意する必要があります。

現状ではWebSocket実装Kaazing – html5-developers-jp | Google グループで紹介されているKaazingというものがあるようです。

使用シナリオ

  • cometの代替手段として(ブラウザ上で動作するゲームのようなものやチャットなどのレスポンス改善)
  • ウェブアプリケーション全般のインタラクティビティ改善

など。

HTML5のbbとして入っているBackground Browser Taskとの組み合わせがよさそうと思ったらbbはHTML5の仕様からなくなってた…. (X)HTML5 Tracking
chromiumのほうでも動きがないのでなくなったのかも。

まとめ

クライアントサイドで考えるとPubSubHubbub(+ReverseHttp)はWeb Socketまでのつなぎ。ただデスクトップの世界ではIEがWeb Socketをサポートするのは期待できないのでWeb Socketを利用できる場面はfirefoxやchromeの拡張機能などに限られる。モバイルであればWebKit rules!なので数ヶ月後にはchromiumにWeb Socketが載ってそれはWebKitにフィードバックされるので1年くらい経てばWeb Socketを使ったアプリケーションを普通に使えるようになるかもしれない(ただ#ifdef ENABLE_WEB_SOCKETSつきなのでイコールsafariがサポートという訳ではない)。

あとデスクトップのクライアントサイドであってもOpera uniteのように自前でoperaunite.comのようなReverseHttpを持つことでPubSubHubbubを直接受け取ることができるようになる。

サーバサイドでは全く拡張性がなく使用目的が限られるXMLRPC pingの代替としてPubSubHubbubは汎用的に使える。

Web Socketは何でも通信できる神機能ソケットではないが、いままでcometのようなハックを駆使する必要があったものをシンプルに作ることができるようになる(これもIEのことがあるのでモバイルの世界限定の話)。

javascriptで整数の変数を強制的に符号なし整数に変換する方法

chromeでPageRankを表示するための拡張機能PageRankToolStripでURLからハッシュ値を出すような処理が入っていて、そこで符号なし整数のビット演算処理があります。もとにしたPHPのコードでは

$a = sprintf('%u', $a);

のような方法で整数値を符号なしで扱うようにしてあったのですがjavascriptにはsprintfなんかないのでどうすればいいのか困ってしまいました。javascriptの整数は内部的には32bit intで扱われていて(Firefox3.1, Chrome2.0の場合)、演算の結果が0×80000000を超えると自動的に負の値になります。

どうしたらいいのか検索したら、符号なしシフト演算子>>>で0ビットシフトしろって書いてありました。

var a = 0xffffffff; // 4294967295
var b = a & 0xffffffff; // -1
var c = b >>> 0; // 4294967295

常識かもしれない&あんまり使う場面ありませんが知らなかったので。

追記

あいまいに32bit intで扱われてると書いていたところを404 Blog Not Found:javascript – には整数はないでとりあげていただいてはてなブックマーク – 404 Blog Not Found:javascript – には整数はないでもいろいろコメントいただいてたのでFirefoxのjavascriptエンジンであるspidermonkeyの実装についてだけもうちょっと詳しく調べました。

ECMAScriptの仕様上は整数は存在しませんが、実装する上はdoubleとintで4バイトも差があるのでメモリ効率が違ってくる(=実行速度も違ってくる)のと、整数演算で済むのと浮動小数点演算になるのとでは必要なCPUサイクルが一桁違うので、intで済む範囲では極力doubleを使わないでintを使うようなっています。FirefoxのjavascriptエンジンであるSpiderMonkeyではどこが境界線になっているか見てみました。

SpiderMonkeyではSpiderMonkey Internals – MDC

operates on values of type jsval—type-tagged pointer-size words that represent the full range of JavaScript values.

とあるようにJavaScriptの変数はすべてjsvalというポインタで表されていて、その実体は32bitのポインタになっています。そしてそのポインタの下位3ビットをタグとして利用しています(なので実際に変数が確保されているアドレスは必ず8の倍数になります)。タグの種類は以下の5つ。

#define JSVAL_OBJECT            0x0     /* untagged reference to object */
#define JSVAL_INT               0x1     /* tagged 31-bit integer value */
#define JSVAL_DOUBLE            0x2     /* tagged reference to double */
#define JSVAL_STRING            0x4     /* tagged reference to string */
#define JSVAL_BOOLEAN           0x6     /* tagged boolean value */

コメントからすると内部実装では数値は31bitで表現できるときはint32で表現して、その範囲を超えたらdoubleになるようになっているようです。

実際のコードを見てみる前に、スタンドアロンのSpiderMonkeyのシェルについているdis()で関数のバイトコードをダンプして見ることができるので、バイトコードを見てみましょう。

js> dis (function () { a=0xffffffff; b=a&a; print (b>>>0)})    
flags: LAMBDA INTERPRETED
main:
00000:  bindname "a"
00003:  double 4294967295
00006:  setname "a"
00009:  pop
00010:  bindname "b"
00013:  name "a"
00016:  name "a"
00019:  bitand
00020:  setname "b"
00023:  pop
00024:  callname "print"
00027:  name "b"
00030:  zero
00031:  ursh
00032:  call 1
00035:  pop
00036:  stop

Source notes:
  0:    32 [  32] xdelta  
  1:    32 [   0] pcbase   offset 8

そうするとたしかに32bitでしか表現できない0xffffffffはdoubleで確保されています!変数aJSVAL_DOUBLEで初期化されることになります。

じゃあそのJSVAL_DOUBLEをどうやってビット演算の&をしているのかバイトコードbitandの中身を見てみましょう。bitandBITWISE_OP(&)というコードで定義されていて、マクロの中身は

#define BITWISE_OP(OP)                                                        \
    JS_BEGIN_MACRO                                                            \
        FETCH_INT(cx, -2, i);                                                 \
        FETCH_INT(cx, -1, j);                                                 \
        i = i OP j;                                                           \
        regs.sp--;                                                            \
        STORE_INT(cx, -1, i);                                                 \
    JS_END_MACRO

こうなっています。引数はスタックに入れてあってそれを持ってきてintに変換して&をとって戻してます。FETCH_INTは中でjs_ValueToECMAInt32()といういかにもな名前の関数を呼んでいてここでdouble->intの変換が行われています。

int32
js_DoubleToECMAInt32(jsdouble d)
{
    int32 i;
    jsdouble two32, two31;

    if (!JSDOUBLE_IS_FINITE(d))
        return 0;

    i = (int32) d;
    if ((jsdouble) i == d)
        return i;

    two32 = 4294967296.0;
    two31 = 2147483648.0;
    d = fmod(d, two32);
    d = (d >= 0) ? floor(d) : ceil(d) + two32;
    return (int32) (d >= two31 ? d - two32 : d);
}

31bitで表せる最大値で割り算をして余りを整数とみなしたときに正負を考慮した上で変換しています。ここで無事doubleの4294967296.0になっていた0xffffffffは整数に戻ってきて0xffffffff & 0xffffffff => 0xffffffffの演算が終了します。

問題は次のSTORE_INTです。SpiderMonkeyの実装ではjsvalの最下位ビットはjsvalが保持する値が整数かどうかを示すフラグになっているので31bitまでの値しか保存できません。32bitでしか表現できない0xffffffffはどうなるのか?
というと、ふつうに31bitでしか表せない値はdoubleで保存されるだけです。

#define STORE_INT(cx, n, i)                                                   \
    JS_BEGIN_MACRO                                                            \
        if (INT_FITS_IN_JSVAL(i))                                             \
            regs.sp[n] = INT_TO_JSVAL(i);                                     \
        else if (!js_NewDoubleInRootedValue(cx, (jsdouble) (i), &regs.sp[n])) \
            goto error;                                                       \
    JS_END_MACRO

ここで出てくるJSVAL_INT_MAX((1<<30) - 1)として定義されています。判別のしかたがトリッキーなのは正負両方で31bitを超えずに表現できるかを同時に判別するためです。

#define INT_FITS_IN_JSVAL(i)    ((jsuint)((i)+JSVAL_INT_MAX) <= 2*JSVAL_INT_MAX)

STORE_INTは0xffffffffをJSVAL_DOUBLEの-1に変換して保存することがわかりました。これでa & aが-1になる理由が実装からもわかりました。

最後に b >>> 0 で-1が4294967296になる仕組みも0xffffffffが-1になる仕組みと似たようなもので、ビットシフトの結果が31bitで表現できる範囲であればJSVAL_INTとして、できなければJSVAL_DOUBLEとして保存されるようになっています。今回の例では0xffffffffで31bitに収まらないのでJSVAL_DOUBLEの4294967296.0として保存される仕組みになっています。

というわけでSpiderMonkeyにおいて31bitを超える整数はdoubleになるという話でした。
ちなみに話がややこしくなるので省略しましたが、aに入れる値を小さくしていくとなぜか26bitで表現できるサイズになったときにint32というバイトコードに変わります。なので整数が常にJSVAL_DOUBLEとして初期化されるわけではありません。

落書きをしたページをTumblrとかに投稿できるブックマークレット javascriper.js

それTomblooでできるよ! Webページのスクリーンショットをとる « ku
いろんなサイトに落書きするブックマークレット – 素人がプログラミングを勉強するブログ
で、ページ上にメモや目印書いたりもできますね。
Fuck食場

そっかー!というノリでさっそくやってみた。

ブックマークレット

javascripter.js (on github)

ら、全面canvasになってるから右クリックできない→Tomblooのメニューが出せなかったので、左上にツールパレットを追加して右クリックできるようにしました。これで落書きしたページをTumblrやそのほかのところにpostできます。

このスクリーンショットを撮るときには消し忘れてちゃって映ってるんですが、左上に出てきたツールパレットはXを押すと消せます。消した後でもパレットがあったところで右クリックすればコンテキストメニューが出てきてTomblooでスクリーンショットを撮ることができます。

ブックマークレットを実行すると左上にツールパレットが出てきて、デフォルトではマウスで落書き、下の矩形選択ツールをクリックすると、Safariの検索結果表示みたいに、ページの特定部分だけを残して暗くして強調表示させられます。

hilighting

自分はブログを書くときに、なるべくスクリーンショットを入れることにしてるんですけど(なぜならスクリーンショットがあれば文章なんか読まずにスクリーンショットだけ見て適当につつけばだいたいなんとかなるからです)これがめんどくさいのです。
特にウェブページのここのところを押してね、みたいなスクリーンショットをとりたいときは、まずスクリーンショットをとって、その後ほかのツールでハイライトしたりしないといけなくて、そんなの面倒すぎるから(というかいいツールを知らないので)やっていなかったのがこれでお手軽にできるようになりました。

ほんとはほかにも四角で囲んだりとか、文字を入れたりとか、線の色とか種類が選べたりとか、基本的なドローツールくらいの機能があるとますますブラウザだけでのウェブライフを満喫できるようになるのでぜひ誰かに作ってほしいです。

あと、ときどきマウスで引いた線がぶつぶつに切れているような見た目になるのではてなハイク お絵描き機能の話 – 川o・-・)<2nd lifeのアルゴリズムとかを誰かが実装してほしい!

chromeに実装されるGM_xmlhttpRequestはドメインの壁を越えられるのか?

追記 2009.5.24

GM_xmlHttpRequestでは今のところ越えられませんが、extensionを経由すれば異なるドメインにリクエストを送ることができます。

ちなみに、Chromiumといえば、GM_xmlhttpRequestのコミットが間近です。 Issue 27037: Implement GM_xmlhttpRequest – Code Review こっちも楽しみ。
期間限定、Google Chrome 2 の HiResTimeでベンチマーク – 0x集積蔵

先日チェックアウトしてきたコードの中をみたら

function GM_xmlhttpRequest(details) {
  throw new Error("not implemented.");
}

こうなっていたので、スタブができただけと思っていたら今度は本当に中身がありました!
chrome/test/data/extensions/greasemonkey_api_test.js – Issue 27037: Implement GM_xmlhttpRequest – Code Review

みたかんじただのXMLHttpRequestGM_xmlhttpRequestの仕様にあわせてラップしているだけに見えます。はたしてこれでクロスドメインのリクエストが送れるのでしょうか。送れるのならXPCNativeWrapperのような仕組みのないchromiumでどうやって安全にクロスドメインでリクエストを送れるようになっているのでしょうか。chromeのuser scriptsはchrome-user-script://というchromeの内部的なプロトコルで実行されているので、このプロトコルが再度chromeのクロスドメインセキュリティチェック探訪chrome-ui://*1
と同様にクロスドメインチェックに細工がしてあって、クロスドメインでリクエストが送れるようになっているのかもしれません。

とりあえずためしてみればいいじゃんというわけで、snapshots 10689のバイナリに入っているGreasemonkeyのjavascriptファイルを書き換えて試してみました。差分をみたかんじC++のコードはテストコードだけなのでスナップショットのバイナリでもテストになると仮定しています。

chrome.dllの書き換え

greasemonkey_api.jsを書き換えればいいのはわかっているものの、chromiumのディレクトリを探してもそんなファイルはなくて、いろんな細かいファイルは全部chrome.dllの中に入ってるようです。

いまどきのWindowsリソース書き換えツールはなんなのかなと探して一番はじめに目についた日本語化工房-KUP – ファイルミラーで紹介されていたXN Resource Editorというのを使ってjavascriptのファイルの中身を書き換えました。紹介されてる方も使い勝手が非常によいと書かれていますが、開くときにファイルをロックしたりしてないところとか確かにいいです。

XN Resource Editorでchrome.dllを開いたらBINDATA29450を開いてGM_xmlhttpRequestの部分を探します。
Xnres

これをパッチのコードで差し替えて、保存したらできあがり。

テスト

	GM_xmlhttpRequest({
	method: "GET",
		url: "http://wedata.net/databases/AutoPagerize/items.json",
		onload: function (a) {
			alert(a.responseText)
		},
		onerror: function (b)  {
			var c = [];
			for (a in b ) {
				c.push(a + "=" + b[a] );
			}
			alert(c.join("\n"));
		}
	} );

というコードでテスト。
Alert
結果はNGでした。

いまのところの結論

少なくともchromiumsnapshots 10689+パッチのGM_xmlhttpRequestはドメインの壁を越えられないみたいです。
でもuserscriptのためにプロトコルが別に定義されているところと、前からあるInspectorの実装をみるかんじ、ドメインの壁はそんなに高くない気がするので正式にリリースされる頃には超えられるようになってるのでは。

  1. 以前はchrome-resource://でしたがchrome-ui://に変わっています

Ubigraph+ubi.jsでリブログの流れを可視化する

仕事でちょっとネットワークの可視化をしたい場面に遭遇したので、先日果たしてtumblrにアルファリブロガーは存在するのかのあとでtwitterでkybintさんに可視化ツールをいろいろ教えてもらったのでちょっと試してみようとやってみたらUbigraphがとてもお手軽でよかったです。インストールからはじめて、自分が持っているネットワークデータを可視化する方法を理解するまで5分、はじめてでも10分もあれば自分のデータを可視化できます!

ダウンロードのページからバイナリをダウンロードします(でも悲しいことに今はOSX版とLinux版しかありません)。ダウンロードするページにインストール方法、起動方法、サンプルコードの実行方法が載っているので、とにかくそれをコピーしてターミナルに貼れば本当に何も考えないでもサンプルが画面で表示されるところまでいけます。

で、おおー、っと思ったところではじめて画面に出てきたものとサンプルのコードを見比べていくと、ほしい数だけ頂点を作って繋がっているぶんだけ頂点2つを指定して辺を作っていくだけでいいのがすぐ分かります。Ubigraphの実体は、視覚化したものを表示するXMLRPCサーバで、それにXMLRPCでリクエストを送ると動的にネットワークが変化していく仕組みになっていて、いろんな言語から使うことができます。perlの場合は

#!/usr/bin/perl
use Frontier::Client;
my $client = Frontier::Client->new( url => 'http://127.0.0.1:20738/RPC2';);
$client->call('ubigraph.clear', 0);
my $a = $client->call('ubigraph.new_vertex');
my $b = $client->call('ubigraph.new_vertex');
$client->call('ubigraph.new_edge', $a, $b)

こういうコードで、画面に頂点がふたつ生成されて、そのふたつが辺で結ばれます。
perlを使うなら実際はFrontier::ClientをオブジェクトっぽくラップしているUbigraphのほうがなじみやすいと思います。RubyはmootohさんのRubigraphがあります。

Ubigraphのコードが分かりやすい、といっても、ほかのツールも同じようなもので、どの頂点とどの頂点が結ばれているかのデータファイルを作ってツールに流し込むだけで単純さはほとんど変わりません。ただ、手持ちのデータからそのツール用のデータファイルを作るためのプログラムを書いてそれをツールに読ませてビュワーで再読み込みさせるのより、手持ちのデータに従ってUbigraphのAPIを呼ぶプログラムを書けばそれで画面に反映されるところは地味に面倒な手順が減っていいなと思いました。

ひととおり使い方が分かったので、前回の果たしてtumblrにアルファリブロガーは存在するのかのときに果たせなかったリブログされる様子のグラフも作ってみました。

Ubigraphは動的に変化するグラフを視覚的に見られるところが本当に素晴らしいのであって、こういうふうに動かない画像にすると全然魅力的に見えないんですが、ほんとはリブログされた時系列順で頂点が増えていきます。Ubigraphは次数(頂点から出ている辺の数)が多いものを中心にしてグラフがレイアウトされるので、なんか全ての始まりであるはずのところが、真ん中のほうにちょこっとついてるだけに見えちゃってますが、赤い球のところが一番はじめのpostです。
はじめにpostしたgipsさんからkielaさんがreblogして、そこから6人のひとに枝分かれして何人もの人を介して広がっていっているのが分かります(っていうのが初めてでも10分くらいで作れるので、ちょっとこれ見てみたいな、という時があったらぜひ試してみるのをお勧めします!)。

一部を拡大することもできます。

Ubigraphの神髄は動的に変化する様子が見られるところで、こういう変化しないものを見るときはレイアウトを細かく設定できるほかのツールのほうが優れてそうです。それでも、小さなネットワークであればUbigraphでも十分形を把握することはできますし、何よりとっときやすいところがよかったです。

Firebug+ubi.js

上のグラフはFirefox+FirebugでXMLHttpRequestを使ってUbigraphにXMLRPCリクエストを送信して作りました。

ほんとはFirefox3.1のクロスサイトXMLHttpRequestを使えたら、ウェブページに置かれたUbigraph用のjavascriptコードを読み込むだけでローカルのUbigraphを動かせるんですが、仕様が厳しくなってtext/xmlはリクエストを送られる側のサーバの許可なしで送れなくなったため*1、Ubigraph自身のページ(127.0.0.1:20738)でFirebugを開いてコードを実行しないといけません。

FirefoxでUbigraphを起動したあとFirefoxでhttp://127.0.0.1:20738/RPC2を開いてFirebugで下記のコードを実行すれば手元のUbigraphでリブログされていく様子を見ることができます。Firefox上でウェブにあるHTMLからデータを取り出してFirebugからUbigraphにデータを送れたらすごくスマートでいいなというわけでUnigraph APIのほんの一部をラップしたubi.jsというのgistに置いたので、ちょっとしたデータをちょっと見てみたいな、という時にはぜひUbigraphともども使ってみてください。

function s (u) {
var s = document.createElement("script");

s.src = u;
s.type = "text/javascript";
document.body.appendChild(s);
}

s("http://gist.github.com/raw/66201/b6784b8431305d74ec0c822f73e7b8de9aee95af/ubi.js");
s("http://re.ido.nu/action/73684294?callback=p")

var vt = {};

window.p = function(ret) {
  var u = new Ubigraph();
 u.clear();

  ret.map ( function (d) {
if ( d.action == "liked" )
            return;

// ignore multiple reblogs.
if ( vt[d.who] )
   return;
    
    var v = vt[ d.who ] = u.new_vertex();
    u.set_vertex_attribute(v, "label", d.who);
    u.set_vertex_attribute(v, "shape", "sphere");

if ( d.action != "reblogged" ) {
    u.set_vertex_attribute(v, "size", 1.5);
    u.set_vertex_attribute(v, "color", "#800000");
    return;
}

if ( vt[d.from]  && vt[d.who] ){
  var e =    u.new_edge(
        vt[ d.from ],
        vt[ d.who ]
     );
   u.set_edge_attribute(e, "arrow", "true");
   u.set_edge_attribute(e, "arrow_position", "1.0");
}
} );
}

参考

  1. HTTP access control – MDC

AutoPagerizeのスクリプト実行順序制約をなくせるようになりました

Tumblrが新しくなって、よく見ていた/show/quotes/by/everyoneがちゃんとページングされなくなって悲しいと思っていたらcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyというスクリプトを書いてくれていました。

しかし21世紀はじめの10年最後の2009年ももう終わろうとしているにも関わらず、未だにTumblr dashboard reblog 4点セットのAutoPagerizeLDRizeMiniBufferreblogCommandの実行される順序をちゃんと覚えておかないといけないなんてローテクすぎる!という怒りにまかせて、順番に関係なく入れておけば動くように細工をしました。

それぞれ上記以降のバージョンであれば、Greasemonkeyで実行される時の順番を気にせず、とにかくインストールしておけばよくなりました。

しくみ

AutoPagerizeが実行されたときに

	var ev = document.createEvent('Events')
	ev.initEvent('GM_AutoPagerizeLoaded', false, true)
	window.dispatchEvent(ev)

というコードが実行されてGM_AutoPagerizeLoadedという名前のイベントが送られてくるようにしました。LDRizeのようにAutoPagerizeに依存してなにかを実行したときに、もしwindow.AutoPagerizeが存在しなかったらGM_AutoPagerizeLoadedイベントが送られてくるまで待ってから実行するようなコードを書けば、スクリプトが実行される順序に関係なく動作させることができるようになります。

以下はAutoPagerizeでページが継ぎ足されたときに、継ぎ足された部分をLDRizeが正しく認識するためのコードです。実行したいコードをaddFilterHandlerという名前の関数にしておいて、window.AutoPagerizeが存在していればそのまま実行、存在していない時はAutoPagerizeからGM_AutoPagerizeLoadedイベントが送られてくるのを待ってから実行することで、AutoPagerizeに依存するスクリプトを順序に関係なく正しく動作させることができます。

      var addFilterHandler = function(){
          window.AutoPagerize.addFilter(
              function(pages){
                  self.removeSpace();
                  setTimeout(function(){
                      self.initParagraph(pages);
                  }, 0);
              });
      }
      if(window.AutoPagerize){
        addFilterHandler();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', addFilterHandler, false);
      }

AutoPagerizeと連動するスクリプトを書く人へのおねがい

AutoPagerizeに依存する部分を

      var f = function () {
        // やりたいこと
      }
      if(window.AutoPagerize){
        f();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', f, false);
      }

と書くとスクリプトが実行される順序を気にしなくてよくなって、使ってくれる人の手間も省けるのでぜひご採用ください。

Webサイト側でのAutoPagerize検出

うれしい副作用として、ユーザスクリプトと同じようにHTMLドキュメント側でもGM_AutoPagerizeLoadedをlistenしておくことでAutoPagerizeが存在するかどうかを検出することができます。

  var isAutoPagerizePresent = false;
  window.addEventListener( 'load', function () {
    alert(isAutoPagerizePresent);
  }, false );
  window.addEventListener( 'GM_AutoPagerizeLoaded', function () {
    isAutoPagerizePresent = true;
  }, false );

Firefox3.1beta2+OSXの場合、GM_AutoPagerizeLoadedイベントが発生するタイミングはjQueryのreadyメソッドよりも後、loadイベントよりも前でした。

オチ

もとのcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyの説明をよく読むと

AutoPagerizeと併用する場合は、「ユーザスクリプトの管理」でAutoPagerizeよりもに置いておく必要があります。

と書かれていたのでした… にしないといけない制約しか考慮していなかったのでにしないといけない場合は、今後も順番を気にし続けるか、AutoPagerizeにそういう細工をして取り込んでくれるようにとswdyhにpull requestを送ってください。

JSCocoaのiPhone用インタラクティブコンソール

English version is available.

JSCocoaのページからリンクされたので日本語版をこっちにうつしました。

iviewで使っているUIViewtransformがなかなか思い通りに操れなくて困っていて、だったらアプリにSpiderMonkeyを入れてHTTP経由で通信して試行錯誤できるよにすればいいやとUIMonkeyというのを作り始めたところで、既にそういうのがいくつかあるのをJohn Resig – JavaScript iPhone Appsで知ってショックーというかアホだったのでJSCocoaベースにちょこっと書き直しました。viewの階層構造やサイズを調べるのに便利です。

ファイルのコピー

svn checkout http://jscocoa.googlecode.com/svn/trunk/ jscocoa-read-only

してjscocoa-read-only/JSCocoa/JSCocoaの中身を全部自分のプロジェクトにコピーします。class.jsだけプロジェクトに追加した時に、ただのリソースファイルとして認識してほしいんだけどソースファイルとして認識されてしまうので、いったんclass.gifという名前でプロジェクトに追加して後から名前を変えたらうまくいきました(どうやるのが正しいんでしょう…)。

JSCocoa本体のほかにHTTPサーバのためのHttpd.m, Httpd.h, console.htmlを追加してください。console.htmlCopy Bundle Reesourcesに入れてアプリケーションに組み込まれるようにしてください。

*_Prefix.pchの修正

JSCocoa用のヘッダファイルを追加します。
今回はインラインでUSE_JSCOCOAdefineしていますが、デバッグビルドの時だけdefineするようにしたりすればいちいち書き換えたりする手間がなくなります。

#define USE_JSCOCOA 1

#if USE_JSCOCOA
  #define JSCocoa_iPhone
  #include "JavascriptCore.h"
#endif

main.mの修正

JSCocoaのサンプルコードiPhoneTest2とおんなじようにmain.mで初期化します。クラスHttpdは自分で作ったなんちゃってHTTPサーバです(最後参照)。

#if USE_JSCOCOA
	// Fetch JS symbols
	[JSCocoaSymbolFetcher populateJavascriptCoreSymbols];
	
	// Load iPhone bridgeSupport
	[[BridgeSupportController sharedController] loadBridgeSupport:[NSString stringWithFormat:@"%@/iPhone.bridgeSupport", [[NSBundle mainBundle] bundlePath]]];
	// Load js class kit
	id c = [JSCocoaController sharedController];
	[c evalJSFile:[NSString stringWithFormat:@"%@/class.js", [[NSBundle mainBundle] bundlePath]]];

	[[[Httpd alloc] initWithPort:38880] autorelease];
#endif	

リンクするライブラリの追加

JSCocoaは関数呼び出しにlibffiを使っているので、プロジェクトの設定を開いてOther linker flags-lffiを追加します。

CGAffineTransformの追加

JSCocoaの中にiPhoneでだけ使われる構造体を定義しているiPhone.bridgesupportというXMLファイルがあります。これにCGAffineTransformを追加します(CGAffineTransformを使わないのであれば必要ありません)。

せっかくXMLファイルなのに構造体メンバを"で区切ってしかもそれを実体参照で書くという酷い仕様。下の行をてきとうにCGSizeを定義している下くらいに追加します。

<struct name='CGAffineTransform' type='{CGAffineTransform=&quot;a&quot;f&quot;b&quot;f&quot;c&quot;f&quot;d&quot;f&quot;tx&quot;f&quot;ty&quot;f}'
type64='{CGAffineTransform=&quot;a&quot;d&quot;b&quot;d&quot;c&quot;d&quot;d&quot;d&quot;tx&quot;d&quot;ty&quot;d}' />

ヘッダファイルからマシンフレンドリーな形式で生成して実行時コストを下げるか、ヒューマンフレンドリーにして手で書きやすくするかどっちかにするべきだと思います。

ブラウザで接続

そうしたらあとはアプリをふつうに起動すれば http://localhost:38880/ でアプリの中のJSCocoaに接続できるので、好きなタイミングでブラウザからJSCocoaにjsのコードを送り込んでアプリを操作できます。

以下iviewLifeの写真を表示してのテスト。

original

JSCocoaではObjective-Cのクラス名、メソッド名でjsからもアクセスできるので、UIApplication.sharedApplicationからアプリ内のオブジェクトにアクセスできます。

メインウインドウのアルファを0.2に変えると

alpha=0.2alpha=0.2

背景が透けて、暗くなります。

CGAffineTransformで横に3倍引き延ばしてちょっと右に100ピクセルずらすと

transformtransform

こうなります。

こういう試行錯誤や、使ったことのないクラスを触ってみるときに、Objective-Cのコードの上でやろうと思ったら毎回コンパイルし直す必要があるので時間がかかりますがjavascriptからできると細かい調整がすごく楽です。

View Tree Walk

takuma104さんが前に

iPhoneにWindowsでいうSpy++みたいなのがほしい。viewの階層構造がわかるやつ。
Twitter / takuma mori: iPhoneにWindowsでいうSpy++みたいな …

と言われていたのを思い出して、UIApplication.sharedApplication.subviewsからviewを列挙してjavascriptオブジェクトとしてみられるようにしてみました。HTMLで視覚的に再構築できればベストなんですけど、とりあえずFirebugのコンソールで値を確認できるようにしてみました。inspect view treeのリンクをクリックすると下の画面みたいにしてviewの階層構造を見ることができます。

JSCocoa感想

JSCocoaがNSArrayを自動的にjavascriptの配列として扱ってくれるのですが、これがあんまりうまくいってないみたいでlengthの値が入ってなかったりするとか、そういう微妙に変なところを配慮してあげればわりと使えそうなかんじでした。
自分が普段SpiderMonkeyにどっぷり浸かっているのでunevalがないとかE4Xがないとか分割代入できないとかもありましたがそうでなければふつうにjavascriptです。

JSCocoa interactive console for iPhone

日本語版もあります。

I’ve been struggling with transform property of UIView for long time. at last, I’ve decided to create UIMonkey that allows to manipulate the variables by trial and error through HTTP in Javascript with SpiderMonkey. Few days after I’ve started the project, I found JSCocoa via John Resig – JavaScript iPhone Apps. I abandoned my project and rewrited my codes on JSCocoa.

Now it works on the top of JSCocoa. It’s very helpful especially for inspecting the hierarchy of the view tree or size of the views etc.

How to setup

Follow the steps below to setup JSCocoa interactive console.

Copy the files

First of all, you need to check out JSCocoa files.

svn checkout http://jscocoa.googlecode.com/svn/trunk/ jscocoa-read-only

After checking out complete, copy all of files existing under jscocoa-read-only/JSCocoa/JSCocoa and import them into your project.

I got into trouble on importing class.js. XCode take it as an source file which is need to be compiled while I want to treat it as a simple resource file. I solved this problem by renaing it as class.gif, import it to the project, and rename it again as class.js(please tell me the right way to do that if you know).

In addition to JSCocoa files, you need some files for the console. Please download and import them to your project. Httpd.m, Httpd.h, console.html.
Please remember to
console.html add to the list of Copy Bundle Reesources to ensure the files is copied into your aplication binary file.

Add header files to *_Prefix.pch

JSCocoa requires some header files.
Open your *_Prefix.pch file and add some lines below.

#define USE_JSCOCOA 1

#if USE_JSCOCOA
  #define JSCocoa_iPhone
  #include "JavascriptCore.h"
#endif

Of course, you can define USE_JSCOCOA in the project settings instead of hard coding in the pre-compiled header.

Add some codes to your main.m

Initializing JSCocoa in the same way that of iPhoneTest2 which is distributed with JSCocoa.
Class Httpd is a so-called http server which is implemented in Httpd.m and Httpd.h.

#if USE_JSCOCOA
	// Fetch JS symbols
	[JSCocoaSymbolFetcher populateJavascriptCoreSymbols];
	
	// Load iPhone bridgeSupport
	[[BridgeSupportController sharedController] loadBridgeSupport:[NSString stringWithFormat:@"%@/iPhone.bridgeSupport", [[NSBundle mainBundle] bundlePath]]];
	// Load js class kit
	id c = [JSCocoaController sharedController];
	[c evalJSFile:[NSString stringWithFormat:@"%@/class.js", [[NSBundle mainBundle] bundlePath]]];

	[[[Httpd alloc] initWithPort:38880] autorelease];
#endif	

Add libffi

JSCocoa using libffi to call Objective-C functions.
OPen your project settings and add -lffi to Other linker flags.

Add CGAffineTransform(If you need)

iPhone.bridgesupport which defines the structures used by only iPhone SDK. If you want to manipulate CGAffineTransform through JSCocoa, add the difinition of CGAffineTransform to the file.

iPhone.bridgesupport is a XML file, but it’s too annoying to write it by hand. You have to separate the members with ". I think it would be nice to generate the file in machine-friendly format to decrease the run-time CPU cost or to make it more human-friendly format to allow humans to write it by hand easily.

Add this definition below to around the next line of that defines CGSize.

<struct name='CGAffineTransform' type='{CGAffineTransform=&quot;a&quot;f&quot;b&quot;f&quot;c&quot;f&quot;d&quot;f&quot;tx&quot;f&quot;ty&quot;f}'
type64='{CGAffineTransform=&quot;a&quot;d&quot;b&quot;d&quot;c&quot;d&quot;d&quot;d&quot;tx&quot;d&quot;ty&quot;d}' />

Connect to JSCocoa through browser

Just launching your application in the iPhone simulator and now you can connect to JSCocoa which resides in it.
Open http://localhost:38880/ and you can send any javascript code to manipulate your application through JSCocoa at any time.

I’ll show you some tests with iview and a photo from Life magazne.

original

JSCocoa allows you to access Objective-C classes and methods. So you can access the objects in your iPhone application through UIApplication.sharedApplication.

Now change the alpha value of the main window.

alpha=0.2alpha=0.2

the background of the main window become transparent and the screen gets black(because background color is black).

Streching Y-axis three times and moving by 100pxices.

transformtransform

Writing in Objective-C consumes huge amount of time that using the classes you are not familiar with, adjusting animation parameters and so on. Bacause it have to be compiled. But with JSCocoa, You don’t need to compiled it, and you can bedone it thorugh trial and error without recompile. It’s very nice.

View Tree Walk

takuma104(who is a developper of Natsu Lion for iPhone that is a swift, neat, open-source twitter client) said on twitter,

iPhoneにWindowsでいうSpy++みたいなのがほしい。viewの階層構造がわかるやつ。
Twitter / takuma mori: iPhoneにWindowsでいうSpy++みたいな …

It means “I need a tool for iPhone like Spy++ of Windows. Shows me hierarchy of the views”.

I remember the tweet and write small code that enumerates all views from UIApplication.sharedApplication.subviews. You can see the geometric values through Firebug console by clicking the inspect view tree link. I think its more useful if it reconstructs the hierarchy in the same visual HTML elements(but not implemented at this time).

My impression of JSCocoa

JSCocoa is very useful After I got familiar with some weird behaviours like that javascript array object which is auto translated from NSArray by JSCocoa does not have length.

and if you unfortunately have get used to SpiderMonkey like me, it makes hard javascript coding on JSCocoa which is based on JavaScriptCore of WebKit that does not have uneval, E4X, destructuring assignment and so on.
yes, i know its not JSCocoa’s fault. it’s my fault who get used to the features only in SpiderMonkey =)

Great thanks for people behind JSCocoa.

chrome-resource:// からだとクロスドメインXMLHttpRequestが(いちおう)使える

再度chromeのクロスドメインセキュリティチェック探訪chrome-resource://からなら、異なるドメインのiframeでもふつうに中身にアクセスできるのを見つけたのでXMLHttpRequestもクロスドメイン制約がなかったりするかなーと思って試してみたら、いちおう異なるドメインのページも読み込めました。

chrome-resource://にファイルを作る

chromeのインストールされている場所のresources/InspectorにてきとうなHTMLファイルを置くと、そのファイルにchrome-resource://****.htmlでアクセスできるようになります。ファイルを置くだけで、再起動とかしなくてすぐ読めるようになります。

path

作ったHTMLファイルの中身はこんなかんじ。

<html>
<head>
<title>chrome-resource:// cross origin XMLHttpRequest</title>
<script type="text/javascript">
window.addEventListener( 'load', function() {
try {
  var r = new XMLHttpRequest();
  r.open("GET", "http://ido.nu/kuma/", false);
  r.send(null);
}catch(e){
  document.body.innerHTML = (r.responseText)
  
  alert(e);
}
}, false);
</script>
</head>
<body></body>
</html>

ページを開くとNETWORK_ERRというのが出てくるものの、ページの中身自体はちゃんと読み込めます。
chrome-resource XMLHttpRequest
ただchromeの設計として読めるのが正しいのか読めないのが正しいのかわかんないです。

さらに非同期にするとreadyStateが2で止まっちゃって中身が空で読み込めないので事実上役に立たないですけど….

safariとchromeの判別

chromeでも動くようになったと知ってoAutoPagerizeを新しくしにいったら、同じWebKitベースのSafariとchromeをどうやって判別するかの話が書かれていて、ちょうど自分も先日やっていたのでほかの方法を紹介。

navigator.vendor

Safariだとnavigator.vendorApple Computer, Inc.が入っています。ほかのブラウザはみんな空です。UAを見てるようなものなので、判別方法としてはおもしろくないです。

execScript()

chrome(というよりはV8)はIEとの互換性のためにグローバルにexecScript()という関数が登録されています(でもなんのために?)。Safariにはないのでこれの有無で判別が可能です。オリジナルのIEのexecScript()はスクリプトの言語を指定できるみたいですが、V8のやつはjavascriptしか評価してくれません。

program portalでのuuid登録を半自動でやるためのjavascript snippet

AdHoc配布はデバイスの登録が地味に大変だと書かれていたのをどこかで読んだので(たぶん2tch関連)、楽にすべくjavascriptのコードを書きました。全自動にするのは難しくてコード書くのよりも手作業にした方が早そうだったので半自動です。+ボタンは人間が押してワクを増やしてあげる必要があります。

Firebug consoleで実行して、手動で+を増やすと自動でデバイス名とuuidが埋まります。

window.setInterval( function () {
[
  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx03f2",
  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx10d4",
].map ( function (uuid, index) {
$('add_deviceNameList_' + index + '_').value = "dev" + "000".substr(0, 3 - String(index).length) + index;
$('add_deviceNumberList_' + index + '_').value = uuid;
} );
}, 100);

文字列からDOMの構築にかかる時間

tumblr経由でGreasemonkeyスクリプトのGM_xmlhttpRequestとかで取得したhtmlとかxmlをいじる場合はxpathだと遅いことが多いかも – さらさら宇宙忍法帖を読んで

というのと、実際の時間は2000ms vs 200msほどなんで、2秒待てる処理なら多少遅くたっていいじゃん、ってのもあるかもしれないです。いや2秒と0.2秒は体感できる違いだなぁ。。

体感でさすがに2秒もかかってない気がした(DOMの構築は同期的に行われるのでその間UIがフリーズするはずだけど、そういうストレスを感じてない)けどどれくらいかかっているのかも知らなかったので調べてみました。

実験

評価環境
2.16GHz/Core2Duo OSX10.4+Firefox3beta3
評価方法
AutoPagerize 0.23で文字列からDOMを構築しているcreateHTMLDocumentByStringの実行にかかっている時間をFirebugのconsole.time()+console.timeEnd()で測定

比較的こみいったHTMLになっているはてなブックマーク – 注目エントリーだと80msくらいでした。シンプルなHTMLのajax – Google 検索の場合10~15msくらい。

ついでにXPathのマッチングをしているところ、

        var page = getElementsByXPath(this.info.pageElement, htmlDoc)
        var url = this.getNextURL(this.info.nextLink, htmlDoc)

も測ってみました。はてなブックマークで3msでgoogle検索結果だと1msでした。XPathの評価はほとんど時間がかからないと思ってよさそう。FirefoxのXPathEvaluatorでもたしかにそんな感じのオーダーだった。

結論

DOMの構築はHTMLの複雑さにもよるけど10~100msのオーダーで終了する。AutoPagerizeは

document.createRange().createContextualFragment(str)

で文字列をDOMにしてるけど、元の記事ではinnerHTMLで変換したときの時間っぽいのでその差とかなのかも。

watchメソッドでオブジェクトが存在するようになるのを待ってから実行する

はじめwatchを全部waitと勘違いして書いてました。id:os0xさんご指摘ありがとうございました。

watchはIEにはないのでとりあえずFirefoxのはなしです。いつも断りなくFirefox前提で書いてますけど。

以前にscriptタグを動的に生成してページがレンダリングされてからjavascriptを読み込ませることで重たい処理をしていたりするjavascriptによるストレスを軽減するような細工をしたことがありました。

そのときに大変だったのは、動的にロードすると書いた順番でスクリプトのロードが完了するとは限らないので、依存関係のあるスクリプト間で同期をとる必要があったことでした。そのときはsetIntervalでポーリングして必要とするオブジェクトがそろったら実行、みたいなことを書きましたがFirefoxだとwatchを使ってスマートに書くことができます。

AutoPagerizeとMinibufferとLDRizeのロード順序をどうすればいいかわからない、みたいなのも、これで解決!てきとうに書いても大丈夫なようにできるよ、と思ったけどそこはGreasemonkeyのsandboxのXPCNativeWrapperの制約でwindow.watchが利用できなくてだめで、意気消沈してこのエントリもお蔵入りしてたのですがwatchつかうと便利なときがあるっていう意味で書いておきます。

MinibufferでAutoPagerizeを参照したいときは


if ( window.AutoPagerize ) {
    // do something
} else {
  window.watch( "AutoPagerize", function (name, oldValue, newValue) {
      // do something
      return newValue;
  } );
}

としておけばポーリングするオーバーヘッドで損することなくAutoPagerizeが設定されたときに do something することができます。実際のGreasemonkeyではwindow.watchがNGなのでだめなんですけどねー。

さらにほんとはwindow.AutoPagerizeが設定されるタイミングとwindow.AutoPagerize.addFilterが設定されるタイミングが違ったりするので、window.AutoPagrizeが設定されたタイミングでさらにwindow.AutoPagerize.addFilterが設定されるのを待つ、みたいなコードが必要になります。

ふつうに書くとひどいコードになるわけですが、これは高階関数にすればすっきりするに違いないと書いてみたらうまく書けたのでうれしかった!のを掘り出してきたけど、今みたら読みたくないです。reduceRightまで入っているのでfx2だと動きません。

function executeOnReadly(parentObject, props, f) {
    var fn = props.reduceRight( function (callback, propname) {
        return function (parentObject) {
            if ( parentObject[propname] ) {
                var newValue = parentObject[propname];
                callback(newValue);
            } else {
                {
                    parentObject.watch( propname, function ( name, oldValue, newValue ) {
                        callback(newValue);
                        unwatch(parentObject, name);
                        return newValue;
                    } );
                }
            }

        };
    }, f);
    fn(parentObject);
}

これを

executeOnReadly(window, ['AutoPagerize', 'addFilter'], function () {console.log("hello")});

と呼び出すと、呼び出したタイミングでwindow.AutoPagerize.addFilterが存在していればそのタイミングでhelloが表示されるし、設定されていなければ設定されたタイミングでhelloが表示される、というはずでしたがGreasemonkey特有の事情でだめでした。

Greasemonkeyじゃないときだと使えるわけですが、watchが必要なのでウェブページで使うわけにもいかないのでFirefox限定だけれどGreasemonkeyはNGなのでFirefox extensionくらいしか使う場面がないので使い道がなくて悲しいです。

ふつうの非同期関数をDeferredにする方法

2007.12.7 追記

MochiKitのドキュメントではないですがTwisted ドキュメント: Deferred の作り方が参考になります。

にわかDeferred信者になったもののMochiKit.Async.DeferredではXMLHttpRequestしか提供していないのでGreasemonkeyの中でGM_xmlhttpRequestや、拡張のコンテキストでnsIChannel#asyncOpenで使おうと思うととたんに困るのでした。DeferredはDeferred管理でない非同期の関数と一緒に使うととたんに破綻します。

GM_xmlhttpRequestはインターフェイスは似ているので(中身は同じなので当然)Curiosity is bliss: XMLHttpRequest – Security Bypassを使ってMochiKit.Async.Deferred.getXMLHttpRequestが返すオブジェクトを差し替えてなんとかしのげましたがnsIChannel#asyncOpenは誰も面倒見てくれません。

あきらめてDeferredの中を読んで自前のDeferredオブジェクトを作る方法を調べました。かんたんでした。

はじめにMochiKit.Async.Deferred.doXHRの中を見てみると

    /** @id MochiKit.Async.sendXMLHttpRequest */
    sendXMLHttpRequest: function (req, /* optional */ sendContent) {
        if (typeof(sendContent) == "undefined" || sendContent === null) {
            sendContent = "";
        }

        var m = MochiKit.Base;
        var self = MochiKit.Async;
        var d = new self.Deferred(m.partial(self._xhr_canceller, req));

        try {
            req.onreadystatechange = m.bind(self._xhr_onreadystatechange,
                req, d);
            req.send(sendContent);

Deferredオブジェクトを作って(コンストラクタの引数はキャンセラ)、それを非同期処理の終了時に呼び出すだけになってます。

    _xhr_onreadystatechange: function (d) {
        // MochiKit.Logging.logDebug('this.readyState', this.readyState);
        var m = MochiKit.Base;
        if (this.readyState == 4) {
            // IE SUCKS
            try {
                this.onreadystatechange = null;
            } catch (e) {
                try {
                    this.onreadystatechange = m.noop;
                } catch (e) {
                }
            }
            var status = null;
            try {
                status = this.status;
                if (!status && m.isNotEmpty(this.responseText)) {
                    // 0 or undefined seems to mean cached or local
                    status = 304;
                }
            } catch (e) {
                // pass
                // MochiKit.Logging.logDebug('error getting status?', repr(items(e)));
            }
            // 200 is OK, 201 is CREATED, 204 is NO CONTENT
            // 304 is NOT MODIFIED, 1223 is apparently a bug in IE
            if (status == 200 || status == 201 || status == 204 ||
                    status == 304 || status == 1223) {
                d.callback(this);
            } else {

かんたん。IEへの憎悪がみなぎっているコードです。

演習: setTimeoutをラップする

いきなりnsIChannel#asyncOpenをラップするのもつらいのでsetTimeoutで練習。

function myDeferredTimeout(msec) {
    var timerid = null;
    var cancelTimeout = function () {
        window.clearTimeout(timerid);
        timerid = null;
    };
    var d = new MochiKit.Async.Deferred( cancelTimeout );

    timerid = window.setTimeout( function () {
        d.callback(msec);
        cancelTimeout();
    }, msec);
    return d;
}

window.onload = function () {
    var d = myDeferredTimeout(600).addCallback( function (v) {
        console.log("MyDeferredTimeout fired.", v);
    } ).addErrback( function (v) {
        console.log("MyDeferredTimeout canceled.", v);
    } );
//    d.cancel();
}

これで600ms経ってからcallbackが呼ばれてMyDeferredTimeout fired.が表示されます。d.callbackを呼び出すときに与えた引数がaddCallbackした関数の引数に渡ってきます。

d.cancel()した場合はnew Deferred(canceler)で与えたcanclerが呼び出されます。setTimeoutの場合はまじめにclearTimeoutを呼んでキャンセルできるのでそうしてます。キャンセルできないやつは、結果が帰ったてきてもそれを無視するような実装にしてあげればokです(doXHRがそうなってました)。

これでなんでもDeferredにしちゃえるようになりました。
ぜひ演習としてGM_xmlhttpRequestのDeferredバージョンを作ってみてください(ほしい)!

実践: nsIChannel#asyncOpenをラップする

PostFormMultipart.jsをラップしてDeferredで使えるようにします。

cancelerは指定しなくてもcancelを呼んだ時点でDeferredのほうでcallbackが呼ばれないように都合つけてくれるようです。cancelするための処理を書くのではなくて、cancelされたときにやったほうがいいことを書くかんじ。

MochiKit.Async – manage asynchronous tasks にはcancelされたときにまず呼ばれると書いてあるだけでした。

The creator of the Deferred may specify a canceller. The canceller is a function that will be called if Deferred.prototype.cancel is called before the Deferred fires.

で、ラップするのはsetTimeout同様かんたんで

function defferedAsyncRequest(method, uri, opts, params) {
    var d = new MochiKit.Async.Deferred(  );

    opts = opts || {};
    opts.async = true;
    opts.onStopRequest = function (request, context, statusCode) {
        d.callback( [this, request, context, statusCode] );
    };

    var req = new HTTP.Request( uri, opts );
    req[method.toLowerCase()](params);

    return d;
}

だけで完成。呼ぶ側も

var filename = '/home/kuma/.zshrc';
var uri = 'http://ido.nu/kuma/echo/';

var d = defferedAsyncRequest( "post", uri, {multipart: true}, {
                id: 129,
                title: "dummy title",
                image_file: HTTP.Request.Util.open_file(filename)
        } ).addCallback( function (res) {
            trace("success", res);
        } ).addErrback( function (res) {
            trace("error", res);
        } );

これで呼べます。かんたんかんたん。