はじめに
「WebAssemblyを触ってみたいけれど、そのためにRustやC++を新しく覚えるのは腰が重い」——そう感じたことはないでしょうか。WebAssemblyは高速な実行が魅力ですが、既存のシステム言語を習得するハードルが、多くのJavaScript/TypeScript開発者にとって参入障壁になっています。
AssemblyScriptは、その障壁を「新しい言語を覚えない」という形で取り払ってくれるツールです。TypeScriptとほぼ同じ構文でコードを書き、それをそのままWebAssemblyバイナリにコンパイルできます。型注釈を厳密にするだけで、普段のTypeScriptの延長線上にWasmの世界が広がっている感覚です。
とはいえ、読むより触った方が早いと思います。ブラウザ上でAssemblyScriptのコードをその場でWasmにコンパイルして実行できるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
AssemblyScriptとは
AssemblyScriptは、TypeScriptのサブセットに近い構文を持つ言語で、Binaryenをバックエンドとして使い、これをWebAssemblyモジュールにコンパイルするコンパイラです。公式リポジトリの説明どおり「npm installするだけで」導入でき、専用のツールチェーンを別途構築する必要がありません。GitHub上で18,000以上のスターを集めており、現在も活発に開発が続いています。
主な特徴
- TypeScriptライクな構文 -
i32やf64といった型注釈を書くだけで、見慣れたTypeScriptの文法のままWasmを書けます - Binaryenによる最適化 - コンパイル時に
--optimizeオプションを付けると、Binaryenの最適化パスが自動でWasmバイナリを軽量化します - npm installだけで導入完了 - 専用のコンパイラをネイティブビルドする必要がなく、Node.jsプロジェクトにそのまま組み込めます
- ブラウザ内コンパイルも可能 - コンパイラ本体(
assemblyscript/asc)自体がJavaScriptで書かれているため、ブラウザ上でソースコードをその場でWasmに変換できます - 軽量なランタイム - ガベージコレクションを含むランタイムを選択でき、用途に応じてバイナリサイズと機能のバランスを調整できます
インストール
npm install --save-dev assemblyscript
npx asinit .
asinitコマンドを実行すると、assembly/ディレクトリとasconfig.jsonが生成され、AssemblyScriptプロジェクトのひな形が整います。
AssemblyScriptのサンプルを動かす
AssemblyScriptのコンパイラ(assemblyscript/asc)はJavaScript製なので、ブラウザ上でも動かせます。下のサンプルでは、asc.main()にAssemblyScriptのソースコードを仮想ファイルとして渡し、その場でWebAssemblyバイナリにコンパイルしたうえで、WebAssembly.instantiate()で実行しています。2つの数値入力欄を書き換えると、コンパイル済みのWasm関数addがその場で呼び出され、結果が更新されます。
コンパイルの中心部分だけを抜き出すと、次のようになります。
import asc from "assemblyscript/asc";
const outputs = {};
await asc.main(["input.ts", "--outFile", "output.wasm", "--optimize"], {
readFile: (name) => (name === "input.ts" ? source : null),
writeFile: (name, contents) => { outputs[name] = contents; },
listFiles: () => [],
});
const { instance } = await WebAssembly.instantiate(outputs["output.wasm"], {});
instance.exports.add(1, 2); // => 3
実際に動かせるものが下です。値を書き換えると、AssemblyScriptでコンパイルされたWasm関数の計算結果がその場で変わります。
add関数のソースコードを書き換えて、たとえばa + bをa * bに変えてから再実行すれば、AssemblyScriptのコンパイル結果がすぐに変わることも確認できます。コンパイルと実行がすべてブラウザの中で完結している点が、AssemblyScriptの手軽さをよく表しています。
基本的な使い方
実際のプロジェクトでは、ブラウザ内コンパイルではなく、.tsファイルをCLIでコンパイルしてから使うのが基本の流れです。まずassembly/index.tsにAssemblyScriptのコードを書きます。
// assembly/index.ts
export function fib(n: i32): i32 {
var a = 0, b = 1
if (n > 0) {
while (--n) {
let t = a + b
a = b
b = t
}
return b
}
return a
}
これをascコマンドでコンパイルします。
npx asc assembly/index.ts --outFile build/release.wasm --optimize
生成された.wasmファイルは、Node.jsでもブラウザでもWebAssembly.instantiateから呼び出せます。
import fs from "node:fs";
const wasmBuffer = fs.readFileSync("build/release.wasm");
const { instance } = await WebAssembly.instantiate(wasmBuffer, {});
console.log(instance.exports.fib(10)); // => 55
i32やf64といった型注釈さえ意識すれば、書き方自体はほとんど通常のTypeScriptと変わりません。
実践的なユースケース
ループ処理を高速化するパターン
AssemblyScriptが最も効果を発揮するのは、ループを多用する数値計算です。ここでは「指定した範囲の素数の個数を数える」という、計算量の大きい処理をAssemblyScriptでコンパイルし、同じアルゴリズムのJavaScript版と実行時間を比較します。performance.now()で計測した時間を見比べることで、Wasmの実行速度を体感できます。
export function countPrimes(limit: i32): i32 {
var count: i32 = 0;
for (let i: i32 = 2; i < limit; i++) {
let isPrime = true;
for (let j: i32 = 2; j * j <= i; j++) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
入力欄の数値を大きくするほど、AssemblyScriptでコンパイルしたWasm版とJavaScript版の実行時間の差が分かりやすくなります。
同じロジックでも、countPrimesに渡す数値を大きくしていくほどWasm側の優位性がはっきり見えてきます。ループの回数が少ないうちは差が小さくても不思議ではありません。AssemblyScriptは「JS実装をそのまま置き換えて速くする」用途に向いていることが分かります。
コンパイルオプションでバイナリサイズを最適化するパターン
もう一つ、AssemblyScriptならではの体験が--optimizeオプションによるバイナリサイズの変化です。同じソースコードを最適化ありとなしでそれぞれコンパイルし、生成されるWasmバイナリのサイズを比較します。
const args = ["input.ts", "--outFile", "output.wasm"];
if (withOptimize) args.push("--optimize");
await asc.main(args, { readFile, writeFile, listFiles: () => [] });
// outputs["output.wasm"].byteLength でサイズを比較できる
ボタンを押すと、--optimizeありとなしの2種類のバイナリを同時にコンパイルして、サイズの差を表示します。
--optimizeを付けたバイナリの方が小さくなることが多いのが分かります。これはBinaryenの最適化パスが不要な命令を削り、コードを畳み込んでくれるためです。本番ビルドでは--optimizeを付けるのが基本ですが、デバッグ中はあえて外して、コンパイル結果の変化を確認するという使い方もできます。
まとめ
AssemblyScriptは、TypeScriptに近い構文のままWebAssemblyを書けるという一点だけでも、JS/TS開発者にとって大きな参入障壁の低さを実現しています。今回のサンプルで見たように、コンパイラ自体がJavaScript製であるため、Node.js環境はもちろんブラウザの中だけでもコンパイルから実行までを完結させられます。
数値計算の重い処理を高速化したい場面や、既存のTypeScriptの資産に近いコードでWasmを試したい場面では、AssemblyScriptが有力な選択肢になります。まずは小さな関数をひとつコンパイルするところから、手元のプロジェクトで試してみてはいかがでしょうか。