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

Capturing a function to call it later? #1017

Closed alshdavid closed 6 months ago

alshdavid commented 6 months ago

Hi, I am exporting a register_plugin function that takes a function as an argument, captures it and intends to call it later.

At the moment I am just trying to store one function handle in Rust - eventually I will keep a store functions per worker thread and coordinate calling them in their own module context in Rust via channels.

At the moment I am failing on the first step where I am trying to capture a function handle in Rust.

Right now I am trying something like this:

const lib = require('./index.node')
lib.register_plugin(() => console.log('hi'))
lib.bootstrap()
fn register_plugin(function_handle: Rc<RefCell<Option<Handle<JsFunction>>>>) -> impl Fn(FunctionContext) -> JsResult<JsUndefined> {
  move |mut cx|  {
    let arg0: Handle<JsFunction> = cx.argument(0)?;
    function_handle.borrow_mut().replace(arg0);
    Ok(cx.undefined())
  }
}

fn bootstrap(function_handle: Rc<RefCell<Option<Handle<JsFunction>>>>) -> impl Fn(FunctionContext) -> JsResult<JsUndefined> {
  move |mut cx|  {
    let func = function_handle.borrow_mut().take().unwrap();
    let result: Handle<JsUndefined> = func.call_with(&mut cx).apply(&mut cx)?;
    Ok(cx.undefined())
  }
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    // Not sure if the Rc<RefCell<T>> is needed
    let mut function_handle = Rc::new(RefCell::new(None::<Handle<JsFunction>>));
    cx.export_function("register_plugin", register_plugin(function_handle))?;
    cx.export_function("bootstrap", bootstrap(function_handle))?;
    Ok(())
}

Alternatively, can I call a function registered on a global variable?

globalThis.pluginFunc = () => console.log('hi')

That way I can keep the index of functions on the JS side and simply call them from fn main() in Rust (assuming require('./index.node) happens after the values are available?)

alshdavid commented 6 months ago

This works:

globalThis.pluginFunc = () => console.log('hi')
require('./index.node')
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    let value = cx.global().get_value(&mut cx, "pluginFunc").unwrap();
    let value: Handle<JsFunction> = value.downcast(&mut cx).unwrap();

    let result: Handle<JsString> = value
      .call_with(&mut cx)
      .arg(cx.string("Hello"))
      .apply(&mut cx)?;

    Ok(())
}
kjvalencik commented 6 months ago

Yep, that is correct for calling a function. If you need to hold the function permanently, the Root type can be used for that. js_func.root().

If you need to call into JS from a Rust thread, the Channel type helps with that.

alshdavid commented 6 months ago

I am trying to call a JS function on different threads to build a javascript "plugin API" for a Rust based project - so I need to orchestrate everything from my main napi thread but call into functions that live on the worker thread contexts.

Right now I am using a static global variable to hold channels and have the main JS napi instance message the JS worker napi modules - but is it possible to wrap the function pointers from the workers in a struct/scope, store it in the global static variable, and call them from main?

Something like

let ctx_func = cx.global().get_value(&mut cx, "callback").unwrap();
let ctx_func: Handle<JsFunction> = ctx_func.downcast(&mut cx).unwrap();

// I know this doesn't work but you get what I'm trying to achieve
let call_callback = || {
  let result = ctx_func
    .call_with(&mut cx)
    .apply::<JsObject, FunctionContext>(&mut cx)?;

  let file_path: Handle<JsString> = result
    .downcast(&mut cx)
    .unwrap();

  return file_path.value(&mut cx);
};

SHARED_STATE.lock().unwrap().push(call_callback);

Current implementation

Plugin to be loaded dynamically https://github.com/alshdavid-labs/reference-node-and-rust/blob/main/plugin/index.js

Javascript entrypoint https://github.com/alshdavid-labs/reference-node-and-rust/blob/main/napi/lib/index.js

Napi function that runs on the main Node context https://github.com/alshdavid-labs/reference-node-and-rust/blob/main/napi/src/register_main.rs

Napi function that runs on the worker Node context https://github.com/alshdavid-labs/reference-node-and-rust/blob/main/napi/src/register_worker.rs

Bonus question How do I resolve a promise from napi so I can make the plugin support async/await syntax?

kjvalencik commented 6 months ago

but is it possible to wrap the function pointers from the workers in a struct/scope, store it in the global static variable, and call them from main? Workers are distinct JS runtimes running on a different thread. Unfortunately, it's not possible to call directly into them from the main thread.

It doesn't need to be a global static. It could be in a JsBox that JS holds or it could use Neon's equivalent to thread local storage, but it still requires a Channel to call into the other thread.

Alternatively, you could use standard message passing between themain thread and the worker.

How do I resolve a promise from napi so I can make the plugin support async/await syntax?

https://docs.rs/neon/1.0.0-alpha.4/neon/types/struct.JsPromise.html

alshdavid commented 6 months ago

Thanks for your help, I know they are basic questions and I really appreciate the support 🙏