diff --git a/src/bindings/compile.rs b/src/bindings/compile.rs index e6ce020..eccb6be 100644 --- a/src/bindings/compile.rs +++ b/src/bindings/compile.rs @@ -28,7 +28,7 @@ pub fn compile<'a>( }; // check for error - context.ensure_no_excpetion()?; + context.ensure_no_exception()?; Ok(value) } @@ -45,7 +45,7 @@ pub fn run_compiled_function<'a>( OwnedJsValue::new(context, v) }; - context.ensure_no_excpetion().map_err(|e| { + context.ensure_no_exception().map_err(|e| { if let ExecutionError::Internal(msg) = e { ExecutionError::Internal(format!("Could not evaluate compiled function: {}", msg)) } else { diff --git a/src/bindings/mod.rs b/src/bindings/mod.rs index 86eab4f..4c7d651 100644 --- a/src/bindings/mod.rs +++ b/src/bindings/mod.rs @@ -6,7 +6,7 @@ mod value; use std::{ ffi::CString, os::raw::{c_int, c_void}, - sync::Mutex, + sync::Mutex, panic::RefUnwindSafe, }; use libquickjs_sys as q; @@ -83,6 +83,30 @@ where ((boxed_f, data), Some(trampoline::)) } +type InterruptHandlerWrapper = dyn Fn(*mut q::JSRuntime) -> i32; +fn interrupt_handler_trampoline( + closure: F, +) -> ((Box, *mut c_void), unsafe extern "C" fn(*mut q::JSRuntime, *mut c_void) -> i32) +where + F: Fn(*mut q::JSRuntime) -> i32 + 'static, +{ + unsafe extern "C" fn trampoline( + rt: *mut q::JSRuntime, + closure_ptr: *mut c_void + ) -> i32 + where + F: Fn(*mut q::JSRuntime) -> i32 + 'static, + { + let closure: &mut F = &mut *(closure_ptr as *mut F); + (*closure)(rt) + } + + let boxed_f = Box::new(closure); + let data_ptr = (&*boxed_f) as *const F as *mut c_void; + + ((boxed_f, data_ptr), trampoline::) +} + /// OwnedValueRef wraps a Javascript value from the quickjs runtime. /// It prevents leaks by ensuring that the inner value is deallocated on drop. pub struct OwnedValueRef<'a> { @@ -340,6 +364,7 @@ pub struct ContextWrapper { /// the closure. // A Mutex is used over a RefCell because it needs to be unwind-safe. callbacks: Mutex, Box)>>, + interrupt_handler: Mutex>> } impl Drop for ContextWrapper { @@ -380,6 +405,7 @@ impl ContextWrapper { runtime, context, callbacks: Mutex::new(Vec::new()), + interrupt_handler: Mutex::new(None) }; Ok(wrapper) @@ -501,7 +527,7 @@ impl ContextWrapper { } /// Returns `Result::Err` when an error ocurred. - pub(crate) fn ensure_no_excpetion(&self) -> Result<(), ExecutionError> { + pub(crate) fn ensure_no_exception(&self) -> Result<(), ExecutionError> { if let Some(e) = self.get_exception() { Err(e) } else { @@ -734,4 +760,19 @@ impl ContextWrapper { global.set_property(name, cfunc.into_value())?; Ok(()) } + + pub fn set_interrupt_handler<'a, F: Fn() -> bool + 'static + RefUnwindSafe>(&'a self, handler: F) { + let wrapper = move |_rt: *mut q::JSRuntime| { + let result = std::panic::catch_unwind(|| handler()); + match result { + Ok(false) => 0, + _ => 1 + } + }; + let pair = interrupt_handler_trampoline(wrapper); + *self.interrupt_handler.lock().unwrap() = Some(pair.0.0); + unsafe { + q::JS_SetInterruptHandler(self.runtime, Some(pair.1), pair.0.1) + }; + } } diff --git a/src/lib.rs b/src/lib.rs index aeaeab5..e4271ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ mod value; #[cfg(test)] mod tests; -use std::{convert::TryFrom, error, fmt}; +use std::{convert::TryFrom, error, fmt, panic::RefUnwindSafe}; pub use self::{ callback::{Arguments, Callback}, @@ -355,7 +355,7 @@ impl Context { /// use quick_js::{Context, JsValue}; /// let context = Context::new().unwrap(); /// - /// // Register a closue as a callback under the "add" name. + /// // Register a closure as a callback under the "add" name. /// // The 'add' function can now be called from Javascript code. /// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap(); /// @@ -373,4 +373,11 @@ impl Context { ) -> Result<(), ExecutionError> { self.wrapper.add_callback(name, callback) } + + /// Set an interrupt handler callback. + /// This is "regularly called by the engine when it is executing code". + /// This must return a `bool`. If its value is `true`, or the handler function panics, JS execution halts. + pub fn set_interrupt_handler bool + 'static + RefUnwindSafe>(&self, handler: F) { + self.wrapper.set_interrupt_handler(handler); + } } diff --git a/src/tests.rs b/src/tests.rs index 00b5a4e..eac88dd 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -622,3 +622,17 @@ fn test_global_setter() { ctx.set_global("a", "a").unwrap(); ctx.eval("a + 1").unwrap(); } + +// test interrupt handler, ensuring that closure data works +#[test] +fn test_interrupt_handler() { + use std::sync::atomic::{AtomicUsize, Ordering}; + let ctx = Context::new().unwrap(); + let arg = AtomicUsize::new(0); + ctx.set_interrupt_handler(move || { + let counter = arg.fetch_add(1, Ordering::Relaxed); + println!("{}", counter); + counter == 10 + }); + assert_eq!(ctx.eval("while (1) {}"), Err(ExecutionError::Exception(JsValue::String("InternalError: interrupted".to_string())))); +} \ No newline at end of file