Path: blob/main/gpu_display/src/gpu_display_win/window_procedure_thread.rs
5394 views
// Copyright 2022 The ChromiumOS Authors1// Use of this source code is governed by a BSD-style license that can be2// found in the LICENSE file.34use std::any::type_name;5use std::any::TypeId;6use std::collections::btree_map::Entry;7use std::collections::BTreeMap;8use std::collections::HashMap;9use std::mem;10use std::os::windows::io::RawHandle;11use std::pin::Pin;12use std::ptr::null_mut;13use std::rc::Rc;14use std::sync::atomic::AtomicI32;15use std::sync::atomic::Ordering;16use std::sync::mpsc::channel;17use std::sync::mpsc::Sender;18use std::sync::Arc;19use std::thread::Builder as ThreadBuilder;20use std::thread::JoinHandle;2122use anyhow::anyhow;23use anyhow::bail;24use anyhow::Context;25use anyhow::Result;26use base::error;27use base::info;28use base::warn;29use base::AsRawDescriptor;30use base::Event;31use base::ReadNotifier;32use base::Tube;33use euclid::size2;34use serde::Deserialize;35use serde::Serialize;36use sync::Mutex;37#[cfg(feature = "kiwi")]38use vm_control::ServiceSendToGpu;39use win_util::syscall_bail;40use win_util::win32_wide_string;41use winapi::shared::minwindef::DWORD;42use winapi::shared::minwindef::FALSE;43use winapi::shared::minwindef::LPARAM;44use winapi::shared::minwindef::LRESULT;45use winapi::shared::minwindef::UINT;46use winapi::shared::minwindef::WPARAM;47use winapi::shared::windef::HWND;48use winapi::um::winbase::INFINITE;49use winapi::um::winbase::WAIT_OBJECT_0;50use winapi::um::winnt::MAXIMUM_WAIT_OBJECTS;51use winapi::um::winuser::*;5253use super::window::get_current_module_handle;54use super::window::GuiWindow;55use super::window::MessageOnlyWindow;56use super::window::MessagePacket;57use super::window_message_dispatcher::WindowMessageDispatcher;58use super::window_message_dispatcher::DISPATCHER_PROPERTY_NAME;59use super::window_message_processor::*;6061// The default app icon id, which is defined in crosvm-manifest.rc.62const APP_ICON_ID: u16 = 1;6364#[derive(Debug)]65enum MessageLoopState {66/// The initial state.67NotStarted = 0,68/// The loop is running normally.69Running,70/// The loop has ended normally.71ExitedNormally,72/// The loop never started because errors occurred.73EarlyTerminatedWithError,74/// The loop has ended because errors occurred.75ExitedWithError,76}7778#[derive(Copy, Clone, PartialEq)]79enum Token {80MessagePump,81ServiceMessage,82}8384/// A context that can wait on both the thread-specific message queue and the given handles.85struct MsgWaitContext {86triggers: HashMap<RawHandle, Token>,87raw_handles: Vec<RawHandle>,88}8990impl MsgWaitContext {91pub fn new() -> Self {92Self {93triggers: HashMap::new(),94raw_handles: Vec::new(),95}96}9798/// Note that since there is no handle associated with `Token::MessagePump`, this token will be99/// used internally by `MsgWaitContext` and the caller should never use it.100pub fn add(&mut self, handle: &dyn AsRawDescriptor, token: Token) -> Result<()> {101if token == Token::MessagePump {102bail!("Token::MessagePump is reserved!");103}104if self.raw_handles.len() == MAXIMUM_WAIT_OBJECTS as usize {105bail!("Number of raw handles exceeding MAXIMUM_WAIT_OBJECTS!");106}107108let raw_descriptor = handle.as_raw_descriptor();109if self.triggers.contains_key(&raw_descriptor) {110bail!("The handle has already been registered in MsgWaitContext!")111}112113self.triggers.insert(raw_descriptor, token);114self.raw_handles.push(raw_descriptor);115Ok(())116}117118/// Blocks the thread until there is any new message available on the message queue, or if any119/// of the given handles is signaled, and returns the associated token.120///121/// If multiple handles are signaled, this will return the token associated with the one that122/// was first added to this context.123///124/// # Safety125///126/// Caller is responsible for ensuring that the handles are still valid.127pub unsafe fn wait(&self) -> Result<Token> {128let num_handles = self.raw_handles.len();129// Safe because the caller is required to guarantee that the handles are valid, and failures130// are handled below.131let result = MsgWaitForMultipleObjects(132num_handles as DWORD,133self.raw_handles.as_ptr(),134/* fWaitAll= */ FALSE,135INFINITE,136QS_ALLINPUT,137);138match (result - WAIT_OBJECT_0) as usize {139// At least one of the handles has been signaled.140index if index < num_handles => Ok(self.triggers[&self.raw_handles[index]]),141// At least one message is available at the message queue.142index if index == num_handles => Ok(Token::MessagePump),143// Invalid cases. This is most likely a `WAIT_FAILED`, but anything not matched by the144// above is an error case.145_ => syscall_bail!(format!(146"MsgWaitForMultipleObjects() unexpectedly returned {}",147result148)),149}150}151}152153trait RegisterWindowClass: 'static {154// Only for debug purpose. Not required to be unique across different implementors.155const CLASS_NAME_PREFIX: &'static str = "";156fn register_window_class(class_name: &str, wnd_proc: WNDPROC) -> Result<()>;157}158159impl RegisterWindowClass for GuiWindow {160const CLASS_NAME_PREFIX: &'static str = "CROSVM";161162fn register_window_class(class_name: &str, wnd_proc: WNDPROC) -> Result<()> {163let hinstance = get_current_module_handle();164// If we fail to load any UI element below, use NULL to let the system use the default UI165// rather than crash.166let hicon = Self::load_custom_icon(hinstance, APP_ICON_ID).unwrap_or(null_mut());167let hcursor = Self::load_system_cursor(IDC_ARROW).unwrap_or(null_mut());168let hbrush_background = Self::create_opaque_black_brush().unwrap_or(null_mut());169let class_name = win32_wide_string(class_name);170let window_class = WNDCLASSEXW {171cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,172style: CS_OWNDC | CS_HREDRAW | CS_VREDRAW,173lpfnWndProc: wnd_proc,174cbClsExtra: 0,175cbWndExtra: 0,176hInstance: hinstance,177hIcon: hicon,178hCursor: hcursor,179hbrBackground: hbrush_background,180lpszMenuName: null_mut(),181lpszClassName: class_name.as_ptr(),182hIconSm: hicon,183};184185// SAFETY:186// Safe because we know the lifetime of `window_class`, and we handle failures below.187if unsafe { RegisterClassExW(&window_class) } == 0 {188syscall_bail!("Failed to call RegisterClassExW()");189}190Ok(())191}192}193194impl RegisterWindowClass for MessageOnlyWindow {195const CLASS_NAME_PREFIX: &'static str = "THREAD_MESSAGE_ROUTER";196197fn register_window_class(class_name: &str, wnd_proc: WNDPROC) -> Result<()> {198let hinstance = get_current_module_handle();199let class_name = win32_wide_string(class_name);200let window_class = WNDCLASSEXW {201cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,202style: 0,203lpfnWndProc: wnd_proc,204cbClsExtra: 0,205cbWndExtra: 0,206hInstance: hinstance,207hIcon: null_mut(),208hCursor: null_mut(),209hbrBackground: null_mut(),210lpszMenuName: null_mut(),211lpszClassName: class_name.as_ptr(),212hIconSm: null_mut(),213};214215// SAFETY:216// Safe because we know the lifetime of `window_class`, and we handle failures below.217if unsafe { RegisterClassExW(&window_class) } == 0 {218syscall_bail!("Failed to call RegisterClassExW()");219}220Ok(())221}222}223224/// This class runs the WndProc thread, and provides helper functions for other threads to225/// communicate with it.226pub struct WindowProcedureThread {227thread: Option<JoinHandle<()>>,228message_router_handle: HWND,229message_loop_state: Option<Arc<AtomicI32>>,230close_requested_event: Event,231}232233impl WindowProcedureThread {234pub fn builder() -> WindowProcedureThreadBuilder {235// We don't implement Default for WindowProcedureThreadBuilder so that the builder function236// is the only way to create WindowProcedureThreadBuilder.237WindowProcedureThreadBuilder {238max_num_windows: 1,239display_tube: None,240#[cfg(feature = "kiwi")]241ime_tube: None,242}243}244245fn start_thread(max_num_windows: u32, gpu_main_display_tube: Option<Tube>) -> Result<Self> {246let (message_router_handle_sender, message_router_handle_receiver) = channel();247let message_loop_state = Arc::new(AtomicI32::new(MessageLoopState::NotStarted as i32));248let close_requested_event = Event::new().unwrap();249250let message_loop_state_clone = Arc::clone(&message_loop_state);251let close_requested_event_clone = close_requested_event252.try_clone()253.map_err(|e| anyhow!("Failed to clone close_requested_event: {}", e))?;254255let thread = ThreadBuilder::new()256.name("gpu_display_wndproc".into())257.spawn(move || {258match close_requested_event_clone.try_clone() {259Ok(close_requested_event) => Self::run_message_loop(260max_num_windows,261message_router_handle_sender,262message_loop_state_clone,263gpu_main_display_tube,264close_requested_event,265),266Err(e) => error!("Failed to clone close_requested_event: {}", e),267}268// The close requested event should have been signaled at this point, unless we hit269// some edge cases, e.g. the WndProc thread terminates unexpectedly during startup.270// We want to make sure it is signaled in all cases.271if let Err(e) = close_requested_event_clone.signal() {272error!("Failed to signal close requested event: {}", e);273}274})275.context("Failed to spawn WndProc thread")?;276277match message_router_handle_receiver.recv() {278Ok(message_router_handle_res) => match message_router_handle_res {279Ok(message_router_handle) => Ok(Self {280thread: Some(thread),281message_router_handle: message_router_handle as HWND,282message_loop_state: Some(message_loop_state),283close_requested_event,284}),285Err(e) => bail!("WndProc internal failure: {:?}", e),286},287Err(e) => bail!("Failed to receive WndProc thread ID: {}", e),288}289}290291pub fn try_clone_close_requested_event(&self) -> Result<Event> {292self.close_requested_event293.try_clone()294.map_err(|e| anyhow!("Failed to clone close_requested_event: {}", e))295}296297pub fn post_display_command(&self, message: DisplaySendToWndProc) -> Result<()> {298self.post_message_to_thread_carrying_object(299WM_USER_HANDLE_DISPLAY_MESSAGE_INTERNAL,300message,301)302.context("When posting DisplaySendToWndProc message")303}304305/// Calls `PostMessageW()` internally.306fn post_message_to_thread(&self, msg: UINT, w_param: WPARAM, l_param: LPARAM) -> Result<()> {307if !self.is_message_loop_running() {308bail!("Cannot post message to WndProc thread because message loop is not running!");309}310// SAFETY:311// Safe because the message loop is still running.312if unsafe { PostMessageW(self.message_router_handle, msg, w_param, l_param) } == 0 {313syscall_bail!("Failed to call PostMessageW()");314}315Ok(())316}317318/// Calls `PostMessageW()` internally. This is a common pattern, where we send a pointer to319/// the given object as the lParam. The receiver is responsible for destructing the object.320fn post_message_to_thread_carrying_object<U>(&self, msg: UINT, object: U) -> Result<()> {321let mut boxed_object = Box::new(object);322self.post_message_to_thread(323msg,324/* w_param= */ 0,325&mut *boxed_object as *mut U as LPARAM,326)327.map(|_| {328// If successful, the receiver will be responsible for destructing the object.329std::mem::forget(boxed_object);330})331}332333fn run_message_loop(334max_num_windows: u32,335message_router_handle_sender: Sender<Result<u32>>,336message_loop_state: Arc<AtomicI32>,337gpu_main_display_tube: Option<Tube>,338close_requested_event: Event,339) {340let gpu_main_display_tube = gpu_main_display_tube.map(Rc::new);341// SAFETY:342// Safe because the dispatcher will take care of the lifetime of the `MessageOnlyWindow` and343// `GuiWindow` objects.344match unsafe { Self::create_windows(max_num_windows) }.and_then(345|(message_router_window, gui_windows)| {346WindowMessageDispatcher::new(347message_router_window,348gui_windows,349gpu_main_display_tube.clone(),350close_requested_event,351)352},353) {354Ok(dispatcher) => {355info!("WndProc thread entering message loop");356message_loop_state.store(MessageLoopState::Running as i32, Ordering::SeqCst);357358let message_router_handle =359// SAFETY:360// Safe because we won't use the handle unless the message loop is still running.361unsafe { dispatcher.message_router_handle().unwrap_or(null_mut()) };362// HWND cannot be sent cross threads, so we cast it to u32 first.363if let Err(e) = message_router_handle_sender.send(Ok(message_router_handle as u32))364{365error!("Failed to send message router handle: {}", e);366}367368let exit_state = Self::run_message_loop_body(dispatcher, gpu_main_display_tube);369message_loop_state.store(exit_state as i32, Ordering::SeqCst);370}371Err(e) => {372error!("WndProc thread didn't enter message loop: {:?}", e);373message_loop_state.store(374MessageLoopState::EarlyTerminatedWithError as i32,375Ordering::SeqCst,376);377if let Err(e) = message_router_handle_sender.send(Err(e)) {378error!("Failed to report message loop early termination: {}", e)379}380}381}382}383384fn run_message_loop_body(385#[cfg_attr(not(feature = "kiwi"), allow(unused_variables, unused_mut))]386mut message_dispatcher: Pin<Box<WindowMessageDispatcher>>,387gpu_main_display_tube: Option<Rc<Tube>>,388) -> MessageLoopState {389let mut msg_wait_ctx = MsgWaitContext::new();390if let Some(tube) = &gpu_main_display_tube {391if let Err(e) = msg_wait_ctx.add(tube.get_read_notifier(), Token::ServiceMessage) {392error!(393"Failed to add service message read notifier to MsgWaitContext: {:?}",394e395);396return MessageLoopState::EarlyTerminatedWithError;397}398}399400loop {401// SAFETY:402// Safe because the lifetime of handles are at least as long as the function call.403match unsafe { msg_wait_ctx.wait() } {404Ok(token) => match token {405Token::MessagePump => {406if !Self::retrieve_and_dispatch_messages() {407info!("WndProc thread exiting message loop normally");408return MessageLoopState::ExitedNormally;409}410}411Token::ServiceMessage => Self::read_and_dispatch_service_message(412&mut message_dispatcher,413// We never use this token if `gpu_main_display_tube` is None, so414// `expect()` should always succeed.415gpu_main_display_tube416.as_ref()417.expect("Service message tube is None"),418),419},420Err(e) => {421error!(422"WndProc thread exiting message loop because of error: {:?}",423e424);425return MessageLoopState::ExitedWithError;426}427}428}429}430431/// Retrieves and dispatches all messages in the queue, and returns whether the message loop432/// should continue running.433fn retrieve_and_dispatch_messages() -> bool {434// Since `MsgWaitForMultipleObjects()` returns only when there is a new event in the queue,435// if we call `MsgWaitForMultipleObjects()` again without draining the queue, it will ignore436// existing events and will not return immediately. Hence, we need to keep calling437// `PeekMessageW()` with `PM_REMOVE` until the queue is drained before returning.438// https://devblogs.microsoft.com/oldnewthing/20050217-00/?p=36423439// Alternatively we could use `MsgWaitForMultipleObjectsEx()` with the `MWMO_INPUTAVAILABLE`440// flag, which will always return if there is any message in the queue, no matter that is a441// new message or not. However, we found that it may also return when there is no message at442// all, so we slightly prefer `MsgWaitForMultipleObjects()`.443loop {444// Safe because if `message` is initialized, we will call `assume_init()` to extract the445// value, which will get dropped eventually.446let mut message = mem::MaybeUninit::uninit();447// SAFETY:448// Safe because `message` lives at least as long as the function call.449if unsafe {450PeekMessageW(451message.as_mut_ptr(),452/* hWnd= */ null_mut(),453/* wMsgFilterMin= */ 0,454/* wMsgFilterMax= */ 0,455PM_REMOVE,456) == 0457} {458// No more message in the queue.459return true;460}461462// SAFETY:463// Safe because `PeekMessageW()` has populated `message`.464unsafe {465let new_message = message.assume_init();466if new_message.message == WM_QUIT {467return false;468}469TranslateMessage(&new_message);470DispatchMessageW(&new_message);471}472}473}474475#[cfg(feature = "kiwi")]476fn read_and_dispatch_service_message(477message_dispatcher: &mut Pin<Box<WindowMessageDispatcher>>,478gpu_main_display_tube: &Tube,479) {480match gpu_main_display_tube.recv::<ServiceSendToGpu>() {481Ok(message) => message_dispatcher.as_mut().process_service_message(message),482Err(e) => {483error!("Failed to receive service message through the tube: {}", e)484}485}486}487488#[cfg(not(feature = "kiwi"))]489fn read_and_dispatch_service_message(490_: &mut Pin<Box<WindowMessageDispatcher>>,491_gpu_main_display_tube: &Tube,492) {493}494495fn is_message_loop_running(&self) -> bool {496self.message_loop_state497.as_ref()498.is_some_and(|state| state.load(Ordering::SeqCst) == MessageLoopState::Running as i32)499}500501/// Normally the WndProc thread should still be running when the `WindowProcedureThread` struct502/// is dropped, and this function will post a message to notify that thread to destroy all503/// windows and release all resources. If the WndProc thread has encountered fatal errors and504/// has already terminated, we don't need to post this message anymore.505fn signal_exit_message_loop_if_needed(&self) {506if !self.is_message_loop_running() {507return;508}509510info!("WndProc thread is still in message loop before dropping. Signaling shutting down");511if let Err(e) = self.post_message_to_thread(512WM_USER_SHUTDOWN_WNDPROC_THREAD_INTERNAL,513/* w_param */ 0,514/* l_param */ 0,515) {516error!("Failed to signal WndProc thread to shut down: {:?}", e);517}518}519520/// Checks if the message loop has exited normally. This should be called after joining with the521/// WndProc thread.522fn check_message_loop_final_state(&mut self) {523match Arc::try_unwrap(self.message_loop_state.take().unwrap()) {524Ok(state) => {525let state = state.into_inner();526if state == MessageLoopState::ExitedNormally as i32 {527info!("WndProc thread exited gracefully");528} else {529warn!("WndProc thread exited with message loop state: {:?}", state);530}531}532Err(e) => error!(533"WndProc thread exited but message loop state retrieval failed: {:?}",534e535),536}537}538539/// # Safety540/// The owner of the returned window objects is responsible for dropping them before we finish541/// processing `WM_NCDESTROY`, because the window handle will become invalid afterwards.542unsafe fn create_windows(max_num_windows: u32) -> Result<(MessageOnlyWindow, Vec<GuiWindow>)> {543let message_router_window = MessageOnlyWindow::new(544/* class_name */545Self::get_window_class_name::<MessageOnlyWindow>()546.with_context(|| {547format!(548"retrieve the window class name for MessageOnlyWindow of {}.",549type_name::<Self>()550)551})?552.as_str(),553/* title */ "ThreadMessageRouter",554)?;555// Gfxstream window is a child window of crosvm window. Without WS_CLIPCHILDREN, the parent556// window may use the background brush to clear the gfxstream window client area when557// drawing occurs. This caused the screen flickering issue during resizing.558// See b/197786842 for details.559let mut gui_windows = Vec::with_capacity(max_num_windows as usize);560for scanout_id in 0..max_num_windows {561gui_windows.push(GuiWindow::new(562scanout_id,563/* class_name */564Self::get_window_class_name::<GuiWindow>()565.with_context(|| {566format!(567"retrieve the window class name for GuiWindow of {}",568type_name::<Self>()569)570})?571.as_str(),572/* title */ Self::get_window_title().as_str(),573/* dw_style */ WS_POPUP | WS_CLIPCHILDREN,574// The window size and style can be adjusted later when `Surface` is created.575&size2(1, 1),576)?);577}578Ok((message_router_window, gui_windows))579}580581unsafe extern "system" fn wnd_proc(582hwnd: HWND,583msg: UINT,584w_param: WPARAM,585l_param: LPARAM,586) -> LRESULT {587let dispatcher_ptr = GetPropW(hwnd, win32_wide_string(DISPATCHER_PROPERTY_NAME).as_ptr())588as *mut WindowMessageDispatcher;589if let Some(dispatcher) = dispatcher_ptr.as_mut() {590if let Some(ret) =591dispatcher.dispatch_window_message(hwnd, &MessagePacket::new(msg, w_param, l_param))592{593return ret;594}595}596DefWindowProcW(hwnd, msg, w_param, l_param)597}598599/// U + T decides one window class. For the same combination of U + T, the same window class600/// name will be returned. This function also registers the Window class if it is not registered601/// through this function yet.602fn get_window_class_name<T: RegisterWindowClass>() -> Result<String> {603static WINDOW_CLASS_NAMES: Mutex<BTreeMap<TypeId, String>> = Mutex::new(BTreeMap::new());604let mut window_class_names = WINDOW_CLASS_NAMES.lock();605let id = window_class_names.len();606let entry = window_class_names.entry(TypeId::of::<T>());607let entry = match entry {608Entry::Occupied(entry) => return Ok(entry.get().clone()),609Entry::Vacant(entry) => entry,610};611// We are generating a different class name everytime we reach this line, so the name612// shouldn't collide with any window classes registered through this function. The613// underscore here is important. If we just use `"{}{}"`, we may collide for prefix = "" and614// prefix = "1".615let window_class_name = format!("{}_{}", T::CLASS_NAME_PREFIX, id);616T::register_window_class(&window_class_name, Some(Self::wnd_proc)).with_context(|| {617format!(618"Failed to register the window class for ({}, {}), with name {}.",619type_name::<Self>(),620type_name::<T>(),621window_class_name622)623})?;624entry.insert(window_class_name.clone());625Ok(window_class_name)626}627628fn get_window_title() -> String {629"crosvm".to_string()630}631}632633impl Drop for WindowProcedureThread {634fn drop(&mut self) {635self.signal_exit_message_loop_if_needed();636match self.thread.take().unwrap().join() {637Ok(_) => self.check_message_loop_final_state(),638Err(e) => error!("Failed to join with WndProc thread: {:?}", e),639}640}641}642643// SAFETY:644// Since `WindowProcedureThread` does not hold anything that cannot be transferred between threads,645// we can implement `Send` for it.646unsafe impl Send for WindowProcedureThread {}647648#[derive(Deserialize, Serialize)]649pub struct WindowProcedureThreadBuilder {650max_num_windows: u32,651display_tube: Option<Tube>,652#[cfg(feature = "kiwi")]653ime_tube: Option<Tube>,654}655656impl WindowProcedureThreadBuilder {657pub fn set_max_num_windows(&mut self, max_num_windows: u32) -> &mut Self {658self.max_num_windows = max_num_windows;659self660}661662pub fn set_display_tube(&mut self, display_tube: Option<Tube>) -> &mut Self {663self.display_tube = display_tube;664self665}666667#[cfg(feature = "kiwi")]668pub fn set_ime_tube(&mut self, ime_tube: Option<Tube>) -> &mut Self {669self.ime_tube = ime_tube;670self671}672673/// This function creates the window procedure thread and windows.674///675/// We have seen third-party DLLs hooking into window creation. They may have deep call stack,676/// and they may not be well tested against late window creation, which may lead to stack677/// overflow. Hence, this should be called as early as possible when the VM is booting.678pub fn start_thread(self) -> Result<WindowProcedureThread> {679cfg_if::cfg_if! {680if #[cfg(feature = "kiwi")] {681let ime_tube = self682.ime_tube683.ok_or_else(|| anyhow!("The ime tube is not set."))?;684WindowProcedureThread::start_thread(685self.max_num_windows,686self.display_tube,687ime_tube,688)689} else {690WindowProcedureThread::start_thread(self.max_num_windows, None)691}692}693}694}695696#[cfg(test)]697mod tests {698use super::*;699700#[test]701fn error_on_adding_reserved_token_to_context() {702let mut ctx = MsgWaitContext::new();703let event = Event::new().unwrap();704assert!(ctx.add(&event, Token::MessagePump).is_err());705}706707#[test]708fn error_on_adding_duplicated_handle_to_context() {709let mut ctx = MsgWaitContext::new();710let event = Event::new().unwrap();711assert!(ctx.add(&event, Token::ServiceMessage).is_ok());712assert!(ctx.add(&event, Token::ServiceMessage).is_err());713}714715#[test]716fn window_procedure_window_class_name_should_include_class_name_prefix() {717const PREFIX: &str = "test-window-class-prefix";718struct TestWindow;719impl RegisterWindowClass for TestWindow {720const CLASS_NAME_PREFIX: &'static str = PREFIX;721fn register_window_class(_class_name: &str, _wnd_proc: WNDPROC) -> Result<()> {722Ok(())723}724}725726let name = WindowProcedureThread::get_window_class_name::<TestWindow>().unwrap();727assert!(728name.starts_with(PREFIX),729"The class name {name} should start with {PREFIX}."730);731}732733#[test]734fn window_procedure_with_same_types_should_return_same_name() {735struct TestWindow;736impl RegisterWindowClass for TestWindow {737fn register_window_class(_class_name: &str, _wnd_proc: WNDPROC) -> Result<()> {738Ok(())739}740}741742let name1 = WindowProcedureThread::get_window_class_name::<TestWindow>().unwrap();743let name2 = WindowProcedureThread::get_window_class_name::<TestWindow>().unwrap();744assert_eq!(name1, name2);745}746747#[test]748fn window_procedure_with_different_types_should_return_different_names() {749struct TestWindow1;750impl RegisterWindowClass for TestWindow1 {751fn register_window_class(_class_name: &str, _wnd_proc: WNDPROC) -> Result<()> {752Ok(())753}754}755struct TestWindow2;756impl RegisterWindowClass for TestWindow2 {757fn register_window_class(_class_name: &str, _wnd_proc: WNDPROC) -> Result<()> {758Ok(())759}760}761762let name1 = WindowProcedureThread::get_window_class_name::<TestWindow1>().unwrap();763let name2 = WindowProcedureThread::get_window_class_name::<TestWindow2>().unwrap();764assert_ne!(name1, name2);765}766}767768769