WebAssembly でも feature  detection したい
Tags: webassembly
Published: 2020-03-30 01:18:49

## TL;DR;

[wasm-feature-detect](https://github.com/GoogleChromeLabs/wasm-feature-detect) を使えば、ブラウザのサポートしている機能を確認できます。

~~~js
import { simd } from "https://unpkg.com/wasm-feature-detect?module";

simd().then(simdSupported => {
  if (simdSupported) {
    /* SIMD support */
  } else {
    /* No SIMD support */
  }
});
~~~

## WebAssembly でも feature detection 必要？

必要です。2019年末の時点で、モダンブラウザと呼ばれるブラウザは WebAssembly の Minimal Viable Product(MVP) と呼ばれる機能をサポートしています。c.f. [caniuse.com](https://caniuse.com/#feat=wasm)

MVPがサポートされてのち、仕様の拡充は進められています。どのような仕様が提案されているかは、（2019年夏の情報ではありますが）Kabuku さんの「[夏休みだョ！WebAssembly Proposal 全員集合！！](https://www.kabuku.co.jp/developers/wasm-proposal)」で概観できます。

* SIMD 命令
* Thread
* 多値関数
* メモリの一括アクセス、変更

これらはいくつかのランタイムでサポートされています。別の言い方をすれば、サポートされていないランタイムも存在します。例えば [Thread は Chromium ではデフォルトで利用できるのに対し、他のエンジンでは実装中です](https://www.chromestatus.com/feature/5724132452859904)。

Web ではいつものことですが、WebAssembly を利用する場合、開発者は次のどちらかを迫られます：

* サポートするブラウザのセットを定め、その上で利用できる機能のみを使う
* Progressive enhancement を行う / advanced な機能が使える環境では、その機能を利用する

前者を考えると、MVP のみを利用するというのが良さそうです。

ここでは後者について考えます。Progressive enhancement を行うには、そのブラウザでどの機能が使えるかどうかを調査する必要があります。

これは JS ではおなじみの作業です。例えば Native Filesystem API をサポートしているかどうかは、次のように API で代表的な関数が存在するかどうかを調査することでわかります：

~~~JavaScript
if(window.chooseFileSystemEntries != null){
// サポートしてる
}else{
// サポートしてない
}
~~~

WebAssembly ではこの手法をそのまま使えません。少し変化が必要です。

## 確認するのは operator の有無

JS の場合確認していたのは関数の有無でした。これは API は [Navigatorオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Navigator)や[Documentオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Document)などの属性値や、global の名前空間のオブジェクトとして露出するからでした。

WebAssembly の機能は、そのように JS の空間には露出していません。サポートの有無を確認するには、wasm ファイルをインスタンス化し、実行する必要があります。

もしブラウザがある機能をサポートしているならば、その機能に定義されているオペレーターがサポートされています。またサポートされていないオペレーターを評価すると、トラップが発生します。このトラップは JavaScript では例外としてハンドルできます。つまり次のようなコードで、サポートを有無を判定できます。

~~~JavaScript
function isXXXSupported(){
  try{
    // WASM モジュールのインスタンス化と実行
    return true;
  }catch(e){
    // 例外がきたらfalseを返す
    return false;
  }
}
~~~

例えば SIMD の場合、次のようなモジュールを呼び出すことで、サポートの有無を調べられます。[i16.x8.splat は数値から v128 を作る命令です](https://github.com/stoklund/portable-simd/blob/master/portable-simd.md#create-vector-with-identical-lanes)。

~~~LISP
(module 
  (func
    i32.const 0
    i16x8.splat
    drop
  )
)
~~~

## 手で WASM を書くのは面倒

面倒ですね。そこでライブラリを利用しましょう。[wasm-feature-detect](https://github.com/GoogleChromeLabs/wasm-feature-detect) というライブラリは、上記の方式を（より賢く）実装しています。これを使うことで、次の機能の有無を調査できます：

* BigInt
* バルクメモリアクセス
* 例外
* 多値関数
* ミュータブルなグローバル変数
* レファレンス型
* floatからintへのトラップなしでの変換
* 符号の変換
* SIMD（固定長）
* 末尾呼び出し
* スレッド

このライブラリは ES モジュールとして実装されています。またそれぞれの機能の調査は、対応する関数を呼び出すことで行えます。例えば SIMD サポートの有無は次のように `simd` 関数を呼び出すことで確認できます。

~~~javascript
import { simd } from "https://unpkg.com/wasm-feature-detect?module";

simd().then(simdSupported => {
  if(simdSupported){
   //  SIMD がサポートされている
  }else{
   // サポートされていない
  }
});
~~~

npm にも登録されています：

~~~
% npm install -g wasm-feature-detect
~~~

## まとめ：feature detection してどうするの？

そのブラウザで利用できる機能がわかります。それに合わせて、機能を利用するビルドをロードするか、それとも MVP だけを利用したビルドをロードするかを決めます。

フォールバックを持っておくことは、progressive enhancement の文脈では大切なので、その方式で WASM を利用するなら、MVPで作ったものと、全部いるの 2 つのビルドは作っておかないといけないということになるでしょう。