neon-bindings / neon

Rust bindings for writing safe and fast native Node.js modules.
https://www.neon-bindings.com/
Apache License 2.0
7.98k stars 282 forks source link

Binding variable from main into callable function #1021

Closed wbelhomsi closed 6 months ago

wbelhomsi commented 6 months ago

Hi! I'm trying to get a bit more into rust and I am using neon as a first step to get there. My main expertise lies within JS/TS, so forgive me if I'm asking something basic within Rust. I'm essentially trying to have the following; I'd like to use a variable that I have defined within main inside of the functions that will be called within Javascript so that I don't have to initialize them every time I call that function.

The below is a dummy example of what I'm trying to do

fn get_num_cpus(mut cx: FunctionContext) -> JsResult<JsNumber> {
     let random_number = cx.argument::<JsNumber>(0)?;
    let transformed_number = custom_class.transform(random_number); // How can I get to a point where I use `custom_class`
    Ok(cx.number(transformed_number))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    println!("Holllaaa");
    let custom_class = CustomClass::new(...);
    cx.export_function("get", get_num_cpus)?;
    Ok(())
}

In Js something like that would be done like:

 (...args)  =>  get_num_cpus(custom_class, ...args)

Apologies if I'm missing something essential from RUST

kjvalencik commented 6 months ago

@wbelhomsi Great question! There are several different options, and which one works best for is going to depend on your needs.

OnceLock

This is the simplest option and is standard Rust. Store the variable in a global that everything can access. This is a good option if you want a singleton for the entire process (e.g., tokio::runtime::Runtime).

// Create a globally shared `String`
fn global() -> &'static str {
    use std::sync::OnceLock;

    static GLOBAL: OnceLock<String> = OnceLock::new();

    GLOBAL.get_or_init(|| String::from("Hello, World!"))
}

LocalKey

This is very similar to OnceLock, but instead of a single global per process, there is one per instance of the JavaScript VM. This is useful if you need a distinct global for each time the module is loaded in a web worker. For example, if you are storing callbacks in a Root<JsFunction>.

Function Closures

This is very similar to LocalKey. You can create a JsFunction from a closure, so it's always possible to create a shared value and move clones of it into function.

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    let msg = Arc::new(String::from("Hello, World!"));

    cx.export_function(&mut cx, {
        let msg = msg.clone();

        move |mut cx| Ok(cx.string(&msg))
    });
}

JsBox

JsBox is a type that lets you wrap a Rust struct with a JavaScript object for later access. This lets the JS garbage collector track and free it when needed. To use it, you can store a reference to it in JavaScript and pass it as an argument to your functions.

This works well when you are trying to implement a class or need multiple copies for some other reason. See this example.

wbelhomsi commented 6 months ago

Hey @kjvalencik, thanks for your answer!

I did the following:

// Create a globally shared `XClient`
fn global() -> &'static XClient {
    use std::sync::OnceLock;

    static GLOBAL: OnceLock<XClient> =
        OnceLock::new();

    let xclient_config = XConfig::default();
    let x_client = XClient::new("http://xxxx", xclient_config).unwrap();

    GLOBAL.get_or_init(|| x_client)
}

In my main I did:


#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    println!("Holllaaa");
    global(); // To initialize it
    cx.export_function("get", get_num_cpus)?;
    cx.export_function("ping", ping)?;
    Ok(())
}

and now I'm using it as the following:

fn ping(mut cx: FunctionContext) -> JsResult<JsObject> {
    let x_client_ref = global();
...
}

Just wanted to validate I understood what you told me.

kjvalencik commented 6 months ago

@wbelhomsi The initialization lines should be inside the closure for get_or_init, otherwise they are going to run each time global() is called.

// Create a globally shared `XClient`
fn global() -> &'static XClient {
    use std::sync::OnceLock;

    static GLOBAL: OnceLock<XClient> =
        OnceLock::new();

    GLOBAL.get_or_init(|| {
        let xclient_config = XConfig::default();
        XClient::new("http://xxxx", xclient_config).unwrap()
    })
}
wbelhomsi commented 6 months ago

@kjvalencik I knew it, was so close but had a ; at the end of the unwrap() which made it not happy.

My gratitude. Thanks for taking the time to help a fellow dev