Skip to content

Commit

Permalink
Merge pull request #543 from neon-bindings/try-catch-internal
Browse files Browse the repository at this point in the history
try_catch
  • Loading branch information
dherman authored Jul 15, 2020
2 parents bd2479c + a03b27b commit 7e1ca41
Show file tree
Hide file tree
Showing 17 changed files with 330 additions and 6 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ legacy-runtime = ["neon-runtime/neon-sys", "neon-build/neon-sys"]
# is disabled by default.
napi-runtime = ["neon-runtime/nodejs-sys"]

# Feature flag to disable external dependencies on docs build
# Feature flag to disable external dependencies on docs build
docs-only = ["neon-runtime/docs-only"]

# Feature flag to enable the try_catch API of RFC 29.
try-catch-api = []

[package.metadata.docs.rs]
features = ["docs-only"]

Expand Down
1 change: 1 addition & 0 deletions crates/neon-runtime/src/nan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pub mod convert;
pub mod class;
pub mod task;
pub mod handler;
pub mod try_catch;
4 changes: 4 additions & 0 deletions crates/neon-runtime/src/nan/try_catch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Wraps a computation with an RAII-allocated Nan::TryCatch.
pub use neon_sys::Neon_TryCatch_With as with;

pub use neon_sys::TryCatchControl;
22 changes: 22 additions & 0 deletions crates/neon-runtime/src/napi/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ use nodejs_sys as napi;

use raw::{Env, Local};

pub unsafe fn is_throwing(env: Env) -> bool {
let mut b: MaybeUninit<bool> = MaybeUninit::zeroed();

let status = napi::napi_is_exception_pending(env, b.as_mut_ptr());

assert_eq!(status, napi::napi_status::napi_ok);

b.assume_init()
}

pub unsafe fn catch_error(env: Env, error: *mut Local) -> bool {
if !is_throwing(env) {
return false;
}

let status = napi::napi_get_and_clear_last_exception(env, error);

assert_eq!(status, napi::napi_status::napi_ok);

true
}

pub unsafe fn clear_exception(env: Env) {
let mut result = MaybeUninit::uninit();
let status = napi::napi_is_exception_pending(env, result.as_mut_ptr());
Expand Down
23 changes: 23 additions & 0 deletions crates/neon-sys/native/src/neon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -541,3 +541,26 @@ extern "C" void Neon_EventHandler_Delete(void * thread_safe_cb) {
neon::EventHandler *cb = static_cast<neon::EventHandler*>(thread_safe_cb);
cb->close();
}

extern "C" try_catch_control_t Neon_TryCatch_With(Neon_TryCatchGlue glue_fn, void *rust_thunk, void *cx, v8::Local<v8::Value> *result, void **unwind_value) {
Nan::TryCatch try_catch;

try_catch_control_t ctrl = glue_fn(rust_thunk, cx, result, unwind_value);

if (ctrl == CONTROL_PANICKED) {
return CONTROL_PANICKED;
}

if (!try_catch.HasCaught()) {
// It's possible, if unlikely, that a Neon user might return `Err(Throw)` even
// though the VM is not actually in a throwing state. In this case we return
// `CONTROL_UNEXPECTED_ERR` to signal that Rust should panic.
if (ctrl == CONTROL_THREW) {
return CONTROL_UNEXPECTED_ERR;
}
return CONTROL_RETURNED;
} else {
*result = try_catch.Exception();
return CONTROL_THREW;
}
}
22 changes: 21 additions & 1 deletion crates/neon-sys/native/src/neon.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
#include <stdint.h>
#include <v8.h>

// corresponding Rust struct `CCallback` defined in fun.rs
// corresponding Rust struct `CCallback` defined in lib.rs
typedef struct {
void* static_callback;
void* dynamic_callback;
} callback_t;

// corresponding Rust enum `TryCatchControl` defined in lib.rs
typedef enum : uint8_t {
CONTROL_RETURNED = 0,
CONTROL_THREW = 1,
CONTROL_PANICKED = 2,
CONTROL_UNEXPECTED_ERR = 3
} try_catch_control_t;

extern "C" {

void Neon_Call_SetReturn(v8::FunctionCallbackInfo<v8::Value> *info, v8::Local<v8::Value> value);
Expand Down Expand Up @@ -139,6 +147,18 @@ extern "C" {
void* Neon_EventHandler_New(v8::Isolate *isolate, v8::Local<v8::Value> self, v8::Local<v8::Function> callback);
void Neon_EventHandler_Schedule(void* thread_safe_cb, void* rust_callback, Neon_EventHandler handler);
void Neon_EventHandler_Delete(void* thread_safe_cb);

// The `result` out-parameter can be assumed to be initialized if and only if this function
// returns `CONTROL_RETURNED`.
// The `unwind_value` out-parameter can be assumed to be initialized if and only if this
// function returns `CONTROL_PANICKED`.
typedef try_catch_control_t (*Neon_TryCatchGlue)(void *rust_thunk, void *cx, v8::Local<v8::Value> *result, void **unwind_value);

// The `result` out-parameter can be assumed to be initialized if and only if this function
// returns `CONTROL_RETURNED` or `CONTROL_THREW`.
// The `unwind_value` out-parameter can be assumed to be initialized if and only if this
// function returns `CONTROL_PANICKED`.
try_catch_control_t Neon_TryCatch_With(Neon_TryCatchGlue glue, void *rust_thunk, void *cx, v8::Local<v8::Value> *result, void **unwind_value);
}

#endif
30 changes: 30 additions & 0 deletions crates/neon-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,28 @@ impl Default for CCallback {
}
}

#[repr(u8)]
pub enum TryCatchControl {
Returned = 0,
Threw = 1,
Panicked = 2,
UnexpectedErr = 3
}

#[derive(Clone, Copy)]
pub struct InheritedHandleScope;

/// A Rust extern "glue function" for C++ to invoke a Rust closure with a `TryCatch`
/// live on the stack.
/// The `result` argument can be assumed to be initialized if and only if the glue
/// function returns `TryCatchControl::Returned`.
/// The `unwind_value` argument can be assumed to be initialized if and only if the
/// glue function returns `TryCatchControl::Panicked`.
pub type TryCatchGlue = extern fn(rust_thunk: *mut c_void,
cx: *mut c_void,
result: *mut Local,
unwind_value: *mut *mut c_void) -> TryCatchControl;

extern "C" {

pub fn Neon_Array_New(out: &mut Local, isolate: Isolate, length: u32);
Expand Down Expand Up @@ -202,4 +221,15 @@ extern "C" {
pub fn Neon_EventHandler_Schedule(thread_safe_cb: *mut c_void, rust_callback: *mut c_void,
complete: unsafe extern fn(Local, Local, *mut c_void));
pub fn Neon_EventHandler_Delete(thread_safe_cb: *mut c_void);

/// Invokes a Rust closure with a `TryCatch` live on the stack.
/// The `result` value can be assumed to be initialized if and only if this function
/// does not return `TryCatchControl::Panicked`.
/// The `unwind_value` value can be assumed to be initialized if and only if this
/// function returns `TryCatchControl::Panicked`.
pub fn Neon_TryCatch_With(glue: TryCatchGlue,
rust_thunk: *mut c_void,
cx: *mut c_void,
result: *mut Local,
unwind_value: *mut *mut c_void) -> TryCatchControl;
}
108 changes: 106 additions & 2 deletions src/context/internal.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
use std;
#[cfg(feature = "legacy-runtime")]
use std::any::Any;
use std::boxed::Box;
use std::cell::Cell;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
#[cfg(feature = "legacy-runtime")]
use std::panic::{AssertUnwindSafe, UnwindSafe, catch_unwind, resume_unwind};
use neon_runtime;
use neon_runtime::raw;
use neon_runtime::scope::Root;
use types::JsObject;
#[cfg(feature = "legacy-runtime")]
use neon_runtime::try_catch::TryCatchControl;
use types::{JsObject, JsValue, Value};
use handle::Handle;
use object::class::ClassMap;
use result::NeonResult;
use result::{JsResult, NeonResult};
use super::ModuleContext;

#[cfg(feature = "legacy-runtime")]
Expand Down Expand Up @@ -116,6 +124,102 @@ pub trait ContextInternal<'a>: Sized {

fn activate(&self) { self.scope_metadata().active.set(true); }
fn deactivate(&self) { self.scope_metadata().active.set(false); }

#[cfg(feature = "legacy-runtime")]
fn try_catch_internal<'b: 'a, T, F>(&mut self, f: F) -> Result<Handle<'a, T>, Handle<'a, JsValue>>
where T: Value,
F: UnwindSafe + FnOnce(&mut Self) -> JsResult<'b, T>
{
// A closure does not have a guaranteed layout, so we need to box it in order to pass
// a pointer to it across the boundary into C++.
let rust_thunk = Box::into_raw(Box::new(f));

let mut local: MaybeUninit<raw::Local> = MaybeUninit::zeroed();
let mut unwind_value: MaybeUninit<*mut c_void> = MaybeUninit::zeroed();

let ctrl = unsafe {
neon_runtime::try_catch::with(try_catch_glue::<Self, T, F>,
rust_thunk as *mut c_void,
(self as *mut Self) as *mut c_void,
local.as_mut_ptr(),
unwind_value.as_mut_ptr())
};

match ctrl {
TryCatchControl::Panicked => {
let unwind_value: Box<dyn Any + Send> = *unsafe {
Box::from_raw(unwind_value.assume_init() as *mut Box<dyn Any + Send>)
};
resume_unwind(unwind_value);
}
TryCatchControl::Returned => {
let local = unsafe { local.assume_init() };
Ok(Handle::new_internal(T::from_raw(local)))
}
TryCatchControl::Threw => {
let local = unsafe { local.assume_init() };
Err(JsValue::new_internal(local))
}
TryCatchControl::UnexpectedErr => {
panic!("try_catch: unexpected Err(Throw) when VM is not in a throwing state");
}
}
}

#[cfg(feature = "napi-runtime")]
fn try_catch_internal<'b: 'a, T, F>(&mut self, f: F) -> Result<Handle<'a, T>, Handle<'a, JsValue>>
where T: Value,
F: FnOnce(&mut Self) -> JsResult<'b, T>
{
let result = f(self);
let mut local: MaybeUninit<raw::Local> = MaybeUninit::zeroed();
unsafe {
if neon_runtime::error::catch_error(self.env().to_raw(), local.as_mut_ptr()) {
Err(JsValue::new_internal(local.assume_init()))
} else if let Ok(result) = result {
Ok(result)
} else {
panic!("try_catch: unexpected Err(Throw) when VM is not in a throwing state");
}
}
}
}

#[cfg(feature = "legacy-runtime")]
extern "C" fn try_catch_glue<'a, 'b: 'a, C, T, F>(rust_thunk: *mut c_void,
cx: *mut c_void,
returned: *mut raw::Local,
unwind_value: *mut *mut c_void) -> TryCatchControl
where C: ContextInternal<'a>,
T: Value,
F: UnwindSafe + FnOnce(&mut C) -> JsResult<'b, T>
{
let f: F = *unsafe { Box::from_raw(rust_thunk as *mut F) };
let cx: &mut C = unsafe { std::mem::transmute(cx) };

// The mutable reference to the context is a fiction of the Neon library,
// since it doesn't actually contain any data in the Rust memory space,
// just a link to the JS VM. So we don't need to do any kind of poisoning
// of the context when a panic occurs. So we suppress the Rust compiler
// errors from using the mutable reference across an unwind boundary.
match catch_unwind(AssertUnwindSafe(|| f(cx))) {
// No Rust panic, no JS exception.
Ok(Ok(result)) => unsafe {
*returned = result.to_raw();
TryCatchControl::Returned
}
// No Rust panic, caught a JS exception.
Ok(Err(_)) => {
TryCatchControl::Threw
}
// Rust panicked.
Err(err) => unsafe {
// A panic value has an undefined layout, so wrap it in an extra box.
let boxed = Box::new(err);
*unwind_value = Box::into_raw(boxed) as *mut c_void;
TryCatchControl::Panicked
}
}
}

#[cfg(feature = "legacy-runtime")]
Expand Down
16 changes: 16 additions & 0 deletions src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,22 @@ pub trait Context<'a>: ContextInternal<'a> {
result
}

#[cfg(all(feature = "try-catch-api", feature = "napi-runtime"))]
fn try_catch<'b: 'a, T, F>(&mut self, f: F) -> Result<Handle<'a, T>, Handle<'a, JsValue>>
where T: Value,
F: FnOnce(&mut Self) -> JsResult<'b, T>
{
self.try_catch_internal(f)
}

#[cfg(all(feature = "try-catch-api", feature = "legacy-runtime"))]
fn try_catch<'b: 'a, T, F>(&mut self, f: F) -> Result<Handle<'a, T>, Handle<'a, JsValue>>
where T: Value,
F: UnwindSafe + FnOnce(&mut Self) -> JsResult<'b, T>
{
self.try_catch_internal(f)
}

/// Convenience method for creating a `JsBoolean` value.
fn boolean(&mut self, b: bool) -> Handle<'a, JsBoolean> {
JsBoolean::new(self, b)
Expand Down
30 changes: 30 additions & 0 deletions test/dynamic/lib/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ describe('JsFunction', function() {
assert.throws(function() { addon.panic_after_throw() }, Error, /^internal error in Neon module: this should override the RangeError$/);
});

it('catches an exception with cx.try_catch', function() {
var error = new Error('Something bad happened');
assert.equal(addon.throw_and_catch(error), error);
assert.equal(addon.throw_and_catch(42), 42);
assert.equal(addon.throw_and_catch('a string'), 'a string');
assert.equal(addon.call_and_catch(() => { throw 'shade' }), 'shade');
assert.equal(addon.call_and_catch(() => {
throw addon.call_and_catch(() => {
throw addon.call_and_catch(() => {
throw 'once';
}) + ' upon';
}) + ' a';
}) + ' time', 'once upon a time');
});

it('gets a regular value with cx.try_catch', function() {
assert.equal(addon.call_and_catch(() => { return 42 }), 42);
});

it('propagates a panic with cx.try_catch', function() {
assert.throws(function() {
addon.panic_and_catch();
return 'unreachable';
}, Error, /^internal error in Neon module: oh no$/);
});

it('panics on unexpected Err(Throw) with cx.try_catch', function() {
assert.throw(addon.unexpected_throw_and_catch, Error, /^internal error in Neon module: try_catch: unexpected Err\(Throw\) when VM is not in a throwing state$/);
})

it('computes the right number of arguments', function() {
assert.equal(addon.num_arguments(), 0);
assert.equal(addon.num_arguments('a'), 1);
Expand Down
2 changes: 1 addition & 1 deletion test/dynamic/native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ crate-type = ["cdylib"]
neon-build = {version = "*", path = "../../../crates/neon-build"}

[dependencies]
neon = {version = "*", path = "../../../", features = ["event-handler-api"]}
neon = {version = "*", path = "../../../", features = ["event-handler-api", "try-catch-api"]}
28 changes: 28 additions & 0 deletions test/dynamic/native/src/js/functions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use neon::prelude::*;
use neon::object::This;
use neon::result::Throw;

fn add1(mut cx: FunctionContext) -> JsResult<JsNumber> {
let x = cx.argument::<JsNumber>(0)?.value();
Expand Down Expand Up @@ -99,3 +100,30 @@ pub fn compute_scoped(mut cx: FunctionContext) -> JsResult<JsNumber> {
}
Ok(i)
}

pub fn throw_and_catch(mut cx: FunctionContext) -> JsResult<JsValue> {
let v = cx.argument_opt(0).unwrap_or_else(|| cx.undefined().upcast());
Ok(cx.try_catch(|cx| {
let _ = cx.throw(v)?;
Ok(cx.string("unreachable").upcast())
}).unwrap_or_else(|err| err))
}

pub fn call_and_catch(mut cx: FunctionContext) -> JsResult<JsValue> {
let f: Handle<JsFunction> = cx.argument(0)?;
Ok(cx.try_catch(|cx| {
let global = cx.global();
let args: Vec<Handle<JsValue>> = vec![];
f.call(cx, global, args)
}).unwrap_or_else(|err| err))
}

pub fn panic_and_catch(mut cx: FunctionContext) -> JsResult<JsValue> {
Ok(cx.try_catch(|_| { panic!("oh no") })
.unwrap_or_else(|err| err))
}

pub fn unexpected_throw_and_catch(mut cx: FunctionContext) -> JsResult<JsValue> {
Ok(cx.try_catch(|_| { Err(Throw) })
.unwrap_or_else(|err| err))
}
Loading

0 comments on commit 7e1ca41

Please sign in to comment.