asm.jsコードの構成
asm.jsコードの記述はそれほど難しくありません. 簡単な例を示します.
asm.js関数宣言
asm.js関数は一般のfunction定義と同じように行います. この関数がasm.jsモジュールを生成します. function名は何でも良く, 無名functionとして即時実行しても構いません. また引数の名称についても制限はなく, 処理の内容に応じて適宜省略することが出来ます.
use asm宣言
先頭で"use asm";
を指定するとfunction全体がasm.jsコードとして解釈されます.
インポート宣言
使用する組み込み関数(Math.xxx)や呼び出したい外部関数をここにリストアップしていきます.
共有変数宣言
関数共通に利用したい変数をここにリストアップします. なお, 演算子を使った初期化は出来ません.
関数定義
処理を行うための関数を記述していきます.
外部への公開
記述した関数のうち外部に公開したいものを連想配列形式にまとめ, returnします.
asm.jsモジュールの呼び出し
asm.jsモジュールを定義した関数を呼び出すと, 上の(6)で生成した関数セットが返されるので, その中身を実行します.
asm.jsコードは見た目上通常のfunction宣言と区別がなく, script要素のどこに記述しても構いませんが, その構成は厳密に守る必要があります.
この特徴故にコンパイルエラーをコード上で見抜き難いという欠点があります.
asm.js関数の引数の意味
asm.js関数として定義したfunctionは最大3つの引数をとります. asm.jsモジュールを使う場合適宜引数を渡します. それらの役割は次のとおりです.
第一引数「stdin
」
asm.jsモジュールを利用するコンテキストを渡します. コンテキストとはスクリプトの実行環境を表し, 通常は変数self
がこれに相当します. 変数self
には通常windowオブジェクトが設定されているのですが, Workerを利用している時などはこの限りではありません. 従って引数にはwindowではなくself
を渡すようにしましょう.
第二引数「foreign
」
asm.jsモジュールから外部の処理を呼び出したい際に, 当該のfunctionをオブジェクト形式で渡します. asm.jsモジュールが内部で呼び出し可能な関数には大きな制約がありますが, foreign
を介せば任意の関数を呼び出すことが可能です. もちろんasm.jsモジュールで作成した関数を渡すことも出来ます.
第三引数「heap
」
asm.jsモジュール内部で利用するメモリ領域をArrayBufferオブジェクトとして渡します. asm.jsモジュール内部では配列の生成はおろか, メモリの追加確保すら出来ません. 従って処理を行うのに十分なサイズのArrayBufferオブジェクトを渡すようにします. なお, 指定可能なArrayBufferオブジェクトのサイズには制約があり, 最小単位を0x10000
(64kB)とし, 以降2の累乗倍のサイズを指定するようにします.
なお第一引数にコンテキストオブジェクトに似たオブジェクトは渡さないでください. JavaScript文法としては正しくとも, asm.jsとしてリンク失敗となり通常のJavaScriptコードとしてフォールバック実行される可能性があります. .
このようにasm.jsを使ったコードを実行した際, asm.jsコードとして解釈できない, もしくは実行不能な場合, ブラウザは通常のJavaScriptコードとして実行出来ないか再試行します. この失敗は通常エラーとしては外部に通知されず(つまりtry-catchできない), 開発ツールのコンソールにのみその旨のメッセージが出力されるため, 問題発生に気付きにくい特徴があります. また, スクリプトの実行速度が急に遅くなった場合はコンパイルエラーが発生していないかを確認しましょう.
"use asm"
宣言
function定義の先頭に"use asm";
と記述すると, JavaScriptインタープリタはこのfunctionをasm.jsコードと解釈し, 事前コンパイルを試みます. なお, 先頭行以外に記述した場合, コンパイルエラーとなりfunctionはフォールバック実行されます.
asm.jsでコンパイル可能なコードは"use asm";
を無効化してもJavaScriptとしては正しいコードとなります. asm.jsコードを手組みする場合は, "use asm";
の有効・無効を切り替え, パフォーマンスにどれだけの差が現れるか確認してください.
インポート宣言
asm.jsモジュール内部で利用する関数を宣言していきます. 主にfunctionの第一, 第二引数で指定したオブジェクトから必要な関数を変数にコピーします. なお, 第一引数からコピーできる関数には制限があり, stdin.Math
に属する数値関数(の一部)である必要があります. なお, asm.jsモジュールはfunctionスコープ外の変数にアクセスすることは出来ません . 従って, windowオブジェクトなどのグローバル変数を直接参照することは出来ません. またMath関数であってもインポートできないものがあります. なお, var
宣言の代わりにconst
を用いることも出来ます.
頻繁にインポートされるMath関数
Math関数のうち, 次の2つは特別な役割が与えられています.
stdin.Math.fround
float(単精度浮動小数点)型の変数を宣言するために使われます.
stdin.Math.imul
int型同士の掛け算を行うために使われます. *演算子によるint変数同士の掛け算は行えません.
この他stdin.Math.floor
などが型変換によく用いられます. 従って, asm.jsコードのテンプレートを作成する場合はこれらの関数をインポートするように記述しておくとよいでしょう.
共有変数・定数宣言
内部で共有する変数(var
)や定数(const
)を定義します. なお, 変数の初期化には数値リテラルの指定のみ が行え, 演算子や制御構文を活用した複雑な値の指定はできません. またheapに対する型付き配列(ArrayBufferView)の生成は共有変数宣言部でのみ許されています. なお詳しい初期化処理が必要な場合は別途初期化用の関数を定義します.
ArrayBufferViewとして利用できるものは[Int/Uint][8/16/32]ArrayもしくはFloat[32/64]Arrayのいずれかです. Uint8ClampedArray, DataViewオブジェクトを指定することは出来ません.
関数定義
asm.jsモジュールが内部で実行するfunctionを定義します. 引数を受けるfunction場合, 引数を省略することは出来ません. また引数と戻り値については明示的に型を指定する必要があります. つまり, 引数の内容に応じてfunctionの処理の内容を変化させる(override/overloadする)ことは出来ません.
ローカル変数を定義することも出来ます. この場合, 共有変数の定義を同じ記法で行います(但しconst
による定数宣言は利用できません ). また, 変数の宣言は具体的な処理の前に行う必要があります (変数定義の巻き上げ解釈は為されません). なお, 使えるものは数値のみでありそれ以外は組み込みオブジェクトであっても使えません.
functionから別のfunctionを呼び出すことも出来ます. 但しこの場合, パラメータを渡す側と受ける側の変数の型, 数が一致している必要があります.
関数内で利用できない構文
functionのネスト(functionをfunction内部で定義)は出来ません. これは関数内部でFunctionオブジェクトを利用できないのと同義です.
この場合, サブ処理も独立した関数として定義します.
この他, 制御構文についても利用する上で細かい部分で制約があります.
モジュールの初期化処理の定義
asm.jsモジュールの初期化が必要な場合は, init関数を定義しその中で初期化処理を記述します.
ヒープ領域のサイズ指定
処理に最低限必要となるヒープ領域のサイズが判っている場合は, 次のように記述することでasm.jsモジュール生成時にheap領域のサイズが足りなかった際にメッセージを表示させることが出来ます.
この操作を行うfunctionを実際に呼び出す必要はありません. 定義しておくだけで有効になります.
外部への公開
asm.jsモジュールの生成処理が完了したら, その内容をオブジェクトにまとめreturnします.
asm.jsモジュールの呼び出し
asm.js関数を実行すると, asm.jsモジュールが生成されます. モジュールを使う側からは一般のfunctionオブジェクトと同じように扱えます. 但しその構成上パラメータとしては数値(及びそれに準じたもの)しか正しい内容を渡せません.
asm.jsでのパラメータ型の構造上, 数値以外のデータは一部を除き0
に読み替えられます. なお, 数値文字列についてはコードからも判るように数値データとして扱われます.
heap領域を使う場合は, asm.jsモジュール呼び出し側でArrayBufferオブジェクトを生成し, その内容をasm.js関数に渡します. 以後, asm.jsモジュールとはこのArrayBufferを介して数値データの受け渡しを行います.
heap領域を介したデータの授受
ヒープサイズの算出
前もって確保すべきヒープ領域のサイズがわからない場合は, 渡すデータが64kBの2n 倍に収まるような0以上の最小のnを求めます.
asm.jsを実装する場所
asm.js関数を定義する場所は原則どこでも構いません. しかし, 工夫することでより使い勝手の良いものとなります.
asm.jsモジュールを非同期的に読み込む
asm.jsを使ったコードを手書きする場合はそれほど気になりませんが, 機械的に出力したものを同期的に読み込むことはWEBページの応答性を著しく阻害します. この場合, スクリプトを非同期的に読み込むことを検討します.
スクリプトを非同期的に読み込むにはscript要素を生成しDOMに挿入します.
この他にdefer属性を使ったロード速度の改善もありますが, 読み込んだライブラリの内容によっては効果が薄いものと考えられます.
Workerを使ってasm.jsモジュールを別スレッドで実行する
asm.jsモジュールにはその役割上重い処理をさせがちですが, このような処理はWorkerを用いてブラウザが動作しているメイン処理と切り離すようにするとよいでしょう. 簡単な例を示します.
Worker内部の処理はメイン処理とは別のスレッドで動作するため, messageイベントを介して非同期的にデータをやりとりします.
asm.jsコードの実装例
以下は画像にフィルタ効果(セピア化)をかける処理をasm.jsで実装したものです.
処理結果
フィルタ効果を掛けたcanvas要素
canvas要素にはグラフィックを描くためのAPIが備わっており, その中のgetImageDataを実行すると画像のピクセルデータを型付き配列(Uint8ClampedArray)オブジェクトとして取得できます. 従って, この内容をasm.jsモジュール管理下のheap領域に転写し, 処理を行った後でcanvas要素に書き戻せば画像の加工処理が完了したことになります.