📕

(メモ)Rust で WebAssembly 上でファイルを読みたい

に公開

WASI の場合

試してないですが、WASI には filesystem API があるので(使う WebAssembly ランタイムがそれをサポートしているなら)ファイルの読み書きはできそうです。

https://github.com/WebAssembly/wasi-filesystem

Node.js などの場合

fs モジュールを提供している JavaScript ランタイムの場合は、それが提供している関数を持ってくれば使えるようになるらしいです。以下は、wasm-bindgen のレポジトリでの質問への回答ですが、Electron の場合の例です。

#[wasm_bindgen]
extern "C" {
    type Buffer;
}

#[wasm_bindgen(module = "fs")]
extern "C" {
    #[wasm_bindgen(js_name = readFileSync, catch)]
    fn read_file(path: &str) -> Result<Buffer, JsValue>;
}

ブラウザ上、同期実行の場合

こういうコードになるらしいです。

https://gitlab.com/bullbytes/read_file_with_wasm

要点は、

  • FileReaderSync を使う
  • web worker 上で動かす必要がある(FileReaderSync がそういうものなので)

ということみたいです。どういうことか、もう少し詳しく見ていきましょう。

wasm-bindgen-file-reader crate の実装を見てみる

↑からコードを抜粋すると、これが実際にファイルを読んでいる箇所です。受け取った web_sys::File という型のオブジェクトを WebSysFile という型に変換することで、seek() とか read_exact() とかが使えるようになるみたいです。

#[wasm_bindgen]
pub fn read_at_offset_sync(file: web_sys::File, offset: u64) -> u8 {
    ...
        let mut wf = WebSysFile::new(file);
    ...
        // The Read API works as with real files
        wf.read_exact(&mut buf).expect("failed to read bytes");

WebSysFilewasm-bindgen-file-reader という crate が提供しています。この実装はそんなに難しくなく、100行ちょっとくらいのものです。

https://github.com/Badel2/wasm-bindgen-file-reader/blob/master/src/lib.rs

重要そうなところをいくつか抜粋してみましょう。まず、WebSysFileは、ファイルとそのカーソル位置を保持するシンプルな struct です。

/// Wrapper around a `web_sys::File` that implements `Read` and `Seek`.
pub struct WebSysFile {
    file: web_sys::File,
    pos: u64,
}

そして、ここが実際にデータを読んでいるところ(Read trait の実装)です。

thread_local! {
    static FILE_READER_SYNC: FileReaderSync = FileReaderSync::new().expect("Failed to create FileReaderSync. Help: make sure this is a web worker context.");
}

...

impl Read for WebSysFile {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
        ...
        let blob = self
            .file
            .slice_with_f64_and_f64(offset_f64, offset_end_f64)
            .expect("failed to slice file");
        let array_buffer = FILE_READER_SYNC.with(|file_reader_sync| {
            file_reader_sync
                .read_as_array_buffer(&blob)
                .expect("failed to read as array buffer")
        });
        let array = Uint8Array::new(&array_buffer);
        ...
        array.copy_to(&mut buf[..actual_read_bytes_usize]);

流れはこうです。

  1. Blob::slice_with_f64_and_f64()[1] で指定した範囲のバイト列を表すオブジェクトをつくる(この実態が何なのかはよくわからないが、この時点ではまだ実際の read は発生してなさそう)
  2. そのオブジェクトを FileReaderSync::read_as_array_buffer()ArrayBuffer としてデータを取り出す
  3. それを Uint8Array に変換して、Uint8Array::copy_to()&[u8] に書き込む

これを使っている JavaScript コードを見てみる

ここで、FileReaderSync は同期実行されるので、web worker でのみ使えるものです。

https://developer.mozilla.org/ja/docs/Web/API/FileReaderSync

上のコード例のレポジトリでも、やはり web worker の中から呼び出すようにしています。

https://gitlab.com/bullbytes/read_file_with_wasm/-/blob/main/www/index.js?ref_type=heads:

var myWorker = new Worker('./worker.js');
...
myWorker.postMessage({ file: file, offset: BigInt(0) });

https://gitlab.com/bullbytes/read_file_with_wasm/-/blob/main/www/worker.js?ref_type=heads:

onmessage = async function(e) {
    console.log("onmessage inside worker.js runs");
    let workerResult = read_at_offset_sync(
        e.data.file,
        e.data.offset,
    );
    postMessage(workerResult);
};

ブラウザ上、非同期実行の場合

非同期実行の API を使う場合は、返り値は Promise 型になっています。これを Rust の側で扱えるようにするには、wasm-bindgen-future を使うといいようです。Wasm 自体には非同期処理を扱う仕組みはないはずなので、どのように実現しているのかはよくわかりません。wasm-bindgen 公式が提供しているものなので、まあ信用してよさそうな気はします。

https://wasm-bindgen.github.io/wasm-bindgen/reference/js-promises-and-rust-futures.html

ただ、わざわざ Rust で書きたいような処理ということは、おそらく重いファイルを読んで重い処理をするようなケースが多そうで、そうなると結局 web worker 上でやった方がよさそうです。あえて非同期処理を選ぶケースは少ないのかもしれません。

あと、たまたま見つけたんですが、自前で非同期処理を扱う仕組みを実装するという道もあるようです。tokio が採用されない理由が興味深かったです。

https://zenn.dev/uhyo/articles/nodejs-wasm-async-communication

脚注
  1. WebSysFile が抱えているのは Blob ではなく File だが、Deref<Target = Blob> で使えるメソッド。 ↩︎

Discussion