Rust中Windows窗口回调获取静态引用的实现问题求助
解决方案:Rust下Windows窗口回调与消息队列实现
问题分析
- 初始方案致命问题:
BEWindow结构体在Rust中发生移动时内存地址会改变,但存入GWLP_USERDATA的是原始指针,后续回调访问时直接变成野指针,触发段错误。 - 静态变量方案失效原因:
unsafe静态变量读写无同步机制,窗口回调线程可能看不到主线程对GLOBAL_WND的修改;且全局静态变量无法支持多窗口场景,本身就是不合理设计。
正确实现思路
核心是利用**堆分配的引用计数类型(Arc)**保证对象地址稳定,结合GWLP_USERDATA存储指针,同时用消息通道(std::sync::mpsc)将窗口消息转发到主线程游戏循环处理。
1. 定义消息类型与窗口结构体
先定义主线程需要处理的窗口消息类型,在BEWindow中包含消息发送器:
use std::sync::{Arc, Mutex, mpsc}; use winapi::{ shared::minwindef::{HWND, UINT, WPARAM, LPARAM, LRESULT}, um::winuser::{WM_CREATE, GWLP_USERDATA, SetWindowLongPtrW, DefWindowProcW, CREATESTRUCTA}, }; // 主线程需处理的窗口消息枚举 #[derive(Debug, Clone)] pub enum WindowMessage { Close, Resize(u32, u32), // 根据需求添加其他消息类型 } pub struct BEWindow { hwnd: HWND, // 消息发送器:将回调中的消息转发到主线程 tx: mpsc::Sender<WindowMessage>, }
2. 实现窗口回调函数
分初始化回调和实际处理回调两步,避免指针失效问题:
impl BEWindow { // 初始化回调:仅处理WM_CREATE,完成用户数据设置和回调替换 extern "system" fn init_proc(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT { unsafe { if msg == WM_CREATE { // 从CREATESTRUCT取出Arc<Mutex<BEWindow>>的指针 let create_struct = *(lparam as *const CREATESTRUCTA); let window_ptr = create_struct.lpCreateParams as *mut Arc<Mutex<BEWindow>>; let window = window_ptr.as_ref().unwrap(); // 将Arc指针存入GWLP_USERDATA(Arc堆分配,地址永久稳定) SetWindowLongPtrW(hwnd, GWLP_USERDATA, *window_ptr as isize); // 替换窗口回调为实际处理函数 SetWindowLongPtrW(hwnd, winapi::um::winuser::GWLP_WNDPROC, Self::main_proc as isize); // 处理WM_CREATE消息 return window.lock().unwrap().handle_message(hwnd, msg, wparam, lparam); } // 其他消息交给默认处理 DefWindowProcW(hwnd, msg, wparam, lparam) } } // 实际消息处理回调 extern "system" fn main_proc(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT { unsafe { // 从GWLP_USERDATA取出Arc<Mutex<BEWindow>>的指针 let window_ptr = SetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut Arc<Mutex<BEWindow>>; if let Some(window) = window_ptr.as_ref() { return window.lock().unwrap().handle_message(hwnd, msg, wparam, lparam); } DefWindowProcW(hwnd, msg, wparam, lparam) } } // 消息处理逻辑:将需主线程处理的消息发送到通道 fn handle_message(&mut self, hwnd: HWND, msg: UINT, _wparam: WPARAM, lparam: LPARAM) -> LRESULT { match msg { winapi::um::winuser::WM_CLOSE => { self.tx.send(WindowMessage::Close).ok(); 0 } winapi::um::winuser::WM_SIZE => { let width = winapi::um::winuser::LOWORD(lparam as u32); let height = winapi::um::winuser::HIWORD(lparam as u32); self.tx.send(WindowMessage::Resize(width, height)).ok(); 0 } // 其他消息交给默认处理 _ => unsafe { DefWindowProcW(hwnd, msg, _wparam, lparam) }, } } }
3. 窗口创建与主线程游戏循环
创建窗口时生成消息通道,将发送器存入BEWindow,并把Arc<Mutex<BEWindow>>指针作为创建参数传入:
impl BEWindow { pub fn new() -> (Arc<Mutex<Self>>, mpsc::Receiver<WindowMessage>) { let (tx, rx) = mpsc::channel(); // 构造窗口实例 let mut window = Self { hwnd: std::ptr::null_mut(), tx, }; let window_arc = Arc::new(Mutex::new(window)); unsafe { // 注册窗口类(按原教程逻辑实现,此处省略) // ... // 创建窗口时,将Arc指针作为lpCreateParams传入 let hwnd = winapi::um::winuser::CreateWindowExW( 0, /* 窗口类名 */, /* 窗口标题 */, winapi::um::winuser::WS_OVERLAPPEDWINDOW, winapi::um::winuser::CW_USEDEFAULT, winapi::um::winuser::CW_USEDEFAULT, 800, 600, std::ptr::null_mut(), std::ptr::null_mut(), /* 实例句柄 */, &*window_arc as *const _ as *mut _, // 传入Arc指针 ); window_arc.lock().unwrap().hwnd = hwnd; } (window_arc, rx) } } // 主线程游戏循环示例 fn main() { let (_window, msg_rx) = BEWindow::new(); loop { // 处理窗口消息 while let Ok(msg) = msg_rx.try_recv() { match msg { WindowMessage::Close => break, WindowMessage::Resize(w, h) => println!("窗口大小调整为:{}x{}", w, h), // 处理其他消息 } } // 游戏逻辑更新与渲染 // ... } }
关键要点
- Arc的作用:
Arc是堆分配的引用计数类型,对象内存地址不会随Rust移动语义改变,存入GWLP_USERDATA的指针始终有效。 - 消息通道:用
mpsc::channel将窗口线程的消息转发到主线程,避免在回调中执行复杂逻辑(Windows消息线程不能阻塞)。 - 线程安全:
Mutex保证多线程访问BEWindow的安全性,窗口回调线程和主线程都会操作窗口对象。
内容的提问来源于stack exchange,提问作者Berni
相关产品推荐
相关产品推荐

