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

RFC & WIP - Libuv powered futures #1050

Closed alshdavid closed 2 months ago

alshdavid commented 2 months ago

This PR is a WIP that introduces Rust future execution managed by libuv with a working implementation. This allows for Rust code to run concurrently on the main thread without blocking the JavaScript thread.

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

It might be a bit confusing for consumers given the API might conflict with the tokio runtime - so there's probably a discussion to be had there.

~I only actually changed 5 files, the other changes are vendoring in libuv-rs to reuse their C bindings. In the future the relevant bindings can be brought over rather than vendoring in the whole crate.~ Published my fork of libuv for Rust to reduce the diff.

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

This also enables the use of async channels on the main thread

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

You can spawn an async future within a closure using cx.execute_async(|| async {}).

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

  cx.execute_async(|mut cx| async move {
    let callback = callback.into_inner(&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 Rust futures executor onto Nodejs's libuv. It uses libuv hooks to trigger the Rust future executor. The futures executor runs until Rust futures are pending then yields back to nodejs's event loop.

This means that the Rust event loop will never block Nodejs's event loop and vice versa, yet both can drive their tasks forward.

I'm using the lightweight local thread executor from the futures crate.

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 executor.

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

alshdavid commented 2 months ago

Changed the branch name because it no longer uses libuv https://github.com/neon-bindings/neon/pull/1051