Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

alshdavid
Copy link

@alshdavid alshdavid commented Jun 25, 2024

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

@alshdavid alshdavid force-pushed the alsh/async_local branch 2 times, most recently from e50907a to 5b64045 Compare June 25, 2024 05:44
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the runtime static because it's thread local (one executor per Nodejs thread) and I wasn't sure how best make it available to call.

Perhaps I could initialize it during the module initialization phase and propagate it through the contexts?

@alshdavid alshdavid force-pushed the alsh/async_local branch 5 times, most recently from 1873750 to 24dd72f Compare June 25, 2024 06:01

self.exports.clone().set(self, key, wrapper)?;
Ok(())
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This essentially creates:

function(...args: Array<Root>): Promise<R>

I included this as a means to test the futures executor out so I can exclude it from the first PR. It is missing methods and other stuff so it's certainly not production ready.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed this to test out the feature. It creates a rooted JsObject and assigns properties to it using Symbols as keys.

Can be removed so the core executor can be introduced where these other bits get ironed out later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant