RustでWebAssemblyを書いてみる

# Rustとは

効率性、安全性などに優れた言語で、元々はMozillaの中の人によって作られました。
コンパイルしてネイティブコードに落とせるので、CやC++の代替として期待されています。

後発なので言語仕様は他の言語の美味しいところを集めてもいますが、なかなか独特です。
学習が難しいと言われつつも、エンジニアに最も愛される言語(Stack Overflow調べ)では連覇を重ねています。

https://news.mynavi.jp/article/20210806-1939902/

私もまとまったものを書くのは初めてですので、拙いところは見逃してくださいませ。

# WebAssemblyとは

WASMと略します。
MDNによれば：

> WebAssembly はモダンなウェブブラウザーで実行できる新しいタイプのコードです。ネイティブに近いパフォーマンスで動作するコンパクトなバイナリー形式の低レベルなアセンブリ風言語です。さらに、 C/C++ や Rust のような言語のコンパイル対象となって、それらの言語をウェブ上で実行することができます。 WebAssembly は JavaScript と並行して動作するように設計されているため、両方を連携させることができます。

# 実験

JavaScriptで書きにくいバイナリ処理をRust=>WASMで書くようなケースを想定して、今回は簡単な画像処理をRustで書いてWasmにコンパイルし、JavaScriptから呼びだして実行するようなサンプルプログラムを書いてみようと思います。

ソースコード一式はこちらに置きました。

https://github.com/hirayama-onagigawa-computing/wasm_sample1

## ワークスペースの作成

vue-cliとwasm-packを使ってワークスペースを作成します。

```shell-session
$ vue create wasm_test
$ cd wasm_test
$ wasm-pack new wasm
```
## Rust側の実装

imageクレート(≒パッケージ)を使用して画像変換を行うコードを実装します。

JS側にexportするメソッドは`#[wasm_bindgen]`というattributeを付けます。

src/lib.rsの中身は以下のようにしました。

```rust
use wasm_bindgen::prelude::*;
use image::*;

enum ConvertMode {
    Grayscale,
    FlipHorizontal,
    FlipVertical,
    RotateLeft,
    RotateRight,
    Blur,
}

fn convert(mode: ConvertMode, width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    let rgba = RgbaImage::from_raw(width, height, raw_data).expect("error");
    let di = DynamicImage::ImageRgba8(rgba);

    let converted = match mode {
        ConvertMode::Grayscale => {
            let gray = di.into_luma8();
            // 再度rgbaに戻す
            DynamicImage::ImageLuma8(gray)
        },
        ConvertMode::FlipHorizontal => di.fliph(),
        ConvertMode::FlipVertical => di.flipv(),
        ConvertMode::RotateLeft => di.rotate270(),
        ConvertMode::RotateRight => di.rotate90(),
        ConvertMode::Blur => di.blur(3.0),
    };

    converted.into_rgba8().to_vec()
}

#[wasm_bindgen]
pub fn grayscale(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::Grayscale, width, height, raw_data)
}

#[wasm_bindgen]
pub fn flip_horizontal(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::FlipHorizontal, width, height, raw_data)
}

#[wasm_bindgen]
pub fn flip_vertical(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::FlipVertical, width, height, raw_data)
}

#[wasm_bindgen]
pub fn rotate_left(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::RotateLeft, width, height, raw_data)
}

#[wasm_bindgen]
pub fn rotate_right(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::RotateRight, width, height, raw_data)
}

#[wasm_bindgen]
pub fn blur(width: u32, height: u32, raw_data: Vec<u8>) -> Vec<u8> {
    convert(ConvertMode::Blur, width, height, raw_data)
}
```

`wasm-pack build` でビルドすると、`wasm/pkg`の下に関連ファイル一式が出力されます。

## JavaScriptから使う

### WASMのロード

このようなメソッドでimportを使ってロードします。

```javascript
        async loadWasm() {
            const wasm = import("../wasm/pkg");

            this.wasm = await wasm;
        }
```

### WASM内のメソッドを呼ぶ

通常のJavaScriptオブジェクトのように呼び出すことができます。

```javascript
this.wasm.grayscale(...)
```

### ソース全体

App.vueの全体はこのようになります。

```vue
<template>
    <div class="container">
        <div class="row my-2">
            <input type="file" ref="file" @change="loadImage">
        </div>
        <div class="row my-2">
            <div class="col ">
                <button class="btn btn-primary" type="button" @click="original">オリジナル</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="grayscale">グレースケール</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="flip_horizontal">左右反転</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="flip_vertical">上下反転</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="rotate_left">左回転</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="rotate_right">右回転</button>
            </div>
            <div class="col">
                <button class="btn btn-primary" type="button" @click="blur">ぼかし</button>
            </div>
        </div>
        <div class="row my-2">
            <div class="col">
                <h4>Original</h4>
                <img ref="original" :style="original_style">
            </div>
            <div class="col">
                <h4>Converted</h4>
                <canvas ref="converted" :style="style"></canvas>
            </div>
        </div>
    </div>
</template>

<script>

const ConvertMode = {
    Grayscale: 1,
    FlipHorizontal: 2,
    FlipVertical: 3,
    RotateLeft: 4,
    RotateRight: 5,
    Blur: 6,
};

const MAX_IMAGE_SIZE = 500;

export default {
    name: 'App',
    data () {
        return {
            wasm: null,
            original_width: 0,
            original_height: 0,
            width: 0,
            height: 0,
        };
    },
    created () {
        this.loadWasm();
    },
    computed: {
        style () {
            return {
                width: this.width + "px",
                height: this.height + "px",
            };
        },
        original_style () {
            return {
                width: this.original_width + "px",
                height: this.original_height + "px",
            };
        },
    },
    methods: {
        async loadWasm() {
            const wasm = import("../wasm/pkg");

            this.wasm = await wasm;
        },
        readFile () {
            const input = this.$refs.file;
            if (!input.files.length) return;

            const file = input.files[0];

            const reader = new FileReader();
            const p = new Promise((resolve, reject) => {
                reader.addEventListener("load", function () {
                    resolve(reader.result);
                });
                reader.addEventListener("error", function () {
                    reject("error.");
                });
            });

            reader.readAsDataURL(file);

            return p;
        },
        drawOriginalImage(url) {
            const img = this.$refs.original;

            const p = new Promise((resolve) => {
                img.addEventListener("load", function () {
                    resolve();
                });
            });

            img.src = url;

            if (img.complete) {
                return Promise.resolve();
            }

            return p;
        },
        original () {
            const out = this.$refs.converted;

            const out_ctx = out.getContext('2d');

            const img = this.$refs.original;

            this.width = this.original_width;
            this.height = this.original_height;

            const max = Math.max(this.width, this.height);
            out_ctx.clearRect(0, 0, max, max);
            out_ctx.drawImage(img, 0, 0, this.original_width, this.original_height);
        },
        convert (mode) {
            const canvas = this.$refs.converted;

            const ctx = canvas.getContext('2d');

            const raw = ctx.getImageData(0, 0, this.width, this.height);

            let ret_data;
            switch (mode) {
                case ConvertMode.Grayscale:
                    ret_data = this.wasm.grayscale(this.width, this.height, raw.data);
                    break;
                case ConvertMode.FlipHorizontal:
                    ret_data = this.wasm.flip_horizontal(this.width, this.height, raw.data);
                    break;
                case ConvertMode.FlipVertical:
                    ret_data = this.wasm.flip_vertical(this.width, this.height, raw.data);
                    break;
                case ConvertMode.RotateLeft:
                    ret_data = this.wasm.rotate_left(this.width, this.height, raw.data);
                    break;
                case ConvertMode.RotateRight:
                    ret_data = this.wasm.rotate_right(this.width, this.height, raw.data);
                    break;
                case ConvertMode.Blur:
                    ret_data = this.wasm.blur(this.width, this.height, raw.data);
                    break;
            }

            const buf = Uint8ClampedArray.from(ret_data);

            if (mode === 4 || mode === 5) {
                const w = this.width;
                const h = this.height;
                this.width = h;
                this.height = w;
                canvas.width = this.width;
                canvas.height = this.height;
            }

            const max = Math.max(this.width, this.height);
            ctx.clearRect(0, 0, max, max);
            ctx.putImageData(new ImageData(buf, this.width, this.height), 0, 0);
        },
        grayscale () {
            this.convert(ConvertMode.Grayscale)
        },
        flip_horizontal () {
            this.convert(ConvertMode.FlipHorizontal);
        },
        flip_vertical () {
            this.convert(ConvertMode.FlipVertical);
        },
        rotate_left() {
            this.convert(ConvertMode.RotateLeft);
        },
        rotate_right() {
            this.convert(ConvertMode.RotateRight);
        },
        blur() {
            this.convert(ConvertMode.Blur);
        },
        async loadImage () {
            const url = await this.readFile();
            await this.drawOriginalImage(url);

            const img = this.$refs.original;

            const w = Math.min(img.naturalWidth, MAX_IMAGE_SIZE);
            const h = img.naturalHeight * (w / img.naturalWidth);

            this.width = this.original_width = w;
            this.height = this.original_height = h

            const out = this.$refs.converted;
            out.width = this.width;
            out.height = this.height;

            this.original();
        },
    },
};
</script>

<style lang="scss">
#app {
    img, canvas {
        margin: 0;
        padding: 0;
    }
}
</style>
```

# まとめ

バイナリなど低レベル操作が必要だったりパフォーマンスが欲しい場合には、C(++)やRustの知識があれば比較的簡単にJavaScriptと連携してウェブアプリに組み込むことができます。ぜひ一度お試しください。