rustfetch-apiwebassemblywasm-bindgenweb-sys

Rust WASM code stalls on awaiting web_sys::Window.fetch_with_request


I am trying to use the Fetch API using web_sys in Rust WASM code. I have defined an async function:

#[wasm_bindgen]
pub async fn fetch_as_vec_u8(resource_name: &str) -> Result<Vec<u8>, JsValue> {
    let opts = RequestInit::new();
    opts.set_method("GET");
    opts.set_mode(RequestMode::Cors);

    let request = Request::new_with_str_and_init(resource_name, &opts)?;

    request.headers().set("Accept", "application/octet-stream")?;

    let window = web_sys::window().unwrap();
    let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;

    assert!(resp_value.is_instance_of::<Response>());
    let resp: Response = resp_value.dyn_into().unwrap();

    let array_buf = JsFuture::from(resp.array_buffer()?).await?;
    assert!(array_buf.is_instance_of::<ArrayBuffer>());

    let typebuf: js_sys::Uint8Array = js_sys::Uint8Array::new(&array_buf);
    let mut body = vec![0; typebuf.length() as usize];
    typebuf.copy_to(&mut body[..]);

    Ok(body)
}

Calling the function from a synchronous code like this:

let vert_shader_src_bytes: Vec<u8> = block_on(fetch_as_vec_u8(
    format!("http://localhost:8080/shaders/{}.vert.glsl", shader_name).as_str(),
))
.expect(format!("Failed to fetch {}.vert.glsl", shader_name).as_str());

This doesn't work, and executing the code in the browser simply hangs without any console output. The expected GET request is not being sent.

Debugging fetch_as_vec_u8 reveals that it is the following line that stalls:

let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;

The requested resource exists and can be downloaded from the local URL. The above fetch implementation is based on the official example.

Unfortunately, I am stuck. Can anyone point me in the direction to dig? Thanks in advance!


Solution

  • You appear to be using a block_on function to block the current thread until the completion of your future. However, in almost all cases, Rust/WebAssembly is subject to the single-threaded JavaScript event loop. In order for the future to complete, event-processing (of the browser/JavaScript engine) and async-runtime code (of Rust) would have to run elsewhere, but you already exhausted the available parallelism.

    A solution, which I used when I made a WebAssembly game, is to use future_to_promise to schedule code after the loading completes. After scheduling, you yield back to JavaScript's event loop and expect for the future to make more progress when an event was handled.

    struct Loaded {
        glsl: Option<String>,
    }
    static LOADED: Mutex<Option<Loaded>> = Mutex::new(None);
    
    #[wasm_bindgen]
    fn load() -> Promise {
        // If we didn't return the promise, it would still complete, but then
        // we'd have to tell JavaScript to call `init` some other way.
        wasm_bindgen_futures::future_to_promise(async move {
            let vert_shader_src_bytes: Vec<u8> = fetch_as_vec_u8(
                format!("http://localhost:8080/shaders/{}.vert.glsl", shader_name).as_str(),
            )).await.expect(format!("Failed to fetch {}.vert.glsl", shader_name).as_str());
    
            // You probably have to load more stuff. Do that here!
    
            // Either the rest of your app goes here or you need to
            // mutate global (static) state so that subsequent
            // synchronous JavaScript -> WebAssembly function calls
            // can access what was loaded.
    
            // Here we do the latter:
            *LOADED.lock().unwrap() = Some(Loaded{
                glsl: String::from_utf8(vert_shader_src_bytes).expect("utf-8 error"),
            });
    
            Ok(JsValue::undefined())
        })
    }
    
    #[wasm_bindgen]
    fn init() {
        let option = LOADED.lock().unwrap();
        let loaded = option.as_ref().unwrap();
        init_rendering(&loaded.glsl);
    }
    

    The JavaScript might look like this:

    wasm.load().await;
    wasm.init();
    // game loop, etc.
    function frame() {
        wasm.frame();
        requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);