RustでFetch API with Emscripten

  • 4
    いいね
  • 0
    コメント

はじめに

RustプログラムをWebブラウザ上で動かせるようになってしばらくが経ちました。(参照: RustでWebフロントエンド開発)
本稿では、ブラウザ上で動くRustプログラムで、Fetch APIを使って外部リソースを取得する方法をご紹介します。

準備

Cargoでプロジェクトを生成します。

$ cargo new --bin fetch-example

Cargo.tomldependenciesを追記します。

Cargo.toml
...

[dependencies]
emscripten-sys = "0.3"
serde_json = "1.0"

また、WebAssemblyにビルドするために、Emscriptenの環境変数を読み込んでおきます。

$ source path/to/emsdk/emsdk_env.sh

プログラム

fetchでJSONファイルを取得して、そのフィールドの値をコンソールに出力するプログラムを作ります。
data.jsonの中身は以下のようにしておきます。

data.json
{
  "a": 12.34
}

まずはじめに、プログラムの全体を掲載します。

src/main.rs
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_ptrCStrに変換できますが、これは入力が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.jsonscriptsセクションを以下のように書き換えます。

package.json
{
  ...,
  "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を以下のように用意します。

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フロントエンドプログラミングでできることが大きく広がりますね。

詳細は以下のページもご覧ください。