第2回では、TypeScriptでWebAssemblyプログラムを開発できるAssemblyScriptを紹介します。差異こそありますがTypeScriptの構文を使ってコードを書けるので、フロントエンドに慣れた開発者にとってはWebAssemblyのための入りやすい選択肢と言えます。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
連載第1回では、WebAssemblyの概要を紹介し、さまざまなプログラミング言語の選択肢に触れました。静的な型付け言語であるC++やRustは、WebAssemblyのための開発言語として最適なのですが、学習コストの点で問題があります。できれば、フロントエンド開発でなじみ深いJavaScriptなどの言語を用いて、WebAssemblyアプリケーションを開発したいと思う人も少なくないはずです。
残念ながら、本稿作成時点において、JavaScriptのコードをWebAssemblyに直接コンパイルする手段は用意されていません。しかし、AltJS(JavaScriptを置き換える言語)であるTypeScriptには、WebAssembly開発のためのツールとしてAssemblyScriptがあります。このAssemblyScriptを用いることで、TypeScriptのコードをWebAssemblyにコンパイルできます。
TypeScriptはその名の通り「型の付いたJavaScript」です。型におおらかなJavaScriptに対して、型の指定を積極的に行うTypeScriptは、WebAssemblyに適しているのです。近年では、ReactやVueといったJavaScriptフレームワークでも利用可能になり、数あるAltJSの代表格といえます。
この場合、JavaScriptの利用は従来の用法(モジュールの読み込み、DOMの構築、操作など)の範囲内に限定し、高速な処理が要求される部分でTypeScriptによるWebAssemblyを導入するといった組み合わせが考えられます。
JavaScriptの習得者にとって、TypeScriptの学習コストはC++やRustほど高くはないと思われるので、有効な選択肢になるでしょう。今回は、このAssemblyScriptを紹介していきます。なお、TypeScriptについては、@ITの記事を参考にしてください。
AssemblyScriptは、上記の通り、TypeScriptのコードをWebAssemblyのバイナリにコンパイルするためのツールです。ただし、TypeScriptの構文そのままでコンパイルできるわけではなく、WebAssemblyのためのサブセットという位置付けになります。そういう意味では、TypeScriptの構文を使ったWebAssemblyのための言語がAssemblyScriptとでしょう。
TypeScriptとAssemblyScriptには、大まかに、以下のような違いがあります。
このうち数値型の違い、イテレータ構文、例外処理については、後述するコード例で紹介します。
AssemblyScriptを使うために、幾つかのソフトウェアを準備しましょう。エディタおよび実行環境としてVisual Studio Code(以下、VSCode)を、ビルドと実行の環境としてNode.js(バージョン16以上)をそれぞれインストールしておきます。
VSCodeをインストールしたら、拡張機能「Open in Default Borwser」をインストールしておきます。インストール後には念のため、設定から「Open In Default Browser > Run: Open With Local Http Server」の「ローカルHTTPサーバーで開く」にチェックが入っていることを確認してください。これは、ローカルにあるHTMLファイルなどからWebAssemblyのバイナリファイルを読み込めるようにするための指定です。
VSCodeとNode.jsを準備できたら、AssemblyScriptをインストールします。まずは、AssemblyScriptのためのプロジェクトフォルダを作成します。本連載のためのatmarkit_wasmフォルダを適当な場所(ドキュメントフォルダなど)に作成し、その中にさらにassemblyscriptフォルダを作成します。以降、このフォルダを起点に操作します。
% mkdir -p atmarkit_wasm/assemblyscript % cd atmarkit_wasm/assemblyscript
フォルダが準備できたら、AssemblyScriptをNode.jsのパッケージマネジャーであるnpmコマンドでインストールします。続けてnpm fundコマンドでインストールされたパッケージを確認し、npm installコマンドを実行して、依存関係にあるパッケージのインストールを済ませておきます。
% npm install -g assemblyscript グローバルにインストール added 3 packages, and audited 4 packages in 3s …略… found 0 vulnerabilities % npm fund 確認 assemblyscript % npm install 依存関係にあるパッケージのインストール added 3 packages, and audited 4 packages in 2s …略…
なお、assemblyscriptパッケージ以外にbinaryen(WebAssemblyのためのツールチェイン)、long(64bit長整数のためのクラス)もインストールされます。これで、AssemblyScriptのインストールは完了です。
AssemblyScriptには、テンプレートを作成する機能があります。このテンプレートを通じて、AssemblyScriptのファイルやツールについて見ていきましょう。
テンプレートは、asinitコマンドで作成します。asinitコマンドを実行すると、途中で本当に作成するか尋ねられるので、「Y」を回答して先に進んでください。
% asinit . カレントフォルダに作成 …略(実行すべきコマンドや、生成されるファイルの情報が表示される)… Have a nice day!
ずらっとメッセージが表示されますが、ほとんどは作成しようとしているフォルダ、作成したフォルダ、続けて実行すべきコマンドなどの情報なので、無視して構いません。主なフォルダとファイルの役割は以下の表1のとおりです。
フォルダ/ファイル | 概要 |
---|---|
assembly | AssemblyScriptのソースコードを格納するフォルダ |
assembly/index.ts | AssemblyScriptプログラムのエントリポイント |
build | ビルドされたWebAssemblyファイルを格納するフォルダ |
tests | テストスクリプトを格納するフォルダ |
index.html | ビルドされたWebAssemblyプログラムを使うHTMLファイル |
表1 asinitコマンドで作成される主なフォルダとファイル |
メッセージにあるnpm run asbuildコマンドでビルドを実行します。asbuildにオプションを与えない場合は、DebugターゲットとReleaseターゲットの双方に対してビルドが実行されます。それぞれのターゲットのオプションは、以下の実行例を参照してください。
% npm run asbuild ビルドを実行(両ターゲット) > asbuild > npm run asbuild:debug && npm run asbuild:release > asbuild:debug > asc assembly/index.ts --target debug > asbuild:release > asc assembly/index.ts --target release
上記の表1の通り、テンプレートには、WebAssemblyバイナリを読み込んで実行結果を表示させる、index.htmlファイルが含まれています。Releaseターゲットにてビルド後、VSCodeのエクスプローラーでindex.htmlを右クリックし、表示されるメニューから[ブラウザで開く]を選択してください。既定のWebブラウザで(この場合はGoogle Chrome)、以下のように結果が表示されます。
ここで、URL欄に注目してください。「http://localhost:52330/index.html」となっている通り(スキーマは省略されていたり、ポート番号は異なる場合があります)、サーバへのリクエストとなっています。連載第1回で触れたように、.wasmファイルの読み込みはSame Originである必要があるため、ローカルなファイルとしてindex.htmlファイルを開いた場合、.wasmファイルは読み込めません。このため、VSCodeの拡張機能「Open in Default Browser」により、ローカルHTTPサーバからレスポンスを返すようにしたのです。
Same Ogirinである必要があるとは、同一生成元ポリシーによる制約を意味します。簡単にいうと、リソースとそれの利用側のURLの一部(オリジン=スキーム+ホスト+ポート番号)が同一でない場合のアクセスに、Webブラウザが制限をかける仕組みです。上記の場合、オリジン「http://localhost:52330/」がindex.htmlと.wasmファイルで同一なので、Same Originということになります。
結果は「3」とだけ表示されていますが、index.htmlファイル自身を見てみましょう。
<!DOCTYPE html> <html lang="en"> <head> <script type="module"> import { add } from "./build/release.js"; (1) document.body.innerText = add(1, 2); (2) </script> </head> <body></body> </html>
重要なのは2行です。(1)は、index.tsファイルのビルドによって生成されるヘルパーであるbuild/release.jsのインポート、そして(2)は、そこでエクスポートされている関数addの呼び出しとDOMの書き換えです。この関数addこそがTypeScriptで記述されているのですが、これについては後述するコード例の節で取り上げます。
このようにAssemblyScriptでは、TypeScriptファイルから生成される各種のファイルを用いることで、HTMLおよびJavaScriptからも簡単に利用できます。
npm testコマンドでコードをテストすることもできます。テストで実行する内容は、testsフォルダにあるスクリプトに記述されています。
% npm test テストを実行 > test > node tests (node:48691) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time (Use `node --trace-warnings ...` to show where the warning was created) ok
警告(Fetch APIの実装は実験的)が表示されますが、ここでは無視して構いません。最後に「ok」と表示されていれば、テストは成功です。
ビルドによって生成されるファイルにも簡単に触れておきます。npm run buildコマンドでは、DebugターゲットとReleaseターゲットそれぞれに以下のファイルが作成されます。拡張子から分かる通り、それぞれWebAssemblyのバイナリファイル、そのマップファイル、WAT(WAST)ファイル、そしてWebAssemblyのバイナリファイルを読み込むJavaScriptファイルとなります(WASTファイルについては連載第1回を参照してください)。
先ほど、asinitコマンドでAssemblyScriptのテンプレートを作成しました。AssemblyScriptのインストールによって利用可能になるコマンドには、他にascコマンドとwasm-optコマンド、wasm2jsコマンドがあります。これらの機能は以下の通りです(個別の利用については割愛します)。
ここからは、エントリポイントのファイルであるindex.tsを手始めに、AssemblyScriptのコード例を幾つか、TypeScriptとの差異を絡めて紹介していきます。
assemblyフォルダにあるindex.tsがエントリポイントとなるファイルです。他にTypeScriptのファイルを作っていく場合、このindex.tsファイルにimport文を記述していくことになります。以下は、index.tsファイルの内容です。
export function add(a: i32, b: i32): i32 { return a + b; }
先ほどHTMLから呼び出していた関数addの定義で、たった3行からなるスクリプトです。関数addは、32bit符号付き整数(i32型)の引数を2個受け取り、加算した結果を同じく32bit符号付き整数(i32型)で返します。本稿冒頭でも触れたように、i32はAssemblyScript固有のデータ型で、TypeScriptにはないデータ型です。
このように、AssemblyScriptではTypeScriptにおいてnumberやbigintといった型で指定していたところを、i32のようなサイズの明確なデータ型で指定する必要があります。これは、コンパイル時に明確に型を決定する必要があるためです。AssemblyScriptには、主に以下の表2に示すデータ型があります。対応するWebAssemblyとAssemblyScriptのデータ型も並記してあります。
AssemblyScript | WebAssembly | TypeScript | 概要 |
---|---|---|---|
i32 | i32 | number | 32bit符号付き整数 |
u32 | i32 | number | 32bit符号なし整数 |
i64 | i64 | bigint | 64bit符号付き整数 |
u64 | i64 | bigint | 64bit符号なし整数 |
isize | i32またはi64 | numberまたはbigint | 32bit符号付き整数(WASM32)または64bit符号付き整数(WASM64) |
usize | i32またはi64 | numberまたはbigint | 32bit符号なし整数(WASM32)または64bit符号なし整数(WASM64) |
f32 | f32 | number | 32bit浮動小数点数 |
f64 | f64 | number | 64bit浮動小数点数 |
bool | i32 | boolean | 真偽値 |
void | ― | void | 戻り値のないことを明示 |
anyref | anyref | ― | 参照 |
表2 AssemblyScriptの主なデータ型 |
このように、数値型は整数と浮動小数点型に大きく分けられ、さらにデータ長や符号の有無に応じて細かく分けられています。また、真偽値を表すbool型や、参照を表すanyref型がAssemblyScript独自の型として設けられています。参照には、Object型に相当するanyrefの他に、Array型への参照であるarrayref型、string型への参照であるstringref型なども設けられています。
使用できない型として、undefinedとanyがあります。これらはいずれも、コードの曖昧さに基づくので、WebAssemblyには不適当であると見なされているのです。
AssemblyScriptのデータ型の詳細は、以下を参照してください。
Types | The AssemblyScript Book
なお、TypeScriptの型であるnumberとbooleanがエイリアスとして使えます。それぞれ、f64とi32に対応します。ただし、特にnumberがf64型に相当することには要注意です。例えば以下のコードは、number由来でコンパイルエラーとなります。
export function sub(a: i32, b: i32): i32 { const result: number = a - b; return result; // f64からi32に変換できないエラー }
ERROR AS200: Conversion from type 'f64' to 'i32' requires an explicit cast.
numberがf64型になるので、関数の戻り値の型であるi32にはキャストが必要となるのです。このような問題を避けるため、numberとbooleanは利用こそできますが、非推奨の扱いです。できるだけ、i32のような数値型やbool型を使うようにしてください。
以下は、number型を使わないか、演算結果を明示的にキャストするよう改めた例です。
const result: i32 = a - b; // numberを使わずi32を用いる return i32(result); // 明示的にキャスト
AssemblyScriptでは、利用頻度の高いfor〜of、for〜inのようなイテレータ構文が使えません。例えば以下のようなコードは、イテレータが実装されていないとしてコンパイルエラーになります。
export function calcSum(): i32 { const ary: i32[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum: i32 = 0; // イテレータは使えない for (let value of ary) { sum += value; } return sum; }
ERROR AS100: Not implemented: Iterators
コンパイルエラーを解消するには、オーソドックスなfor文で記述し直す必要があります。これは、for〜of文も同じです。
// オーソドックスなfor文を使う必要あり for (let i: i32 = 0; i < ary.length; i++) { sum += ary[i]; }
AssemblyScriptでは、try〜catch構文のような例外処理が実装されていません。例えば、以下のコードは0による除算を例外としてキャッチしようとしますが、例外処理が実装されていないというコンパイルエラーとなります。
export function divide(a: i32, b: i32): i32 { let result: i32 = 0; try { result = a / b; } catch(e) { result = 0; } return result; }
ERROR AS100: Not implemented: Exceptions
コンパイルエラーを解消するには、if文などで事前に条件判定するように記述し直す必要があります。tryブロックの中で発生する可能性のある例外を、事前にチェックするようにします。
// 条件判定する記述に変更する必要あり if (b == 0) { result = 0; } else { result = a / b; }
今回は、TypeScriptでWebAssemblyプログラムを開発できるAssemblyScriptを紹介しました。差異こそありますが、TypeScriptの構文を使ってコードを書けて、利用側ではテンプレートで生成されたファイルを取り込むだけというシンプルさを理解いただけたのではないかと思います。
次回は、C#と.NETでWebAssemblyプログラムを開発できるBlazor WebAssemblyを紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.