I'm new to Rust and trying to implement a web page that shows a graph with many edges. I plan to use WebAssembly to lay out the graph and determine the positions of the nodes (and a WebGL library to draw the graph).
I want Rust/wasm to make a streaming request to a biggish (7mb potentially)binary composed of sixteen bit integers, delimited by the max value, representing target node indices for each index.
Because of the potentially big file, I'd like Rust to stream it and start laying out the graph as soon as it has the first chunk.
Eventually, I'd like to turn the JsValue
chunks of the response body into vectors of 16-bit integers, but just coercing the chunks into some kind of array type would be enough to unblock me.
Below is the start of my lib.rs
, which as far as I can tell at least works.
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{ReadableStreamDefaultReader, Response};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub async fn fetch_and_compute_graph() -> Result<JsValue, JsValue> {
let window = web_sys::window().unwrap();
let resp_promise = window.fetch_with_str(&"./edges.bin");
let resp_value = JsFuture::from(resp_promise).await?;
let resp: Response = resp_value.dyn_into().unwrap();
log(&format!("Response status code: {}", resp.status()));
if resp.status() != 200 {
return Err(JsValue::FALSE);
}
let reader_obj = resp.body().unwrap().get_reader();
let stream_reader: ReadableStreamDefaultReader = reader_obj.dyn_into().unwrap();
Below are two snippets I've tried putting after the above code:
Update: I've realised that JsFuture::from(stream_reader.read()).await?;
results in a JS object with two properties. So maybe I can just cast to Object
let chunk_obj = JsFuture::from(stream_reader.read()).await?;
let chunk_bytes: Uint8Array = chunk_obj.dyn_into().unwrap();
I would have expected this to work, given that the other type casts do. Maybe I've got the type wrong; the API docs aren't clear on what the promise should resolve to. However, manually inspecting one of the chunks resulting from a fetch
call in my browser console confirmed that it was a Uint8Array
.
Here's as much of a stack trace as I could get:
Uncaught (in promise) RuntimeError: unreachable executed
__wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
real http://localhost:8080/pkg/rust_wasm_centrality.js:189
promise callback*getImports/imports.wbg.__wbg_then_11f7a54d67b4bfad http://localhost:8080/pkg/rust_wasm_centrality.js:326
__wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
real http://localhost:8080/pkg/rust_wasm_centrality.js:189
rust_wasm_centrality_bg.wasm:24649:1
Uncaught (in promise) RuntimeError: unreachable executed
__wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
real http://localhost:8080/pkg/rust_wasm_centrality.js:189
promise callback*getImports/imports.wbg.__wbg_then_11f7a54d67b4bfad http://localhost:8080/pkg/rust_wasm_centrality.js:326
__wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
real http://localhost:8080/pkg/rust_wasm_centrality.js:189
rust_wasm_centrality_bg.wasm:24649:1
let chunk_obj = JsFuture::from(stream_reader.read()).await?;
let bytes = serde_wasm_bindgen::from_value(chunk_obj)?;
This seems tantalisingly close to working. The output in the browser console suggests that the chunk has been converted to a JS or JSON object, which seems odd to me.
Loading graph result: Error: invalid type: JsValue(Object({"done":false,"value":{"0":170,"1":4,"2":180,"3":4,"4":194,"5":6,"6":213,"7":18,"8":40,"9":19,"10":38,"11":3,"12":175,"13":2,"14":90,"15":10,"16":204,"17":1,"18":110,"19":0,"20":223,"21":31,"22":1,"23":1,"24":55,"25":2,"26":77,"27":2,"28":75,"29":2,"30":78,"31":3,"32":36,"33":2,"34":3,"35":6,"36":112,"37":27,"38":187,"39":8,"40":37,"41":0,"42":19,"43":0,"44":7,"45":0,"46":32,"47":0,"48":148,"49":0,"50":27,"51":51,"52":39,"53":0,"54":6,"55":0,"56":14,"57":0,"58":8,"59":0,"60":12,"61":0,"62":22,"63":0,"64":236,"65":0,"66":211,"67":13,"68":11,"69":0,"70":2,"71":0,"72":54,"73":1,"74":232,"75":32,"76":196,"77":54,"78":159,"79":20,"80":156,"81":0,"82":120,"83":35,"84":118,"85":2,"86":250,"87":23,"88":217,"89":27,"90":190,"91":6,"92":121,"93":2,"94":211,"95":25,"96":206,"97":9,"98":111,"99":19,"100":22,"101":40,"102":207,"103":9,"104":30,"105":58,"106":34,"107":22,"108":141,"109":40,"110":218,"111":15,"112":144,"113":10,"114":68,"115":…
localhost:8080:93:17
The contents of my Cargo.toml
may also be useful.
name = "rust-wasm-centrality"
version = "0.1.0"
authors = ["Simon Crowe <[email protected]>"]
description = "Display a network graph and cenrality-ranked table of nodes"
license = "MIT"
repository = "https://github.com/simoncrowe/rust-wasm-centrality"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = "thin"
[dependencies]
wasm-bindgen = "0.2.63"
wasm-bindgen-futures = "0.4.33"
js-sys = "0.3.60"
serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11"
serde-wasm-bindgen = "0.4"
[dependencies.web-sys]
version = "0.3.60"
features = [
'console',
'ReadableStream',
'ReadableStreamDefaultReader',
'Response',
'Window',
]
I realised what was wrong soon after I posted this question. The chunks that the promise returned by fetch
resolve into are objects like this: {"done": false, "value":[...]}
. So I just needed to do some casting and reflection using Rust's JS bindings to get the array object I needed.
I'm leaving this question and answer up in case someone else has similar issues when getting to grips with Rust and WebAssembly.
Specifically, I needed to cast the result of the read
method on ReadableStreamDefaultReader
from JsValue
to Object
, then access its value
property and cast that to Uint8Array
. Once I had the array, I could just call to_vec
on it. Below is the code needed to make a GET request and convert the first chunk of the response body to Vec<u8>
.
let window = web_sys::window().unwrap();
let resp_promise = window.fetch_with_str(&"./edges.bin");
let resp_value = JsFuture::from(resp_promise).await?;
let resp: Response = resp_value.dyn_into().unwrap();
log(&format!("Response status code: {}", resp.status()));
if resp.status() != 200 {
return Err(JsValue::FALSE);
}
let reader_value = resp.body().unwrap().get_reader();
let reader: ReadableStreamDefaultReader = reader_value.dyn_into().unwrap();
let result_value = JsFuture::from(reader.read()).await?;
let result: Object = result_value.dyn_into().unwrap();
let chunk_value = js_sys::Reflect::get(&result, &JsValue::from_str("value")).unwrap();
let chunk_array: Uint8Array = chunk_value.dyn_into().unwrap();
let chunk = chunk_array.to_vec();