Skip to content

Commit e4f75dc

Browse files
committed
feat(neon): Implementation for JsPromise and TaskBuilder
RFC: neon-bindings/rfcs#35
1 parent cb884ee commit e4f75dc

File tree

24 files changed

+898
-56
lines changed

24 files changed

+898
-56
lines changed

.cargo/config.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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-
check-napi = "check --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --features proc-macros,try-catch-api,napi-experimental"
4+
check-napi = "check --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --features proc-macros,try-catch-api,napi-experimental,promise-api,task-api"
55
check-legacy = "check --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p tests -p static_tests --features event-handler-api,proc-macros,try-catch-api,legacy-runtime"
66
clippy-legacy = "clippy --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p tests -p static_tests --features event-handler-api,proc-macros,try-catch-api,legacy-runtime -- -A clippy::missing_safety_doc"
7-
clippy-napi = "clippy --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --features proc-macros,try-catch-api,napi-experimental -- -A clippy::missing_safety_doc"
7+
clippy-napi = "clippy --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --features proc-macros,try-catch-api,napi-experimental,promise-api,task-api -- -A clippy::missing_safety_doc"
88
neon-test = "test --no-default-features --features napi-experimental"
9-
neon-doc = "rustdoc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api -- --cfg docsrs"
9+
neon-doc = "rustdoc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api,promise-api,task-api -- --cfg docsrs"
10+
neon-doc-test = "test --doc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api,promise-api,task-api"

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ event-queue-api = ["channel-api"]
7878
# Feature flag to include procedural macros
7979
proc-macros = ["neon-macros"]
8080

81+
# Enable `JsPromise` and `Deferred`
82+
# https://github.com/neon-bindings/rfcs/pull/35
83+
promise-api = []
84+
# Enable `TaskBuilder`
85+
# https://github.com/neon-bindings/rfcs/pull/35
86+
task-api = []
87+
8188
[package.metadata.docs.rs]
8289
no-default-features = true
8390
rustdoc-args = ["--cfg", "docsrs"]
@@ -86,6 +93,8 @@ features = [
8693
"napi-experimental",
8794
"proc-macros",
8895
"try-catch-api",
96+
"promise-api",
97+
"task-api",
8998
]
9099

91100
[workspace]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! Rust wrappers for Node-API simple asynchronous operations
2+
//!
3+
//! Unlike `napi_async_work` which threads a single mutable pointer to a data
4+
//! struct to both the `execute` and `complete` callbacks, the wrapper follows
5+
//! a more idiomatic Rust ownership pattern by passing the output of `execute`
6+
//! into the input of `complete`.
7+
//!
8+
//! https://nodejs.org/api/n-api.html#n_api_simple_asynchronous_operations
9+
10+
use std::ffi::c_void;
11+
use std::mem;
12+
use std::ptr;
13+
14+
use crate::napi::bindings as napi;
15+
use crate::raw::Env;
16+
17+
type Execute<T, O> = fn(input: T) -> O;
18+
type Complete<O> = fn(env: Env, output: O);
19+
20+
/// Schedule work to execute on the libuv thread pool
21+
///
22+
/// # Safety
23+
/// * `env` must be a valid `napi_env` for the current thread
24+
pub unsafe fn schedule<T, O>(env: Env, input: T, execute: Execute<T, O>, complete: Complete<O>)
25+
where
26+
T: Send + 'static,
27+
O: Send + 'static,
28+
{
29+
let mut data = Box::new(Data {
30+
state: State::Input(input),
31+
execute,
32+
complete,
33+
// Work is initialized as a null pointer, but set by `create_async_work`
34+
// `data` must not be used until this value has been set.
35+
work: ptr::null_mut(),
36+
});
37+
38+
// Store a pointer to `work` before ownership is transferred to `Box::into_raw`
39+
let work = &mut data.work as *mut _;
40+
41+
// Create the `async_work`
42+
assert_eq!(
43+
napi::create_async_work(
44+
env,
45+
ptr::null_mut(),
46+
super::string(env, "neon_async_work"),
47+
Some(call_execute::<T, O>),
48+
Some(call_complete::<T, O>),
49+
Box::into_raw(data).cast(),
50+
work,
51+
),
52+
napi::Status::Ok,
53+
);
54+
55+
// Queue the work
56+
match napi::queue_async_work(env, *work) {
57+
napi::Status::Ok => {}
58+
status => {
59+
// If queueing failed, delete the work to prevent a leak
60+
napi::delete_async_work(env, *work);
61+
assert_eq!(status, napi::Status::Ok);
62+
}
63+
}
64+
}
65+
66+
/// A pointer to data is passed to the `execute` and `complete` callbacks
67+
struct Data<T, O> {
68+
state: State<T, O>,
69+
execute: Execute<T, O>,
70+
complete: Complete<O>,
71+
work: napi::AsyncWork,
72+
}
73+
74+
/// State of the task that is transitioned by `execute` and `complete`
75+
enum State<T, O> {
76+
/// Initial data input passed to `execute`
77+
Input(T),
78+
/// Transient state while `execute` is running
79+
Executing,
80+
/// Return data of `execute` passed to `complete`
81+
Output(O),
82+
}
83+
84+
impl<T, O> State<T, O> {
85+
/// Return the input if `State::Input`, replacing with `State::Executing`
86+
fn take_execute_input(&mut self) -> Option<T> {
87+
match mem::replace(self, Self::Executing) {
88+
Self::Input(input) => Some(input),
89+
_ => None,
90+
}
91+
}
92+
93+
/// Return the output if `State::Output`, replacing with `State::Executing`
94+
fn into_output(self) -> Option<O> {
95+
match self {
96+
Self::Output(output) => Some(output),
97+
_ => None,
98+
}
99+
}
100+
}
101+
102+
/// Callback executed on the libuv thread pool
103+
///
104+
/// # Safety
105+
/// * `Env` should not be used because it could attempt to call JavaScript
106+
/// * `data` is expected to be a pointer to `Data<T, O>`
107+
unsafe extern "C" fn call_execute<T, O>(_: Env, data: *mut c_void) {
108+
let data = &mut *data.cast::<Data<T, O>>();
109+
// `unwrap` is ok because `call_execute` should be called exactly once
110+
// after initialization
111+
let input = data.state.take_execute_input().unwrap();
112+
let output = (data.execute)(input);
113+
114+
data.state = State::Output(output);
115+
}
116+
117+
/// Callback executed on the JavaScript main thread
118+
///
119+
/// # Safety
120+
/// * `data` is expected to be a pointer to `Data<T, O>`
121+
unsafe extern "C" fn call_complete<T, O>(env: Env, status: napi::Status, data: *mut c_void) {
122+
let Data {
123+
state,
124+
complete,
125+
work,
126+
..
127+
} = *Box::<Data<T, O>>::from_raw(data.cast());
128+
129+
napi::delete_async_work(env, work);
130+
131+
match status {
132+
// `unwrap` is okay because `call_complete` should be called exactly once
133+
// if and only if `call_execute` has completed successfully
134+
napi::Status::Ok => complete(env, state.into_output().unwrap()),
135+
napi::Status::Cancelled => {}
136+
_ => assert_eq!(status, napi::Status::Ok),
137+
}
138+
}

crates/neon-runtime/src/napi/bindings/functions.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ mod napi1 {
5757
fn is_buffer(env: Env, value: Value, result: *mut bool) -> Status;
5858
fn is_error(env: Env, value: Value, result: *mut bool) -> Status;
5959
fn is_array(env: Env, value: Value, result: *mut bool) -> Status;
60+
fn is_promise(env: Env, value: Value, result: *mut bool) -> Status;
6061

6162
fn get_value_string_utf8(
6263
env: Env,
@@ -209,6 +210,22 @@ mod napi1 {
209210
) -> Status;
210211

211212
fn run_script(env: Env, script: Value, result: *mut Value) -> Status;
213+
214+
fn create_async_work(
215+
env: Env,
216+
async_resource: Value,
217+
async_resource_name: Value,
218+
execute: AsyncExecuteCallback,
219+
complete: AsyncCompleteCallback,
220+
data: *mut c_void,
221+
result: *mut AsyncWork,
222+
) -> Status;
223+
224+
fn delete_async_work(env: Env, work: AsyncWork) -> Status;
225+
fn queue_async_work(env: Env, work: AsyncWork) -> Status;
226+
fn create_promise(env: Env, deferred: *mut Deferred, promise: *mut Value) -> Status;
227+
fn resolve_deferred(env: Env, deferred: Deferred, resolution: Value) -> Status;
228+
fn reject_deferred(env: Env, deferred: Deferred, rejection: Value) -> Status;
212229
}
213230
);
214231
}

crates/neon-runtime/src/napi/bindings/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ macro_rules! generate {
169169
use std::sync::Once;
170170

171171
pub(crate) use functions::*;
172-
pub use types::TypedArrayType;
173172
pub(crate) use types::*;
173+
pub use types::{Deferred, TypedArrayType};
174174

175175
mod functions;
176176
mod types;

crates/neon-runtime/src/napi/bindings/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub type CallbackInfo = *mut CallbackInfo__;
2929
pub struct EscapableHandleScope__ {
3030
_unused: [u8; 0],
3131
}
32+
3233
pub type EscapableHandleScope = *mut EscapableHandleScope__;
3334

3435
#[repr(C)]
@@ -203,3 +204,24 @@ impl std::ops::BitAndAssign for KeyFilter {
203204
self.0 &= rhs.0;
204205
}
205206
}
207+
208+
#[repr(C)]
209+
#[derive(Debug, Copy, Clone)]
210+
pub struct AsyncWork__ {
211+
_unused: [u8; 0],
212+
}
213+
214+
pub type AsyncWork = *mut AsyncWork__;
215+
216+
pub type AsyncExecuteCallback = Option<unsafe extern "C" fn(env: Env, data: *mut c_void)>;
217+
218+
pub type AsyncCompleteCallback =
219+
Option<unsafe extern "C" fn(env: Env, status: Status, data: *mut c_void)>;
220+
221+
#[repr(C)]
222+
#[derive(Debug, Copy, Clone)]
223+
pub struct Deferred__ {
224+
_unused: [u8; 0],
225+
}
226+
227+
pub type Deferred = *mut Deferred__;

crates/neon-runtime/src/napi/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod array;
22
pub mod arraybuffer;
3+
pub mod async_work;
34
pub mod buffer;
45
pub mod call;
56
pub mod convert;
@@ -13,6 +14,7 @@ pub mod lifecycle;
1314
pub mod mem;
1415
pub mod object;
1516
pub mod primitive;
17+
pub mod promise;
1618
pub mod raw;
1719
pub mod reference;
1820
pub mod scope;
@@ -23,4 +25,29 @@ pub mod tsfn;
2325
pub mod typedarray;
2426

2527
mod bindings;
28+
2629
pub use bindings::*;
30+
31+
use std::mem::MaybeUninit;
32+
33+
/// Create a JavaScript `String`, panicking if unsuccessful
34+
///
35+
/// # Safety
36+
/// * `env` is a `napi_env` valid for the current thread
37+
/// * The returned value does not outlive `env`
38+
unsafe fn string(env: Env, s: impl AsRef<str>) -> raw::Local {
39+
let s = s.as_ref();
40+
let mut result = MaybeUninit::uninit();
41+
42+
assert_eq!(
43+
create_string_utf8(
44+
env,
45+
s.as_bytes().as_ptr() as *const _,
46+
s.len(),
47+
result.as_mut_ptr(),
48+
),
49+
Status::Ok,
50+
);
51+
52+
result.assume_init()
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! JavaScript Promise and Deferred handle
2+
//!
3+
//! https://nodejs.org/api/n-api.html#n_api_promises
4+
5+
use std::mem::MaybeUninit;
6+
use std::ptr;
7+
8+
use crate::napi::bindings as napi;
9+
use crate::raw::Env;
10+
11+
/// Create a `Promise` and a `napi::Deferred` handle for resolving it
12+
///
13+
/// # Safety
14+
/// * `env` is a valid `napi_env` for the current thread
15+
/// * The returned `napi::Value` does not outlive `env`
16+
pub unsafe fn create(env: Env) -> (napi::Deferred, napi::Value) {
17+
let mut deferred = MaybeUninit::uninit();
18+
let mut promise = MaybeUninit::uninit();
19+
20+
assert_eq!(
21+
napi::create_promise(env, deferred.as_mut_ptr(), promise.as_mut_ptr()),
22+
napi::Status::Ok,
23+
);
24+
25+
(deferred.assume_init(), promise.assume_init())
26+
}
27+
28+
/// Resolve a promise from a `napi::Deferred` handle
29+
///
30+
/// # Safety
31+
/// * `env` is a valid `napi_env` for the current thread
32+
/// * `resolution` is a valid `napi::Value`
33+
pub unsafe fn resolve(env: Env, deferred: napi::Deferred, resolution: napi::Value) {
34+
assert_eq!(
35+
napi::resolve_deferred(env, deferred, resolution),
36+
napi::Status::Ok,
37+
);
38+
}
39+
40+
/// Rejects a promise from a `napi::Deferred` handle
41+
///
42+
/// # Safety
43+
/// * `env` is a valid `napi_env` for the current thread
44+
/// * `rejection` is a valid `napi::Value`
45+
pub unsafe fn reject(env: Env, deferred: napi::Deferred, rejection: napi::Value) {
46+
assert_eq!(
47+
napi::reject_deferred(env, deferred, rejection),
48+
napi::Status::Ok,
49+
);
50+
}
51+
52+
/// Rejects a promise from a `napi::Deferred` handle with a string message
53+
///
54+
/// # Safety
55+
/// * `env` is a valid `napi_env` for the current thread
56+
pub unsafe fn reject_err_message(env: Env, deferred: napi::Deferred, msg: impl AsRef<str>) {
57+
let msg = super::string(env, msg);
58+
let mut err = MaybeUninit::uninit();
59+
60+
assert_eq!(
61+
napi::create_error(env, ptr::null_mut(), msg, err.as_mut_ptr()),
62+
napi::Status::Ok,
63+
);
64+
65+
reject(env, deferred, err.assume_init());
66+
}

crates/neon-runtime/src/napi/tag.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,16 @@ pub unsafe fn is_date(env: Env, val: Local) -> bool {
9999
);
100100
result
101101
}
102+
103+
/// Is `val` a Promise?
104+
///
105+
/// # Safety
106+
/// * `env` is a valid `napi_env` for the current thread
107+
pub unsafe fn is_promise(env: Env, val: Local) -> bool {
108+
let mut result = false;
109+
assert_eq!(
110+
napi::is_promise(env, val, &mut result as *mut _),
111+
napi::Status::Ok
112+
);
113+
result
114+
}

0 commit comments

Comments
 (0)