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

Initial implementation for JsPromise and TaskBuilder #789

Merged
merged 1 commit into from
Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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