Skip to content

Commit 9223337

Browse files
committed
feat(neon): Add support for async functions in #[neon::export]
1 parent 6b541a2 commit 9223337

File tree

16 files changed

+505
-61
lines changed

16 files changed

+505
-61
lines changed

.cargo/config.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[alias]
22
# Neon defines mutually exclusive feature flags which prevents using `cargo clippy --all-features`
33
# The following aliases simplify linting the entire workspace
4-
neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers,serde"
5-
neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers,serde -- -A clippy::missing_safety_doc"
6-
neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,futures,external-buffers,serde"
7-
neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers,sys,serde -- --cfg docsrs"
4+
neon-check = " check --all --all-targets --features napi-experimental,external-buffers,serde,tokio"
5+
neon-clippy = "clippy --all --all-targets --features napi-experimental,external-buffers,serde,tokio -- -A clippy::missing_safety_doc"
6+
neon-test = " test --all --features=doc-dependencies,doc-comment,napi-experimental,external-buffers,serde,tokio"
7+
neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,external-buffers,sys,serde,tokio -- --cfg docsrs"

crates/neon-macros/src/export/function/meta.rs

+33-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub(crate) struct Meta {
88

99
#[derive(Default)]
1010
pub(super) enum Kind {
11+
Async,
12+
AsyncFn,
1113
#[default]
1214
Normal,
1315
Task,
@@ -28,7 +30,8 @@ impl Meta {
2830

2931
fn force_context(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
3032
match self.kind {
31-
Kind::Normal => {}
33+
Kind::Normal | Kind::AsyncFn => {}
34+
Kind::Async => return Err(meta.error(super::ASYNC_CX_ERROR)),
3235
Kind::Task => return Err(meta.error(super::TASK_CX_ERROR)),
3336
}
3437

@@ -37,6 +40,16 @@ impl Meta {
3740
Ok(())
3841
}
3942

43+
fn make_async(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
44+
if matches!(self.kind, Kind::AsyncFn) {
45+
return Err(meta.error(super::ASYNC_FN_ERROR));
46+
}
47+
48+
self.kind = Kind::Async;
49+
50+
Ok(())
51+
}
52+
4053
fn make_task(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
4154
if self.context {
4255
return Err(meta.error(super::TASK_CX_ERROR));
@@ -48,13 +61,25 @@ impl Meta {
4861
}
4962
}
5063

51-
pub(crate) struct Parser;
64+
pub(crate) struct Parser(syn::ItemFn);
65+
66+
impl Parser {
67+
pub(crate) fn new(item: syn::ItemFn) -> Self {
68+
Self(item)
69+
}
70+
}
5271

5372
impl syn::parse::Parser for Parser {
54-
type Output = Meta;
73+
type Output = (syn::ItemFn, Meta);
5574

5675
fn parse2(self, tokens: proc_macro2::TokenStream) -> syn::Result<Self::Output> {
76+
let Self(item) = self;
5777
let mut attr = Meta::default();
78+
79+
if item.sig.asyncness.is_some() {
80+
attr.kind = Kind::AsyncFn;
81+
}
82+
5883
let parser = syn::meta::parser(|meta| {
5984
if meta.path.is_ident("name") {
6085
return attr.set_name(meta);
@@ -68,6 +93,10 @@ impl syn::parse::Parser for Parser {
6893
return attr.force_context(meta);
6994
}
7095

96+
if meta.path.is_ident("async") {
97+
return attr.make_async(meta);
98+
}
99+
71100
if meta.path.is_ident("task") {
72101
return attr.make_task(meta);
73102
}
@@ -77,6 +106,6 @@ impl syn::parse::Parser for Parser {
77106

78107
parser.parse2(tokens)?;
79108

80-
Ok(attr)
109+
Ok((item, attr))
81110
}
82111
}

crates/neon-macros/src/export/function/mod.rs

+21-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use crate::export::function::meta::Kind;
22

33
pub(crate) mod meta;
44

5+
static ASYNC_CX_ERROR: &str = "`FunctionContext` is not allowed in async functions";
6+
static ASYNC_FN_ERROR: &str = "`async` attribute should not be used with an `async fn`";
57
static TASK_CX_ERROR: &str = "`FunctionContext` is not allowed with `task` attribute";
68

79
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
4042
.unwrap_or_else(|| quote::quote!(#name))
4143
});
4244

43-
// Import the value or JSON trait for conversion
44-
let result_trait_name = if meta.json {
45-
quote::format_ident!("NeonExportReturnJson")
45+
// Tag whether we should JSON wrap results
46+
let return_tag = if meta.json {
47+
quote::format_ident!("NeonJsonTag")
4648
} else {
47-
quote::format_ident!("NeonExportReturnValue")
49+
quote::format_ident!("NeonValueTag")
4850
};
4951

5052
// Convert the result
5153
// N.B.: Braces are intentionally included to avoid leaking trait to function body
5254
let result_extract = quote::quote!({
53-
use neon::macro_internal::#result_trait_name;
55+
use neon::macro_internal::{ToNeonMarker, #return_tag as NeonReturnTag};
5456

55-
res.try_neon_export_return(&mut cx)
57+
(&res).to_neon_marker::<NeonReturnTag>().neon_into_js(&mut cx, res)
5658
});
5759

5860
// 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
6365

6466
// Generate the call to the original function
6567
let call_body = match meta.kind {
68+
Kind::Async | Kind::AsyncFn => quote::quote!(
69+
let (#(#tuple_fields,)*) = cx.args()?;
70+
let fut = #name(#context_arg #(#args),*);
71+
let fut = {
72+
use neon::macro_internal::{ToNeonMarker, NeonValueTag};
73+
74+
(&fut).to_neon_marker::<NeonValueTag>().into_neon_result(&mut cx, fut)?
75+
};
76+
77+
neon::macro_internal::spawn(&mut cx, fut, |mut cx, res| #result_extract)
78+
),
6679
Kind::Normal => quote::quote!(
6780
let (#(#tuple_fields,)*) = cx.args()?;
6881
let res = #name(#context_arg #(#args),*);
@@ -160,7 +173,8 @@ fn has_context_arg(meta: &meta::Meta, sig: &syn::Signature) -> syn::Result<bool>
160173

161174
// Context is only allowed for normal functions
162175
match meta.kind {
163-
Kind::Normal => {}
176+
Kind::Normal | Kind::Async => {}
177+
Kind::AsyncFn => return Err(syn::Error::new(first.span(), ASYNC_CX_ERROR)),
164178
Kind::Task => return Err(syn::Error::new(first.span(), TASK_CX_ERROR)),
165179
}
166180

crates/neon-macros/src/export/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub(crate) fn export(
1313
match item {
1414
// Export a function
1515
syn::Item::Fn(item) => {
16-
let meta = syn::parse_macro_input!(attr with function::meta::Parser);
16+
let parser = function::meta::Parser::new(item);
17+
let (item, meta) = syn::parse_macro_input!(attr with parser);
1718

1819
function::export(meta, item)
1920
}

crates/neon/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,17 @@ external-buffers = []
5656

5757
# Experimental Rust Futures API
5858
# https://github.com/neon-bindings/rfcs/pull/46
59-
futures = ["tokio"]
59+
futures = ["dep:tokio"]
6060

6161
# Enable low-level system APIs. The `sys` API allows augmenting the Neon API
6262
# from external crates.
6363
sys = []
6464

65+
# Enable async runtime
66+
tokio = ["tokio-rt-multi-thread"] # Shorter alias
67+
tokio-rt = ["futures", "tokio/rt"]
68+
tokio-rt-multi-thread = ["tokio-rt", "tokio/rt-multi-thread"]
69+
6570
# Default N-API version. Prefer to select a minimum required version.
6671
# DEPRECATED: This is an alias that should be removed
6772
napi-runtime = ["napi-8"]

crates/neon/src/context/internal.rs

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ pub trait ContextInternal<'cx>: Sized {
5555
}
5656

5757
fn default_main(mut cx: ModuleContext) -> NeonResult<()> {
58+
#[cfg(feature = "tokio-rt-multi-thread")]
59+
crate::executor::tokio::init(&mut cx)?;
5860
crate::registered().export(&mut cx)
5961
}
6062

crates/neon/src/executor/mod.rs

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::{future::Future, pin::Pin};
2+
3+
use crate::{context::Cx, thread::LocalKey};
4+
5+
#[cfg(feature = "tokio-rt")]
6+
pub(crate) mod tokio;
7+
8+
type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
9+
10+
pub(crate) static RUNTIME: LocalKey<Box<dyn Runtime>> = LocalKey::new();
11+
12+
pub trait Runtime: Send + Sync + 'static {
13+
fn spawn(&self, fut: BoxFuture);
14+
}
15+
16+
/// Register a [`Future`] executor runtime globally to the addon.
17+
///
18+
/// Returns `Ok(())` if a global executor has not been set and `Err(runtime)` if it has.
19+
///
20+
/// If the `tokio` feature flag is enabled and the addon does not provide a
21+
/// [`#[neon::main]`](crate::main) function, a multithreaded tokio runtime will be
22+
/// automatically registered.
23+
///
24+
/// **Note**: Each instance of the addon will have its own runtime. It is recommended
25+
/// to initialize the async runtime once in a process global and share it across instances.
26+
///
27+
/// ```
28+
/// # #[cfg(feature = "tokio-rt-multi-thread")]
29+
/// # fn example() {
30+
/// # use neon::prelude::*;
31+
/// use once_cell::sync::OnceCell;
32+
/// use tokio::runtime::Runtime;
33+
///
34+
/// static RUNTIME: OnceCell<Runtime> = OnceCell::new();
35+
///
36+
/// #[neon::main]
37+
/// fn main(mut cx: ModuleContext) -> NeonResult<()> {
38+
/// let runtime = RUNTIME
39+
/// .get_or_try_init(Runtime::new)
40+
/// .or_else(|err| cx.throw_error(err.to_string()))?;
41+
///
42+
/// let _ = neon::set_global_executor(&mut cx, runtime);
43+
///
44+
/// Ok(())
45+
/// }
46+
/// # }
47+
/// ```
48+
pub fn set_global_executor<R>(cx: &mut Cx, runtime: R) -> Result<(), R>
49+
where
50+
R: Runtime,
51+
{
52+
if RUNTIME.get(cx).is_some() {
53+
return Err(runtime);
54+
}
55+
56+
RUNTIME.get_or_init(cx, || Box::new(runtime));
57+
58+
Ok(())
59+
}

crates/neon/src/executor/tokio.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::sync::Arc;
2+
3+
use super::{BoxFuture, Runtime};
4+
5+
impl Runtime for tokio::runtime::Runtime {
6+
fn spawn(&self, fut: BoxFuture) {
7+
spawn(self.handle(), fut);
8+
}
9+
}
10+
11+
impl Runtime for Arc<tokio::runtime::Runtime> {
12+
fn spawn(&self, fut: BoxFuture) {
13+
spawn(self.handle(), fut);
14+
}
15+
}
16+
17+
impl Runtime for &'static tokio::runtime::Runtime {
18+
fn spawn(&self, fut: BoxFuture) {
19+
spawn(self.handle(), fut);
20+
}
21+
}
22+
23+
impl Runtime for tokio::runtime::Handle {
24+
fn spawn(&self, fut: BoxFuture) {
25+
spawn(self, fut);
26+
}
27+
}
28+
29+
impl Runtime for &'static tokio::runtime::Handle {
30+
fn spawn(&self, fut: BoxFuture) {
31+
spawn(self, fut);
32+
}
33+
}
34+
35+
fn spawn(handle: &tokio::runtime::Handle, fut: BoxFuture) {
36+
#[allow(clippy::let_underscore_future)]
37+
let _ = handle.spawn(fut);
38+
}
39+
40+
#[cfg(feature = "tokio-rt-multi-thread")]
41+
pub(crate) fn init(cx: &mut crate::context::ModuleContext) -> crate::result::NeonResult<()> {
42+
use once_cell::sync::OnceCell;
43+
use tokio::runtime::{Builder, Runtime};
44+
45+
use crate::context::Context;
46+
47+
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
48+
49+
super::RUNTIME.get_or_try_init(cx, |cx| {
50+
let runtime = RUNTIME
51+
.get_or_try_init(|| {
52+
#[cfg(feature = "tokio-rt-multi-thread")]
53+
let mut builder = Builder::new_multi_thread();
54+
55+
#[cfg(not(feature = "tokio-rt-multi-thread"))]
56+
let mut builder = Builder::new_current_thread();
57+
58+
builder.enable_all().build()
59+
})
60+
.or_else(|err| cx.throw_error(err.to_string()))?;
61+
62+
Ok(Box::new(runtime))
63+
})?;
64+
65+
Ok(())
66+
}

crates/neon/src/lib.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ mod types_impl;
102102
#[cfg_attr(docsrs, doc(cfg(feature = "sys")))]
103103
pub mod sys;
104104

105+
#[cfg(all(feature = "napi-6", feature = "futures"))]
106+
#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-6", feature = "futures"))))]
107+
pub use executor::set_global_executor;
105108
pub use types_docs::exports as types;
106109

107110
#[doc(hidden)]
@@ -114,12 +117,15 @@ use crate::{context::ModuleContext, handle::Handle, result::NeonResult, types::J
114117
#[cfg(feature = "napi-6")]
115118
mod lifecycle;
116119

120+
#[cfg(all(feature = "napi-6", feature = "futures"))]
121+
mod executor;
122+
117123
#[cfg(feature = "napi-8")]
118124
static MODULE_TAG: once_cell::sync::Lazy<crate::sys::TypeTag> = once_cell::sync::Lazy::new(|| {
119125
let mut lower = [0; std::mem::size_of::<u64>()];
120126

121127
// Generating a random module tag at runtime allows Neon builds to be reproducible. A few
122-
// alternativeswere considered:
128+
// alternatives considered:
123129
// * Generating a random value at build time; this reduces runtime dependencies but, breaks
124130
// reproducible builds
125131
// * A static random value; this solves the previous issues, but does not protect against ABI
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::future::Future;
2+
3+
use crate::{
4+
context::{Context, Cx, TaskContext},
5+
result::JsResult,
6+
types::JsValue,
7+
};
8+
9+
pub fn spawn<'cx, F, S>(cx: &mut Cx<'cx>, fut: F, settle: S) -> JsResult<'cx, JsValue>
10+
where
11+
F: Future + Send + 'static,
12+
F::Output: Send,
13+
S: FnOnce(TaskContext, F::Output) -> JsResult<JsValue> + Send + 'static,
14+
{
15+
let rt = match crate::executor::RUNTIME.get(cx) {
16+
Some(rt) => rt,
17+
None => return cx.throw_error("must initialize with neon::set_global_executor"),
18+
};
19+
20+
let ch = cx.channel();
21+
let (d, promise) = cx.promise();
22+
23+
rt.spawn(Box::pin(async move {
24+
let res = fut.await;
25+
let _ = d.try_settle_with(&ch, move |cx| settle(cx, res));
26+
}));
27+
28+
Ok(promise.upcast())
29+
}

0 commit comments

Comments
 (0)