RustでWebAssemblyしてみました

この記事は[Wanoグループ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/wano-group)の22日目の記事です。

RustはWebAssemblyを出力する事ができ、更にWebAssemblyで使用する為の[stdweb](https://github.com/koute/stdweb)というライブラリがあります。
今回はこれを使ってHello World+α程度の事をやってみたいと思います。

## まずはwasm生成の為に必要なツールの準備

https://github.com/koute/stdweb#getting-started を参考にしてまずは必要なツールを準備します。

いきなり資料に書いてない事なのですが、nightly版のツールチェインでないとwasmの生成が出来ないのでまずは作業ディレクトリでnightly版を使えるようにrustupで設定します。

```shell
$ rustup override set nightly
```

次に最新のnightlyに更新します。
私は更新をしないで進めたら後の工程が上手く動きませんでした。

```shell
$ rustup update nightly
```

更新をしたら次にwasm32-unknown-unknownターゲットをインストールします。

```shell
$ rustup target add wasm32-unknown-unknown
```

次に[cargo-web](https://github.com/koute/cargo-web)をインストールします。
wasm用のコードを実装していくのに便利なツールです。

```shell
$ cargo install -f cargo-web
```

これで準備が整いました。


## Hello Worldをしてみます

とりあえずHello Worldします。

```shell
$ cargo new hello --bin
```

プロジェクトを作って、Cargo.tomlのdependenciesに下記の内容を追記します。

```toml
stdweb = "*"
```

main.rsを次の様に書き換えます。

```rust
#[macro_use]
extern crate stdweb;

fn main() {
    stdweb::initialize();

    let message = "はろーわーるど";
    js! {
        alert( @{message} );
        console.log( @{message} );
    }

    stdweb::event_loop();
}
```

`js!`マクロを使う事でJavaScriptのコードをRustのコード内で書くことが出来ます。
更に`@{変数名}`とすることでRustの変数を`js!`内に含めることが出来ます。

そして、次のコマンドを叩いてブラウザで確認できる状態にします。

```shell
$ cargo web start --target-webasm
```

これでwasmのビルドと動作確認用のHTTPサーバーが立ち上がります。
デフォルトでは`::1`のポート8000でリッスンします。問題がある場合は`--host`と`--port`で変更します。

ブラウザで確認してみます。
![rust_wasm_hello_world.gif](https://qiita-image-store.s3.amazonaws.com/0/195271/a0c650af-8cf9-cbcb-3f8e-df87be318f0f.gif)

動きました。


## DOMをいじったりログを出力したりサーバに飛ばしたりしてみます

DOMをいじったり、[log](https://crates.io/crates/log)を使ってloggerを作ってみます。

まずはCargo.tomlのdependenciesに次の内容を追記します。

```toml
log = "*"
serde = "1.0"
serde_derive = "1.0"
```

main.rsを下記の様に変更します。

```rust
#[macro_use]
extern crate log;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate stdweb;

use stdweb::web::*;

#[derive(Debug, Serialize, Deserialize)]
pub struct LogMessage {
    pub message: String
}

js_serializable!( LogMessage );

#[derive(Debug, Clone)]
enum LoggerOutput {
    Remote(String),
    Console,
}

#[derive(Debug, Clone)]
struct RemoteLogger {
    output: Vec<LoggerOutput>,
    filter_level: log::LogLevelFilter,
}

impl RemoteLogger {
    fn init(output: Vec<LoggerOutput>, filter_level: log::LogLevelFilter) {
        let logger = RemoteLogger { output, filter_level };
        log::set_logger(|max_log_level_filter| {
            max_log_level_filter.set(logger.filter_level);
            Box::new(logger)
        }).unwrap();
    }

    fn format_log(record: &log::LogRecord) -> String {
        format!(
            "[{}] {} ({}: {})",
            record.level(),
            record.args(),
            record.location().file(),
            record.location().line(),
        )
    }

    fn log_to_remote<S: AsRef<str>>(record: &log::LogRecord, endpoint: S) {
        let endpoint = endpoint.as_ref();
        let message = &LogMessage {
            message: RemoteLogger::format_log(record)
        };
        js! {
            fetch( @{endpoint}, {
                method: "POST",
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/json; charset=utf-8"
                },
                body: JSON.stringify( @{message} ),
            });
        }
    }

    fn log_to_console(record: &log::LogRecord) {
        let message = RemoteLogger::format_log(record);
        js! { console.log( @{message} ); }
    }
}

impl log::Log for RemoteLogger {
    fn enabled(&self, metadata: &log::LogMetadata) -> bool {
        metadata.level() <= self.filter_level
    }

    fn log(&self, record: &log::LogRecord) {
        if self.enabled(record.metadata()) {
            for dest in &self.output {
                match dest {
                    &LoggerOutput::Remote(ref endpoint) => RemoteLogger::log_to_remote(record, endpoint),
                    &LoggerOutput::Console => RemoteLogger::log_to_console(record),
                }
            }
        }
    }
}

fn main() {
    stdweb::initialize();
    RemoteLogger::init(
        vec![LoggerOutput::Console, LoggerOutput::Remote("/log_endpoint".to_string())],
        log::LogLevelFilter::Debug,
    );

    debug!("program initilized");

    info!("Hello, world!");

    let message = "はろーわーるど";

    js! {
        alert( @{message} );
        console.log( @{message} );
    }

    if let Some(body) = document().query_selector("body") {
        let message = document().create_text_node(message);
        body.append_child(&message);
    } else {
        error!("body element is not found");
        return;
    }

    let hello = |t: String| {
        info!("[Rust] {}", t);
        js! { console.log( "[JS] " + @{format!("- {} -", t)} ); }
    };

    js!{
        const hello = @{hello};
        hello("fff");
        hello.drop(); // ここすごいめんどくさそう
    }

    debug!("start: stdweb::event_loop()");
    stdweb::event_loop();
}
```

一気にコード量が増えましたが、半分くらいは`Log` traitを実装しているだけです。

```rust
#[derive(Debug, Serialize, Deserialize)]
pub struct LogMessage {
    pub message: String
}

js_serializable!( LogMessage );
```

ここではサーバーに投げるログJSONを定義しています。
この定義をwasmとサーバー共通のライブラリでする事でクライアントとサーバーで同じコードを使用する事ができます。
同様の事は型定義以外の処理（例えばパースとかバリデーションとか）でも出来ると思います。

`Serialize` traitを実装し、`js_serializable!`マクロを通すと`js!`マクロを通してJSコードに渡せる型にすることが出来ます。

```rust
    fn log_to_remote<S: AsRef<str>>(record: &log::LogRecord, endpoint: S) {
        let endpoint = endpoint.as_ref();
        let message = &LogMessage {
            message: RemoteLogger::format_log(record)
        };
        js! {
            fetch( @{endpoint}, {
                method: "POST",
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/json; charset=utf-8"
                },
                body: JSON.stringify( @{message} ),
            });
        }
    }
```

上で`js_serializable!`に通した`LogMessage`を`js!`マクロを通して`fetch`に渡しています。
このようにしてJS側に構造体を渡すことが出来ます。

```rust
fn main() {
    stdweb::initialize();
    RemoteLogger::init(
        vec![LoggerOutput::Console, LoggerOutput::Remote("/log_endpoint".to_string())],
        log::LogLevelFilter::Debug,
    );
```

`Log` traitを実装したので、main関数の開始直後にロガーを初期化します。
ログをフィルタするレベルをDebugにして、console.logとサーバーへのログ送信の両方を行うようにしています。
後は`log` crateで定義されている`debug!`/`info!`/`warn!`/`error!`マクロを使う事でログ出力する事ができます。

```rust
    if let Some(body) = document().query_selector("body") {
        let message = document().create_text_node(message);
        body.append_child(&message);
    } else {
        error!("body element is not found");
        return;
    }
```

main関数内のこのコードはrustからDOMを操作しています。
ここではbody要素にテキストノードを追加するだけの単純な処理を行っています。

これらの関数は`stdweb::web`の中で定義されています。
ここではmain.rsの最初の方で`use stdweb::web::*;`として関数名等を名前空間に取り込んでいます。

では実際に動かしてみます。

まずは`$ cargo web start --target-webasm`をしてビルドとHTTPサーバーの起動をします。

そしてブラウザで表示します。

![rust_wasm_log_and_dom_3.gif](https://qiita-image-store.s3.amazonaws.com/0/195271/9b6fb19b-ecae-8f4f-2c30-db437571b233.gif)

サーバー側の実装を用意していないのでサーバーにログを投げている所は404になってしまっていますが、DOMの書き換え、consoleへのログ出力、サーバーへのログ送信ができました。


## 今回書いてみて思った事

今までHello World程度しかwasmには触れてなかったのですが、今回少しコードを書いてみてサーバー側とコードを共有できる部分について結構便利かもしれないなと思いました。
またstdwebの`js!`マクロも思ってた以上にjsがそのまま書ける感じでしたし、rust側のAPIも思ってたよりは揃っている感じで趣味程度の物ならもう十分作っていけそうかなと感じました。