You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中Windows窗口回调获取静态引用的实现问题求助

解决方案:Rust下Windows窗口回调与消息队列实现

问题分析

  1. 初始方案致命问题:BEWindow结构体在Rust中发生移动时内存地址会改变,但存入GWLP_USERDATA的是原始指针,后续回调访问时直接变成野指针,触发段错误。
  2. 静态变量方案失效原因: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 18:40:42