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

WIP: Non-blocking Rust futures running on local thread #1051

Open alshdavid opened 2 months ago

alshdavid commented 2 months ago

This PR introduces Rust future execution concurrently on the local thread without blocking JavaScript execution.

It's still a work in progress, requesting comments for ways I can improve the implementation if it's a good candidate for contribution.

Examples

Async Function Declaration

Simple Async

Declare an async function that returns a promise and does not block the main thread.

import napi from './napi.node'
napi.foo().then(() => console.log('Hi'))
async fn foo<'a>(mut cx: AsyncFunctionContext) -> JsResult<'a, JsUndefined> {
  println!("Rust started");
  task::sleep(Duration::from_secs(1)).await;
  println!("Rust sleeped");
  Ok(cx.undefined())
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
  cx.export_function_async("foo", foo)?;
  Ok(())
}

Async MPSC Channel

async fn foo<'a>(mut cx: AsyncFunctionContext) -> JsResult<'a, JsUndefined> {
  let callback: Handle<JsFunction> = cx.argument(0)?;
  let (tx, rx) = unbounded::<u32>();

  thread::spawn(move || {
    thread::sleep(Duration::from_secs(1));
    tx.send_blocking(42).unwrap();
  });

  rx.recv().await.unwrap();
  callback.call_with(&cx).exec(&mut cx)?;
  Ok(cx.undefined())
}

Function Async Closure

fn foo(mut cx: FunctionContext) -> JsResult<JsUndefined> {
  let callback = cx.argument::<JsFunction>(0)?.to_static(&mut cx);

  cx.execute_async(|mut cx| async move {
    let callback = callback.from_static(&mut cx);
    task::sleep(Duration::from_secs(1)).await;
    callback.call_with(&cx).exec(&mut cx).unwrap();
  });

  Ok(cx.undefined())
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
  cx.export_function("foo", foo)?;
  Ok(())
}

How does this work? Tokio?

This works by essentially bolting a custom Rust futures executor onto Nodejs's event loop triggered via a thread safe function.

The futures executor yields back to JavaScript execution as soon as it hits a pending Future, later resuming when woken up by a future that has work to do.

This means that the Rust "event loop" will never block Nodejs's event loop and vice versa.

For channels, sleep, and other common async tasks, utilities from async-std can be used.

Tokio is not suitable for this use case as it's designed to be the only executor running and cannot be driven (or at least I cannot figure out how to drive it) from an external event loop.

TODO: [] Tests [] Error handing [] Documentation [] Missing functionality