Rust + WebAssembly でpngデコードを行うnode_moduleを作ってみる

この記事は[「WACUL Advent Calendar 2017」](http://qiita.com/advent-calendar/2017/wacul)の5日目です。
WACULでフロントエンドエンジニアをしている@bokuwebと申します。

表題の通りですが`Rust`と`WebAssembly`を使用して`png`デコードを行う`node_module`を作ってみました。

## モチベーション

- あるmoduleで使用している`pngjs`によるデコード処理が時間を食っており`wasm`で高速化できないかの調査
- `wasm32-unknown-unknown`を使って`node_module`を作るとこまで体験しときたい

## リポジトリ

https://github.com/bokuweb/rust-wasm-png-decoder-example

## RustとWebAssembly

これまでは`wasm32-unknown-emscripten`を指定して、`emscripten`を介して`wasm`を出力する必要があったんですが、先日のリリースにおいて`1.24.0-nightly`で`wasm32-unknown-unknown`というtargetが指定可能となりました。これにより`emscripten`なしに`Rust`から`wasm`を吐けるようになりました。めでたい :tada:

## まずは簡単なモジュールを作ってみる

まずは引数に1を加算して返すだけの単純なモジュールを作ってみます。

### Rust側 

セットアップ。

``` sh
rustup update
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo init --bin add
```

`main.rs`を編集します。

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

#[no_mangle]
pub fn add_one(x: i32) -> i32 {
    x + 1
}
```

ビルド。

```
rustc +nightly --target wasm32-unknown-unknown src/main.rs -o main.wasm
```

`main.wasm`が出力されていたら成功です。
以下は蛇足で今回の調査で知ったんですが、`wasm-gc`という`Rust`製のツールがあるようで、こいつを使用することで未使用関数の削除などが行われサイズダウンができるようです。

```
cargo install --git https://github.com/alexcrichton/wasm-gc
wasm-gc main.wasm main.min.wasm
```

これにより本ケースでは15kiBが108Bまで落ちました。（この時点では、めっちゃおちるじゃないですか。やだー。ってなってたんですが、現実は甘くなかったです。後述。)

### JS側

``` Javascript:add/index.js
const fs = require('fs');

const wasm = fs.readFileSync('./main.wasm');
const mod = new WebAssembly.Instance(new WebAssembly.Module(wasm));

console.log(mod.exports.add_one(2)); // -> 3
```

```
node index.js
```

良さそうです。

## pngのデコードを行う

では本題のpngデコードを行ってみます。

### Rust側

`emscripten`がないため`alloc`,`free`は自分で用意する必要があります。
JSとのやり取りも含め以下のリポジトリを参考にさせていただきました。

https://github.com/KOBA789/rust-wasm

今回初めて知ったんですが、`mem::forget()`を使用しているのがポイントらしく、これによりスコープを抜けた後も確保したヒープが解放されないようです。`free`のほうは指定領域のヒープを再確保してやり`Drop`時に解放してもらう感じでしょうか。

pngのデコードには以下の`crate`を使用していて、`JS`側からもらったバッファをこいつに食わせることでデコードを行っています。

https://github.com/PistonDevelopers/image-png

後はwidthやheightといった情報とデコード結果へのポインタをシリアライズして`JS`側に返しています。この辺シリアライズ・デシリアライズのコストも勿体無いのでなんとかしたいのですが、いい方法がわかっていません。

``` Rust:src/main.rs
extern crate png;

use std::os::raw::{c_char, c_void};
use std::mem;
use std::ffi::CString;

#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;

#[no_mangle]
pub fn alloc(size: usize) -> *mut c_void {
    let mut buf = Vec::with_capacity(size);
    let ptr = buf.as_mut_ptr();
    mem::forget(buf);
    return ptr as *mut c_void;
}

#[no_mangle]
pub fn free(ptr: *mut c_void, size: usize) {
    unsafe {
        let _buf = Vec::from_raw_parts(ptr, 0, size);
    }
}

fn main() {}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DecodingResult {
    ptr: u32,
    len: u32,
    width: u32,
    height: u32,
}

#[no_mangle]
pub fn decode(ptr: *mut u8, len: usize) -> *mut c_char {
    let buf: &[u8] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    let decoder = png::Decoder::new(buf);
    let (info, mut reader) = match decoder.read_info() {
        Ok(i) => i,
        Err(why) => panic!(why.to_string()),
    };
    let mut img_data = vec![0; info.buffer_size()];
    reader.next_frame(&mut img_data).unwrap();
    let result = DecodingResult {
        ptr: img_data.as_mut_ptr() as u32,
        len: info.buffer_size() as u32,
        width: info.width,
        height: info.height,
    };
    mem::forget(img_data);
    let res = serde_json::to_string(&result).unwrap();
    let c_str = CString::new(res).unwrap();
    c_str.into_raw()
}
```

### JS側

JSONをパースしたりポインタからデータを取り出したりしてるだけですね。注意点は`free`し忘れないことですね。`JS`書いてる気分でいると抜けます。

``` Javascript:index.js
'use strict';

const fs = require('fs');
const wasm = fs.readFileSync('./wasm/main.wasm');
const mod = new WebAssembly.Instance(new WebAssembly.Module(wasm));

module.exports = (buf) => {
    const heap = new Uint8Array(mod.exports.memory.buffer);
    const ptr = mod.exports.alloc(buf.length);
    heap.set(buf, ptr);

    const resultPtr = mod.exports.decode(ptr, buf.length);
    mod.exports.free(ptr, buf.length)
    const resultBuf = new Uint8Array(mod.exports.memory.buffer, resultPtr);
    const getSize = (buf) => {
        let i = 0;
        while (buf[i] !== 0) i++;
        return i;
    }
    const resultSize = getSize(resultBuf);
    const json = String.fromCharCode.apply(null, resultBuf.slice(0, resultSize));
    mod.exports.free(resultPtr, resultSize)
    const result = JSON.parse(json);
    const data = new Uint8Array(result.len);
    const target = new Uint8Array(mod.exports.memory.buffer, result.ptr, result.len);
    data.set(target);
    const ret = {
        width: result.width,
        height: result.height,
        buf: data
    }
    mod.exports.free(result.ptr, result.len);
    return ret;
}
```

### サイズ

今回サンプルの最終的なサイズは以下になりました。なんだかんだ、大きくなりますね。。。かなしい。

|項目  |サイズ  |
|---|---|
|rust -> wasm後  |370KiB|
|wasm-gc後  |354KiB  |
|wasm-gc後にgzip  |73KiB  |

### ベンチマーク

それでは速度比較を行ってみます。
対象となるpngはpng crateに含まれていたテスト用pngから10枚選択しています。
結果は以下。

```　txt:log
## 0.png
rust-png wasm x 10,912 ops/sec ±0.87% (90 runs sampled)
pngjs x 2,882 ops/sec ±1.30% (85 runs sampled)
Fastest is rust-png wasm

## 1.png
rust-png wasm x 6,913 ops/sec ±14.55% (62 runs sampled)
pngjs x 2,839 ops/sec ±1.11% (89 runs sampled)
Fastest is rust-png wasm

## 2.png
rust-png wasm x 12,349 ops/sec ±1.83% (85 runs sampled)
pngjs x 2,839 ops/sec ±1.42% (90 runs sampled)
Fastest is rust-png wasm

## 3.png
rust-png wasm x 8,360 ops/sec ±0.84% (90 runs sampled)
pngjs x 2,801 ops/sec ±1.61% (88 runs sampled)
Fastest is rust-png wasm

## 4.png
rust-png wasm x 8,407 ops/sec ±1.08% (92 runs sampled)
pngjs x 6,935 ops/sec ±2.71% (85 runs sampled)
Fastest is rust-png wasm

## 5.png
rust-png wasm x 5,732 ops/sec ±1.49% (91 runs sampled)
pngjs x 2,414 ops/sec ±1.14% (90 runs sampled)
Fastest is rust-png wasm

## 6.png
rust-png wasm x 6,643 ops/sec ±1.01% (91 runs sampled)
pngjs x 7,138 ops/sec ±2.16% (80 runs sampled)
Fastest is pngjs

## 7.png
rust-png wasm x 2,220 ops/sec ±0.88% (90 runs sampled)
pngjs x 1,957 ops/sec ±1.28% (89 runs sampled)
Fastest is rust-png wasm

## 8.png
rust-png wasm x 11,944 ops/sec ±1.01% (91 runs sampled)
pngjs x 2,929 ops/sec ±1.12% (90 runs sampled)
Fastest is rust-png wasm

## 9.png
rust-png wasm x 5,229 ops/sec ±0.92% (87 runs sampled)
pngjs x 10,119 ops/sec ±1.12% (92 runs sampled)
Fastest is pngjs
```

![スクリーンショット 2017-12-04 20.39.42.png](https://qiita-image-store.s3.amazonaws.com/0/91510/b0d1c58e-5587-e7b6-bb0a-e113811fc481.png)


＊キャプチャだと単位等いろいろぬけているので下記リンクを参照してください
https://bokuweb.github.io/rust-wasm-png-decoder-example/index.html

`wasm`の方が高速でよかったですねー。と終わりたかったんですが、JS版がwasm版より倍近く速いケースが見つかりました。(9.png)。僅差でＪＳの方が速い（6.pngのような）ケースは発生しうるんじゃないかと思っていましたが、なぜこれほどの差が発生するのかは分かっておらず、要調査。

## まとめ

`wasm32-unknown-unknown`で`emscripten`なしに`node_module`を作製してみました。グルーコードを自身で用意する必要はありますが、`emscripten`分の容量を削減できたり、`emscripten`の古いnodeに苦しめられることもなくなるので可能な限りこちらを使用したいですね。

速度に関しては、「wasmの方が速いよー」とは現時点では言うことができなそうで、要調査。

また、今回のサンプルモジュールは以下で使用できます。

```
npm i https://github.com/bokuweb/rust-wasm-png-decoder-example
```

``` Javascript
const fs = require('fs');
const decode = require("wasm-png-decoder");
console.log(decode(fs.readFileSync('hoge.png')));
```