はじめに
RustプログラムをWebブラウザ上で動かせるようになってしばらくが経ちました。(参照: RustでWebフロントエンド開発)
本稿では、ブラウザ上で動くRustプログラムで、Fetch APIを使って外部リソースを取得する方法をご紹介します。
準備
Cargoでプロジェクトを生成します。
$ cargo new --bin fetch-example
Cargo.toml
のdependencies
を追記します。
...
[dependencies]
emscripten-sys = "0.3"
serde_json = "1.0"
また、WebAssemblyにビルドするために、Emscriptenの環境変数を読み込んでおきます。
$ source path/to/emsdk/emsdk_env.sh
プログラム
fetchでJSONファイルを取得して、そのフィールドの値をコンソールに出力するプログラムを作ります。
data.jsonの中身は以下のようにしておきます。
{
"a": 12.34
}
まずはじめに、プログラムの全体を掲載します。
extern crate emscripten_sys;
extern crate serde_json;
use emscripten_sys::{emscripten_fetch_t, emscripten_fetch_attr_t, emscripten_fetch_attr_init,
emscripten_fetch, emscripten_fetch_close};
fn body_string(fetch: &emscripten_fetch_t) -> String {
let data = unsafe { std::mem::transmute::<*const i8, *mut u8>((*fetch).data) };
let len = (*fetch).totalBytes as usize;
let slice = unsafe { std::slice::from_raw_parts(data, len) };
let mut v = Vec::with_capacity(len);
v.resize(len, 0);
v.copy_from_slice(slice);
String::from_utf8(v).ok().unwrap()
}
fn print_json(fetch: &emscripten_fetch_t) {
let body = body_string(fetch);
match serde_json::from_str::<serde_json::Value>(&body) {
Ok(obj) => {
println!("{}", obj["a"]);
}
Err(e) => {
println!("error: line: {}, column: {}", e.line(), e.column());
}
}
}
extern "C" fn handle_success(fetch: *mut emscripten_fetch_t) {
unsafe {
print_json(&*fetch);
emscripten_fetch_close(fetch);
}
}
extern "C" fn handle_error(fetch: *mut emscripten_fetch_t) {
unsafe {
println!("error: status code {}", (*fetch).status);
emscripten_fetch_close(fetch);
}
}
fn main() {
unsafe {
let mut fetch_arg: emscripten_fetch_attr_t = std::mem::uninitialized();
emscripten_fetch_attr_init(&mut fetch_arg);
fetch_arg.attributes = 1;
fetch_arg.onsuccess = Some(handle_success);
fetch_arg.onerror = Some(handle_error);
let url = std::ffi::CString::new("data.json").unwrap();
emscripten_fetch(&mut fetch_arg, url.as_ptr());
}
}
ffi絡みなのでunsafeだらけですね。
EmscriptenのAPIにアクセスするためにemscripten-sysを使用しています。
これは、C/C++でのemscripten.h
に相当します。
main
関数では、オプションとURLを作成して、fetchの実行をしています。
fetch_arg.attributes = 1;
の部分はEMSCRIPTEN_FETCH_LOAD_TO_MEMORY
というモード設定をしています。
emscripten-sysでは#define
による定数定義が消えてしまっているので、ここでは取り急ぎそれが表す値を直接代入しています。
このモードでは、非同期にデータ取得を行いメモリに読み込むという、JavaScriptでのfetchと同等の動作をします。
fetch_arg.onsuccess = Some(handle_success);
とfetch_arg.onerror = Some(handle_error);
は、それぞれfetch成功時と失敗時のコールバックを指定しています。
それぞれの関数はextern "C"
としてマークしておく必要があります。
body_string
関数では、emscripten_fetch_t
からメッセージボディをString
として取得します。
メッセージボディは(*fetch).data
に格納されており、その型は*const c_char
です。
一般的には、*const c_char
からCStr::from_ptr
でCStr
に変換できますが、これは入力がNULL終端であることを前提としています。
残念ながら、(*fetch).data
はNULL終端になっていないので別の手段を取る必要があります。
また、emscripten_fetch_close
のタイミングで、(*fetch).data
の指す領域は解放されてしまうので、別の領域にコピーをしておく必要があります。
そういった事情で、ここではメッセージボディをVec<u8>
にコピーし、そこからString
を作成するという方法にしています。
良い方法があったら教えて欲しい…。
handle_success
関数では、メッセージボディを取得し、serde_jsonでJSONへとデシリアライズします。
serde_jsonでは、任意のStructとしてデシリアライズ結果を受け取ることもできます。
ビルドと実行
上記のプログラムを実行しましょう。
まず、以下のコマンドでpackage.json
を生成し、http-server
をインストールします。
$ npm init -y
$ npm i -D http-server
npm start
でビルドからhttp-server
の立ち上げまでを行えるように、package.json
のscripts
セクションを以下のように書き換えます。
{
...,
"scripts": {
"build": "EMMAKEN_CFLAGS=\"-s FETCH=1\" cargo build --target=wasm32-unknown-emscripten --release",
"copy": "npm run copy:js && npm run copy:wasm",
"copy:js": "cp target/wasm32-unknown-emscripten/release/*.js ./fetch_example.js",
"copy:wasm": "cp target/wasm32-unknown-emscripten/release/deps/*.wasm ./fetch_example.wasm",
"start": "npm run build && npm run copy && http-server"
},
...
}
index.html
を以下のように用意します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title></title>
</head>
<body>
<script>
var Module = {}
fetch('fetch_example.wasm')
.then((response) => response.arrayBuffer())
.then((buffer) => {
Module.wasmBinary = buffer
const script = document.createElement('script')
script.src = 'fetch_example.js'
document.body.appendChild(script)
})
</script>
</body>
</html>
以下のコマンドを実行した後、ブラウザでlocalhost:8080
にアクセスします。
$ npm start
コンソールを開くと、12.34
と表示されていることが確認できるかと思います。
おわりに
外部リソースの取得ができるようになると、Webフロントエンドプログラミングでできることが大きく広がりますね。
詳細は以下のページもご覧ください。