(メモ)Rust で WebAssembly 上でファイルを読みたい
WASI の場合
試してないですが、WASI には filesystem API があるので(使う WebAssembly ランタイムがそれをサポートしているなら)ファイルの読み書きはできそうです。
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>;
}
ブラウザ上、同期実行の場合
こういうコードになるらしいです。
要点は、
-
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");
WebSysFile は wasm-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]);
流れはこうです。
-
Blob::slice_with_f64_and_f64()[1] で指定した範囲のバイト列を表すオブジェクトをつくる(この実態が何なのかはよくわからないが、この時点ではまだ実際の read は発生してなさそう) - そのオブジェクトを
FileReaderSync::read_as_array_buffer()でArrayBufferとしてデータを取り出す - それを
Uint8Arrayに変換して、Uint8Array::copy_to()で&[u8]に書き込む
これを使っている JavaScript コードを見てみる
ここで、FileReaderSync は同期実行されるので、web worker でのみ使えるものです。
上のコード例のレポジトリでも、やはり 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 公式が提供しているものなので、まあ信用してよさそうな気はします。
ただ、わざわざ Rust で書きたいような処理ということは、おそらく重いファイルを読んで重い処理をするようなケースが多そうで、そうなると結局 web worker 上でやった方がよさそうです。あえて非同期処理を選ぶケースは少ないのかもしれません。
あと、たまたま見つけたんですが、自前で非同期処理を扱う仕組みを実装するという道もあるようです。tokio が採用されない理由が興味深かったです。
-
WebSysFileが抱えているのはBlobではなくFileだが、Deref<Target = Blob>で使えるメソッド。 ↩︎
Discussion