From 9223337f341caed253ec51df310ea646f1500785 Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Thu, 5 Sep 2024 11:21:03 -0400 Subject: [PATCH 1/2] feat(neon): Add support for async functions in `#[neon::export]` --- .cargo/config.toml | 8 +- .../neon-macros/src/export/function/meta.rs | 37 +++++- crates/neon-macros/src/export/function/mod.rs | 28 +++-- crates/neon-macros/src/export/mod.rs | 3 +- crates/neon/Cargo.toml | 7 +- crates/neon/src/context/internal.rs | 2 + crates/neon/src/executor/mod.rs | 59 ++++++++++ crates/neon/src/executor/tokio.rs | 66 +++++++++++ crates/neon/src/lib.rs | 8 +- crates/neon/src/macro_internal/futures.rs | 29 +++++ crates/neon/src/macro_internal/mod.rs | 111 +++++++++++++----- crates/neon/src/macros.rs | 59 +++++++++- test/napi/Cargo.toml | 2 +- test/napi/lib/futures.js | 59 ++++++++++ test/napi/src/js/futures.rs | 77 ++++++++++-- test/napi/src/lib.rs | 11 ++ 16 files changed, 505 insertions(+), 61 deletions(-) create mode 100644 crates/neon/src/executor/mod.rs create mode 100644 crates/neon/src/executor/tokio.rs create mode 100644 crates/neon/src/macro_internal/futures.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index e508732f2..481e6eb2e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ [alias] # Neon defines mutually exclusive feature flags which prevents using `cargo clippy --all-features` # The following aliases simplify linting the entire workspace -neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers,serde" -neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers,serde -- -A clippy::missing_safety_doc" -neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,futures,external-buffers,serde" -neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers,sys,serde -- --cfg docsrs" +neon-check = " check --all --all-targets --features napi-experimental,external-buffers,serde,tokio" +neon-clippy = "clippy --all --all-targets --features napi-experimental,external-buffers,serde,tokio -- -A clippy::missing_safety_doc" +neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,external-buffers,serde,tokio" +neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,external-buffers,sys,serde,tokio -- --cfg docsrs" diff --git a/crates/neon-macros/src/export/function/meta.rs b/crates/neon-macros/src/export/function/meta.rs index 77858bd3d..948677b0c 100644 --- a/crates/neon-macros/src/export/function/meta.rs +++ b/crates/neon-macros/src/export/function/meta.rs @@ -8,6 +8,8 @@ pub(crate) struct Meta { #[derive(Default)] pub(super) enum Kind { + Async, + AsyncFn, #[default] Normal, Task, @@ -28,7 +30,8 @@ impl Meta { fn force_context(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { match self.kind { - Kind::Normal => {} + Kind::Normal | Kind::AsyncFn => {} + Kind::Async => return Err(meta.error(super::ASYNC_CX_ERROR)), Kind::Task => return Err(meta.error(super::TASK_CX_ERROR)), } @@ -37,6 +40,16 @@ impl Meta { Ok(()) } + fn make_async(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + if matches!(self.kind, Kind::AsyncFn) { + return Err(meta.error(super::ASYNC_FN_ERROR)); + } + + self.kind = Kind::Async; + + Ok(()) + } + fn make_task(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { if self.context { return Err(meta.error(super::TASK_CX_ERROR)); @@ -48,13 +61,25 @@ impl Meta { } } -pub(crate) struct Parser; +pub(crate) struct Parser(syn::ItemFn); + +impl Parser { + pub(crate) fn new(item: syn::ItemFn) -> Self { + Self(item) + } +} impl syn::parse::Parser for Parser { - type Output = Meta; + type Output = (syn::ItemFn, Meta); fn parse2(self, tokens: proc_macro2::TokenStream) -> syn::Result { + let Self(item) = self; let mut attr = Meta::default(); + + if item.sig.asyncness.is_some() { + attr.kind = Kind::AsyncFn; + } + let parser = syn::meta::parser(|meta| { if meta.path.is_ident("name") { return attr.set_name(meta); @@ -68,6 +93,10 @@ impl syn::parse::Parser for Parser { return attr.force_context(meta); } + if meta.path.is_ident("async") { + return attr.make_async(meta); + } + if meta.path.is_ident("task") { return attr.make_task(meta); } @@ -77,6 +106,6 @@ impl syn::parse::Parser for Parser { parser.parse2(tokens)?; - Ok(attr) + Ok((item, attr)) } } diff --git a/crates/neon-macros/src/export/function/mod.rs b/crates/neon-macros/src/export/function/mod.rs index 5177815a4..994cecb5b 100644 --- a/crates/neon-macros/src/export/function/mod.rs +++ b/crates/neon-macros/src/export/function/mod.rs @@ -2,6 +2,8 @@ use crate::export::function::meta::Kind; pub(crate) mod meta; +static ASYNC_CX_ERROR: &str = "`FunctionContext` is not allowed in async functions"; +static ASYNC_FN_ERROR: &str = "`async` attribute should not be used with an `async fn`"; static TASK_CX_ERROR: &str = "`FunctionContext` is not allowed with `task` attribute"; pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenStream { @@ -40,19 +42,19 @@ pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenS .unwrap_or_else(|| quote::quote!(#name)) }); - // Import the value or JSON trait for conversion - let result_trait_name = if meta.json { - quote::format_ident!("NeonExportReturnJson") + // Tag whether we should JSON wrap results + let return_tag = if meta.json { + quote::format_ident!("NeonJsonTag") } else { - quote::format_ident!("NeonExportReturnValue") + quote::format_ident!("NeonValueTag") }; // Convert the result // N.B.: Braces are intentionally included to avoid leaking trait to function body let result_extract = quote::quote!({ - use neon::macro_internal::#result_trait_name; + use neon::macro_internal::{ToNeonMarker, #return_tag as NeonReturnTag}; - res.try_neon_export_return(&mut cx) + (&res).to_neon_marker::().neon_into_js(&mut cx, res) }); // Default export name as identity unless a name is provided @@ -63,6 +65,17 @@ pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenS // Generate the call to the original function let call_body = match meta.kind { + Kind::Async | Kind::AsyncFn => quote::quote!( + let (#(#tuple_fields,)*) = cx.args()?; + let fut = #name(#context_arg #(#args),*); + let fut = { + use neon::macro_internal::{ToNeonMarker, NeonValueTag}; + + (&fut).to_neon_marker::().into_neon_result(&mut cx, fut)? + }; + + neon::macro_internal::spawn(&mut cx, fut, |mut cx, res| #result_extract) + ), Kind::Normal => quote::quote!( let (#(#tuple_fields,)*) = cx.args()?; let res = #name(#context_arg #(#args),*); @@ -160,7 +173,8 @@ fn has_context_arg(meta: &meta::Meta, sig: &syn::Signature) -> syn::Result // Context is only allowed for normal functions match meta.kind { - Kind::Normal => {} + Kind::Normal | Kind::Async => {} + Kind::AsyncFn => return Err(syn::Error::new(first.span(), ASYNC_CX_ERROR)), Kind::Task => return Err(syn::Error::new(first.span(), TASK_CX_ERROR)), } diff --git a/crates/neon-macros/src/export/mod.rs b/crates/neon-macros/src/export/mod.rs index 021fa6cf0..cccbb2ccf 100644 --- a/crates/neon-macros/src/export/mod.rs +++ b/crates/neon-macros/src/export/mod.rs @@ -13,7 +13,8 @@ pub(crate) fn export( match item { // Export a function syn::Item::Fn(item) => { - let meta = syn::parse_macro_input!(attr with function::meta::Parser); + let parser = function::meta::Parser::new(item); + let (item, meta) = syn::parse_macro_input!(attr with parser); function::export(meta, item) } diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index 3c8fde88e..76429c221 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -56,12 +56,17 @@ external-buffers = [] # Experimental Rust Futures API # https://github.com/neon-bindings/rfcs/pull/46 -futures = ["tokio"] +futures = ["dep:tokio"] # Enable low-level system APIs. The `sys` API allows augmenting the Neon API # from external crates. sys = [] +# Enable async runtime +tokio = ["tokio-rt-multi-thread"] # Shorter alias +tokio-rt = ["futures", "tokio/rt"] +tokio-rt-multi-thread = ["tokio-rt", "tokio/rt-multi-thread"] + # Default N-API version. Prefer to select a minimum required version. # DEPRECATED: This is an alias that should be removed napi-runtime = ["napi-8"] diff --git a/crates/neon/src/context/internal.rs b/crates/neon/src/context/internal.rs index 52164a5d6..763e642a1 100644 --- a/crates/neon/src/context/internal.rs +++ b/crates/neon/src/context/internal.rs @@ -55,6 +55,8 @@ pub trait ContextInternal<'cx>: Sized { } fn default_main(mut cx: ModuleContext) -> NeonResult<()> { + #[cfg(feature = "tokio-rt-multi-thread")] + crate::executor::tokio::init(&mut cx)?; crate::registered().export(&mut cx) } diff --git a/crates/neon/src/executor/mod.rs b/crates/neon/src/executor/mod.rs new file mode 100644 index 000000000..4d17389e5 --- /dev/null +++ b/crates/neon/src/executor/mod.rs @@ -0,0 +1,59 @@ +use std::{future::Future, pin::Pin}; + +use crate::{context::Cx, thread::LocalKey}; + +#[cfg(feature = "tokio-rt")] +pub(crate) mod tokio; + +type BoxFuture = Pin + Send + 'static>>; + +pub(crate) static RUNTIME: LocalKey> = LocalKey::new(); + +pub trait Runtime: Send + Sync + 'static { + fn spawn(&self, fut: BoxFuture); +} + +/// Register a [`Future`] executor runtime globally to the addon. +/// +/// Returns `Ok(())` if a global executor has not been set and `Err(runtime)` if it has. +/// +/// If the `tokio` feature flag is enabled and the addon does not provide a +/// [`#[neon::main]`](crate::main) function, a multithreaded tokio runtime will be +/// automatically registered. +/// +/// **Note**: Each instance of the addon will have its own runtime. It is recommended +/// to initialize the async runtime once in a process global and share it across instances. +/// +/// ``` +/// # #[cfg(feature = "tokio-rt-multi-thread")] +/// # fn example() { +/// # use neon::prelude::*; +/// use once_cell::sync::OnceCell; +/// use tokio::runtime::Runtime; +/// +/// static RUNTIME: OnceCell = OnceCell::new(); +/// +/// #[neon::main] +/// fn main(mut cx: ModuleContext) -> NeonResult<()> { +/// let runtime = RUNTIME +/// .get_or_try_init(Runtime::new) +/// .or_else(|err| cx.throw_error(err.to_string()))?; +/// +/// let _ = neon::set_global_executor(&mut cx, runtime); +/// +/// Ok(()) +/// } +/// # } +/// ``` +pub fn set_global_executor(cx: &mut Cx, runtime: R) -> Result<(), R> +where + R: Runtime, +{ + if RUNTIME.get(cx).is_some() { + return Err(runtime); + } + + RUNTIME.get_or_init(cx, || Box::new(runtime)); + + Ok(()) +} diff --git a/crates/neon/src/executor/tokio.rs b/crates/neon/src/executor/tokio.rs new file mode 100644 index 000000000..e4c497006 --- /dev/null +++ b/crates/neon/src/executor/tokio.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use super::{BoxFuture, Runtime}; + +impl Runtime for tokio::runtime::Runtime { + fn spawn(&self, fut: BoxFuture) { + spawn(self.handle(), fut); + } +} + +impl Runtime for Arc { + fn spawn(&self, fut: BoxFuture) { + spawn(self.handle(), fut); + } +} + +impl Runtime for &'static tokio::runtime::Runtime { + fn spawn(&self, fut: BoxFuture) { + spawn(self.handle(), fut); + } +} + +impl Runtime for tokio::runtime::Handle { + fn spawn(&self, fut: BoxFuture) { + spawn(self, fut); + } +} + +impl Runtime for &'static tokio::runtime::Handle { + fn spawn(&self, fut: BoxFuture) { + spawn(self, fut); + } +} + +fn spawn(handle: &tokio::runtime::Handle, fut: BoxFuture) { + #[allow(clippy::let_underscore_future)] + let _ = handle.spawn(fut); +} + +#[cfg(feature = "tokio-rt-multi-thread")] +pub(crate) fn init(cx: &mut crate::context::ModuleContext) -> crate::result::NeonResult<()> { + use once_cell::sync::OnceCell; + use tokio::runtime::{Builder, Runtime}; + + use crate::context::Context; + + static RUNTIME: OnceCell = OnceCell::new(); + + super::RUNTIME.get_or_try_init(cx, |cx| { + let runtime = RUNTIME + .get_or_try_init(|| { + #[cfg(feature = "tokio-rt-multi-thread")] + let mut builder = Builder::new_multi_thread(); + + #[cfg(not(feature = "tokio-rt-multi-thread"))] + let mut builder = Builder::new_current_thread(); + + builder.enable_all().build() + }) + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(Box::new(runtime)) + })?; + + Ok(()) +} diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index a471b59b5..1073c0a8a 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -102,6 +102,9 @@ mod types_impl; #[cfg_attr(docsrs, doc(cfg(feature = "sys")))] pub mod sys; +#[cfg(all(feature = "napi-6", feature = "futures"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-6", feature = "futures"))))] +pub use executor::set_global_executor; pub use types_docs::exports as types; #[doc(hidden)] @@ -114,12 +117,15 @@ use crate::{context::ModuleContext, handle::Handle, result::NeonResult, types::J #[cfg(feature = "napi-6")] mod lifecycle; +#[cfg(all(feature = "napi-6", feature = "futures"))] +mod executor; + #[cfg(feature = "napi-8")] static MODULE_TAG: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { let mut lower = [0; std::mem::size_of::()]; // Generating a random module tag at runtime allows Neon builds to be reproducible. A few - // alternativeswere considered: + // alternatives considered: // * Generating a random value at build time; this reduces runtime dependencies but, breaks // reproducible builds // * A static random value; this solves the previous issues, but does not protect against ABI diff --git a/crates/neon/src/macro_internal/futures.rs b/crates/neon/src/macro_internal/futures.rs new file mode 100644 index 000000000..f850e4b63 --- /dev/null +++ b/crates/neon/src/macro_internal/futures.rs @@ -0,0 +1,29 @@ +use std::future::Future; + +use crate::{ + context::{Context, Cx, TaskContext}, + result::JsResult, + types::JsValue, +}; + +pub fn spawn<'cx, F, S>(cx: &mut Cx<'cx>, fut: F, settle: S) -> JsResult<'cx, JsValue> +where + F: Future + Send + 'static, + F::Output: Send, + S: FnOnce(TaskContext, F::Output) -> JsResult + Send + 'static, +{ + let rt = match crate::executor::RUNTIME.get(cx) { + Some(rt) => rt, + None => return cx.throw_error("must initialize with neon::set_global_executor"), + }; + + let ch = cx.channel(); + let (d, promise) = cx.promise(); + + rt.spawn(Box::pin(async move { + let res = fut.await; + let _ = d.try_settle_with(&ch, move |cx| settle(cx, res)); + })); + + Ok(promise.upcast()) +} diff --git a/crates/neon/src/macro_internal/mod.rs b/crates/neon/src/macro_internal/mod.rs index bc4b1ecdb..06ef021a6 100644 --- a/crates/neon/src/macro_internal/mod.rs +++ b/crates/neon/src/macro_internal/mod.rs @@ -1,14 +1,25 @@ //! Internals needed by macros. These have to be exported for the macros to work +use std::marker::PhantomData; + pub use linkme; use crate::{ - context::{Cx, ModuleContext}, + context::{Context, Cx, ModuleContext}, handle::Handle, result::{JsResult, NeonResult}, types::{extract::TryIntoJs, JsValue}, }; +#[cfg(feature = "serde")] +use crate::types::extract::Json; + +#[cfg(all(feature = "napi-6", feature = "futures"))] +pub use self::futures::*; + +#[cfg(all(feature = "napi-6", feature = "futures"))] +mod futures; + type Export<'cx> = (&'static str, Handle<'cx, JsValue>); #[linkme::distributed_slice] @@ -17,46 +28,86 @@ pub static EXPORTS: [for<'cx> fn(&mut ModuleContext<'cx>) -> NeonResult fn(ModuleContext<'cx>) -> NeonResult<()>]; -// Provides an identically named method to `NeonExportReturnJson` for easy swapping in macros -pub trait NeonExportReturnValue<'cx> { - fn try_neon_export_return(self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsValue>; +// Wrapper for the value type and return type tags +pub struct NeonMarker(PhantomData, PhantomData); + +// Markers to determine the type of a value +#[cfg(feature = "serde")] +pub struct NeonJsonTag; +pub struct NeonValueTag; +pub struct NeonResultTag; + +pub trait ToNeonMarker { + type Return; + + fn to_neon_marker(&self) -> NeonMarker; } -impl<'cx, T> NeonExportReturnValue<'cx> for T -where - T: TryIntoJs<'cx>, -{ - fn try_neon_export_return(self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsValue> { - self.try_into_js(cx).map(|v| v.upcast()) +// Specialized implementation for `Result` +impl ToNeonMarker for Result { + type Return = NeonResultTag; + + fn to_neon_marker(&self) -> NeonMarker { + NeonMarker(PhantomData, PhantomData) } } -#[cfg(feature = "serde")] -// Trait used for specializing `Json` wrapping of `T` or `Result` in macros -// Leverages the [autoref specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) technique -pub trait NeonExportReturnJson<'cx> { - fn try_neon_export_return(self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsValue>; +// Default implementation that takes lower precedence due to autoref +impl ToNeonMarker for &T { + type Return = NeonValueTag; + + fn to_neon_marker(&self) -> NeonMarker { + NeonMarker(PhantomData, PhantomData) + } +} + +impl NeonMarker { + pub fn neon_into_js<'cx, T>(self, cx: &mut Cx<'cx>, v: T) -> JsResult<'cx, JsValue> + where + T: TryIntoJs<'cx>, + { + v.try_into_js(cx).map(|v| v.upcast()) + } } #[cfg(feature = "serde")] -// More specific behavior wraps `Result::Ok` in `Json` -impl<'cx, T, E> NeonExportReturnJson<'cx> for Result -where - T: serde::Serialize, - E: TryIntoJs<'cx>, -{ - fn try_neon_export_return(self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsValue> { - self.map(crate::types::extract::Json).try_into_js(cx) +impl NeonMarker { + pub fn neon_into_js<'cx, T>(self, cx: &mut Cx<'cx>, v: T) -> JsResult<'cx, JsValue> + where + Json: TryIntoJs<'cx>, + { + Json(v).try_into_js(cx).map(|v| v.upcast()) } } #[cfg(feature = "serde")] -// Due to autoref behavior, this is less specific than the other implementation -impl<'cx, T> NeonExportReturnJson<'cx> for &T -where - T: serde::Serialize, -{ - fn try_neon_export_return(self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsValue> { - crate::types::extract::Json(self).try_into_js(cx) +impl NeonMarker { + pub fn neon_into_js<'cx, T, E>( + self, + cx: &mut Cx<'cx>, + res: Result, + ) -> JsResult<'cx, JsValue> + where + Result, E>: TryIntoJs<'cx>, + { + res.map(Json).try_into_js(cx).map(|v| v.upcast()) + } +} + +impl NeonMarker { + pub fn into_neon_result(self, _cx: &mut Cx, v: T) -> NeonResult { + Ok(v) + } +} + +impl NeonMarker { + pub fn into_neon_result<'cx, T, E>(self, cx: &mut Cx<'cx>, res: Result) -> NeonResult + where + E: TryIntoJs<'cx>, + { + match res { + Ok(v) => Ok(v), + Err(err) => err.try_into_js(cx).and_then(|err| cx.throw(err)), + } } } diff --git a/crates/neon/src/macros.rs b/crates/neon/src/macros.rs index b32c838f6..1b220c839 100644 --- a/crates/neon/src/macros.rs +++ b/crates/neon/src/macros.rs @@ -6,7 +6,9 @@ /// This attribute should only be used _once_ in a module and will /// be called each time the module is initialized in a context. /// -/// If a `main` function is not provided, all registered exports will be exported. +/// If a `main` function is not provided, all registered exports will be exported. If +/// the `tokio` feature flag is enabled, a multithreaded tokio runtime will also be +/// registered globally. /// /// ``` /// # use neon::prelude::*; @@ -122,6 +124,61 @@ pub use neon_macros::main; /// } /// ``` /// +/// ### Async Functions +/// +/// The [`export`] macro can export `async fn`, converting to a JavaScript `Promise`, if a global +/// future executor is registered. See [`neon::set_global_executor`](crate::set_global_executor) for +/// more details. +/// +/// ``` +/// # #[cfg(all(feature = "napi-6", feature = "futures"))] +/// # { +/// #[neon::export] +/// async fn add(a: f64, b: f64) -> f64 { +/// a + b +/// } +/// # } +/// ``` +/// +/// A `fn` that returns a [`Future`](std::future::Future) can be annotated with `#[neon::export(async)]` +/// if it needs to perform some setup on the JavaScript main thread before running asynchronously. +/// +/// ``` +/// # #[cfg(all(feature = "napi-6", feature = "futures"))] +/// # { +/// # use std::future::Future; +/// # use neon::prelude::*; +/// #[neon::export(async)] +/// fn add(a: f64, b: f64) -> impl Future { +/// println!("Hello from the JavaScript main thread!"); +/// +/// async move { +/// a + b +/// } +/// } +/// # } +/// ``` +/// +/// If work needs to be performed on the JavaScript main thread _after_ the asynchronous operation, +/// the [`With`](crate::types::extract::With) extractor can be used to execute a closure before returning. +/// +/// ``` +/// # #[cfg(all(feature = "napi-6", feature = "futures"))] +/// # { +/// # use neon::types::extract::{TryIntoJs, With}; +/// #[neon::export] +/// async fn add(a: f64, b: f64) -> impl for<'cx> TryIntoJs<'cx> { +/// let sum = a + b; +/// +/// With(move |_cx| { +/// println!("Hello from the JavaScript main thread!"); +/// +/// sum +/// }) +/// } +/// # } +/// ``` +/// /// ### Error Handling /// /// If an exported function returns a [`Result`], a JavaScript exception will be thrown diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index b671819d6..a9236a668 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -17,4 +17,4 @@ tokio = { version = "1.34.0", features = ["rt-multi-thread"] } [dependencies.neon] version = "1.0.0" path = "../../crates/neon" -features = ["futures", "napi-experimental", "external-buffers", "serde"] +features = ["futures", "napi-experimental", "external-buffers", "serde", "tokio"] diff --git a/test/napi/lib/futures.js b/test/napi/lib/futures.js index ae38df5c1..2c3025a7e 100644 --- a/test/napi/lib/futures.js +++ b/test/napi/lib/futures.js @@ -55,4 +55,63 @@ describe("Futures", () => { }, /exception/i); }); }); + + describe("Exported Async Functions", () => { + it("should be able to call `async fn`", async () => { + assert.strictEqual(await addon.async_fn_add(1, 2), 3); + }); + + it("should be able to call fn with async block", async () => { + assert.strictEqual(await addon.async_add(1, 2), 3); + }); + + it("should be able to call fallible `async fn`", async () => { + assert.strictEqual(await addon.async_fn_div(10, 2), 5); + + await assertRejects(() => addon.async_fn_div(10, 0), /Divide by zero/); + }); + + it("should be able to call fallible `async fn`", async () => { + assert.strictEqual(await addon.async_fn_div(10, 2), 5); + + await assertRejects(() => addon.async_fn_div(10, 0), /Divide by zero/); + }); + + it("should be able to call fallible fn with async block", async () => { + assert.strictEqual(await addon.async_div(10, 2), 5); + + await assertRejects(() => addon.async_div(10, 0), /Divide by zero/); + }); + + it("should be able to code on the event loop before and after async", async () => { + let startCalled = false; + let endCalled = false; + const eventHandler = (event) => { + switch (event) { + case "start": + startCalled = true; + break; + case "end": + endCalled = true; + break; + } + }; + + process.on("async_with_events", eventHandler); + + try { + let res = await addon.async_with_events([ + [1, 2], + [3, 4], + [5, 6], + ]); + + assert.deepStrictEqual([...res], [2, 12, 30]); + assert.ok(startCalled, "Did not emit start event"); + assert.ok(endCalled, "Did not emit end event"); + } finally { + process.off("async_with_events", eventHandler); + } + }); + }); }); diff --git a/test/napi/src/js/futures.rs b/test/napi/src/js/futures.rs index 3c71975bb..3b02c7e4a 100644 --- a/test/napi/src/js/futures.rs +++ b/test/napi/src/js/futures.rs @@ -1,16 +1,14 @@ -use { - neon::{prelude::*, types::buffer::TypedArray}, - once_cell::sync::OnceCell, - tokio::runtime::Runtime, +use std::future::Future; + +use neon::{ + prelude::*, + types::{ + buffer::TypedArray, + extract::{Error, Json, TryIntoJs, With}, + }, }; -fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { - static RUNTIME: OnceCell = OnceCell::new(); - - RUNTIME - .get_or_try_init(Runtime::new) - .or_else(|err| cx.throw_error(err.to_string())) -} +use crate::runtime; // Accepts two functions that take no parameters and return numbers. // Resolves with the sum of the two numbers. @@ -80,3 +78,60 @@ pub fn lazy_async_sum(mut cx: FunctionContext) -> JsResult { Ok(promise) } + +#[neon::export] +async fn async_fn_add(a: f64, b: f64) -> f64 { + a + b +} + +#[neon::export(async)] +fn async_add(a: f64, b: f64) -> impl Future { + async move { a + b } +} + +#[neon::export] +async fn async_fn_div(a: f64, b: f64) -> Result { + if b == 0.0 { + return Err(Error::from("Divide by zero")); + } + + Ok(a / b) +} + +#[neon::export(async)] +fn async_div(cx: &mut FunctionContext) -> NeonResult>> { + let (a, b): (f64, f64) = cx.args()?; + + Ok(async move { + if b == 0.0 { + return Err(Error::from("Divide by zero")); + } + + Ok(a / b) + }) +} + +#[neon::export(async)] +fn async_with_events( + cx: &mut FunctionContext, + Json(data): Json>, +) -> NeonResult TryIntoJs<'cx>>> { + fn emit(cx: &mut Cx, state: &str) -> NeonResult<()> { + cx.global::("process")? + .call_method_with(cx, "emit")? + .arg(cx.string("async_with_events")) + .arg(cx.string(state)) + .exec(cx) + } + + emit(cx, "start")?; + + Ok(async move { + let res = data.into_iter().map(|(a, b)| a * b).collect::>(); + + With(move |cx| -> NeonResult<_> { + emit(cx, "end")?; + Ok(res) + }) + }) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 627b3f80b..11890f5a2 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -1,4 +1,6 @@ use neon::prelude::*; +use once_cell::sync::OnceCell; +use tokio::runtime::Runtime; use crate::js::{ arrays::*, boxed::*, coercions::*, date::*, errors::*, functions::*, numbers::*, objects::*, @@ -27,6 +29,9 @@ mod js { #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { + let rt = runtime(&mut cx)?; + + neon::set_global_executor(&mut cx, rt).or_else(|_| cx.throw_error("executor already set"))?; neon::registered().export(&mut cx)?; assert!(neon::registered().into_iter().next().is_some()); @@ -417,3 +422,9 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { Ok(()) } + +fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { + static RUNTIME: OnceCell = OnceCell::new(); + + RUNTIME.get_or_try_init(|| Runtime::new().or_else(|err| cx.throw_error(err.to_string()))) +} From df091a2eb76e46923ba79f8960eb915ebb29954d Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Tue, 16 Jul 2024 17:09:15 -0400 Subject: [PATCH 2/2] test(neon): Add new feature flags to the build matrix and unignore the test --- Cargo.lock | 1 + crates/neon/Cargo.toml | 1 + crates/neon/src/lib.rs | 27 ++++++++------------------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 065d7b326..5884ca49b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,7 @@ dependencies = [ "doc-comment", "easy-cast", "getrandom", + "itertools", "libloading 0.8.1", "linkify", "linkme", diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index 76429c221..4fa442453 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -11,6 +11,7 @@ exclude = ["neon.jpg", "doc/**/*"] edition = "2021" [dev-dependencies] +itertools = "0.10.5" semver = "1.0.20" psd = "0.3.4" # used for a doc example anyhow = "1.0.75" # used for a doc example diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 1073c0a8a..13ff73251 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -149,18 +149,21 @@ pub struct Exports(()); impl Exports { /// Export all values exported with [`neon::export`](export) /// - /// ```ignore + /// ``` + /// # fn main() { /// # use neon::prelude::*; /// #[neon::main] /// fn main(mut cx: ModuleContext) -> NeonResult<()> { /// neon::registered().export(&mut cx)?; /// Ok(()) /// } + /// # } /// ``` /// /// For more control, iterate over exports. /// - /// ```ignore + /// ``` + /// # fn main() { /// # use neon::prelude::*; /// #[neon::main] /// fn main(mut cx: ModuleContext) -> NeonResult<()> { @@ -172,6 +175,7 @@ impl Exports { /// /// Ok(()) /// } + /// # } /// ``` pub fn export(self, cx: &mut ModuleContext) -> NeonResult<()> { for create in self { @@ -202,33 +206,18 @@ pub fn registered() -> Exports { } #[test] -#[ignore] fn feature_matrix() { use std::{env, process::Command}; - const EXTERNAL_BUFFERS: &str = "external-buffers"; - const FUTURES: &str = "futures"; - const SERDE: &str = "serde"; const NODE_API_VERSIONS: &[&str] = &[ "napi-1", "napi-2", "napi-3", "napi-4", "napi-5", "napi-6", "napi-7", "napi-8", ]; - // If the number of features in Neon grows, we can use `itertools` to generate permutations. - // https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.permutations - const FEATURES: &[&[&str]] = &[ - &[], - &[EXTERNAL_BUFFERS], - &[FUTURES], - &[SERDE], - &[EXTERNAL_BUFFERS, FUTURES], - &[EXTERNAL_BUFFERS, SERDE], - &[FUTURES, SERDE], - &[EXTERNAL_BUFFERS, FUTURES, SERDE], - ]; + const FEATURES: &[&str] = &["external-buffers", "futures", "serde", "tokio", "tokio-rt"]; let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - for features in FEATURES { + for features in itertools::Itertools::powerset(FEATURES.iter()) { for version in NODE_API_VERSIONS.iter().map(|f| f.to_string()) { let features = features.iter().fold(version, |f, s| f + "," + s); let status = Command::new(&cargo)