この記事は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
で変更します。
動きました。
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!
に通したLogMessage
をjs!
マクロを通して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サーバーの起動をします。
そしてブラウザで表示します。
サーバー側の実装を用意していないのでサーバーにログを投げている所は404になってしまっていますが、DOMの書き換え、consoleへのログ出力、サーバーへのログ送信ができました。
今回書いてみて思った事
今までHello World程度しかwasmには触れてなかったのですが、今回少しコードを書いてみてサーバー側とコードを共有できる部分について結構便利かもしれないなと思いました。
またstdwebのjs!
マクロも思ってた以上にjsがそのまま書ける感じでしたし、rust側のAPIも思ってたよりは揃っている感じで趣味程度の物ならもう十分作っていけそうかなと感じました。