NaClでローカルディスクからファイルを読み込む方法

December 3rd, 2011

しばらく放置していたMilkyTrackerのNaClポートを少し更新してローカルディスクからファイルを読み込めるようにしてみました。

機能としては単純なものなのですが、これがなかなか一筋縄には行かず、ハマりにハマりまくったのでポイントを整理して晒しておきます。(JavaScriptは簡単のためにjQueryベースで書きます)

PostMessageは64kbが上限

まずアプローチとしてNaClは基本的にJavaScriptに許される範囲内のインターフェースしか持たないため、ローカルのファイルにアクセスするためにはHTML5で定義されているFileReaderを使うことが考えられます。JavaScriptで読み込んだらそれをBase64形式でNaClにPostMessageしてやればデータの受け渡しができます。コードはこんな感じです。

$("#file").change(function(ev) {
  var reader = new FileReader();
  reader.onloadend = function(e) {
    e.target.result.match(/.*base64,(.*)$/);
    module.postMessage("import:" + ev.target.files[0].name + "," + RegExp.$1);
  };
  reader.readAsDataURL(ev.target.files[0]);
};

しかし、ドキュメントに書かれていなかったので実装してから分かったのですが、PostMessageに渡すことの出来るデータサイズには64kbの制限が有り、これを越えるデータを渡すとエラーが発生します。MilkyTrackerの場合は読み込むデータソースが64kbに収まらない事のほうが多いと思いますので、その場合はデータを64kb単位で細切れにして送らなければいけません。いけてません。

createObjectURLで取得したアドレスをNaClから直接ダウンロード

FileReaderによる64kb分割送信大作戦は、そもそもバイナリーデータをbase64エンコードしている時点でデータサイズが膨らんでいる上に、分割してオーバーヘッドが発生しまくるという点であまり格好良くありません。そこでもっとスマートにできる方法を見つけました。

window.URL.createObjectURL を使います。このAPIにFileオブジェクトやBlobオブジェクトを渡すと、”blob:(URIエンコードされた一時的なローカルファイルを指すURI)”というようなURIが取得できます。このURIは、例えば画像ファイルのURIを取得してJavaScriptでimgタグのsrcにセットするだけで画像が表示されたりするもので、ブラウザ内ではサーバー上のファイルを指すURIと同じように使うことが出来ます。このURIをNaClのpp::URLLoaderで(NaCl内に)ダウンロードします。コードはこんな感じです。(Chromeではwindow.webkitURL.createObjectURLになる)

$("#file").change(function(ev) {
  var url = window.webkitURL.createObjectURL(ev.target.files[0]);
  module.postMessage("import_url:" + ev.target.files[0].name + "," + url);
};

NaCl側のコードは長いので割愛させて頂きますが、NaCl-Quakeのソースが参考になります。これはサーバー上からゲームアセットをダウンロードするためにURLLoaderを使用していますが、同じ手法でcreateObjectURLによって生成されたURIを使用してローカルファイルをダウンロード出来ます。

これでバイナリデータを直接NaCl内に取り込むことが出きるようになりました。PostMessageはコマンドとURIを受け渡すためだけに使われます。恐らく現時点ではこれが一番よい方法かとおもいます。

JavaScriptからファイル選択ボックスを表示したい

あと一点だけ触れておきたいのが「ファイルを開く」ダイアログボックスをどうやって表示するかです。実はこれに一番ハマりました。結論を先に言っておくと、NaClには現状そのようなインターフェースはありませんし、NaClからJavaScriptのメッセージングを駆使してJavaScriptに表示させることも出来ませんでした。

HTML/JavaScriptのレイヤではファイル選択ボックスはHTMLタグで<input type=”file”/>で定義されたボタンをユーザーがクリックした時に表示されます。これはかなり厳しい制約で、例えばJavaScriptから$(“#file”).click();と呼び出してユーザーのクリックを偽装しようとしてもダメです。このファイル選択ボックスはJavaScriptがローカルディスクのファイルにアクセスする唯一の方法ですので、自由にスクリプトからユーザーに表示出来るとセキュリティ的によろしくないということでしょう。

しかし、このブラウザ標準のボタンは装飾が地味で、必ずしもページのデザインに馴染まないということで、いろいろな裏技が開発されています。

  1. invisibleなinput要素を見せたいデザインのdiv要素に重ねる
  2. $(“#file”).show().focus().click().hide();

この二つ目に注目です。先ほどJavaScriptからinputオブジェクトのclickイベントを偽装することは出来ないと書きましたが、オブジェクトが表示状態に有り、かつフォーカスを持っている状態で、かつ「JavaScriptで捕捉した他のオブジェクトのclickイベント内」であれば可能みたいです。(自分で試しただけなので正確ではないかもしれません)

これを使えるのではないかと期待しましたが残念ながら無理でした。「他のオブジェクトのclickイベント内」というのがポイントで、あくまでこのトリックはボタンの外見をカスタマイズするためにわざわざブラウザに実装されたもののようで、例えばsetTimeoutやmousemoveやonload等他のハンドラで呼び出してもファイル選択ボックスは表示されません。

念のため、NaClオブジェクトのmessageハンドラから呼び出してみましたが、やっぱりダメでした。

セキュリティのためとはいえ、描画を完全にNaCl内で完結するMilkyTrackerのようなアプリの場合、NaClコンテナ内でのイベント(例えば[LOAD]ボタンをクリック)をトリガーとしてファイル選択ボックスを表示することが出来ないと言うのは悲しすぎます。ファイルを選択するだけであればブラウザに対するドラッグ&ドロップは一つの打開策ではありますが、やはりファイルを選ばせるというUIは捨てがたいです。

NaCl内でもマウスがクリックされた時の特別なハンドラが提供され、そこからであればJavaScriptと同じようにファイル選択ボックスを表示できる、というロジックであれば現状のセキュリティレベルを落とすことなくNaClアプリをもっとええ感じに出きそう。

1の方法をNaClの埋め込みオブジェクト上でやるというのはまだ試してないけどひょっとしたらうまく行くのかも。どなたか他によい方法があればご教示下さいませ。

Leave a Reply

*