diff --git a/src/binding.cc b/src/binding.cc index e5378593..316b653c 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -27,6 +27,9 @@ static_assert(sizeof(v8::ScriptCompiler::Source) == sizeof(size_t) * 8, static_assert(sizeof(v8::ReturnValue) == sizeof(size_t) * 1, "ReturnValue size mismatch"); +static_assert(sizeof(v8::TryCatch) == sizeof(size_t) * 6, + "TryCatch size mismatch"); + extern "C" { void v8__V8__SetFlagsFromCommandLine(int* argc, char** argv) { @@ -325,6 +328,56 @@ int v8__StackTrace__GetFrameCount(v8::StackTrace* self) { return self->GetFrameCount(); } +void v8__TryCatch__CONSTRUCT(uninit_t& buf, + v8::Isolate* isolate) { + construct_in_place(buf, isolate); +} + +void v8__TryCatch__DESTRUCT(v8::TryCatch& self) { self.~TryCatch(); } + +bool v8__TryCatch__HasCaught(const v8::TryCatch& self) { + return self.HasCaught(); +} + +bool v8__TryCatch__CanContinue(const v8::TryCatch& self) { + return self.CanContinue(); +} + +bool v8__TryCatch__HasTerminated(const v8::TryCatch& self) { + return self.HasTerminated(); +} + +v8::Value* v8__TryCatch__Exception(const v8::TryCatch& self) { + return local_to_ptr(self.Exception()); +} + +v8::Value* v8__TryCatch__StackTrace(const v8::TryCatch& self, + v8::Local context) { + return maybe_local_to_ptr(self.StackTrace(context)); +} + +v8::Message* v8__TryCatch__Message(const v8::TryCatch& self) { + return local_to_ptr(self.Message()); +} + +void v8__TryCatch__Reset(v8::TryCatch& self) { self.Reset(); } + +v8::Value* v8__TryCatch__ReThrow(v8::TryCatch& self) { + return local_to_ptr(self.ReThrow()); +} + +bool v8__TryCatch__IsVerbose(const v8::TryCatch& self) { + return self.IsVerbose(); +} + +void v8__TryCatch__SetVerbose(v8::TryCatch& self, bool value) { + self.SetVerbose(value); +} + +void v8__TryCatch__SetCaptureMessage(v8::TryCatch& self, bool value) { + self.SetCaptureMessage(value); +} + v8::Script* v8__Script__Compile(v8::Context* context, v8::String* source, v8::ScriptOrigin* origin) { return maybe_local_to_ptr( diff --git a/src/handle_scope.rs b/src/handle_scope.rs index ed707960..70d903a4 100644 --- a/src/handle_scope.rs +++ b/src/handle_scope.rs @@ -31,6 +31,18 @@ impl<'sc> HandleScope<'sc> { } } +impl<'sc> AsRef> for HandleScope<'sc> { + fn as_ref(&self) -> &Self { + self + } +} + +impl<'sc> AsMut> for HandleScope<'sc> { + fn as_mut(&mut self) -> &mut Self { + self + } +} + impl<'sc> AsRef for HandleScope<'sc> { fn as_ref(&self) -> &Isolate { unsafe { v8__HandleScope__GetIsolate(self) } diff --git a/src/lib.rs b/src/lib.rs index 2399c009..1d9c6d91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod property; mod script; mod string; mod support; +mod try_catch; mod value; pub mod array_buffer; @@ -59,4 +60,5 @@ pub use property::PropertyCallbackInfo; pub use script::{Script, ScriptOrigin}; pub use string::NewStringType; pub use string::String; +pub use try_catch::TryCatch; pub use value::Value; diff --git a/src/try_catch.rs b/src/try_catch.rs new file mode 100644 index 00000000..a1bf42de --- /dev/null +++ b/src/try_catch.rs @@ -0,0 +1,228 @@ +use std::marker::PhantomData; +use std::mem::size_of; +use std::mem::size_of_val; +use std::mem::take; +use std::mem::MaybeUninit; + +use crate::Context; +use crate::HandleScope; +use crate::Isolate; +use crate::Local; +use crate::Message; +use crate::Value; + +extern "C" { + // Note: the C++ CxxTryCatch object *must* live on the stack, and it must + // not move after it is constructed. + fn v8__TryCatch__CONSTRUCT( + buf: &mut MaybeUninit, + isolate: *mut Isolate, + ); + + fn v8__TryCatch__DESTRUCT(this: &mut CxxTryCatch); + + fn v8__TryCatch__HasCaught(this: &CxxTryCatch) -> bool; + + fn v8__TryCatch__CanContinue(this: &CxxTryCatch) -> bool; + + fn v8__TryCatch__HasTerminated(this: &CxxTryCatch) -> bool; + + fn v8__TryCatch__Exception(this: &CxxTryCatch) -> *mut Value; + + fn v8__TryCatch__StackTrace( + this: &CxxTryCatch, + context: Local, + ) -> *mut Value; + + fn v8__TryCatch__Message(this: &CxxTryCatch) -> *mut Message; + + fn v8__TryCatch__Reset(this: &mut CxxTryCatch); + + fn v8__TryCatch__ReThrow(this: &mut CxxTryCatch) -> *mut Value; + + fn v8__TryCatch__IsVerbose(this: &CxxTryCatch) -> bool; + + fn v8__TryCatch__SetVerbose(this: &mut CxxTryCatch, value: bool); + + fn v8__TryCatch__SetCaptureMessage(this: &mut CxxTryCatch, value: bool); +} + +// Note: the 'tc lifetime is there to ensure that after entering a TryCatchScope +// once, the same TryCatch object can't be entered again. + +/// An external exception handler. +pub struct TryCatch<'tc>(TryCatchState<'tc>); + +#[repr(transparent)] +pub struct TryCatchScope<'tc>(CxxTryCatch, PhantomData<&'tc ()>); + +#[repr(C)] +struct CxxTryCatch([usize; 6]); + +enum TryCatchState<'tc> { + New { isolate: *mut Isolate }, + Uninit(MaybeUninit>), + Constructed(TryCatchScope<'tc>), +} + +impl<'tc> TryCatch<'tc> { + /// Creates a new try/catch block and registers it with v8. Note that + /// all TryCatch blocks should be stack allocated because the memory + /// location itself is compared against JavaScript try/catch blocks. + pub fn new(scope: &mut impl AsMut) -> Self { + Self(TryCatchState::New { + isolate: scope.as_mut(), + }) + } + + pub fn enter(&'tc mut self) -> &'tc mut TryCatchScope { + use TryCatchState::*; + let state = &mut self.0; + + let isolate = match take(state) { + New { isolate } => isolate, + _ => unreachable!(), + }; + + let buf = match state { + Uninit(b) => b, + _ => unreachable!(), + }; + + TryCatchScope::construct(buf, isolate); + + *state = match take(state) { + Uninit(b) => Constructed(unsafe { b.assume_init() }), + _ => unreachable!(), + }; + + match state { + Constructed(v) => v, + _ => unreachable!(), + } + } +} + +impl<'tc> TryCatchScope<'tc> { + /// Returns true if an exception has been caught by this try/catch block. + pub fn has_caught(&self) -> bool { + unsafe { v8__TryCatch__HasCaught(&self.0) } + } + + /// For certain types of exceptions, it makes no sense to continue execution. + /// + /// If CanContinue returns false, the correct action is to perform any C++ + /// cleanup needed and then return. If CanContinue returns false and + /// HasTerminated returns true, it is possible to call + /// CancelTerminateExecution in order to continue calling into the engine. + pub fn can_continue(&self) -> bool { + unsafe { v8__TryCatch__CanContinue(&self.0) } + } + + /// Returns true if an exception has been caught due to script execution + /// being terminated. + /// + /// There is no JavaScript representation of an execution termination + /// exception. Such exceptions are thrown when the TerminateExecution + /// methods are called to terminate a long-running script. + /// + /// If such an exception has been thrown, HasTerminated will return true, + /// indicating that it is possible to call CancelTerminateExecution in order + /// to continue calling into the engine. + pub fn has_terminated(&self) -> bool { + unsafe { v8__TryCatch__HasTerminated(&self.0) } + } + + /// Returns the exception caught by this try/catch block. If no exception has + /// been caught an empty handle is returned. + /// + /// The returned handle is valid until this TryCatch block has been destroyed. + pub fn exception(&self) -> Option> { + unsafe { Local::from_raw(v8__TryCatch__Exception(&self.0)) } + } + + /// Returns the .stack property of the thrown object. If no .stack + /// property is present an empty handle is returned. + pub fn stack_trace<'sc>( + &self, + _scope: &mut impl AsMut>, + context: Local, + ) -> Option> { + unsafe { Local::from_raw(v8__TryCatch__StackTrace(&self.0, context)) } + } + + /// Returns the message associated with this exception. If there is + /// no message associated an empty handle is returned. + /// + /// The returned handle is valid until this TryCatch block has been + /// destroyed. + pub fn message(&self) -> Option> + where + Self: 'tc, + { + unsafe { Local::from_raw(v8__TryCatch__Message(&self.0)) } + } + + /// Clears any exceptions that may have been caught by this try/catch block. + /// After this method has been called, HasCaught() will return false. Cancels + /// the scheduled exception if it is caught and ReThrow() is not called before. + /// + /// It is not necessary to clear a try/catch block before using it again; if + /// another exception is thrown the previously caught exception will just be + /// overwritten. However, it is often a good idea since it makes it easier + /// to determine which operation threw a given exception. + pub fn reset(&mut self) { + unsafe { v8__TryCatch__Reset(&mut self.0) }; + } + + /// Throws the exception caught by this TryCatch in a way that avoids + /// it being caught again by this same TryCatch. As with ThrowException + /// it is illegal to execute any JavaScript operations after calling + /// ReThrow; the caller must return immediately to where the exception + /// is caught. + pub fn rethrow<'a>(&'_ mut self) -> Option> { + unsafe { Local::from_raw(v8__TryCatch__ReThrow(&mut self.0)) } + } + + /// Returns true if verbosity is enabled. + pub fn is_verbose(&self) -> bool { + unsafe { v8__TryCatch__IsVerbose(&self.0) } + } + + /// Set verbosity of the external exception handler. + /// + /// By default, exceptions that are caught by an external exception + /// handler are not reported. Call SetVerbose with true on an + /// external exception handler to have exceptions caught by the + /// handler reported as if they were not caught. + pub fn set_verbose(&mut self, value: bool) { + unsafe { v8__TryCatch__SetVerbose(&mut self.0, value) }; + } + + /// Set whether or not this TryCatch should capture a Message object + /// which holds source information about where the exception + /// occurred. True by default. + pub fn set_capture_message(&mut self, value: bool) { + unsafe { v8__TryCatch__SetCaptureMessage(&mut self.0, value) }; + } + + fn construct(buf: &mut MaybeUninit, isolate: *mut Isolate) { + unsafe { + assert_eq!(size_of_val(buf), size_of::()); + let buf = &mut *(buf as *mut _ as *mut MaybeUninit); + v8__TryCatch__CONSTRUCT(buf, isolate); + } + } +} + +impl Drop for CxxTryCatch { + fn drop(&mut self) { + unsafe { v8__TryCatch__DESTRUCT(self) } + } +} + +impl<'tc> Default for TryCatchState<'tc> { + fn default() -> Self { + Self::Uninit(MaybeUninit::uninit()) + } +} diff --git a/tests/test_api.rs b/tests/test_api.rs index 8b2670d3..1d577ebc 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -108,6 +108,75 @@ fn v8_str<'sc>( v8::String::new(scope, s, v8::NewStringType::Normal).unwrap() } +#[test] +fn try_catch() { + fn eval<'sc>( + scope: &mut HandleScope<'sc>, + context: Local, + code: &'static str, + ) -> Option> { + let source = v8_str(scope, code); + let mut script = + v8::Script::compile(&mut *scope, context, source, None).unwrap(); + script.run(scope, context) + }; + + let _g = setup(); + let mut params = v8::Isolate::create_params(); + params.set_array_buffer_allocator( + v8::array_buffer::Allocator::new_default_allocator(), + ); + let isolate = v8::Isolate::new(params); + let _locker = v8::Locker::new(&isolate); + v8::HandleScope::enter(&isolate, |scope| { + let mut context = v8::Context::new(scope); + context.enter(); + { + // Error thrown - should be caught. + let mut try_catch = v8::TryCatch::new(scope); + let tc = try_catch.enter(); + let result = eval(scope, context, "throw new Error('foo')"); + assert!(result.is_none()); + assert!(tc.has_caught()); + assert!(tc.exception().is_some()); + assert!(tc.stack_trace(scope, context).is_some()); + assert!(tc.message().is_some()); + assert_eq!( + tc.message().unwrap().get(scope).to_rust_string_lossy(scope), + "Uncaught Error: foo" + ); + }; + { + // No error thrown. + let mut try_catch = v8::TryCatch::new(scope); + let tc = try_catch.enter(); + let result = eval(scope, context, "1 + 1"); + assert!(result.is_some()); + assert!(!tc.has_caught()); + assert!(tc.exception().is_none()); + assert!(tc.stack_trace(scope, context).is_none()); + assert!(tc.message().is_none()); + assert!(tc.rethrow().is_none()); + }; + { + // Rethrow and reset. + let mut try_catch_1 = v8::TryCatch::new(scope); + let tc1 = try_catch_1.enter(); + { + let mut try_catch_2 = v8::TryCatch::new(scope); + let tc2 = try_catch_2.enter(); + eval(scope, context, "throw 'bar'"); + assert!(tc2.has_caught()); + assert!(tc2.rethrow().is_some()); + tc2.reset(); + assert!(!tc2.has_caught()); + } + assert!(tc1.has_caught()); + }; + context.exit(); + }); +} + #[test] fn isolate_add_message_listener() { let g = setup();