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

fix(napi): improve error propagation #1303

Merged
merged 1 commit into from Sep 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/napi/src/bindgen_runtime/js_values.rs
Expand Up @@ -233,7 +233,7 @@ where
Ok(v) => unsafe { T::to_napi_value(env, v) },
Err(e) => {
let error_code = unsafe { String::to_napi_value(env, format!("{:?}", e.status))? };
let reason = unsafe { String::to_napi_value(env, e.reason)? };
let reason = unsafe { String::to_napi_value(env, e.reason.clone())? };
let mut error = ptr::null_mut();
check_status!(
unsafe { sys::napi_create_error(env, error_code, reason, &mut error) },
Expand Down
13 changes: 4 additions & 9 deletions crates/napi/src/bindgen_runtime/js_values/promise.rs
Expand Up @@ -6,7 +6,7 @@ use std::task::{Context, Poll};

use tokio::sync::oneshot::{channel, Receiver, Sender};

use crate::{check_status, sys, Error, Result, Status};
use crate::{check_status, sys, Error, JsUnknown, NapiValue, Result, Status};

use super::{FromNapiValue, TypeName, ValidateNapiValue};

Expand Down Expand Up @@ -224,16 +224,11 @@ unsafe extern "C" fn catch_callback<T: FromNapiValue>(
"Get callback info from Promise::catch failed"
);
let rejected_value = rejected_value[0];
let mut error_ref = ptr::null_mut();
let create_ref_status =
unsafe { sys::napi_create_reference(env, rejected_value, 1, &mut error_ref) };
debug_assert!(
create_ref_status == sys::Status::napi_ok,
"Create Error reference failed"
);
let sender = unsafe { Box::from_raw(data as *mut Sender<*mut Result<T>>) };
sender
.send(Box::into_raw(Box::new(Err(Error::from(error_ref)))))
.send(Box::into_raw(Box::new(Err(Error::from(unsafe {
JsUnknown::from_raw_unchecked(env, rejected_value)
})))))
.expect("Send Promise resolved value error");
this
}
2 changes: 1 addition & 1 deletion crates/napi/src/env.rs
Expand Up @@ -994,7 +994,7 @@ impl Env {
}

pub fn create_error(&self, e: Error) -> Result<JsObject> {
let reason = e.reason;
let reason = &e.reason;
let reason_string = self.create_string(reason.as_str())?;
let mut result = ptr::null_mut();
check_status!(unsafe {
Expand Down
85 changes: 70 additions & 15 deletions crates/napi/src/error.rs
Expand Up @@ -12,7 +12,7 @@ use serde::{de, ser};
#[cfg(feature = "serde-json")]
use serde_json::Error as SerdeJSONError;

use crate::{check_status, sys, Status};
use crate::{check_status, sys, Env, JsUnknown, NapiValue, Status};

pub type Result<T> = std::result::Result<T, Error>;

Expand All @@ -24,9 +24,8 @@ pub struct Error {
pub status: Status,
pub reason: String,
// Convert raw `JsError` into Error
// Only be used in `async fn(p: Promise<T>)` scenario
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
pub(crate) maybe_raw: sys::napi_ref,
maybe_raw: sys::napi_ref,
maybe_env: sys::napi_env,
}

unsafe impl Send for Error {}
Expand Down Expand Up @@ -61,13 +60,18 @@ impl From<SerdeJSONError> for Error {
}
}

#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
impl From<sys::napi_ref> for Error {
fn from(value: sys::napi_ref) -> Self {
impl From<JsUnknown> for Error {
fn from(value: JsUnknown) -> Self {
let mut result = std::ptr::null_mut();
let status = unsafe { sys::napi_create_reference(value.0.env, value.0.value, 0, &mut result) };
if status != sys::Status::napi_ok {
return Error::new(Status::from(status), "".to_owned());
}
Self {
status: Status::InvalidArg,
status: Status::GenericFailure,
reason: "".to_string(),
maybe_raw: value,
maybe_raw: result,
maybe_env: value.0.env,
}
}
}
Expand All @@ -94,26 +98,26 @@ impl Error {
Error {
status,
reason,
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
}

pub fn from_status(status: Status) -> Self {
Error {
status,
reason: "".to_owned(),
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
}

pub fn from_reason<T: Into<String>>(reason: T) -> Self {
Error {
status: Status::GenericFailure,
reason: reason.into(),
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
}
}
Expand All @@ -123,8 +127,8 @@ impl From<std::ffi::NulError> for Error {
Error {
status: Status::GenericFailure,
reason: format!("{}", error),
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
}
}
Expand All @@ -134,8 +138,21 @@ impl From<std::io::Error> for Error {
Error {
status: Status::GenericFailure,
reason: format!("{}", error),
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
maybe_raw: ptr::null_mut(),
maybe_env: ptr::null_mut(),
}
}
}

impl Drop for Error {
fn drop(&mut self) {
if !self.maybe_env.is_null() && !self.maybe_raw.is_null() {
let delete_reference_status =
unsafe { sys::napi_delete_reference(self.maybe_env, self.maybe_raw) };
debug_assert!(
delete_reference_status == sys::Status::napi_ok,
"Delete Error Reference failed"
);
}
}
}
Expand Down Expand Up @@ -188,11 +205,22 @@ macro_rules! impl_object_methods {
///
/// This function is safety if env is not null ptr.
pub unsafe fn into_value(self, env: sys::napi_env) -> sys::napi_value {
if !self.0.maybe_raw.is_null() {
let mut err = ptr::null_mut();
let get_err_status =
unsafe { sys::napi_get_reference_value(env, self.0.maybe_raw, &mut err) };
debug_assert!(
get_err_status == sys::Status::napi_ok,
"Get Error from Reference failed"
);
return err;
}

let error_status = format!("{:?}", self.0.status);
let status_len = error_status.len();
let error_code_string = CString::new(error_status).unwrap();
let reason_len = self.0.reason.len();
let reason = CString::new(self.0.reason).unwrap();
let reason = CString::new(self.0.reason.as_str()).unwrap();
let mut error_code = ptr::null_mut();
let mut reason_string = ptr::null_mut();
let mut js_error = ptr::null_mut();
Expand All @@ -209,6 +237,11 @@ macro_rules! impl_object_methods {
js_error
}

pub fn into_unknown(self, env: Env) -> JsUnknown {
let value = unsafe { self.into_value(env.raw()) };
unsafe { JsUnknown::from_raw_unchecked(env.raw(), value) }
}

/// # Safety
///
/// This function is safety if env is not null ptr.
Expand Down Expand Up @@ -297,3 +330,25 @@ macro_rules! check_status {
}
}};
}

#[doc(hidden)]
#[macro_export]
macro_rules! check_pending_exception {
($env: expr, $code:expr) => {{
let c = $code;
match c {
$crate::sys::Status::napi_ok => Ok(()),
$crate::sys::Status::napi_pending_exception => {
let mut error_result = std::ptr::null_mut();
assert_eq!(
unsafe { $crate::sys::napi_get_and_clear_last_exception($env, &mut error_result) },
$crate::sys::Status::napi_ok
);
return Err(Error::from(unsafe {
JsUnknown::from_raw_unchecked($env, error_result)
}));
}
_ => Err($crate::Error::new($crate::Status::from(c), "".to_owned())),
}
}};
}
8 changes: 4 additions & 4 deletions crates/napi/src/js_values/function.rs
Expand Up @@ -7,7 +7,7 @@ use crate::{
bindgen_runtime::ToNapiValue,
threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction},
};
use crate::{check_status, ValueType};
use crate::{check_pending_exception, ValueType};
use crate::{sys, Env, Error, JsObject, JsUnknown, NapiRaw, NapiValue, Result, Status};

pub struct JsFunction(pub(crate) Value);
Expand Down Expand Up @@ -56,7 +56,7 @@ impl JsFunction {
.map(|arg| unsafe { arg.raw() })
.collect::<Vec<sys::napi_value>>();
let mut return_value = ptr::null_mut();
check_status!(unsafe {
check_pending_exception!(self.0.env, unsafe {
sys::napi_call_function(
self.0.env,
raw_this,
Expand All @@ -83,7 +83,7 @@ impl JsFunction {
})
.ok_or_else(|| Error::new(Status::GenericFailure, "Get raw this failed".to_owned()))?;
let mut return_value = ptr::null_mut();
check_status!(unsafe {
check_pending_exception!(self.0.env, unsafe {
sys::napi_call_function(
self.0.env,
raw_this,
Expand All @@ -110,7 +110,7 @@ impl JsFunction {
.iter()
.map(|arg| unsafe { arg.raw() })
.collect::<Vec<sys::napi_value>>();
check_status!(unsafe {
check_pending_exception!(self.0.env, unsafe {
sys::napi_new_instance(
self.0.env,
self.0.value,
Expand Down
24 changes: 2 additions & 22 deletions crates/napi/src/promise.rs
Expand Up @@ -109,28 +109,8 @@ unsafe extern "C" fn call_js_cb<
debug_assert!(status == sys::Status::napi_ok, "Resolve promise failed");
}
Err(e) => {
let status = unsafe {
sys::napi_reject_deferred(
env,
deferred,
if e.maybe_raw.is_null() {
JsError::from(e).into_value(env)
} else {
let mut err = ptr::null_mut();
let get_err_status = sys::napi_get_reference_value(env, e.maybe_raw, &mut err);
debug_assert!(
get_err_status == sys::Status::napi_ok,
"Get Error from Reference failed"
);
let delete_reference_status = sys::napi_delete_reference(env, e.maybe_raw);
debug_assert!(
delete_reference_status == sys::Status::napi_ok,
"Delete Error Reference failed"
);
err
},
)
};
let status =
unsafe { sys::napi_reject_deferred(env, deferred, JsError::from(e).into_value(env)) };
debug_assert!(status == sys::Status::napi_ok, "Reject promise failed");
}
};
Expand Down
11 changes: 11 additions & 0 deletions examples/napi-compat-mode/__test__/function.spec.ts
Expand Up @@ -20,3 +20,14 @@ test('should set "this" properly', (t) => {
t.is(this, obj)
})
})

test('should handle errors', (t) => {
bindings.testCallFunctionError(
() => {
throw new Error('Testing')
},
(err: Error) => {
t.is(err.message, 'Testing')
},
)
})
14 changes: 13 additions & 1 deletion examples/napi-compat-mode/src/function.rs
@@ -1,4 +1,4 @@
use napi::{CallContext, JsFunction, JsNull, JsObject, Result};
use napi::{CallContext, JsError, JsFunction, JsNull, JsObject, JsUnknown, Result};

#[js_function(1)]
pub fn call_function(ctx: CallContext) -> Result<JsNull> {
Expand Down Expand Up @@ -32,12 +32,24 @@ pub fn call_function_with_this(ctx: CallContext) -> Result<JsNull> {
ctx.env.get_null()
}

#[js_function(2)]
pub fn call_function_error(ctx: CallContext) -> Result<JsUnknown> {
let js_func = ctx.get::<JsFunction>(0)?;
let error_func = ctx.get::<JsFunction>(1)?;

match js_func.call_without_args(None) {
Ok(v) => Ok(v),
Err(e) => error_func.call(None, &[JsError::from(e).into_unknown(*ctx.env)]),
}
}

pub fn register_js(exports: &mut JsObject) -> Result<()> {
exports.create_named_method("testCallFunction", call_function)?;
exports.create_named_method(
"testCallFunctionWithRefArguments",
call_function_with_ref_arguments,
)?;
exports.create_named_method("testCallFunctionWithThis", call_function_with_this)?;
exports.create_named_method("testCallFunctionError", call_function_error)?;
Ok(())
}