Electron
WebAssembly

ElectronでWebAssemblyを試してみる際の詰まりポイント〜Fetch APIについて〜

(WebAssemblyというよりも)JavaScript初心者向け記事です

はじめに

モダンなブラウザでは既に対応がされていて,ネイティブなライブラリをクロスプラットフォームで動作させる一つの手段となったWebAssembly.
これをElectronで使ってみようとした際に詰まってしまったこと,またそのことへの対処法を載せたいと思います.

なぜElectronでWebAssembly?

ElectronはNode.jsのランタイムを使用しているため,WebAssemblyなどの手段を使用しなくてもNode Integrationなどを使用しNAN(Native Abstractions for Node.js)で作成したネイティブ拡張を使えば,WebAssemblyなどで可能になるネイティブライブラリの運用が同じく行うことが出来ます.
しかしNANを使いこなすための学習コストが低くはないこと,Node.jsで使うためだけのラップになるため資産の使い回しが行いづらいなどのデメリットがあるため,自分は導入を見送っていました(もちろんそのデメリット以上のメリットがNANにあることも事実ですが).

そのため一度書いてしまえば基本的にはブラウザでも,Electronを使用してオフラインローカルのデスクトップアプリケーションであってもそのまま使えるWebAssemblyがアプリケーションを作っていくうえで最適と感じ,ElectronでWebAssemblyを使用してみようとなりました.

ElectronでWebAssemblyを使ってみようとすると何が起こるか

以下の検証は

  • macOSX 10.12.6
  • Electron 1.7.9
  • Node 8.5.0

の条件下で行ったものです.

ここからが本題となります.
Electronは要は一種のブラウザのようなものなので,ローカルのページを表示する他,Web上のページも表示が可能です.
ここで生じる問題は,ローカルのページでWebAssemblyを使用しようとする際のものです.

生じる問題というのはfile schemeが使えないと言った点です.実際どんなものか見てみましょう.

まずWebAssemblyを使用するためwasmファイルをfetch APIなどを使って読み込むと思います.

return new Promise((resolve, reject) => {
    fetch('./wasm/myawesomelibrary.wasm')
    .then(response => {
        if (!response.ok) {
            throw Error(response.statusText)
        }
        return response.arrayBuffer()
    })
    .then(buffer => new Uint8Array(buffer)) // 以下読み込み

こんな風に.

Webサイト上で行うとドメインなどをまたいだりしなければ基本的には正常に動作するコードです.
しかしElectronのBrowserWindowで表示したページで行うと正常に動きません.

吐き出されるエラーは

Fetch API cannot load file:///Path/To/Your/Awesome/Library.wasm. URL scheme "file" is not supported.

こんなものになっていると思います.
注目してほしいのが,このURL scheme "file" is not supported.の一文.
そう,Fetch APIはfile schemeに対応していないのです...(Electronの問題じゃないじゃん!って言うツッコミ飛んできそう)

こうなるとブラウザ向けに作っていたWebAssemblyのライブラリが使いまわしできません,さてどのように解決しましょうか,ということを次の章でご紹介します.

問題解決のために行うこと

上記のfile scheme問題解決のための手段は2つあり,

1点目はfile schemeを使えるようにする(Electronで使用するFetch APIはセキュリティの観点から使えなくなっている?)
2点目はFetch APIを使わずにXMLHttpRequestで代用する

といったものです.

file schemeを使えるようにする

まずは元ネタ
electron/electron Document file scheme behavior for different request mechanisms #9474
こちらにあるように,仕様としてはChromeのものと同じためfile scheme自体は使用可能ではあるそうです.

そのため,Fetch APIを使用する前にRenderer Processで

const { webFrame } = require('electron')
webFrame.registerURLSchemeAsPrivileged('file')

のようにfile schemeを許可することでWebAssembly関係のコードを変えることなくFetch APIでローカルのファイルを取ってこれるようになります.
ただしregisterURLSchemeAsPrivilegedは無条件にセキュリティポリシをバイパスして許可してしまうため,セキュリティに関しての注意が必要と考えられます.

Fetch APIをやめる

明確にfile schemeがFetch APIでサポートされていないのであれば,別の方式でfile schemeを使えばいいじゃんということで,XMLHttpRequestを使ってやってみようというアプローチです.

こちらが元ネタ
github/fetch Add support for file protocol (File URI Schema) #92

このコメントにあるように,(以下引用したコードを一部改変したもの)

function fetchLocal(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest
    xhr.onload = function() {
      resolve(new Response(xhr.responseText, {status: xhr.status}))
    }
    xhr.onerror = function() {
      reject(new TypeError('Local request failed'))
    }
    xhr.open('GET', url)
    xhr.responseType = 'arraybuffer'
    xhr.send(null)
  })
}

こんな感じでArrayBufferを受け取るようにして,

return new Promise((resolve, reject) => {
    fetchLocal('./wasm/myawesomelibrary.wasm') // 以下読み込み

とすることでfile schemeのセキュリティに関することを考慮することなく,ブラウザでもElectronでも動作するコードにすることが出来ます.

おわりに

現在WebページなどでWebAssemblyを使っていて,それをElectronを使ってローカルで使用しようとする際に引っかかるであろうポイントをご紹介しました.
Node.jsが必須なNANの力を借りずともネイティブの資産を使いまわすことの出来るWebAssemblyの登場,またWebAssemblyとElectronの組み合わせで更にWebとネイティブなデスクトップアプリとの垣根が低くなったように感じます.
動作などの制限こそありますが,更に利用者が増え多くの知見がたまればいいなと思っています.

また今回の検証に使用したプロジェクト,およびWebページに公開しているものとElectronで表示する際の比較は
yamachu/WorldForWeb
より確認ができますので,参考にどうぞ(ここではfile schemeを使えるようにする手法1を使用しています).

まとめ

  • Fetch APIを使用するとfile schemeの関係でwasmファイルが読み取れない
  • registerURLSchemeAsPrivilegedメソッドを使うことでセキュリティポリシをバイパスしてfile schemeを使えるように出来る
  • XMLHttpRequestを使えばWebページ,Electronの両方で使用できるようになる