use std::{
any::Any,
ffi::c_void,
mem::MaybeUninit,
panic::{catch_unwind, AssertUnwindSafe},
ptr,
};
use super::{
bindings as napi,
debug_send_wrapper::DebugSendWrapper,
error::fatal_error,
raw::{Env, Local},
};
type Panic = Box<dyn Any + Send + 'static>;
const UNKNOWN_PANIC_MESSAGE: &str = "Unknown panic";
pub struct FailureBoundary {
pub both: &'static str,
pub exception: &'static str,
pub panic: &'static str,
}
impl FailureBoundary {
#[track_caller]
pub unsafe fn catch_failure<F>(&self, env: Env, deferred: Option<napi::Deferred>, f: F)
where
F: FnOnce(Option<Env>) -> Local,
{
#[allow(clippy::unnecessary_lazy_evaluations)]
let env = can_call_into_js(env).then(|| env);
let panic = catch_unwind(AssertUnwindSafe(move || f(env)));
let env = if let Some(env) = env {
env
} else {
if let Err(panic) = panic {
let msg = panic_msg(&panic).unwrap_or(UNKNOWN_PANIC_MESSAGE);
fatal_error(msg);
}
return;
};
let exception = catch_exception(env);
let msg = match (exception, panic.as_ref()) {
(Some(_), Err(_)) => self.both,
(Some(err), Ok(_)) => {
if let Some(deferred) = deferred {
reject_deferred(env, deferred, err);
return;
}
self.exception
}
(None, Err(_)) => self.panic,
(None, Ok(value)) => {
if let Some(deferred) = deferred {
resolve_deferred(env, deferred, *value);
}
return;
}
};
if let Some(deferred) = deferred {
let error = create_error(env, msg, exception, panic.err());
reject_deferred(env, deferred, error);
return;
}
let error = create_error(env, msg, exception, panic.err());
fatal_exception(env, error);
}
}
fn can_call_into_js(env: Env) -> bool {
!env.is_null() && unsafe { napi::throw(env, ptr::null_mut()) == Err(napi::Status::InvalidArg) }
}
unsafe fn fatal_exception(env: Env, error: Local) {
let mut deferred = MaybeUninit::uninit();
let mut promise = MaybeUninit::uninit();
let deferred = match napi::create_promise(env, deferred.as_mut_ptr(), promise.as_mut_ptr()) {
Ok(()) => deferred.assume_init(),
_ => fatal_error("Failed to create a promise"),
};
if napi::reject_deferred(env, deferred, error) != Ok(()) {
fatal_error("Failed to reject a promise");
}
}
#[track_caller]
unsafe fn create_error(
env: Env,
msg: &str,
exception: Option<Local>,
panic: Option<Panic>,
) -> Local {
let error = error_from_message(env, msg);
if let Some(exception) = exception {
set_property(env, error, "cause", exception);
};
if let Some(panic) = panic {
set_property(env, error, "panic", error_from_panic(env, panic));
}
error
}
#[track_caller]
unsafe fn resolve_deferred(env: Env, deferred: napi::Deferred, value: Local) {
if napi::resolve_deferred(env, deferred, value) != Ok(()) {
fatal_error("Failed to resolve promise");
}
}
#[track_caller]
unsafe fn reject_deferred(env: Env, deferred: napi::Deferred, value: Local) {
if napi::reject_deferred(env, deferred, value) != Ok(()) {
fatal_error("Failed to reject promise");
}
}
#[track_caller]
unsafe fn catch_exception(env: Env) -> Option<Local> {
if !is_exception_pending(env) {
return None;
}
let mut error = MaybeUninit::uninit();
if napi::get_and_clear_last_exception(env, error.as_mut_ptr()) != Ok(()) {
fatal_error("Failed to get and clear the last exception");
}
Some(error.assume_init())
}
#[track_caller]
unsafe fn error_from_message(env: Env, msg: &str) -> Local {
let msg = create_string(env, msg);
let mut err = MaybeUninit::uninit();
let status = napi::create_error(env, ptr::null_mut(), msg, err.as_mut_ptr());
match status {
Ok(()) => err.assume_init(),
Err(_) => fatal_error("Failed to create an Error"),
}
}
#[track_caller]
unsafe fn error_from_panic(env: Env, panic: Panic) -> Local {
if let Some(msg) = panic_msg(&panic) {
error_from_message(env, msg)
} else {
let error = error_from_message(env, UNKNOWN_PANIC_MESSAGE);
let panic = external_from_panic(env, panic);
set_property(env, error, "cause", panic);
error
}
}
#[track_caller]
unsafe fn set_property(env: Env, object: Local, key: &str, value: Local) {
let key = create_string(env, key);
if napi::set_property(env, object, key, value).is_err() {
fatal_error("Failed to set an object property");
}
}
#[track_caller]
unsafe fn panic_msg(panic: &Panic) -> Option<&str> {
if let Some(msg) = panic.downcast_ref::<&str>() {
Some(msg)
} else if let Some(msg) = panic.downcast_ref::<String>() {
Some(msg)
} else {
None
}
}
unsafe fn external_from_panic(env: Env, panic: Panic) -> Local {
let fail = || fatal_error("Failed to create a neon::types::JsBox from a panic");
let mut result = MaybeUninit::uninit();
if napi::create_external(
env,
Box::into_raw(Box::new(DebugSendWrapper::new(panic))).cast(),
Some(finalize_panic),
ptr::null_mut(),
result.as_mut_ptr(),
)
.is_err()
{
fail();
}
let external = result.assume_init();
#[cfg(feature = "napi-8")]
if napi::type_tag_object(env, external, &*crate::MODULE_TAG).is_err() {
fail();
}
external
}
extern "C" fn finalize_panic(_env: Env, data: *mut c_void, _hint: *mut c_void) {
unsafe {
drop(Box::from_raw(data.cast::<Panic>()));
}
}
#[track_caller]
unsafe fn create_string(env: Env, msg: &str) -> Local {
let mut string = MaybeUninit::uninit();
if napi::create_string_utf8(env, msg.as_ptr().cast(), msg.len(), string.as_mut_ptr()).is_err() {
fatal_error("Failed to create a String");
}
string.assume_init()
}
unsafe fn is_exception_pending(env: Env) -> bool {
let mut throwing = false;
if napi::is_exception_pending(env, &mut throwing).is_err() {
fatal_error("Failed to check if an exception is pending");
}
throwing
}