On Windows, catch window callback panics and forward them to the calling thread (#703)

* Catch windows callback panics

* Unwind through calling thread

* Add CHANGELOG entry

* Fix 1.24.1 builds

* Reformat CHANGELOG entry

* Make changes from review

* Wrap thread event target in panic catcher, reformat panic resume message

* Fix me being bad at git
This commit is contained in:
Osspial 2018-11-17 14:20:04 -05:00 committed by Francesca Plebani
parent df5d66b5e8
commit 3ba808e3c6
4 changed files with 104 additions and 24 deletions

View file

@ -2,6 +2,7 @@
- On X11, fixed panic caused by dropping the window before running the event loop.
- Introduce `WindowBuilderExt::with_app_id` to allow setting the application ID on Wayland.
- On Windows, catch panics in event loop child thread and forward them to the parent thread. This prevents an invocation of undefined behavior due to unwinding into foreign code.
- On Windows, fix issue where resizing or moving window combined with grabbing the cursor would freeze program.
- On Windows, fix issue where resizing or moving window would eat `Awakened` events.

View file

@ -35,6 +35,9 @@ cocoa = "0.18.4"
core-foundation = "0.6"
core-graphics = "0.17.3"
[target.'cfg(target_os = "windows")'.dependencies]
backtrace = "0.3"
[target.'cfg(target_os = "windows")'.dependencies.winapi]
version = "0.3.6"
features = [

View file

@ -98,6 +98,8 @@ extern crate serde;
#[cfg(target_os = "windows")]
extern crate winapi;
#[cfg(target_os = "windows")]
extern crate backtrace;
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[macro_use]
extern crate objc;

View file

@ -12,12 +12,14 @@
//! The closure passed to the `execute_in_thread` method takes an `Inserter` that you can use to
//! add a `WindowState` entry to a list of window to be used by the callback.
use std::{mem, ptr, thread};
use std::{mem, panic, ptr, thread};
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::windows::io::AsRawHandle;
use std::sync::{Arc, mpsc, Mutex};
use backtrace::Backtrace;
use winapi::ctypes::c_int;
use winapi::shared::minwindef::{
BOOL,
@ -138,9 +140,14 @@ pub struct EventsLoop {
// Id of the background thread from the Win32 API.
thread_id: DWORD,
// Receiver for the events. The sender is in the background thread.
receiver: mpsc::Receiver<Event>,
receiver: mpsc::Receiver<EventsLoopEvent>,
// Sender instance that's paired with the receiver. Used to construct an `EventsLoopProxy`.
sender: mpsc::Sender<Event>,
sender: mpsc::Sender<EventsLoopEvent>,
}
enum EventsLoopEvent {
WinitEvent(Event),
Panic(PanicError),
}
impl EventsLoop {
@ -164,6 +171,7 @@ impl EventsLoop {
let (init_tx, init_rx) = mpsc::sync_channel(0);
let thread_sender = tx.clone();
let panic_sender = tx.clone();
let thread = thread::spawn(move || {
let tx = thread_sender;
let thread_msg_target = thread_event_target_window();
@ -174,6 +182,7 @@ impl EventsLoop {
windows: HashMap::with_capacity(4),
file_drop_handlers: HashMap::with_capacity(4),
mouse_buttons_down: 0,
panic_error: None,
});
});
@ -191,6 +200,16 @@ impl EventsLoop {
loop {
if winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) == 0 {
// If a panic occurred in the child callback, forward the panic information
// to the parent thread.
let panic_payload_opt = CONTEXT_STASH.with(|stash|
stash.borrow_mut().as_mut()
.and_then(|s| s.panic_error.take())
);
if let Some(panic_payload) = panic_payload_opt {
panic_sender.send(EventsLoopEvent::Panic(panic_payload)).unwrap();
};
// Only happens if the message is `WM_QUIT`.
debug_assert_eq!(msg.message, winuser::WM_QUIT);
break;
@ -224,8 +243,12 @@ impl EventsLoop {
{
loop {
let event = match self.receiver.try_recv() {
Ok(e) => e,
Err(_) => return
Ok(EventsLoopEvent::WinitEvent(e)) => e,
Ok(EventsLoopEvent::Panic(panic)) => {
eprintln!("resuming child thread unwind at: {:?}", Backtrace::new());
panic::resume_unwind(panic)
},
Err(_) => break,
};
callback(event);
@ -237,8 +260,12 @@ impl EventsLoop {
{
loop {
let event = match self.receiver.recv() {
Ok(e) => e,
Err(_) => return
Ok(EventsLoopEvent::WinitEvent(e)) => e,
Ok(EventsLoopEvent::Panic(panic)) => {
eprintln!("resuming child thread unwind at: {:?}", Backtrace::new());
panic::resume_unwind(panic)
},
Err(_) => break,
};
let flow = callback(event);
@ -282,7 +309,7 @@ impl Drop for EventsLoop {
#[derive(Clone)]
pub struct EventsLoopProxy {
thread_msg_target: HWND,
sender: mpsc::Sender<Event>,
sender: mpsc::Sender<EventsLoopEvent>,
}
unsafe impl Send for EventsLoopProxy {}
@ -290,7 +317,7 @@ unsafe impl Sync for EventsLoopProxy {}
impl EventsLoopProxy {
pub fn wakeup(&self) -> Result<(), EventsLoopClosed> {
self.sender.send(Event::Awakened).map_err(|_| EventsLoopClosed)
self.sender.send(EventsLoopEvent::WinitEvent(Event::Awakened)).map_err(|_| EventsLoopClosed)
}
/// Executes a function in the background thread.
@ -407,45 +434,78 @@ fn thread_event_target_window() -> HWND {
// in a thread-local variable.
thread_local!(static CONTEXT_STASH: RefCell<Option<ThreadLocalData>> = RefCell::new(None));
struct ThreadLocalData {
sender: mpsc::Sender<Event>,
sender: mpsc::Sender<EventsLoopEvent>,
windows: HashMap<HWND, Arc<Mutex<WindowState>>>,
file_drop_handlers: HashMap<HWND, FileDropHandler>, // Each window has its own drop handler.
mouse_buttons_down: u32,
panic_error: Option<PanicError>,
}
type PanicError = Box<Any + Send + 'static>;
// Utility function that dispatches an event on the current thread.
pub fn send_event(event: Event) {
CONTEXT_STASH.with(|context_stash| {
let context_stash = context_stash.borrow();
let _ = context_stash.as_ref().unwrap().sender.send(event); // Ignoring if closed
let _ = context_stash.as_ref().unwrap().sender.send(EventsLoopEvent::WinitEvent(event)); // Ignoring if closed
});
}
/// Capture mouse input, allowing `window` to receive mouse events when the cursor is outside of
/// the window.
unsafe fn capture_mouse(window: HWND) {
CONTEXT_STASH.with(|context_stash| {
let set_capture = CONTEXT_STASH.with(|context_stash| {
let mut context_stash = context_stash.borrow_mut();
if let Some(context_stash) = context_stash.as_mut() {
context_stash.mouse_buttons_down += 1;
winuser::SetCapture(window);
true
} else {
false
}
});
if set_capture {
winuser::SetCapture(window);
}
}
/// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor
/// is outside the window.
unsafe fn release_mouse() {
CONTEXT_STASH.with(|context_stash| {
let release_capture = CONTEXT_STASH.with(|context_stash| {
let mut context_stash = context_stash.borrow_mut();
if let Some(context_stash) = context_stash.as_mut() {
context_stash.mouse_buttons_down = context_stash.mouse_buttons_down.saturating_sub(1);
if context_stash.mouse_buttons_down == 0 {
winuser::ReleaseCapture();
return true;
}
}
false
});
if release_capture {
winuser::ReleaseCapture();
}
}
pub unsafe fn run_catch_panic<F, R>(error: R, f: F) -> R
where F: panic::UnwindSafe + FnOnce() -> R
{
// If a panic has been triggered, cancel all future operations in the function.
if CONTEXT_STASH.with(|stash| stash.borrow().as_ref().map(|s| s.panic_error.is_some()).unwrap_or(false)) {
return error;
}
let callback_result = panic::catch_unwind(f);
match callback_result {
Ok(lresult) => lresult,
Err(err) => CONTEXT_STASH.with(|context_stash| {
let mut context_stash = context_stash.borrow_mut();
if let Some(context_stash) = context_stash.as_mut() {
context_stash.panic_error = Some(err);
winuser::PostQuitMessage(-1);
}
error
})
}
}
/// Any window whose callback is configured to this function will have its events propagated
@ -460,6 +520,17 @@ pub unsafe extern "system" fn callback(
msg: UINT,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
// Unwinding into foreign code is undefined behavior. So we catch any panics that occur in our
// code, and if a panic happens we cancel any future operations.
run_catch_panic(-1, || callback_inner(window, msg, wparam, lparam))
}
unsafe fn callback_inner(
window: HWND,
msg: UINT,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
winuser::WM_CREATE => {
@ -564,7 +635,7 @@ pub unsafe extern "system" fn callback(
event: Resized(logical_size),
};
cstash.sender.send(event).ok();
cstash.sender.send(EventsLoopEvent::WinitEvent(event)).ok();
});
0
},
@ -1214,12 +1285,15 @@ pub unsafe extern "system" fn thread_event_target_callback(
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
_ if msg == *EXEC_MSG_ID => {
let mut function: Box<Box<FnMut()>> = Box::from_raw(wparam as usize as *mut _);
function();
0
},
_ => winuser::DefWindowProcW(window, msg, wparam, lparam)
}
// See `callback` comment.
run_catch_panic(-1, || {
match msg {
_ if msg == *EXEC_MSG_ID => {
let mut function: Box<Box<FnMut()>> = Box::from_raw(wparam as usize as *mut _);
function();
0
},
_ => winuser::DefWindowProcW(window, msg, wparam, lparam)
}
})
}