rustyscript icon indicating copy to clipboard operation
rustyscript copied to clipboard

Question: How to send Uint8Array from Rust to JavaScript fast

Open hanakla opened this issue 10 months ago • 8 comments

Hello! I am currently researching the possibility of using Deno's WebGPU functionality on Adobe Illustrator using rustyscript.

I've been touching the current rustyscript API and it seems that json_args! does not allow me to pass large binary data like ArrayBuffer or Uint8Array to the TypeScript runtime without converting it to JSON. (It doesn't look like it would be easy from TypeScript to Rust either)

Some APIs like Deno.readFile in Deno seem to be able to transfer huge data from the Rust side to the TypeScript runtime, but how can we do the equivalent in rustyscript?

Regards.

hanakla avatar Feb 12 '25 21:02 hanakla

Hello Did you use v8::Value to build the value you want to pass? Can you provide an example so I could help? replit or any similar thing would be nice.

utyfua avatar Feb 12 '25 22:02 utyfua

Here how would I do the thing. It takes less then 4ms on debug mode on my machine. My example a bit clumsy but it's more like a proof of concept so you need to think how you can apply to your code.

use rustyscript::{deno_core::v8, js_value, Error, Runtime};
use std::time::Instant;

fn main() -> Result<(), Error> {
    let input_data = "Some Data".repeat(10_000_000).as_bytes().to_vec();

    let mut runtime = Runtime::new(Default::default())?;

    let t = Instant::now();
    let fn_ref = runtime
        .eval::<js_value::Value>(r#"(input)=>{console.log(input)}"#)?
        .into_v8();
    let scope = &mut runtime.deno_runtime().handle_scope();
    let value = v8::ArrayBuffer::with_backing_store(
        scope,
        &v8::ArrayBuffer::new_backing_store_from_bytes(input_data).make_shared(),
    );
    let fn_ref = v8::Local::<v8::Value>::new(scope, fn_ref);
    let fn_ref = v8::Local::<v8::Function>::try_from(fn_ref).unwrap();
    let args = vec![value.into()];
    fn_ref.call(scope, fn_ref.into(), &args);
    println!("Time: {:?}", t.elapsed());

    Ok(())
}

utyfua avatar Feb 12 '25 23:02 utyfua

Perhaps a new js_value variant is warranted?

rscarson avatar Feb 12 '25 23:02 rscarson

Perhaps a new js_value variant is warranted?

I dont thing so. You can mix with using rusy v8 api anytime just like I did this time.

Also if you need performance v8::Local will be much better then converting it to v8:Global each time you need any atomic operation.

To be honest I am using rustyscript::js_value only to get v8::Global so I can convert it to v8::Local(not in this example but in general)

utyfua avatar Feb 12 '25 23:02 utyfua

@utyfua Thank you! I wanted to do exactly what this sample does! I wasn't familiar with the V8 API, so this was really helpful! I was able to exchange ArrayBuffers between Rust and JavaScript using your sample code!

I'll write some sample code to help other users too!

use deno_core::v8;
use tokio::time::Instant;

///!
///! This example is meant to demonstrate the large-binary data conversion between Rust and JavaScript
///!
///! - Converting Rust Vec<u8> to JavaScript ArrayBuffer
///! - Passing large array data between Rust and JavaScript runtime
///! - Memory efficient handling of binary data across language boundaries
///! - Direct memory mapping using V8 ArrayBuffer
///!
///! The example creates a large binary data, converts it to a JavaScript
///! Uint8Array, and verifies the roundtrip conversion integrity
///!
use rustyscript::{js_value, Error, Runtime};

fn main() -> Result<(), Error> {
    let mut input_data = "Some Data".repeat(10_000_000).as_bytes().to_vec();
    let input_data_orig = input_data.clone();

    let mut runtime = Runtime::new(Default::default())?;

    let t = Instant::now();
    let fn_ref = runtime
        .eval::<js_value::Value>(r#"(input)=>{ return new Uint8Array(input); }"#)?
        .into_v8();

    let scope = &mut runtime.deno_runtime().handle_scope();
    let array_buffer = to_array_buffer(scope, input_data).unwrap();

    let fn_ref = v8::Local::<v8::Value>::new(scope, fn_ref);
    let fn_ref = v8::Local::<v8::Function>::try_from(fn_ref).unwrap();
    let args = vec![array_buffer.into()];

    let js_result = fn_ref.call(scope, fn_ref.into(), &args).unwrap();
    let output_data = extract_uint8_array(scope, js_result).unwrap();

    println!("Time taken: {:?}", t.elapsed());
    println!("{}", input_data_orig.iter().zip(output_data.iter()).all(|(a, b)| a == b));

    Ok(())
}

fn to_array_buffer<'a>(scope: &mut v8::HandleScope<'a>,  value: Vec<u8>) -> Option<v8::Local<'a,v8::ArrayBuffer>> {
    let ab = v8::ArrayBuffer::with_backing_store(
        scope,
        &v8::ArrayBuffer::new_backing_store_from_bytes(value).make_shared(),
    );
    Some(ab.into())
}

fn extract_uint8_array(scope: &mut v8::HandleScope,  value: v8::Local<v8::Value>) -> Option<Vec<u8>> {
    if !value.is_uint8_array() { return None; }

    let uint8array = value.try_cast::<v8::Uint8Array>().unwrap();
    let ab = uint8array.buffer(scope).unwrap();
    let buf = ab.get_backing_store().to_owned();
    let len = ab.byte_length();

    if len == 0 {
        return Some(vec![]);
    }

    Some(unsafe {
        std::slice::from_raw_parts_mut(
            buf.data().unwrap().cast::<u8>().as_ptr(),
            len
        ).into()
    })
}

hanakla avatar Feb 13 '25 13:02 hanakla

@hanakla I dont like the use of unsafe.

I think you dont fully understand how can you use v8::BackingStore to get the data from v8. You already have reference to the data.

Here one more example which modifies existing one and creates a new one.

use deno_core::v8;
use rustyscript::{js_value, Error, Runtime};
use tokio::time::Instant;

fn main() -> Result<(), Error> {
    let input_data = "Some Data ".repeat(10_000_000).as_bytes().to_vec();

    let mut runtime = Runtime::new(Default::default())?;

    let t = Instant::now();
    // fn to modify the input data
    let fn_ref = runtime
        .eval::<js_value::Value>(
            r#"(input)=>{
                const text = "V8からこんにちは!";
                // modify existing input data
                const encoder = new TextEncoder();
                const overwrite = encoder.encode(text);
                (new Uint8Array(input)).set(overwrite, 10);

                // create new data
                let t = Date.now();
                const new_data = encoder.encode(text.repeat(1_000_000));
                console.log("Created new data in", Date.now() - t, "ms");
                return new_data;
            }"#,
        )?
        .into_v8();

    let scope = &mut runtime.deno_runtime().handle_scope();

    let input_store = v8::ArrayBuffer::new_backing_store_from_bytes(input_data).make_shared();
    let input_buffer = v8::ArrayBuffer::with_backing_store(scope, &input_store);

    let fn_ref = v8::Local::<v8::Value>::new(scope, fn_ref);
    let fn_ref = v8::Local::<v8::Function>::try_from(fn_ref).unwrap();
    let args = vec![input_buffer.into()];

    let output_buffer = fn_ref.call(scope, fn_ref.into(), &args).unwrap();
    let output_buffer = v8::Local::<v8::Uint8Array>::try_from(output_buffer).unwrap();
    let output_store = output_buffer.get_backing_store().unwrap();
    let output_data = output_store
        .iter()
        .take(52)
        .map(|v| v.get())
        .collect::<Vec<_>>();
    println!(
        "Output data: {:?}",
        // careful here cuz Japanese characters are 3 bytes
        // and if you cut in the middle of a character, it will panic by unwrap
        String::from_utf8(output_data).unwrap()
    );

    let modified_data = input_store
        .iter()
        .take(50)
        .map(|v| v.get())
        .collect::<Vec<_>>();
    println!(
        "Modified input data: {:?}",
        String::from_utf8(modified_data).unwrap()
    );

    println!("Elapsed: {:?}", t.elapsed());

    Ok(())
}

utyfua avatar Feb 13 '25 15:02 utyfua

A pr with that added to examples, or even the book would be appreciated

rscarson avatar Feb 13 '25 15:02 rscarson

A pr with that added to examples, or even the book would be appreciated

Do you think its possible to comeup with some cookbook or something for the buffers?

There a lot of things what you can do. Most of them is specific for each case, dont you think?

Besides, I don't have time to work on such big project right now. After I settle down I'll try to make my contribution.

utyfua avatar Feb 13 '25 15:02 utyfua