2012-07-17

ChromeOS SKKの開発: 開発環境編


このシリーズは今回で最終回の予定です。

ChromeOS向けの「IME API」を使ってSKKを作りました、という話のシリーズなのですが、こういうものを作るときに何が一番厄介かといって、やっぱりデバッグだろうと思うわけです。
IMEはデバッグをするのが非常に厄介なたぐいのソフトウェアです。うっかり変なことをすると完全に壊れて何もできなくなることすらあります。ChromeOSのIME APIでは、IMEは単なるChrome extensionなために完全にsandbox化された環境で動作しますし、developer toolsもあるのでデバッグは相当楽なほうですが、それでもなかなか面倒な面もあります。
面倒さの一つには、実機での動作確認が面倒だということが挙げられると思います。ChromeOSでは、まだセルフホスティングの開発環境が整っていません。普通にたとえばWindowsやMacでChrome extensionを開発するなら、適当にJSファイルを書いてunpacked extensionとしてロードして手元で動かして見ることができます。ですが、ChromeOSではローカルファイルを編集するためのテキストエディタがありませんし(と思ったら、そういう拡張機能もあるようですが)、なんだかんだでまだまだ大変な面が強いわけです。
そういうわけで、今回はMacで開発をしたのですが、じゃあどうすればいいのでしょうか。Macのほうで拡張機能のパッケージ化を行い、適当なサーバにアップロードして、ChromeOSのほうでダウンロードして……などとやれば出来ますが、毎度それをやるのはあまり実際的でもありません。

といった状況を踏まえ、まず開発をするための実行環境を作ってみました。それがソースコード中のtestpageというディレクトリです。
こいつのやることは簡単です。まずmock.jsというJSを読み込みます。こいつはchrome.input.imeというオブジェクトをグローバルに追加して、setCompositionとかclearCompositionとかのメソッドを登録します。この関数の実行結果は、適当にHTMLで画面に表示させます。一方でキー入力も適当なeditableな要素のほうで受け付けておき、onkeydown/onkeyupを使って適当なイベントリスナにキーイベントを横流しできるようにしておきます。
そうしておいて、のこりの拡張機能で使うJSファイルをロードすると、ある程度は動作するようになるという仕組みです。実際に、前回の記事で書いたAppEngineのホストにくっつけて公開もしています→こちら

前々回の記事で、AJAX-IMEのようにページ内に寄生するIMEっぽいJSの話に軽く触れました。今回はこれと似たアプローチで、ただし、変換エンジンの処理を直接書き下すのではなく、IME API互換のレイヤを間に挟んでいる、という解釈ができるかと思います。
この手法はなかなかうまく行きました。ちょっとコードを書いたら、すぐ実際にキー入力してみてどう表示されるのか確認できる環境がある、というのはいいものです。すぐ確認できることによる全体的な開発の高速化もありますし、実際に動いているのを目にすることができるというのは、とくに開発初期においてモチベーションを高めるという意味もあると思います。わりとすぐにそこそこのものが開発できたのも、この方式のおかげというのがあるかなあと個人的には考えています。

まあ、実際には、APIのドキュメントをちゃんと読まずに実装したので、いざ実機にロードしてみたらちゃんと動かなかった、みたいなよくある失敗もあるし、JSやイベント処理まわりの無理解のために動かなかった、みたいなしょぼい問題もたくさん踏んでいるので、あんまり自慢するようなポイントではないような気もします。また、現状ではonKeyEventの返り値を見ていないので、一部の機能は全然動いていないように見えます。それに、動かして確認するといった単純な手法よりは、細かいコーナーケースではユニットテストをちゃんと書くほうが大事だったりします……このコードではテストを書いてませんが、IMEはコーナーケースばっかりなので本当はテストが大事です。
なのですが、個人的には、テスト環境を実装してみたら割と簡単に実装できて、これが思いの外便利だった、というハックがちょっと面白かったので、紹介してみた次第です。


photo: http://www.flickr.com/photos/mjmyap/147909798/in/photostream/ by mjmyap

2012-07-11

SKK for ChromeOSの開発:辞書データの話


前回書いたように、ChromeOSで動作するSKKを開発しました。当初の予定を変更して、今回は辞書の話を書くことにします。

実は私は昔、AJAX-SKKというものを書いてみたことがあります。とはいえ、JavaScriptの練習用コードだったということもあっていろいろ不備があるわけですが、何といっても変換には、適当なcgiをでっち上げて変換サーバにしていました。今回は、そういうことはやめようと決意し、全部JavaScriptで動くことを目指しました。

SKKの辞書には、バリエーションがいくらかありますが、最大のL辞書は相当大きく、これをそのまま扱うのは簡単ではないように思えました。そこで当初の構想としては、処理にwebworkerを使うだとか、保存にはIndexedDBを使うだとか、そういう現代的なテクノロジーを活用することを目論んでいました。
目論んでいたんですが、結果的にはこういうのを使うのはやめてしまいました。実際に、そういうコードを書き初めていたのですが、完成前にふと思いついてものすごく単純な手法を試してみたところ、あっさり動作してしまったしパフォーマンス上もたいして問題ではないように見えたので、それで行くことにしました。
その手法というのは、SKKの辞書データを単純にJavaScriptオブジェクトに変換してしまい、全部オンメモリに持つ、というものです。また、JavaScriptオブジェクトはJSON.stringifyでJSONフォーマットにシリアライズし、FileSystem APIを使ってローカルなファイルに保存します。次回以降は、ローカルなファイルを読んでJSON.parseするだけで辞書データが復元できます。
SKK-JISYO.Lをダウンロードしてパーズと保存を全部組み合わせた場合、手元の最新のChromebook (Series-5)で試すと、数秒で処理が完了してしまいます(しかもほとんどの遅延は辞書のダウンロード時間な気がします)。JSON.parseでローカルから復旧する場合は1秒強、といったところでした。辞書データは、バックグラウンドページがロードされたとき(つまりログイン時)に一度だけロードすればよいので、この程度の性能なら特に問題ないように思います。ログイン時のもろもろの処理に紛れてしまうんじゃないでしょうか。
むかし、SCIM-SKKを開発したときは、SKK-JISYO.L全体をはじめにパーズすると無視できない遅延が考えられるため、いろいろ効率化を工夫した記憶があるのですが、ああいう細かい工夫はいったいなんだったのかなぁという思いの去来する出来事でした。最近のコンピュータも、最近のJSエンジンも、速いよね……。


さて、それなのにソースコードには「server」の文字が含まれます。これは何をしているのかというと、辞書ファイルのミラーリングと、簡単な前処理です。
SKKは歴史があるので、辞書の配信もいささか時代がかった方式になっています。ファイル名自体が.gzの拡張子を持っていて、Content-Type: x-gzipのレスポンスヘッダがあります。コンテント自体を圧縮して配信するなら、Content-Encodingを使うのがHTTP的には正しかろう、とは思いますがブラウザのバグだとか歴史的な事情を考えると、この方式はむしろ自然だと言えるでしょう。ただ、自然だといっても、このままではJavaScriptで.gzを伸長するはめになります。これは今ひとつ現実的ではない気がします。
それに、SKK辞書はEUC-JPでエンコードされているという問題があります。.gzを伸長しても得られるのはEUC文字列ですから、Unicodeに変換しなければならない。これをJSでやるのはさすがにアホくさい。
そういった事情があり、AppEngine側でSKKの辞書のミラーリングをすると同時に、もうちょっと楽に扱えるように前処理を施します。
AppEngineではscheduled taskで1日に1回、openlabの辞書の配信元に問い合わせます(現在はopenlabが停止しているのでスケジュールも止めています)。で、更新のあった辞書ファイルをダウンロードしてきたらzlibで伸長し、そのデータをblobstoreに保存します。
拡張機能のほうからデータを持ってくるときには、単にサーバに辞書ファイルを問い合わせます。すると、blobstoreに保存してあるデータをContent-Type: text/plain; charset=euc-jpで返します。文字コードの変換はChromeがやってくれるので、JSレベルでは気づかぬうちに扱いやすい普通のテキスト形式でデータが手に入る、というわけです。

ちなみに、辞書ファイルは誰でも取ってこれます。たとえば http://skk-dict-mirror.appspot.com/SKK-JISYO.S.gz にアクセスしてみてください。openlabが停止している今では、偶然ですがキャッシュとして使えるかもしれません。


ところで、この話を会社でしたところ、そんなものはXHRでなんとかなるのではないかという指摘を受けました。あんまり詳しくなかったのですが、XHRでは返ってきたレスポンスのContent-Typeを上書きすることはできます。なのでContent-Typeだけならそれでもいいのですが、今回の場合はContent-Encoding: gzipも足してあげないとChromeはレスポンスボディを解釈できません。XHRではほかのレスポンスヘッダはいじることができないようなので、これだけでは無理なのではないかと思いました。ただ、Chrome拡張機能のwebRequest APIを使えばレスポンスヘッダをいじれるため、組み合わせればAppEngineがなくても良かったかもしれません。気づいてからずっとopenlabが落ちているのでまだ試していないのですが、復旧したら試してみるのもいいかもしれませんね。

image: http://www.flickr.com/photos/62396887@N00/1405476175/

2012-07-08

ChromeOSで動作するSKKを作った

ChromeOSには標準でいろんな言語がサポートされています。言語サポートとひとくちにいってもいろいろあるわけですが、入力方法もそうしたサポートのひとつでしょう。日本語にはMozcが使われており、中国語や韓国語も各種のOSSライブラリが使われています。CJK以外のアジア諸語ではlibm17nが使われています。ラテン文字を使った言語についても、様々なキーボードレイアウトをサポートすることで入力に対処しています。
とはいっても、そういうのは全然完全じゃないわけです。Mozcだけでは日本語はサポートしきれません。中国語でも、主として本土の人向けのピンイン入力や、台湾でよく使われるzhuyin入力や、Canjie(部首変換)はサポートされていますが、boshiamyのようなプロプラエタリなインプットメソッドは導入できません。
そういったわけで、ChromeOS用にインプットメソッド拡張機能APIというものが提案されており、これが現在、バージョン21(devチャンネル)でのみ利用できる状態になっています。このAPIを使えば、たんにJavaScriptで拡張機能を書くだけでインプットメソッドを導入することができます。
というわけで、昔とった杵柄、という塩梅でSKKを書いてみた、というのがこちらになります。ソースコードはgithubに公開しています。そういえばライセンスとかreadmeとか、ちゃんと書かないとな……。
折角なので、苦労話とか実装の話を適当に何回かにわけて書こうかと思います。ところでSKKといえば、openlab.jpがもうかれこれ2週間以上、ストップしているように思われ、何があったのか不安な面が強いのですが、辞書については諸般の事情から、てきとうなappengineインスタンスを作ってそちらでサーブしていますので、この拡張機能の辞書のダウンロードについては心配ありません。

さて、手始めにIME拡張機能とはいったいなんなのか、どう動くのか、という話を書いてみたいと思います。
はじめに、おそらく誤解をしている人も多いと思うので書いておきましょう。IME拡張機能は、よくあるAJAX-IMEのブックマークレットなどのようなものとは根本的に異なります。どちらが良い悪いというのではないです。ブックマークレットだけでブラウザ内で動くというのはとても優れたハックだし、すごい話だなと思うのです。ですがその場合、インプットメソッドはウェブのコンテントエリア内でしか動きません。
ChromeOSの場合はnothing but webですから、ウェブ以外に何があるのだろうと疑問に思うかもしれませんが、実際にはたくさんの入力エリアがあります。たとえばomniboxや検索ボックス。ネットワーク設定用のダイアログ。アプリケーションリストのポップアップ。などなど、実は意外とあるのですね。また、候補ウィンドウの描画などを、ホストするウェブページと独立にうまく描画するのは、そう単純な話でもないのです。
ChromeOSでは、内部的には現在、ibusというIMEフレームワークが使われています。ibusはマルチプロセスなIPCベースのフレームワークで、各IMEの入力エンジンはibus-daemonと別なプロセスで動作します。Chromeでキーイベントが発生すると、まずはibus-daemonを介して入力エンジンへキーイベントが届けられ、処理結果がまたIPCメッセージとしてChromeに戻ってくるという構造になっています。ちなみにChromeOSでは候補ウィンドウはibusではなくChrome自身が描画するという実装になっています。
図にするとこんな感じかな。
表現としてはわかりづらいですが、composition(未確定文字列)とかcandidates(変換候補)はブラウザプロセス内で表示/描画されます。
一方、AJAX-IMEブックマークレットなどの構成では、ウェブページ内にIMEが寄生します。てことで、ざっくり書くとこんな感じ?
表記上、わけられていますが、レンダラプロセス内にあるJSエンジン(v8)のなかにIMEの処理がロードされ、ブラウザプロセスから届けられたキーイベントを横取りして処理するというイメージ。この場合、すべての処理はレンダラプロセス内にあり、compositionやcandidatesの描画もレンダラを使って行ないます。htmlを使って自由に描画できるという面ではいいのかもしれないけど、ホストとなるウェブページの描画事情に応じて様々な厄介な問題が引き起こされがちです。あともちろん、omniboxなどウェブページの外側にあるモノにはアクセスできません。
IME-APIでは拡張機能を使うので、やはりIMEの処理がレンダラプロセスの中にロードされます。と書くと後者と似ているように聞こえるかもしれませんが、やはり異なります。なぜかというと、IMEの処理がロードされるレンダラプロセスは、ユーザがいま見て入力しようとしているウェブページのものとは異なるものだからです。
Chromeの拡張機能には「バックグラウンドページ」というものがいます。これは、その拡張機能が有効になっているあいだは、ずっと起動されているレンダラプロセスで、様々な処理を受け持つことができます(ちなみについ最近、こないだのGoogle I/Oにて、バックグラウンドページも必要なあいだだけ起動してあとは止めておく、という機能が紹介されています)。IME-APIを使った拡張機能でも、このバックグラウンドページにIMEの処理を持たせることが想定されています。この場合、バックグラウンドページのコンテンツというのは存在せず、ユーザが実際に目にすることはありません。どちらかといえば、さまざまな拡張機能APIにアクセスするJSコードのデーモン的な位置付けになっていると言えると思います。
ここに来ると、IME-APIというのは既存のibusに相当するレイヤとみなすことができます。ibusはd-busをベースにしたIPCフレームワークを使っていますが、IME-APIではChromeがブラウザプロセスとレンダラプロセスのあいだで行われるIPCレイヤによってibusと同じようなことを代替するという試みとなります。
このため、compositionやcandidatesの描画は、ibusと同等にブラウザプロセス内で動作します。ブラウザから見た場合、ある変換エンジンが拡張機能を使ってJavaScriptで書かれているのか、それともibusクライアントになっているのか、というのは大きな差がないようになっているわけです。

実装上はどうなっているかというと、バックグラウンドページは、基本的にはいくつかの初期化処理をしたら後はなんにもしない状態にしておきます。初期化のなかには、chrome.input.ime.onKeyEventというイベントハンドラにaddListenerによってハンドラ関数を登録します。そうするとユーザがキーを打鍵すると、そのたびに登録したハンドラ関数が呼ばれます。関数のなかでは、setCompositionやcommitText、candidatesの設定などの関数を呼ぶことができ、IMEの状態を任意に変えることができます。起動時のonActivateや、フォーカスの入出にかかわるonFocus/onBlurも使えば便利であろうと思われます。
そんな感じで、あとはキーイベントハンドラを上手く処理するJSコードを書きさえすれば完成という次第です。
次では、開発プロセスに関係することを書こうかと思っています。