Is there a more efficient way of returning read-only views of nested float arrays from WASM?

I've written a WASM interface to a Rust function using wasm-bindgen. The function returns a Vec<Vec<f64>>, and I only need a read-only view of it on the JS side:

use js_sys::Array;
use js_sys::Float64Array;
use wasm_bindgen::prelude::*;

pub fn f(data: &[f64], n: u8) -> Result<Vec<Vec<f64>>, MyErr> {
// data is processed here
}

fn inner_vec_to_js_array(data: &[f64]) -> Float64Array {
    unsafe { Float64Array::view(data) }
}


fn wrapper_vec_to_js_array(data: Vec<Vec<f64>>) -> Array {
    data.iter().map(|v| inner_vec_to_js_array(v)).collect()
}

#[wasm_bindgen]
pub fn ckmeans_wasm(data: &[f64], n: u8) -> Result<Array, JsError> {
    Ok(wrapper_vec_to_js_array(f(data, nclusters)?))
}

This seems to work OK, but do I have to return an Array of the Float64Array views? Is there a way I can return "just" a view of the wrapper?

I'm very much unaware of how wasm_bindgen works, but to me it looks like you create dangling references in inner_vec_to_js_array, because data gets dropped in wrapper_vec_to_js_array.

1 Like

Yes it's not totally clear from the unsafety warning Float64Array in js_sys - Rust whether I need to give it back when the JS side is finished with it.

I don't think the problem is whether JS deallocates it or not (which is what I assume you mean by giving it back or not). I think you give out dangling references to JS to begin with. I believe data is dropped in wrapper_vec_to_js_array, causing the Float64Array views you create to point to data that got dropped before you even hand over the array to JS.

I think this is a violation to the last paragraph of the safety section of the docs of Float64Array you've linked:

Finally, the returned object is disconnected from the input slice’s lifetime, so there’s no guarantee that the data is read at the right time.

1 Like

It's pretty clear from the signature, though (and that's even better). If the function wanted to assume ownership, it would have taken an owned value (e.g., Vec<f64> or Box<[f64]>) to ensure correct ownership structure by means of the type system, or at the very minimum, a raw pointer (*mut f64 or *mut [f64]) in order to indicate ambiguity. Since the function takes a slice, it's unambiguously not trying to deallocate your array (since deallocating arbitrary slices is wrong, because a slice doesn't necessary come from a heap allocation).

The problem @jofas is trying to point out is this:

Here, you give ownership of data to this function, which means that it's dropped before the function returns. Consequently, any references pointing into it will be invalid immediately, by the time you even obtained the return value.

2 Likes

OK, that makes sense. But how can I prevent this? If I mem::forget the incoming data before I return the Array:

fn wrapper_vec_to_js_array(data: Vec<Vec<f64>>) -> Array {
    let arr = data.iter().map(|v| inner_vec_to_js_array(v)).collect();
    mem::forget(data);
    arr
}

I've leaked it. How can I fix that?

Use the safe From<&'a [f64]> implementation Float64Array::from(data) instead of Float64Array::view(data):

fn inner_vec_to_js_array(data: &[f64]) -> Float64Array {
-    unsafe { Float64Array::view(data) }
+    Float64Array::from(data) 
}
2 Likes

The immediate fix would be to accept a borrowed slice instead of an owned Vec. (That doesn't resolve some other safety concerns, though, eg. the prohibition of heap-allocating while JS has the array view). But it would not be unconditionally wrong, at least.

2 Likes