Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
16
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

RustでWebAssemblyしてみました

この記事はWanoグループ Advent Calendar 2017の22日目の記事です。

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

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

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

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

$ rustup override set nightly

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

$ rustup update nightly

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

$ rustup target add wasm32-unknown-unknown

次にcargo-webをインストールします。
wasm用のコードを実装していくのに便利なツールです。

$ cargo install -f cargo-web

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

Hello Worldをしてみます

とりあえずHello Worldします。

$ cargo new hello --bin

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

stdweb = "*"

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

#[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!内に含めることが出来ます。

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

$ cargo web start --target-webasm

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

ブラウザで確認してみます。
rust_wasm_hello_world.gif

動きました。

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

DOMをいじったり、logを使ってloggerを作ってみます。

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

log = "*"
serde = "1.0"
serde_derive = "1.0"

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

#[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を実装しているだけです。

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

js_serializable!( LogMessage );

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

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

    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!に通したLogMessagejs!マクロを通してfetchに渡しています。
このようにしてJS側に構造体を渡すことが出来ます。

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!マクロを使う事でログ出力する事ができます。

    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

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

今回書いてみて思った事

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
16
Help us understand the problem. What are the problem?