Skip to content

Commit

Permalink
Merge pull request #789 from neon-bindings/kv/jspromise
Browse files Browse the repository at this point in the history
Initial implementation for JsPromise and TaskBuilder
  • Loading branch information
kjvalencik authored Sep 17, 2021
2 parents cb884ee + e4f75dc commit 8a4ca10
Show file tree
Hide file tree
Showing 24 changed files with 898 additions and 56 deletions.
7 changes: 4 additions & 3 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[alias]
# Neon defines mutually exclusive feature flags which prevents using `cargo clippy --all-features`
# The following aliases simplify linting the entire workspace
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"
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"
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"
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"
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"
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"
neon-test = "test --no-default-features --features napi-experimental"
neon-doc = "rustdoc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api -- --cfg docsrs"
neon-doc = "rustdoc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api,promise-api,task-api -- --cfg docsrs"
neon-doc-test = "test --doc --no-default-features --features=channel-api,napi-experimental,proc-macros,try-catch-api,promise-api,task-api"
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ event-queue-api = ["channel-api"]
# Feature flag to include procedural macros
proc-macros = ["neon-macros"]

# Enable `JsPromise` and `Deferred`
# https://github.com/neon-bindings/rfcs/pull/35
promise-api = []
# Enable `TaskBuilder`
# https://github.com/neon-bindings/rfcs/pull/35
task-api = []

[package.metadata.docs.rs]
no-default-features = true
rustdoc-args = ["--cfg", "docsrs"]
Expand All @@ -86,6 +93,8 @@ features = [
"napi-experimental",
"proc-macros",
"try-catch-api",
"promise-api",
"task-api",
]

[workspace]
Expand Down
138 changes: 138 additions & 0 deletions crates/neon-runtime/src/napi/async_work.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! Rust wrappers for Node-API simple asynchronous operations
//!
//! Unlike `napi_async_work` which threads a single mutable pointer to a data
//! struct to both the `execute` and `complete` callbacks, the wrapper follows
//! a more idiomatic Rust ownership pattern by passing the output of `execute`
//! into the input of `complete`.
//!
//! https://nodejs.org/api/n-api.html#n_api_simple_asynchronous_operations

use std::ffi::c_void;
use std::mem;
use std::ptr;

use crate::napi::bindings as napi;
use crate::raw::Env;

type Execute<T, O> = fn(input: T) -> O;
type Complete<O> = fn(env: Env, output: O);

/// Schedule work to execute on the libuv thread pool
///
/// # Safety
/// * `env` must be a valid `napi_env` for the current thread
pub unsafe fn schedule<T, O>(env: Env, input: T, execute: Execute<T, O>, complete: Complete<O>)
where
T: Send + 'static,
O: Send + 'static,
{
let mut data = Box::new(Data {
state: State::Input(input),
execute,
complete,
// Work is initialized as a null pointer, but set by `create_async_work`
// `data` must not be used until this value has been set.
work: ptr::null_mut(),
});

// Store a pointer to `work` before ownership is transferred to `Box::into_raw`
let work = &mut data.work as *mut _;

// Create the `async_work`
assert_eq!(
napi::create_async_work(
env,
ptr::null_mut(),
super::string(env, "neon_async_work"),
Some(call_execute::<T, O>),
Some(call_complete::<T, O>),
Box::into_raw(data).cast(),
work,
),
napi::Status::Ok,
);

// Queue the work
match napi::queue_async_work(env, *work) {
napi::Status::Ok => {}
status => {
// If queueing failed, delete the work to prevent a leak
napi::delete_async_work(env, *work);
assert_eq!(status, napi::Status::Ok);
}
}
}

/// A pointer to data is passed to the `execute` and `complete` callbacks
struct Data<T, O> {
state: State<T, O>,
execute: Execute<T, O>,
complete: Complete<O>,
work: napi::AsyncWork,
}

/// State of the task that is transitioned by `execute` and `complete`
enum State<T, O> {
/// Initial data input passed to `execute`
Input(T),
/// Transient state while `execute` is running
Executing,
/// Return data of `execute` passed to `complete`
Output(O),
}

impl<T, O> State<T, O> {
/// Return the input if `State::Input`, replacing with `State::Executing`
fn take_execute_input(&mut self) -> Option<T> {
match mem::replace(self, Self::Executing) {
Self::Input(input) => Some(input),
_ => None,
}
}

/// Return the output if `State::Output`, replacing with `State::Executing`
fn into_output(self) -> Option<O> {
match self {
Self::Output(output) => Some(output),
_ => None,
}
}
}

/// Callback executed on the libuv thread pool
///
/// # Safety
/// * `Env` should not be used because it could attempt to call JavaScript
/// * `data` is expected to be a pointer to `Data<T, O>`
unsafe extern "C" fn call_execute<T, O>(_: Env, data: *mut c_void) {
let data = &mut *data.cast::<Data<T, O>>();
// `unwrap` is ok because `call_execute` should be called exactly once
// after initialization
let input = data.state.take_execute_input().unwrap();
let output = (data.execute)(input);

data.state = State::Output(output);
}

/// Callback executed on the JavaScript main thread
///
/// # Safety
/// * `data` is expected to be a pointer to `Data<T, O>`
unsafe extern "C" fn call_complete<T, O>(env: Env, status: napi::Status, data: *mut c_void) {
let Data {
state,
complete,
work,
..
} = *Box::<Data<T, O>>::from_raw(data.cast());

napi::delete_async_work(env, work);

match status {
// `unwrap` is okay because `call_complete` should be called exactly once
// if and only if `call_execute` has completed successfully
napi::Status::Ok => complete(env, state.into_output().unwrap()),
napi::Status::Cancelled => {}
_ => assert_eq!(status, napi::Status::Ok),
}
}
17 changes: 17 additions & 0 deletions crates/neon-runtime/src/napi/bindings/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ mod napi1 {
fn is_buffer(env: Env, value: Value, result: *mut bool) -> Status;
fn is_error(env: Env, value: Value, result: *mut bool) -> Status;
fn is_array(env: Env, value: Value, result: *mut bool) -> Status;
fn is_promise(env: Env, value: Value, result: *mut bool) -> Status;

fn get_value_string_utf8(
env: Env,
Expand Down Expand Up @@ -209,6 +210,22 @@ mod napi1 {
) -> Status;

fn run_script(env: Env, script: Value, result: *mut Value) -> Status;

fn create_async_work(
env: Env,
async_resource: Value,
async_resource_name: Value,
execute: AsyncExecuteCallback,
complete: AsyncCompleteCallback,
data: *mut c_void,
result: *mut AsyncWork,
) -> Status;

fn delete_async_work(env: Env, work: AsyncWork) -> Status;
fn queue_async_work(env: Env, work: AsyncWork) -> Status;
fn create_promise(env: Env, deferred: *mut Deferred, promise: *mut Value) -> Status;
fn resolve_deferred(env: Env, deferred: Deferred, resolution: Value) -> Status;
fn reject_deferred(env: Env, deferred: Deferred, rejection: Value) -> Status;
}
);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/neon-runtime/src/napi/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ macro_rules! generate {
use std::sync::Once;

pub(crate) use functions::*;
pub use types::TypedArrayType;
pub(crate) use types::*;
pub use types::{Deferred, TypedArrayType};

mod functions;
mod types;
Expand Down
22 changes: 22 additions & 0 deletions crates/neon-runtime/src/napi/bindings/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub type CallbackInfo = *mut CallbackInfo__;
pub struct EscapableHandleScope__ {
_unused: [u8; 0],
}

pub type EscapableHandleScope = *mut EscapableHandleScope__;

#[repr(C)]
Expand Down Expand Up @@ -203,3 +204,24 @@ impl std::ops::BitAndAssign for KeyFilter {
self.0 &= rhs.0;
}
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct AsyncWork__ {
_unused: [u8; 0],
}

pub type AsyncWork = *mut AsyncWork__;

pub type AsyncExecuteCallback = Option<unsafe extern "C" fn(env: Env, data: *mut c_void)>;

pub type AsyncCompleteCallback =
Option<unsafe extern "C" fn(env: Env, status: Status, data: *mut c_void)>;

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Deferred__ {
_unused: [u8; 0],
}

pub type Deferred = *mut Deferred__;
27 changes: 27 additions & 0 deletions crates/neon-runtime/src/napi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod array;
pub mod arraybuffer;
pub mod async_work;
pub mod buffer;
pub mod call;
pub mod convert;
Expand All @@ -13,6 +14,7 @@ pub mod lifecycle;
pub mod mem;
pub mod object;
pub mod primitive;
pub mod promise;
pub mod raw;
pub mod reference;
pub mod scope;
Expand All @@ -23,4 +25,29 @@ pub mod tsfn;
pub mod typedarray;

mod bindings;

pub use bindings::*;

use std::mem::MaybeUninit;

/// Create a JavaScript `String`, panicking if unsuccessful
///
/// # Safety
/// * `env` is a `napi_env` valid for the current thread
/// * The returned value does not outlive `env`
unsafe fn string(env: Env, s: impl AsRef<str>) -> raw::Local {
let s = s.as_ref();
let mut result = MaybeUninit::uninit();

assert_eq!(
create_string_utf8(
env,
s.as_bytes().as_ptr() as *const _,
s.len(),
result.as_mut_ptr(),
),
Status::Ok,
);

result.assume_init()
}
66 changes: 66 additions & 0 deletions crates/neon-runtime/src/napi/promise.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! JavaScript Promise and Deferred handle
//!
//! https://nodejs.org/api/n-api.html#n_api_promises

use std::mem::MaybeUninit;
use std::ptr;

use crate::napi::bindings as napi;
use crate::raw::Env;

/// Create a `Promise` and a `napi::Deferred` handle for resolving it
///
/// # Safety
/// * `env` is a valid `napi_env` for the current thread
/// * The returned `napi::Value` does not outlive `env`
pub unsafe fn create(env: Env) -> (napi::Deferred, napi::Value) {
let mut deferred = MaybeUninit::uninit();
let mut promise = MaybeUninit::uninit();

assert_eq!(
napi::create_promise(env, deferred.as_mut_ptr(), promise.as_mut_ptr()),
napi::Status::Ok,
);

(deferred.assume_init(), promise.assume_init())
}

/// Resolve a promise from a `napi::Deferred` handle
///
/// # Safety
/// * `env` is a valid `napi_env` for the current thread
/// * `resolution` is a valid `napi::Value`
pub unsafe fn resolve(env: Env, deferred: napi::Deferred, resolution: napi::Value) {
assert_eq!(
napi::resolve_deferred(env, deferred, resolution),
napi::Status::Ok,
);
}

/// Rejects a promise from a `napi::Deferred` handle
///
/// # Safety
/// * `env` is a valid `napi_env` for the current thread
/// * `rejection` is a valid `napi::Value`
pub unsafe fn reject(env: Env, deferred: napi::Deferred, rejection: napi::Value) {
assert_eq!(
napi::reject_deferred(env, deferred, rejection),
napi::Status::Ok,
);
}

/// Rejects a promise from a `napi::Deferred` handle with a string message
///
/// # Safety
/// * `env` is a valid `napi_env` for the current thread
pub unsafe fn reject_err_message(env: Env, deferred: napi::Deferred, msg: impl AsRef<str>) {
let msg = super::string(env, msg);
let mut err = MaybeUninit::uninit();

assert_eq!(
napi::create_error(env, ptr::null_mut(), msg, err.as_mut_ptr()),
napi::Status::Ok,
);

reject(env, deferred, err.assume_init());
}
13 changes: 13 additions & 0 deletions crates/neon-runtime/src/napi/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,16 @@ pub unsafe fn is_date(env: Env, val: Local) -> bool {
);
result
}

/// Is `val` a Promise?
///
/// # Safety
/// * `env` is a valid `napi_env` for the current thread
pub unsafe fn is_promise(env: Env, val: Local) -> bool {
let mut result = false;
assert_eq!(
napi::is_promise(env, val, &mut result as *mut _),
napi::Status::Ok
);
result
}
Loading

0 comments on commit 8a4ca10

Please sign in to comment.