Rust/WebAssembly初心者知見大放出 - その1

# 長い前置き
のんたんと申します。趣味開発ではモデリングツールを制作しています。

知っていた方は「まだやってるのか...」とお思いになるかもしれません。知らなかった方はじき完成するツールのリリースに期待していてくれると嬉しいです:grinning:

ところで、この記事のタグはご覧になりましたか？

- `rust`
- `Emscripten`
- `WebAssembly`
- `asm.js`
- `TypeScript`

とても~~バズワード的~~キャッチーな響きをしていますね。

僕はモデリングツールを実装するにあたってこれらの技術を利用しています。もともとはネイティブアプリとして開発していたのですが、Webアプリ開発の方が圧倒的に楽[^1]であることに気付かされこっちにお引越ししてきました。

世の中には、「RustとEmscriptenで(HelloWorld|fib|マンデルブロ集合|移植)やってみた！」といった記事がゴロゴロ落ちていて、またとても参考になりますが、ゴリゴリ書いたという話はあまり無いように思います。様々なWeb技術を連携させつつゴリゴリ書いた[^2]経験を元に知見の共有ができれば良いかなと思ってこの記事を書きました。特にEEIC向けというわけではありませんが、EEICの皆様も是非。

また、僕自身はこれらについてとても理解が深い人間ではありません。誤りを見つけた場合や「こっちのほうが良いよ！」という場合はツッコミを入れてもらえると非常に(自分の)ためになります。よろしくお願いします。

# 概要
タグの通り、`rust`、`Emscripten`、`WebAssembly`、`asm.js`、`TypeScript`について、特に順序を気にすること無く、落とし穴、知見、awesome的何か等を徒然なるままにコード片を交えつつ書きます。ハードな話はありません。逆引き辞典的になれば良いなと思っています。非常に雑多です。

長くなりそうなので2回に分けて書こうと思います。WebGL絡みの話を次回に書こうと思います。

# 前提知識
[The book](https://doc.rust-lang.org/book/)と[rustnomicon](https://doc.rust-lang.org/nomicon/)を読んでおくと色々はかどります。

# セットアップ
[公式資料](https://users.rust-lang.org/t/compiling-to-the-web-with-rust-and-emscripten/7627)を元にインストールします。基本的にこれに従ってインストールすれば良いのですが注意点があるのでそれも合わせて記述します。

## rustup
```sh
$ curl -L https://sh.rustup.rs | sh -s -- -y --default-toolchain=stable
$ source ~/.cargo/env
$ rustup target add asmjs-unknown-emscripten
$ rustup target add wasm32-unknown-emscripten
```
公式ではnightlyツールチェインでやっていますが、stableでも問題がなさそうです。僕はstableで利用しています。

## emsdk
```sh
$ curl -O https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz
$ tar -xzf emsdk-portable.tar.gz
$ source emsdk_portable/emsdk_env.sh
$ emsdk update
$ emsdk install clang-incoming-64bit
$ emsdk install emscripten-incoming-64bit
$ emsdk install binaryen-master-64bit
$ emsdk activate clang-incoming-64bit
$ emsdk activate emscripten-incoming-64bit
$ emsdk activate binaryen-master-64bit
```
昔はincoming版を利用しなければビルドできないとされていたため、これを利用しています。ソースからビルドする必要があるため非常にビルドに時間がかかります。現在はバイナリパッケージでも利用可能になっている可能性があります。

全て一括でインストールできる`sdk-*-64bit`というパッケージがありますが、これを使うとemsdkの用意したnodeがインストールされ、emsdk環境を起動したときにemsdkのnodeにパスが通るようになります。アプリ全体のビルドに独自でnodeを使う(webpack, yarn, gulp等を利用する)場合には複数バージョンのnodeが存在することが都合が悪いのでこのようにしました。node 8系を利用していますが問題に遭遇したことはありません。

# Cargo設定例
次のようなプログラムがあるとします。

```rust:src/main.rs
fn main() {
}

#[no_mangle]
pub fn hoge() -> i32 {
  1
}
```

Emscriptenを用いてビルドするためemccに対してコンパイルオプションを渡す必要がありますが、設定は`cargo/.config`に次のように書くと良いです。オプションについては基本的に[Emscripten 公式](https://kripken.github.io/emscripten-site/index.html)が参考になりますが、Undocumentedな部分が大きいので[該当するソースコード](https://github.com/kripken/emscripten/blob/1.37.24/src/settings.js)を生で読むと良いでしょう。

```toml:cargo/.config
[target.asmjs-unknown-emscripten]
rustflags = [
    "-Clink-args= -O3 -s EXPORTED_FUNCTIONS=@exported_functions.json -s RESERVED_FUNCTION_POINTERS=1000 -s TOTAL_MEMORY=1073741824",
]

[target.wasm32-unknown-emscripten]
rustflags = [
    "-Clink-args= -O3 -s EXPORTED_FUNCTIONS=@exported_functions.json -s RESERVED_FUNCTION_POINTERS=1000 -s TOTAL_MEMORY=16777216 -s ALLOW_MEMORY_GROWTH=1",
]
```

```json:exported_functions.json
[
    "_hoge"
]
```

## link-args
`-Clink-args= -O3`でEmscripten側の最適化を有効にしています [参考](https://github.com/rust-lang/rust/issues/39626)

## EXPORTED_FUNCTIONS
[Emscripten FAQ](https://kripken.github.io/emscripten-site/docs/getting_started/FAQ.html)にあるように公開する関数は`EXPORTED_FUNCTIONS`としてオプションから渡してやる必要があります。
`emcc --help`によれば

```
Options that are modified or new in *emcc* are listed below:
...skipping...
      -s DEAD_FUNCTIONS=@/path/to/file
      -s DEAD_FUNCTIONS=@/path/to/file

   Note: * In this case the file might contain a JSON-formatted list
     of

       functions: "["_func1", "func2"]".

     * The specified file path must be absolute, not relative.
```

とのことなので、リストをJSONとして別ファイルにまとめておいてcurlの様に`@`を用いてemccに渡しています。`The specified file path must be absolute`とありますが、`Cargo.toml`起点の相対パスを渡せています（？）`cargo/.config`起点ではないことに注意。

## RESERVED_FUNCTION_POINTERS
コールバック関数の登録可能最大数を指定します [参考](https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html)
コールバック関数は開放することができるので、適当に大きめで切りが良い数にしています。

## TOTAL_MEMORY
使用する全メモリサイズを指定します。

# TypeScript
## Emscripten型定義
[@types/emscripten](https://www.npmjs.com/package/@types/emscripten)が利用可能です。
グローバルな空間に`declare namespace Module`されています。
webpackでimportする方式は厳しいかもしれません。それは良いとしても、
`HEAP`と言った無い(場合がある)メンバが登録されていて実行時エラーになったり、JSのstringとWebAssembly側でのバイナリ表現との間を変換する関数(`stringToUTF8`, `UTF8ToString`)が登録されていなかったり、と若干の不満点があります。

## WebGL2型定義
[@types/webgl2](https://www.npmjs.com/package/@types/webgl2)が利用可能です。
こちらは特に不満点なし。

## TypeScriptからEmscripten呼び出し

```typescript
interface FunctionsType {
  createContext: (contextConfig: number) => number;
  hoge: (context: number) => number;
  piyo: (context: number, objectHandle: number, objSource: number) => number;
  ...
}

let functions: FunctionsType = null;

export function isFunctionsInitialized() {
  return functions !== null;
}

export function initializeFunctions() {
  functions = {
    createContext: Module.cwrap('create_context', 'number', ['number']),
    hoge: Module.cwrap('hoge', 'number', ['number']),
    piyo: Module.cwrap('piyo', 'number', ['number', 'number', 'number']),
    ...
  };
}
```

バインド用のコードは型定義と実際にバインドするコードをあわせてこのような感じ。この例では`objSource`は文字列の引数ですが、cwrapの引数指定に`'string'`を用いていません。これを使うとスタック領域に文字列がコピーされるため、大きな文字列を渡すと簡単にスタックが溢れるからです。そこで次のようなユーティリティ関数を用意しています。

```typescript
export function withStringPtr<T>(s: string, proc: (stringPtr: number) => T): T {
    const dstLength: number = (<any> Module).lengthBytesUTF8(s) + 1;
    const dstPointer = Module._malloc(dstLength);
    const dstView = new Uint8Array(Module.HEAPU8.buffer, dstPointer, dstLength);
    (<any> Module).stringToUTF8(s, dstPointer, dstLength);
    let result: T;
    try {
        dstView[dstLength - 1] = 0;

        result = proc(dstPointer);
    } catch (e) {
        throw e; // Re-throw
    } finally {
        Module._free(dstPointer);
    }

    return result;
}
```

この関数は`Module._malloc`を用いてヒープにメモリを確保しUTF8でコピーした後、そのポインタを引数に渡された`proc`を呼び出します。出るときには必ず開放するようにしています。こいつは便利だと思ってます。

## EmscriptenからTypeScript呼び出し
Module.Runtime.addFunctionでJavascriptの関数をEmscripten側に登録し関数ポインタをえることができます。
次のようにして一括登録しています。

```typescript
const callbacks = {
    foo: (ctx: number) => {
        ...
    },

    bar: (ctx: number) => {
        ...
    },

    baz: (ctx: number) => {
        ...
    },

    ...
};

let callbackPtrs: { [key: string]: number } = null;

export function isCallbacksInitialized() {
    return callbackPtrs !== null;
}

export function initializeCallbacks() {
    callbackPtrs = mapValues(callbacks, (value) => Module.Runtime.addFunction(value));
}
```

そして、(自分でRustで実装した)コンテキストを作成するときに、次のようにしてごっそりと渡します。

```typescript
const pointerSize = 4;
const contextConfigData = [
    0,
    callbackPtrs.foo,
    callbackPtrs.bar,
    callbackPtrs.baz,
];

const numElements = contextConfigData.length;
const contextConfigPtr = Module._malloc(pointerSize * numElements);

for (let i = 0; i < numElements; i++) {
    Module.setValue(contextConfigPtr + pointerSize * i, contextConfigData[i], "*");
}

this._contextPtr = functions.createContext(contextConfigPtr);
```

Rust側ではContextConfigは次のようになっています。

```rust
use libc::c_void;

#[repr(C)]
#[derive(Clone)]
pub struct ContextConfig {
    ctx: *mut c_void,
    foo: extern fn (*mut c_void) -> i32,
    bar: extern fn (*mut c_void) -> i32,
    baz: extern fn (*mut c_void) -> i32,
}

#[no_mangle]
pub unsafe fn create_context(config_ptr: *const ContextConfig) {
    let config = &*config_ptr;
    config.foo(config.ctx);
    ...
}
```

これで、コールバックの設定をまとめて行うことができました。

また、文字列をRustからTypeScriptに返す際にも、コールバックを用いて実現しています。

```typescript
export function withStringWriteFuncPtr(proc: (writeFuncPtr: number) => any): string {
    let result = '';
    const writeFunc = (pointer: number, length: number) => {
        const str = (<any> Module).UTF8ToString(pointer);
        result += str;
    };

    const writeFuncPtr = Module.Runtime.addFunction(writeFunc);
    try {
        proc(writeFuncPtr);
    } catch (e) {
        throw e; // Re-throw
    } finally {
        Module.Runtime.removeFunction(writeFuncPtr);
    }

    return result;
}
```

```rust
use std::io;
use std::ffi::CString;
use libc::{ c_char, c_void };

pub type WriteFunc = extern fn (*const c_char, usize) -> c_void;

pub struct CWrite {
    write_func: WriteFunc,
}

impl CWrite {
    pub fn new(write_func: WriteFunc) -> CWrite {
        CWrite { write_func: write_func }
    }
}

impl io::Write for CWrite {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match CString::new(buf) {
            Ok(cstr) => {
                let ptr = cstr.as_ptr() as *const c_char;
                let len = buf.len() as usize;

                unsafe {
                    (self.write_func)(ptr, len);
                }

                Ok(len)
            },
            Err(_) => {
                Err(io::Error::from(io::ErrorKind::InvalidData))
            },
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        // Do nothing
        Ok(())
    }
}

#[no_mangle]
pub fn return_string(write_func: WriteFunc) {
    let writer = CWrite::new(write_func);
    ...
}
```

このようにすることで`Write`トレイトを用いて文字列を返すことができるようになって便利です。文字列を`+=`で連結するこのに不満があるならばロープ的な仕組みに改造すると良いと思います。

これでJS側とEmscripten側で文字列を自由にやり取りできるようになったので[serde_json](https://crates.io/crates/serde_json)とかと組み合わせて、情報交換がはかどります。

## メモリの制約
ここまで、ちまちましたノウハウについて話をしてきましたが少し趣向を変えた話をします。こっからはポエティックです。
Emscripten、WebAssembly、asm.jsのメモリに関する制約には次のようなものがあります

- WebAssembly, asm.js上で実行されるコードはEmsciptenによって用意されたメモリ領域以外触ることができない
- Emscriptenでasm.jsのコードを吐き出した場合("almost asm"を回避する場合)最初に確保したメモリ領域を広げることができない 

というわけで、頻繁に使う高速に書き換えたい情報はEmscriptenのメモリ領域に残し、しばらく使わない情報はEmscriptenのメモリ領域から外に出しておくのがベターかと思っています。
そんなときに便利なのが[serde](https://crates.io/crates/serde)+[serde_derive](https://crates.io/crates/serde_derive)と[bincode](https://crates.io/crates/bincode)です。
bincodeは`Serialize`を実装していれば`Vec<u8>`に、逆に`Deserialize`を実装していれば｀&[u8]`から元に復元できます。

`as_ptr`でポインタを取得してしまえばコールバックを使ってやり取りできるようになります。

## 終わりに
長くなってきたので、その1はこのへんにしようと思います。

明日は@potato_omomさんの「趣味の話」です。ニューラルネットワークの話でしょうか？気になりますね。

[^1]: 良いコードが書けるとは言っていない
[^2]: ゴリゴリと言ってもまだ20KLOCくらいしかなかったりする