From 472dc94c943e0a760416a6cdf5e1efd2ae6804c1 Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Mon, 20 Oct 2025 21:46:23 +0200 Subject: [PATCH 01/11] attempt to implement JUCE keyboard hook system --- src/win/hook.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++ src/win/mod.rs | 1 + src/win/window.rs | 5 ++- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/win/hook.rs diff --git a/src/win/hook.rs b/src/win/hook.rs new file mode 100644 index 00000000..deda4e34 --- /dev/null +++ b/src/win/hook.rs @@ -0,0 +1,111 @@ +use std::{ffi::c_int, ptr, sync::{Mutex, Once}}; + +use winapi::{shared::{minwindef::{LPARAM, WPARAM}, windef::{HHOOK, POINT}}, um::winuser::{CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER}}; + +use crate::win::{wnd_proc_inner, WindowState}; + + +static HOOK: Mutex = Mutex::new(WinKeyboardHook::new()); +static ONCE: Once = Once::new(); + + +pub fn init_keyboard_hook() { + ONCE.call_once(|| { + HOOK.lock().unwrap().attach_hook(); + }); +} + + +struct WinKeyboardHook { + hook: HHOOK, +} + +impl WinKeyboardHook { + const fn new() -> Self { + Self { + hook: ptr::null_mut(), + } + } + + fn attach_hook(&mut self) { + self.hook = unsafe { SetWindowsHookExA( + WH_GETMESSAGE, + Some(keyboard_hook_callback), + ptr::null_mut(), + 0 + ) }; + } +} + +impl Drop for WinKeyboardHook { + fn drop(&mut self) { + if !self.hook.is_null() { + unsafe { UnhookWindowsHookEx(self.hook); } + } + } +} + +unsafe impl Send for WinKeyboardHook {} +unsafe impl Sync for WinKeyboardHook {} + + +unsafe extern "system" fn keyboard_hook_callback( + n_code: c_int, wparam: WPARAM, lparam: LPARAM, +) -> isize { + let msg = lparam as *mut MSG; + + if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) { + *msg = MSG { + hwnd: ptr::null_mut(), + message: WM_USER, + wParam: 0, + lParam: 0, + time: 0, + pt: POINT { x: 0, y: 0 } + }; + + 0 + } else { + CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam) + } +} + + +fn offer_message_to_baseview(msg: *mut MSG) -> bool { + if msg.is_null() || !msg.is_aligned() { + return false + } + + let msg = unsafe { *(msg as *const MSG) }; + + // if this isn't a keyboard message, ignore it + match msg.message { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {}, + + _ => return false + } + + // check if this is a baseview window (gross) + unsafe { + let mut classname = [0u8; 9]; + + // SAFETY: It's Probably ASCII Lmao + if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { + if &classname[0..8] == "Baseview".as_bytes() { + let window_state_ptr = GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; + + // should we do anything with the return value here? + let _ = wnd_proc_inner( + msg.hwnd, + msg.message, + msg.wParam, + msg.lParam, + &*window_state_ptr + ); + + return true + } + } + } + false +} \ No newline at end of file diff --git a/src/win/mod.rs b/src/win/mod.rs index 00effa43..b914b08c 100644 --- a/src/win/mod.rs +++ b/src/win/mod.rs @@ -1,5 +1,6 @@ mod cursor; mod drop_target; +mod hook; mod keyboard; mod window; diff --git a/src/win/window.rs b/src/win/window.rs index ac7824b9..06770729 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -33,6 +33,7 @@ use raw_window_handle::{ const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1; +use crate::win::hook; use crate::{ Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, @@ -165,7 +166,7 @@ unsafe extern "system" fn wnd_proc( /// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after /// handling any deferred tasks. otherwise the default window procedure is invoked. -unsafe fn wnd_proc_inner( +pub(crate) unsafe fn wnd_proc_inner( hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState, ) -> Option { match msg { @@ -686,6 +687,8 @@ impl Window<'_> { ); // todo: manage error ^ + hook::init_keyboard_hook(); + #[cfg(feature = "opengl")] let gl_context: Option = options.gl_config.map(|gl_config| { let mut handle = Win32WindowHandle::empty(); From 2c490ce02dbe75812e352a52aa8b35316b3f438a Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Mon, 20 Oct 2025 22:01:02 +0200 Subject: [PATCH 02/11] maybe this'll work idk --- src/win/hook.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index deda4e34..77e979a9 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -1,6 +1,6 @@ use std::{ffi::c_int, ptr, sync::{Mutex, Once}}; -use winapi::{shared::{minwindef::{LPARAM, WPARAM}, windef::{HHOOK, POINT}}, um::winuser::{CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER}}; +use winapi::{shared::{minwindef::{LPARAM, WPARAM}, windef::{HHOOK, POINT}}, um::{libloaderapi::GetModuleHandleA, processthreadsapi::GetCurrentThreadId, winuser::{CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER}}}; use crate::win::{wnd_proc_inner, WindowState}; @@ -31,8 +31,8 @@ impl WinKeyboardHook { self.hook = unsafe { SetWindowsHookExA( WH_GETMESSAGE, Some(keyboard_hook_callback), - ptr::null_mut(), - 0 + GetModuleHandleA(ptr::null()), + GetCurrentThreadId() ) }; } } From 85f314b24a6fd9c1ab07bcf6757eaf97882cc88f Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Mon, 20 Oct 2025 22:30:20 +0200 Subject: [PATCH 03/11] dtor test --- Cargo.toml | 1 + src/win/hook.rs | 192 +++++++++++++++++++++++++--------------------- src/win/window.rs | 10 ++- 3 files changed, 112 insertions(+), 91 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d65e460..578a2ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ nix = "0.22.0" [target.'cfg(target_os="windows")'.dependencies] winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] } uuid = { version = "0.8", features = ["v4"], optional = true } +dtor = "=0.0.6" [target.'cfg(target_os="macos")'.dependencies] cocoa = "0.24.0" diff --git a/src/win/hook.rs b/src/win/hook.rs index 77e979a9..5f1ad03e 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -1,111 +1,129 @@ -use std::{ffi::c_int, ptr, sync::{Mutex, Once}}; - -use winapi::{shared::{minwindef::{LPARAM, WPARAM}, windef::{HHOOK, POINT}}, um::{libloaderapi::GetModuleHandleA, processthreadsapi::GetCurrentThreadId, winuser::{CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER}}}; +use std::{ + ffi::c_int, + ptr, + sync::{Mutex, Once}, +}; + +use dtor::dtor; +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::{HHOOK, POINT}, + }, + um::{ + libloaderapi::GetModuleHandleA, + processthreadsapi::GetCurrentThreadId, + winuser::{ + CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, + UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, + WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, + }, + }, +}; use crate::win::{wnd_proc_inner, WindowState}; - static HOOK: Mutex = Mutex::new(WinKeyboardHook::new()); static ONCE: Once = Once::new(); - pub fn init_keyboard_hook() { - ONCE.call_once(|| { - HOOK.lock().unwrap().attach_hook(); - }); + ONCE.call_once(|| { + HOOK.lock().unwrap().attach_hook(); + }); } +#[dtor] +fn deinit_keyboard_hook() { + let hook = HOOK.lock().unwrap(); -struct WinKeyboardHook { - hook: HHOOK, + if !hook.hook.is_null() { + unsafe { + UnhookWindowsHookEx(hook.hook); + } + } } -impl WinKeyboardHook { - const fn new() -> Self { - Self { - hook: ptr::null_mut(), - } - } - - fn attach_hook(&mut self) { - self.hook = unsafe { SetWindowsHookExA( - WH_GETMESSAGE, - Some(keyboard_hook_callback), - GetModuleHandleA(ptr::null()), - GetCurrentThreadId() - ) }; - } +struct WinKeyboardHook { + hook: HHOOK, } -impl Drop for WinKeyboardHook { - fn drop(&mut self) { - if !self.hook.is_null() { - unsafe { UnhookWindowsHookEx(self.hook); } - } - } +impl WinKeyboardHook { + const fn new() -> Self { + Self { hook: ptr::null_mut() } + } + + fn attach_hook(&mut self) { + self.hook = unsafe { + SetWindowsHookExA( + WH_GETMESSAGE, + Some(keyboard_hook_callback), + GetModuleHandleA(ptr::null()), + GetCurrentThreadId(), + ) + }; + } } unsafe impl Send for WinKeyboardHook {} unsafe impl Sync for WinKeyboardHook {} - unsafe extern "system" fn keyboard_hook_callback( - n_code: c_int, wparam: WPARAM, lparam: LPARAM, + n_code: c_int, wparam: WPARAM, lparam: LPARAM, ) -> isize { - let msg = lparam as *mut MSG; - - if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) { - *msg = MSG { - hwnd: ptr::null_mut(), - message: WM_USER, - wParam: 0, - lParam: 0, - time: 0, - pt: POINT { x: 0, y: 0 } - }; - - 0 - } else { - CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam) - } + let msg = lparam as *mut MSG; + + if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) { + *msg = MSG { + hwnd: ptr::null_mut(), + message: WM_USER, + wParam: 0, + lParam: 0, + time: 0, + pt: POINT { x: 0, y: 0 }, + }; + + 0 + } else { + CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam) + } } - fn offer_message_to_baseview(msg: *mut MSG) -> bool { - if msg.is_null() || !msg.is_aligned() { - return false - } - - let msg = unsafe { *(msg as *const MSG) }; - - // if this isn't a keyboard message, ignore it - match msg.message { - WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {}, - - _ => return false - } - - // check if this is a baseview window (gross) - unsafe { - let mut classname = [0u8; 9]; - - // SAFETY: It's Probably ASCII Lmao - if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { - if &classname[0..8] == "Baseview".as_bytes() { - let window_state_ptr = GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; - - // should we do anything with the return value here? - let _ = wnd_proc_inner( - msg.hwnd, - msg.message, - msg.wParam, - msg.lParam, - &*window_state_ptr - ); - - return true - } - } - } - false -} \ No newline at end of file + if msg.is_null() || !msg.is_aligned() { + return false; + } + + let msg = unsafe { *(msg as *const MSG) }; + + // if this isn't a keyboard message, ignore it + match msg.message { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {} + + _ => return false, + } + + // check if this is a baseview window (gross) + unsafe { + let mut classname = [0u8; 9]; + + // SAFETY: It's Probably ASCII Lmao + if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { + if &classname[0..8] == "Baseview".as_bytes() { + let window_state_ptr = + GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; + + // should we do anything with the return value here? + let _ = wnd_proc_inner( + msg.hwnd, + msg.message, + msg.wParam, + msg.lParam, + &*window_state_ptr, + ); + + return true; + } + } + } + false +} diff --git a/src/win/window.rs b/src/win/window.rs index 06770729..d796fb80 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -690,10 +690,12 @@ impl Window<'_> { hook::init_keyboard_hook(); #[cfg(feature = "opengl")] - let gl_context: Option = options.gl_config.map(|gl_config| { - let mut handle = Win32WindowHandle::empty(); - handle.hwnd = hwnd as *mut c_void; - let handle = RawWindowHandle::Win32(handle); + let gl_context: Option = options + .gl_config + .map(|gl_config| { + let mut handle = Win32WindowHandle::empty(); + handle.hwnd = hwnd as *mut c_void; + let handle = RawWindowHandle::Win32(handle); GlContext::create(&handle, gl_config).expect("Could not create OpenGL context") }); From 48c3665b034581d90511df298ee2125fdd44d0c0 Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Mon, 20 Oct 2025 22:53:42 +0200 Subject: [PATCH 04/11] cleanup --- src/win/hook.rs | 78 ++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index 5f1ad03e..4e01b58f 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -26,9 +26,21 @@ use crate::win::{wnd_proc_inner, WindowState}; static HOOK: Mutex = Mutex::new(WinKeyboardHook::new()); static ONCE: Once = Once::new(); -pub fn init_keyboard_hook() { +// initialize keyboard hook +// some DAWs (particularly Ableton) intercept incoming keyboard messages, +// but we're naughty so we intercept them right back +// +// this is invoked by Window::open() since Rust doesn't have runtime static ctors +pub(crate) fn init_keyboard_hook() { ONCE.call_once(|| { - HOOK.lock().unwrap().attach_hook(); + HOOK.lock().unwrap().hook = unsafe { + SetWindowsHookExA( + WH_GETMESSAGE, + Some(keyboard_hook_callback), + GetModuleHandleA(ptr::null()), + GetCurrentThreadId(), + ) + }; }); } @@ -51,19 +63,9 @@ impl WinKeyboardHook { const fn new() -> Self { Self { hook: ptr::null_mut() } } - - fn attach_hook(&mut self) { - self.hook = unsafe { - SetWindowsHookExA( - WH_GETMESSAGE, - Some(keyboard_hook_callback), - GetModuleHandleA(ptr::null()), - GetCurrentThreadId(), - ) - }; - } } +// SAFETY: it's a pointer behind a mutex. we'll live unsafe impl Send for WinKeyboardHook {} unsafe impl Sync for WinKeyboardHook {} @@ -88,12 +90,10 @@ unsafe extern "system" fn keyboard_hook_callback( } } -fn offer_message_to_baseview(msg: *mut MSG) -> bool { - if msg.is_null() || !msg.is_aligned() { - return false; - } - - let msg = unsafe { *(msg as *const MSG) }; +// check if `msg` is a keyboard message addressed +// to a baseview window, and intercept it if so +unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { + let msg = &*msg; // if this isn't a keyboard message, ignore it match msg.message { @@ -103,27 +103,27 @@ fn offer_message_to_baseview(msg: *mut MSG) -> bool { } // check if this is a baseview window (gross) - unsafe { - let mut classname = [0u8; 9]; - - // SAFETY: It's Probably ASCII Lmao - if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { - if &classname[0..8] == "Baseview".as_bytes() { - let window_state_ptr = - GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; - - // should we do anything with the return value here? - let _ = wnd_proc_inner( - msg.hwnd, - msg.message, - msg.wParam, - msg.lParam, - &*window_state_ptr, - ); - - return true; - } + let mut classname = [0u8; 9]; + + // SAFETY: It's Probably ASCII Lmao + if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { + if &classname[0..8] == "Baseview".as_bytes() { + let window_state_ptr = + GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; + + // NASTY to invoke wnd_proc_inner directly like this But It Works + // should we do anything with the return value here? + let _ = wnd_proc_inner( + msg.hwnd, + msg.message, + msg.wParam, + msg.lParam, + &*window_state_ptr, + ); + + return true; } } + false } From 0025f7177c918c49d21a1a341c45e0dacac67532 Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Tue, 21 Oct 2025 11:43:18 +0200 Subject: [PATCH 05/11] Use wnd_proc instead of wnd_proc_inner inside keyboard hook --- src/win/hook.rs | 10 ++-------- src/win/window.rs | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index 4e01b58f..58d13710 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -21,7 +21,7 @@ use winapi::{ }, }; -use crate::win::{wnd_proc_inner, WindowState}; +use crate::win::{wnd_proc, WindowState}; static HOOK: Mutex = Mutex::new(WinKeyboardHook::new()); static ONCE: Once = Once::new(); @@ -108,17 +108,11 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { // SAFETY: It's Probably ASCII Lmao if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { if &classname[0..8] == "Baseview".as_bytes() { - let window_state_ptr = - GetWindowLongPtrW(msg.hwnd, GWLP_USERDATA) as *mut WindowState; - - // NASTY to invoke wnd_proc_inner directly like this But It Works - // should we do anything with the return value here? - let _ = wnd_proc_inner( + let _ = wnd_proc( msg.hwnd, msg.message, msg.wParam, msg.lParam, - &*window_state_ptr, ); return true; diff --git a/src/win/window.rs b/src/win/window.rs index d796fb80..a25e93d5 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -119,7 +119,7 @@ impl Drop for ParentHandle { } } -unsafe extern "system" fn wnd_proc( +pub(crate) unsafe extern "system" fn wnd_proc( hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if msg == WM_CREATE { @@ -166,7 +166,7 @@ unsafe extern "system" fn wnd_proc( /// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after /// handling any deferred tasks. otherwise the default window procedure is invoked. -pub(crate) unsafe fn wnd_proc_inner( +unsafe fn wnd_proc_inner( hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState, ) -> Option { match msg { From 36989ff0ba344dcd2bfc82bfc572a4928bb5f4ce Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Tue, 21 Oct 2025 12:18:47 +0200 Subject: [PATCH 06/11] Replace `dtor` with HashSet-based approach --- Cargo.toml | 1 - src/win/hook.rs | 113 ++++++++++++++++++++++++++-------------------- src/win/window.rs | 21 +++++---- 3 files changed, 77 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 578a2ec4..2d65e460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ nix = "0.22.0" [target.'cfg(target_os="windows")'.dependencies] winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] } uuid = { version = "0.8", features = ["v4"], optional = true } -dtor = "=0.0.6" [target.'cfg(target_os="macos")'.dependencies] cocoa = "0.24.0" diff --git a/src/win/hook.rs b/src/win/hook.rs index 58d13710..24f68b7e 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -1,74 +1,99 @@ use std::{ + collections::HashSet, ffi::c_int, ptr, - sync::{Mutex, Once}, + sync::{LazyLock, Mutex, RwLock}, }; -use dtor::dtor; use winapi::{ shared::{ minwindef::{LPARAM, WPARAM}, - windef::{HHOOK, POINT}, + windef::{HHOOK, HWND, POINT}, }, um::{ libloaderapi::GetModuleHandleA, processthreadsapi::GetCurrentThreadId, winuser::{ - CallNextHookEx, GetClassNameA, GetWindowLongPtrW, SetWindowsHookExA, - UnhookWindowsHookEx, GWLP_USERDATA, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, - WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, + CallNextHookEx, SetWindowsHookExA, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE, + WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, + WM_USER, }, }, }; -use crate::win::{wnd_proc, WindowState}; +use crate::win::wnd_proc; -static HOOK: Mutex = Mutex::new(WinKeyboardHook::new()); -static ONCE: Once = Once::new(); +static HOOK: Mutex> = Mutex::new(None); + +// track all windows opened by this instance of baseview +// we use an RwLock here since the vast majority of uses (event interceptions) +// will only need to read from the HashSet +static OPEN_WINDOWS: LazyLock>> = LazyLock::new(|| RwLock::default()); + +pub(crate) struct KeyboardHookHandle(HWNDWrapper); + +struct KeyboardHook(HHOOK); + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +struct HWNDWrapper(HWND); + +// SAFETY: it's a pointer behind a mutex. we'll live +unsafe impl Send for KeyboardHook {} +unsafe impl Sync for KeyboardHook {} + +// SAFETY: ditto +unsafe impl Send for HWNDWrapper {} +unsafe impl Sync for HWNDWrapper {} + +impl Drop for KeyboardHookHandle { + fn drop(&mut self) { + deinit_keyboard_hook(self.0); + } +} // initialize keyboard hook // some DAWs (particularly Ableton) intercept incoming keyboard messages, // but we're naughty so we intercept them right back -// -// this is invoked by Window::open() since Rust doesn't have runtime static ctors -pub(crate) fn init_keyboard_hook() { - ONCE.call_once(|| { - HOOK.lock().unwrap().hook = unsafe { +pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle { + // register hwnd to global window set + OPEN_WINDOWS.write().unwrap().insert(HWNDWrapper(hwnd)); + + let hook = &mut *HOOK.lock().unwrap(); + + if hook.is_some() { + // keyboard hook already exists, just return handle + KeyboardHookHandle(HWNDWrapper(hwnd)) + } else { + // keyboard hook doesn't exist (no windows open before this), create it + let new_hook = KeyboardHook(unsafe { SetWindowsHookExA( WH_GETMESSAGE, Some(keyboard_hook_callback), GetModuleHandleA(ptr::null()), GetCurrentThreadId(), ) - }; - }); -} + }); -#[dtor] -fn deinit_keyboard_hook() { - let hook = HOOK.lock().unwrap(); + *hook = Some(new_hook); - if !hook.hook.is_null() { - unsafe { - UnhookWindowsHookEx(hook.hook); - } + KeyboardHookHandle(HWNDWrapper(hwnd)) } } -struct WinKeyboardHook { - hook: HHOOK, -} +fn deinit_keyboard_hook(hwnd: HWNDWrapper) { + let windows = &mut *OPEN_WINDOWS.write().unwrap(); + + windows.remove(&hwnd); -impl WinKeyboardHook { - const fn new() -> Self { - Self { hook: ptr::null_mut() } + if windows.is_empty() { + if let Ok(Some(hook)) = HOOK.lock().as_deref() { + unsafe { + UnhookWindowsHookEx(hook.0); + } + } } } -// SAFETY: it's a pointer behind a mutex. we'll live -unsafe impl Send for WinKeyboardHook {} -unsafe impl Sync for WinKeyboardHook {} - unsafe extern "system" fn keyboard_hook_callback( n_code: c_int, wparam: WPARAM, lparam: LPARAM, ) -> isize { @@ -91,7 +116,7 @@ unsafe extern "system" fn keyboard_hook_callback( } // check if `msg` is a keyboard message addressed -// to a baseview window, and intercept it if so +// to a window in OPEN_WINDOWS, and intercept it if so unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { let msg = &*msg; @@ -102,21 +127,11 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { _ => return false, } - // check if this is a baseview window (gross) - let mut classname = [0u8; 9]; + // check if this is one of our windows. if so, intercept it + if OPEN_WINDOWS.read().unwrap().contains(&HWNDWrapper(msg.hwnd)) { + let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); - // SAFETY: It's Probably ASCII Lmao - if GetClassNameA(msg.hwnd, &mut classname as *mut u8 as *mut i8, 9) != 0 { - if &classname[0..8] == "Baseview".as_bytes() { - let _ = wnd_proc( - msg.hwnd, - msg.message, - msg.wParam, - msg.lParam, - ); - - return true; - } + return true; } false diff --git a/src/win/window.rs b/src/win/window.rs index a25e93d5..e2caa490 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -33,7 +33,7 @@ use raw_window_handle::{ const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1; -use crate::win::hook; +use crate::win::hook::{self, KeyboardHookHandle}; use crate::{ Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, @@ -508,6 +508,11 @@ pub(super) struct WindowState { scale_policy: WindowScalePolicy, dw_style: u32, + // handle to the win32 keyboard hook + // we don't need to read from this, just carry it around so the Drop impl can run + #[allow(dead_code)] + kb_hook: KeyboardHookHandle, + /// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably /// borrowing the fields from `WindowState` more than once. For instance, when the window /// handler requests a resize in response to a keyboard event, the window state will already be @@ -687,15 +692,13 @@ impl Window<'_> { ); // todo: manage error ^ - hook::init_keyboard_hook(); + let kb_hook = hook::init_keyboard_hook(hwnd); #[cfg(feature = "opengl")] - let gl_context: Option = options - .gl_config - .map(|gl_config| { - let mut handle = Win32WindowHandle::empty(); - handle.hwnd = hwnd as *mut c_void; - let handle = RawWindowHandle::Win32(handle); + let gl_context: Option = options.gl_config.map(|gl_config| { + let mut handle = Win32WindowHandle::empty(); + handle.hwnd = hwnd as *mut c_void; + let handle = RawWindowHandle::Win32(handle); GlContext::create(&handle, gl_config).expect("Could not create OpenGL context") }); @@ -721,6 +724,8 @@ impl Window<'_> { deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), + kb_hook, + #[cfg(feature = "opengl")] gl_context, }); From 4be2fec79ff90257c8e029a8f5fe9f33cf720d7f Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Tue, 21 Oct 2025 12:21:01 +0200 Subject: [PATCH 07/11] Use W versions of Win32 functions --- src/win/hook.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index 24f68b7e..e542fc2f 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -11,10 +11,10 @@ use winapi::{ windef::{HHOOK, HWND, POINT}, }, um::{ - libloaderapi::GetModuleHandleA, + libloaderapi::GetModuleHandleW, processthreadsapi::GetCurrentThreadId, winuser::{ - CallNextHookEx, SetWindowsHookExA, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE, + CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE, WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, }, @@ -66,10 +66,10 @@ pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle { } else { // keyboard hook doesn't exist (no windows open before this), create it let new_hook = KeyboardHook(unsafe { - SetWindowsHookExA( + SetWindowsHookExW( WH_GETMESSAGE, Some(keyboard_hook_callback), - GetModuleHandleA(ptr::null()), + GetModuleHandleW(ptr::null()), GetCurrentThreadId(), ) }); From ed97533a5674c1d14d1d0bf422826bde537909cb Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Tue, 21 Oct 2025 12:27:58 +0200 Subject: [PATCH 08/11] Actually clear hook wrapper (whoops) --- src/win/hook.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index e542fc2f..d8afe306 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -86,9 +86,13 @@ fn deinit_keyboard_hook(hwnd: HWNDWrapper) { windows.remove(&hwnd); if windows.is_empty() { - if let Ok(Some(hook)) = HOOK.lock().as_deref() { - unsafe { - UnhookWindowsHookEx(hook.0); + if let Ok(mut hook) = HOOK.lock() { + if let Some(KeyboardHook(hhook)) = &mut *hook { + unsafe { + UnhookWindowsHookEx(*hhook); + } + + *hook = None; } } } @@ -128,7 +132,9 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { } // check if this is one of our windows. if so, intercept it - if OPEN_WINDOWS.read().unwrap().contains(&HWNDWrapper(msg.hwnd)) { + let Ok(windows) = OPEN_WINDOWS.read() else { return false }; + + if windows.contains(&HWNDWrapper(msg.hwnd)) { let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); return true; From 48259cfe735e5fffa0c3fba3d00d54e6bd8eaf2f Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Wed, 22 Oct 2025 19:36:31 +0200 Subject: [PATCH 09/11] Combine OPEN_WINDOWS and HOOK into HOOK_STATE RwLock --- src/win/hook.rs | 63 ++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index d8afe306..8bf9a8a8 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -2,7 +2,7 @@ use std::{ collections::HashSet, ffi::c_int, ptr, - sync::{LazyLock, Mutex, RwLock}, + sync::{LazyLock, RwLock}, }; use winapi::{ @@ -15,33 +15,34 @@ use winapi::{ processthreadsapi::GetCurrentThreadId, winuser::{ CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE, - WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, - WM_USER, + WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, }, }, }; use crate::win::wnd_proc; -static HOOK: Mutex> = Mutex::new(None); - // track all windows opened by this instance of baseview // we use an RwLock here since the vast majority of uses (event interceptions) // will only need to read from the HashSet -static OPEN_WINDOWS: LazyLock>> = LazyLock::new(|| RwLock::default()); +static HOOK_STATE: LazyLock> = LazyLock::new(|| RwLock::default()); pub(crate) struct KeyboardHookHandle(HWNDWrapper); -struct KeyboardHook(HHOOK); +#[derive(Default)] +struct KeyboardHookState { + hook: Option, + open_windows: HashSet, +} #[derive(Hash, PartialEq, Eq, Clone, Copy)] struct HWNDWrapper(HWND); -// SAFETY: it's a pointer behind a mutex. we'll live -unsafe impl Send for KeyboardHook {} -unsafe impl Sync for KeyboardHook {} +// SAFETY: it's a pointer behind an RwLock. we'll live +unsafe impl Send for KeyboardHookState {} +unsafe impl Sync for KeyboardHookState {} -// SAFETY: ditto +// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet unsafe impl Send for HWNDWrapper {} unsafe impl Sync for HWNDWrapper {} @@ -55,45 +56,43 @@ impl Drop for KeyboardHookHandle { // some DAWs (particularly Ableton) intercept incoming keyboard messages, // but we're naughty so we intercept them right back pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle { - // register hwnd to global window set - OPEN_WINDOWS.write().unwrap().insert(HWNDWrapper(hwnd)); + let state = &mut *HOOK_STATE.write().unwrap(); - let hook = &mut *HOOK.lock().unwrap(); + // register hwnd to global window set + state.open_windows.insert(HWNDWrapper(hwnd)); - if hook.is_some() { + if state.hook.is_some() { // keyboard hook already exists, just return handle KeyboardHookHandle(HWNDWrapper(hwnd)) } else { // keyboard hook doesn't exist (no windows open before this), create it - let new_hook = KeyboardHook(unsafe { + let new_hook = unsafe { SetWindowsHookExW( WH_GETMESSAGE, Some(keyboard_hook_callback), GetModuleHandleW(ptr::null()), GetCurrentThreadId(), ) - }); + }; - *hook = Some(new_hook); + state.hook = Some(new_hook); KeyboardHookHandle(HWNDWrapper(hwnd)) } } fn deinit_keyboard_hook(hwnd: HWNDWrapper) { - let windows = &mut *OPEN_WINDOWS.write().unwrap(); - - windows.remove(&hwnd); + let state = &mut *HOOK_STATE.write().unwrap(); - if windows.is_empty() { - if let Ok(mut hook) = HOOK.lock() { - if let Some(KeyboardHook(hhook)) = &mut *hook { - unsafe { - UnhookWindowsHookEx(*hhook); - } + state.open_windows.remove(&hwnd); - *hook = None; + if state.open_windows.is_empty() { + if let Some(hhook) = state.hook { + unsafe { + UnhookWindowsHookEx(hhook); } + + state.hook = None; } } } @@ -119,8 +118,8 @@ unsafe extern "system" fn keyboard_hook_callback( } } -// check if `msg` is a keyboard message addressed -// to a window in OPEN_WINDOWS, and intercept it if so +// check if `msg` is a keyboard message addressed to a window +// in KeyboardHookState::open_windows, and intercept it if so unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { let msg = &*msg; @@ -132,9 +131,9 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { } // check if this is one of our windows. if so, intercept it - let Ok(windows) = OPEN_WINDOWS.read() else { return false }; + let state = HOOK_STATE.read().unwrap(); - if windows.contains(&HWNDWrapper(msg.hwnd)) { + if state.open_windows.contains(&HWNDWrapper(msg.hwnd)) { let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); return true; From f082632a98d6dadebeace2c9f7475a1bcd0687c3 Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Wed, 22 Oct 2025 20:04:34 +0200 Subject: [PATCH 10/11] Forgot a word whoops --- src/win/hook.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index 8bf9a8a8..eb9a6187 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -42,7 +42,7 @@ struct HWNDWrapper(HWND); unsafe impl Send for KeyboardHookState {} unsafe impl Sync for KeyboardHookState {} -// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet +// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet entry unsafe impl Send for HWNDWrapper {} unsafe impl Sync for HWNDWrapper {} From bf5cf5310352a6a434717ce4a1e937aa756eedfb Mon Sep 17 00:00:00 2001 From: ash taylor?! Date: Thu, 23 Oct 2025 09:40:42 +0200 Subject: [PATCH 11/11] Drop read lock before calling wnd_proc --- src/win/hook.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/win/hook.rs b/src/win/hook.rs index eb9a6187..bdb49dfd 100644 --- a/src/win/hook.rs +++ b/src/win/hook.rs @@ -131,9 +131,7 @@ unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { } // check if this is one of our windows. if so, intercept it - let state = HOOK_STATE.read().unwrap(); - - if state.open_windows.contains(&HWNDWrapper(msg.hwnd)) { + if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) { let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); return true;