AssemblyScriptを使ってTypeScriptのコードを早くしよう

TL;DR; AssemblyScriptを使うと、TypeScriptコードをWebAssemblyに変換できます。オブジェクト指向プログラミングをしている場合は、オブジェクトが保存されるメモリ領域を自分で管理しなくてはならないので、その手間とのトレードオフを見極めてください。

なお、使用しているascのバージョンは 0.9.2 です。

# C書けない私にWebAssemblyをつくれと言われましても

WebAssembly（以下、WASM）とはWebブラウザで動くプログラムのバイナリ表現です。Safari, Edge, Chrome, Firefoxと、モダンなWebブラウザへの搭載も終わり、本格的に利用できるようになってきました。その特徴はスピードです。ネイティブに近いスピードで動作します。画像処理やエンコード、暗号といったCPUの処理能力に依存するような処理を行うモジュールをWebAssemblyにすると、従来よりも高いパフォーマンスを引き出すことができます。

そんなWASMですが、高級なプログラミング言語を変換して作成することが、よくある作成方法です。CやC++の変換にはEmscriptenと呼ばれるツールがよく使われます。とはいえ、普段Webを作っている我々にとってCやC++は縁遠く
「C使えば簡単に作れるよ！！」
と言われても、
「簡単とは？」
みたいな気持ちになると思います。

Cは書きたくないが、遅い処理を早くしたい。そんな我らの味方がAssemblyScriptです。

# AssemblyScript

[AssemblyScript](https://github.com/AssemblyScript/assemblyscript)とは、TypeScript（正しくは、そのサブセット）を変換してWebAssemblyを出力するコンパイラです。つまりCを書かなくても、TypeScriptを書くことができればWASMを作れます。

例えば次のような整数同士の足し算を行う関数があったとします。TypeScriptに``u32``なんていう型はありませんが、その辺りに目をつむると、よく知るコードだと思います：

~~~typescript
export function add(a: u32, b: u32) : u32 {
  return a + b;
}
~~~

これをAssemblyScriptで処理すると、次のようなWASMコードになります：

~~~WAT
(module
 (type $iii (func (param i32 i32) (result i32)))
 (memory $0 1)
 (export "add" (func $add))
 (export "memory" (memory $0))
 (func $add (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (get_local $0)
   (get_local $1)
  )
 )
)
~~~

これはテキスト形式ですが、もちろんバイナリ形式のファイルも出力されます。これをWASMの使い方にしたがって、ロード、コンパイル、インスタンス化することで、WASMになった`add`関数を呼び出せます：

~~~js
WebAssembly.instantiateStreaming(fetch("add.wasm"), {}).then(mod => {
  const add = mod.instance.exports.add;
  const result = add(1, 2);
  console.log(result);
});
~~~

## npmでインストール可能

「そう言っても開発環境を整えるのは面倒なんでしょう？」
「いやいや、そんなことはありません。npmでさっくりインストールできますよ」
「えええー」

asm.jsになったBinaryenとTypeScriptのcompiler APIを利用しているので、AssemblyScript自身はJSだけで実装されています。そのおかげで、コンパイラーやCmakeのようなビルド環境を整備しなくても、npmを使うだけでさっくりインストールできます。

## グローバル環境にインストールする場合

~~~sh
% npm install -g assemblyscript 
~~~

インストールすると、`asc`というコマンドが利用できるようになります。これは`tsc`コマンドのAssemblyScript版で、入力された`.ts`ファイルからWASMを出力します。例えば`add.ts`というファイルを処理したければ、次のように実行します。

## AssemblyScript プロジェクトを作る場合

AssemblyScript を使うプロジェクトを今から始める場合は、`asinit` を使うと便利です。コンパイラのインストール以外にも、テンプレートにしたがって様々な設定やディレクトリの作成が行われます。

まずプロジェクト用のディレクトリを作ります。その中で、`npm init`をして、`package.json`を作ります。

~~~sh
% mkdir as_project
% cd as_project
% npm init
~~~

次に`@assemblyscript/loader`と`assemblyscript`をインストールします。

~~~sh
% npm i @assemblyscript/loader
% npm i -D assemblyscript
~~~

これで`asinit`コマンドが実行できるようになります。次のように`npx`コマンドを使って、実行します。

~~~sh
% npx asinit .
~~~

終了後は、次のようにフォルダが作成されています。

~~~sh
% ls
assembly		index.js		package-lock.json	tests
build			node_modules		package.json
~~~

それぞれのフォルダは次の通りの役割を持っています。

|フォルダ名|役割|
|----|----|
|assembly|AssemblyScript のソースコード用フォルダ|
|build|ビルドされたWASMファイルの保存場所|
|tests|テストの保存場所|
|index.js|ビルドされたプログラムのエントリーポイント|

`asbuild`スクリプトを実行すれば、`assembly/index.ts`をエントリーポイントに、ビルドが開始します。

~~~
% npm run asbuild
> @ asbuild /Users/chikoski/dojo/hello-as
> npm run asbuild:untouched && npm run asbuild:optimized


> @ asbuild:untouched /Users/chikoski/dojo/hello-as
> asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --debug --runtime none


> @ asbuild:optimized /Users/chikoski/dojo/hello-as
> asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --optimize --runtime none
~~~

このスクリプトは最適化されたリリースビルドと、デバッグ用のビルドの2種類を作成します。`optimized.wasm`が最適化されたもの、`untouched.wasm`がデバッグ用となっています。

またWATファイルとソースマップも、同時に作成します。

~~~sh 
% ls build
optimized.wasm		optimized.wat		untouched.wasm.map
optimized.wasm.map	untouched.wasm		untouched.wat
~~~

# asc の使い方

~~~sh
% asc -o add.wasm add.ts
~~~

`add.ts`の中で他のtsファイルをインポートしている場合は、自動的にその依存関係を解決して、よしなに処理をしてくれます。

`-o`オプションで、出力するファイルの名前を指定できます。出力されるファイルの内容は、次の表のように拡張子によって変わります。なお省略するとS式を標準出力に出力します。

|拡張子|出力されるファイル形式|
|-----|-------------------|
|.wasm|WebbAssembly（バイナリ）|
|.wast|S式（テキスト）|
|.wat|線形のテキスト表現|
|.js|asm.js|

## `--help` オプション

`--help` オプションをつけると、利用可能なコマンドラインオプションが全て出力されます。

## `--runtime` オプション

上述の方法で出力すると、そのままでの実行が前提となるWASMファイルが出力されます。プロジェクト全体をWASMにするなら問題ありませんが、CPUに依存する処理を高速化するといった該当する処理のみWASMになれば良い場合にはバイナリサイズが巨大になってしまいます。

そういう場合には、`--runtime`オプションを使います。`--runtime none` と指定すれば、ランタイムを省いがWASMファイルが出力されます。前出のWASMファイルは、このオプションをつけて出力しています。

`--runtime`オプションには、`none` 以外にも、以下の値を指定できます。

| 値 | 説明 |
|---|---|
|full|標準のランタイム。メモリ管理に TLSF と参照カウントを利用|
|half|fullと同様だが、ランタイムのコードは出力されない|
|stub|free と GC を省いた最小構成|
|none|stubと同様だが、ランタイムのコードは出力されない|


# AssemblyScriptとTypeScriptの違い

ここまでAssemblyScriptはTypeScriptを処理できるかのように書いてきましたが、実はそうではありません。WASMへの出力を行う関係で、型関連に若干の違いがあります。

## 追加された型

まず次の型が追加されています。TypeScriptでの数値はJavaScriptと同じく`number`だけですが、WASMが整数、実数、データ長や符号の有無を区別する関係で、より細かく拡張されています。また真偽を表すプリミティブ型や、`void`も追加されています。

|型|WASM での型|sizeof演算子の評価値|説明|
|---|---------|-----------------|----|
|i8|i8|1|8bitの整数値|
|i16|i16|2|16bitの整数値（符号あり）|
|u16|u16|2|16bitの整数値（符号なし）|
|i32|i32|4|32bitの整数値（符号あり）|
|u32|u32|4|32bitの整数値（符号なし）|
|i64|i64|8|64bitの整数値（符号あり）|
|u64|u64|8|64bitの整数値（符号なし）|
|f32|f32|4|32bitの浮動小数点|
|f64|f64|8|64bitの浮動小数点|
|bool|i32|1|真偽値|

### 代入できるもの、できないもの

`number` が整数と不動小数点に細分化され、サイズや符号の有無による派生も生まれています。この変更で、数値であっても代入できる場合とできない場合があります。ざっくりまとめると、次のルールにしたがっています。

* 整数値を整数型の変数に代入できます：例：`a:i32 = -18;`
* 不動小数を不動小数点型の変数に代入できます。例：`b:f64 = 3.14;`
* サイズの小さい値を、サイズの大きい型の変数へ代入できます。例：`a:i8 = 1; b:i32 = a;`
* サイズの大きい値を、サイズの小さい型の変数へは代入できません。できない例：`a:i32 = 8; b:i8 = a;`
* 符号付きの値を符号なしの変数に代入できます。逆も可能です。例：`a:u32 = -1; b:i32 = a;`

ほかにも `u64/i64` を除く整数を不動小数点型の変数に代入できるとか、符号なしの値を符号ありにした時の変換ルールとか、サイズの小さい値を大きなサイズの型へ代入した時、上位のビットはどうなるのか、とかいろいろ細かい規則があります。詳しくは [こちらの表](https://docs.assemblyscript.org/basics/types#assignability)を参照してください。

## 特別な型

次の型も利用できます。`anyref` は Reference Type の仕様で定義されている参照型を表現します。

|型|WASM での型|説明|
|---|---------|---|
|isize|i32(WASM32) / i64 (WASM64)|WASM32 では i32 のエイリアス、WASM64 では i64 のエイリアス|
|usize|u32(WASM32) / u64 (WASM64)|WASM32 では u32 のエイリアス、WASM64 では u64 のエイリアス|
|void||返り値がないことを表す型|
|anyref|anyref|参照型|

## 型のエイリアス

JS/TS でお馴染みの型も利用できますが、内部ではエイリアスとして処理されます。ただし使用は推奨されません。


|型|実際の型|WASM での型|
|---|---------|---|
|number|f64|f64|
|boolean|bool|i32|

## 型が変更された値

`NaN`と`Infinity`は、文脈によって型が決定されます。

|値|型|
|---|---|
|NaN|f32 もしくは f64||
|Infinity|f32 もしくは f64|

## 使用できない型

次の型は使用できません。

* undefined
* any
* `クラス名 | null`を除く`union`

またオプション引数には必ず初期化項が必要です。

# オブジェクト指向のコードはどうなってしまうのか

例えば次のような2次元ベクトルを表すクラスがあったとします。メソッドも持っていて、ベクトル同士の足し算の計算が可能です。

~~~ts
export class Vec {
  mX: i32;
  mY: i32;
  constructor(x: i32 = 0, y: i32 = 0) {
    this.mX = x;
    this.mY = y;
  }
  add(b: Vec): Vec {
    return new Vec(this.mX + b.mX, this.mY + b.mY);
  }
  getX(): i32 {
    return this.mX;
  }
  getY(): i32 {
    return this.mY;
  }

  get x(): i32 {
    return this.mX;
  }
  get y(): i32 {
    return this.mY;
  }
  set x(value: i32) {
    this.mX = value;
  }
  set y(value: i32) {
    this.mY = value;
  }
}
~~~

これを変換すると次のようになります（`--noRuntime -f linear -O`オプションをつけています）。

~~~
(module
 (type $iiii (func (param i32 i32 i32) (result i32)))
 (type $iii (func (param i32 i32) (result i32)))
 (type $ii (func (param i32) (result i32)))
 (import "lib" "malloc" (func $lib:malloc (param i32) (result i32)))
 (import "lib" "memset" (func $lib:memset (param i32 i32 i32) (result i32)))
 (memory $0 1)
 (export "Vec" (func $Vec))
 (export "Vec#add" (func $Vec#add))
 (export "Vec#getX" (func $Vec#getX))
 (export "Vec#getY" (func $Vec#getY))
 (export "memory" (memory $0))
 (func $Vec (type $iiii) (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
  (i32.store
   (get_local $0)
   (get_local $1)
  )
  (i32.store offset=4
   (get_local $0)
   (get_local $2)
  )
  (return
   (get_local $0)
  )
 )
 (func $Vec#add (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (return
   (call $Vec
    (call $lib:memset
     (call $lib:malloc
      (i32.const 8)
     )
     (i32.const 0)
     (i32.const 8)
    )
    (i32.add
     (i32.load
      (get_local $0)
     )
     (i32.load
      (get_local $1)
     )
    )
    (i32.add
     (i32.load offset=4
      (get_local $0)
     )
     (i32.load offset=4
      (get_local $1)
     )
    )
   )
  )
 )
 (func $Vec#getX (type $ii) (param $0 i32) (result i32)
  (return
   (i32.load
    (get_local $0)
   )
  )
 )
 (func $Vec#getY (type $ii) (param $0 i32) (result i32)
  (return
   (i32.load offset=4
    (get_local $0)
   )
  )
 )
)
~~~

これを見てわかる人は
「ははぁ。なるほど。Cでオブジェクト指向やった時みたいになるんだねえ」
などと思って見ていいただければ良いのですが、ざっくり説明すると次のように変換されています。

* コンストラクタとメソッドは、それぞれ別の関数へと変換される
* getter / setter はWASMからは消える
* フィールドはWASMインスタンスの持つメモリ領域に保存される
* メモリ領域の操作は、`memset`と`memcopy`という関数によって行われる

まだ何を言っているかわからないと思うので、順番に見てゆきましょう。

## 変換されたコードの使い方：コンストラクタ

TypeScriptでは、次のようにインスタンス化します。

~~~ts
var zero = new Vec(1, 0);
var one = new Vec(2, 3);
~~~

変換されたWASMでは、コンストラクタは`Vec`という関数になっていて、次のように使います。なお例の`mod`は、インスタンス化されたWASMモジュールです。

~~~js
const zero = mod.exports.Vec(0, 1, 0);
const one = mod.exports.Vec(8, 2, 3);
~~~

元のコンストラクタと比べて引数が1つ増えています。増えたのは第1引数です。
第2、第3引数はコンストラクタの第1、第2引数に対応しています。

この第1引数は、データを保存するメモリ領域の開始アドレスを表します。WASMインスタンスは、JSとは異なるメモリ領域を持ちます。そう聞くと難しそうですが、要は配列です。

ただ異なる点ももちろんあります。それは保存するデータの種類によって、消費されるセルの数が変わるという点です。今回扱っている`i32`という型は4つのセルを消費します。

AssemblyScriptは、その配列にオブジェクトの属性を、宣言された順に保存します。`Vec`オブジェクトは`mX`と`mY`の属性を持ちますが、これを次のように保存します。なお図中の数字はセルの添え字です。`mX`, `mY`ともに`i32`という型なので、4つのセルを使っています。

~~~
|zero.mX|zero.mY|
0       4       8
~~~

上記のコードを実行して、`Vec`を2つ作成すると、WASMのメモリ領域は次のようになります。

~~~
|zero.mX|zero.mY|one.mX|one.mY|
0       4       8      12     16
~~~

コンストラクタに追加された最初の引数は、データを保存するセルの添え字を表しています。つまり `mod.exports.Vec(8, 2, 3);`は次のように翻訳できます：

「メモリ領域の8番目のセルから順に、2と3を保存して」

この時に指定した第1引数の値が、コンストラクタの返り値となります。

## メソッドの呼び出し

`zero`に`one`を足します。TypeScriptでは、zeroに対して`add`メソッドを呼び出すことで、実現できます。

~~~ts
var result = zero.add(one);
~~~

変換された`add`メソッドは、`Vec#add`という関数となっています。
「`zero`に`one`を足す」
というコードを、この関数を使って書くと次のようになります。

~~~js
const result = mod.exports["Vec#add"](zero, one);
~~~

第1引数は、そのメソッドを呼び出すオブジェクトの先頭アドレスです。これはコンストラクタと同様です。

コンスラクタと異なるのは、変換された関数の名前に`#`が含まれる点です。JavaScriptでは`#`を変数名や関数名に利用できないため、呼び出す時には文字列を与えることで属性を参照して、それが指す関数オブジェクトを得なければなりません。

なんどもやるのは面倒な場合は、単純に他の変数へ代入してもよいでしょう：

~~~js
const add = mod.exports["Vec#add"];
const result = add(zero, one);
~~~

## 属性値を操作する関数をgetter/setterとは別に用意した方がよい

もしgetter/setterを定義している場合は、それらと同じ動きをする関数を別途用意しておいた方がいいでしょう。なぜならWASMのコードからgetter/setterが消去されてしまうからです。

これは意外なところに効いてきます。それはgetter/setter経由で属性値を操作している場合です。例えば上記の例で定義されている`add`メソッドは、getterメソッドを使って次のように書くこともできます：

~~~ts
add(b: Vec): Vec {
  return new Vec(this.x + b.x, this.y + b.y);
}
~~~

しかし、このコードをWASMに変換できません。ascは`x`や`y`をgetterメソッドの呼び出しではなく、属性の参照として解決しようとするからです。しかし、そのようなそのような属性は定義されていないため、コンパイルエラーととなってしまいます。

これは将来的に解決されるかもしれませんが、現時点（0.3.0）ではコンパイルエラーとなります。

## メモリ操作をする関数をインポートさせる必要があります

内部で`new`を使用する関数やメソッドを書いている場合、それらは内部でWASMメモリを操作することになります。

AssemblyScriptは、`memset`や`malloc`といったメモリの操作を行うための関数群が定義されていることを前提に、コードの変換を行います。これらのコードは自動的に追加されるので、通常はきにする必要がありません。

しかし`--runtime stub`を指定するといった形で、メモリ管理をランタイムから削除している場合は、 WASMをインスタンス化する際にこれらの関数をインポートしなければなりません。

自分で実装してもいいですが、標準で追加される実装は[AssemblyScriptのmemory management runtime](https://github.com/AssemblyScript/runtime)として別レポジトリで配布されています。ここから必要なものだけ読み込むというのでも良いでしょう。

# まとめ

AssemblyScriptを使うと、TypeScriptを「ほぼそのまま」WASMへ変換できます。

そうはいっても、メモリ管理を自分でしなければならなかったり、getter/setterあたりの変更が必要だったり、`i32`のように数値をより細かく型指定しなくてはいけなかったり、変更点はいくつかあります。

全てのコードをいきなり突っ込むのではなく、本当にパフォーマンス改善が必要な点だけを別関数にして、その関数のみをWASMにする、といったような使い方の方が現実的なのかもしれません。

とはいえ、CやC++を書けなくてもWASMの力を利用できるようになったのは、大きな選択肢を与えてくれていると思います。

パフォーマンスが欲しい時、

「AssemblyScriptつかってみるか」

と思っていただければ、幸いです。