Pass JS Float64Array to Rust using WASM

I want to pass an array of floating point numbers from JavaScript to Rust using WASM. I think I should create a Float64Array in the JavaScript code and pass it to a Rust function that takes &[f64]. Is this currently possible? If so, can someone point me to an example that does this or something similar?

You can pass a &Float64Array and call view on it.

Could you point me to an example of using this that shows both the JS and Rust code?

not really…

JS:

var arr = new Float64Array(…);
wasm_bindgen.foo(arr);

Rust:

#[wasm_bindgen]
fn foo(arr: &Float64Array) {
   let data = unsafe { arr.view() };
   println!("{}", data[0]);
}

Float64Array::view makes a Float64Array which is a view into the specified WASM linear memory covering just the slice. You can use .copy_to() or .to_vec() to get the contents of the array.

This line gives the error "no method named view found for reference &Float64Array in the current scope; this is an associated function, not a method":

   let data = unsafe { arr.view() };

So I changed it to this:

   let data = unsafe { Float64Array::view(&arr) };

But that gives the error "mismatched types; expected slice [f64], found &Float64Array"

I can use the to_vec method to copy the Float64Array into a vector. But I was hoping to find a way to access shared memory so the Rust code can use data from JavaScript without an copying. Is that possible? Is this what the copy_to and to_vec methods do?

You have to copy it into the linear memory one way or the other, as this is the only thing wasm can directly access. Everything else has to go through javascript. The copy_to and to_vec methods copy a Float64Array into the linear memory of the current wasm module.

It’s the “one way or the other” part that is confusing me. I can’t find a single example on web of doing this. Has anyone seen code that does this?

Right.

I have running code using Uint8Array:
JS: pdf_render/index.js at master · pdf-rs/pdf_render · GitHub
Rust: pdf_render/lib.rs at master · pdf-rs/pdf_render · GitHub

And indeed it uses to_vec.

Ah, this helped a lot! I have it working now, but the Rust version still is much slower than the JS version.
See wasm-bind-demo/index.js at 11d4cf749c102078e0b8687b6527ac7f77bee8f5 · mvolkmann/wasm-bind-demo · GitHub
and wasm-bind-demo/lib.rs at 11d4cf749c102078e0b8687b6527ac7f77bee8f5 · mvolkmann/wasm-bind-demo · GitHub.
Am I still doing something wrong that would explain the performance issue?

Rust can only access data that is in the Wasm memory. Typed arrays are not and so they have to be copied into memory accessible by Rust first.

Can you point me to an example of copying a typed array into wasm memory?

When you call .to_vec() it copies the contents into Rusts memory.

You could avoid copying by creating a Vec<f64> in Rust and using Float64Array::view to create a Float64Array for JavaScript code to access it. This will let Rust and JavaScript share the same memory, instead of copying data back and forth.

I currently create a Vector<f64> in Rust here: wasm-bind-demo/lib.rs at 11d4cf749c102078e0b8687b6527ac7f77bee8f5 · mvolkmann/wasm-bind-demo · GitHub. That function returns a pointer to this back to JavaScript. Where are you suggesting I should call Float64Array::view?

That Vec is dropped as soon as the get_vector_pointer function exits, so it returns a dangling pointer.

Luckily, your JavaScript code never actually uses that pointer, since Float64Array(numbers, ...) creates an array from the numbers list which was allocated in JavaScript. You then pass this JS-created Float64Array to your sum function.

One zero-copy possibility would be the following Rust code:

#[wasm_bindgen]
pub fn sum(ptr: *mut f64, count: usize) -> f64 {
    let data = unsafe { std::slice::from_raw_parts(ptr, count) };
    data.iter().sum()
}

#[wasm_bindgen]
pub fn get_vec_pointer(count: usize) -> *mut f64 {
    let mut v = Vec::with_capacity(count);
    let ptr = v.as_mut_ptr();
    std::mem::forget(v);
    ptr
}

#[wasm_bindgen]
pub fn get_array(ptr: *mut f64, count: usize) -> Float64Array {
    unsafe { Float64Array::view(std::slice::from_raw_parts(ptr, count)) }
}

Which would be called from JavaScript like this:

function populateNumbers(numbers) {
  for (let i = 0; i < numbers.length; i++) {
    numbers[i] = Math.random() * 100;
  }
}

const ptr = m.get_vec_pointer(COUNT);
const arr = m.get_array(ptr, COUNT);
populateNumbers(arr);

startMs = Date.now();
result = m.sum(ptr, COUNT);
endMs = Date.now();

In release mode, this runs in 7ms on my laptop.

Yes, there are examples in the wasm-bindgen refererence. In your code, it would look like this:

Rust:

#[wasm_bindgen]
pub fn sum(data: &[f64]) -> f64 {
    data.iter().sum()
}

JavaScript:

const numbers = getNumbers(COUNT);
const arr = new Float64Array(numbers);

startMs = Date.now();
result = m.sum(numbers);
endMs = Date.now();

This version takes 13ms in release mode on my laptop. So in this micro-benchmark, the copying overhead almost doubles the runtime. But if you were benchmarking a more expensive computation, the copying overhead might not be noticeable.

2 Likes

Thanks so much for taking the time to look at this!

My package.json has two npm scripts, "build" (runs webpack) and "serve" (runs webpack-dev-server). Neither builds a release version of the Rust code. I tried running cargo build --release followed by npm run serve, but the performance didn't improve so I'm guessing it isn't really using the release version. How did you get it to build and use a release version?

I changed the mode property in webpack.config.js from 'development' to 'production'.

Whoa! That made a huge difference! The time for the Rust code went from 119 ms on my machine to 3 ms and now it is about 1/6 the time of the JavaScript code.