Ain't It Funny?

"Ain't It Funny" is a 2009 Jennifer Lopez hit. But here I'm thinking about rustc.

EDIT: The originally shown program wasn't correct and I've replaced it. I apologize for the mistake.

A Windows program handles GUI in the main thread, MT, and collects data from the web in a worker thread, WT. When the data is ready, WT posts a message to the window procedure in MT to have it displayed. To post the message, WT uses the window handle, hwnd: HWND, from MT. hwnd is generated once at the program's start and stays unchanged for the program's lifetime, so passing it between threads is perfectly safe. The program below simulates this situation.

//[dependencies]
//winapi = { version = "0.3.9", features = ["wingdi", "winuser", "libloaderapi", "combaseapi", "objbase", "shobjidl", "winerror"] }
//lazy_static = "1.4.0"

use std::error::Error;
use std::ptr::{null, null_mut};
use std::sync::Mutex;
use std::thread;
use std::time;
use winapi::shared::minwindef::*;
use winapi::shared::windef::*;
use winapi::um::libloaderapi::GetModuleHandleW;
use winapi::um::winuser::*;

pub const WM_WEBUPDT: UINT = 0xFEDC;

/// Turns a Rust string slice into a null-terminated utf-16 vector.
pub fn wide_null(s: &str) -> Vec<u16> {
    s.encode_utf16().chain(Some(0)).collect()
}

static TEXT: Mutex<String> = Mutex::new(String::new());

// Window procedure to handle events
pub unsafe extern "system" fn window_proc(
    hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
) -> LRESULT {
    match msg {
        WM_CLOSE => {
            DestroyWindow(hwnd);
        }
        WM_DESTROY => {
            PostQuitMessage(0);
        }
        WM_WEBUPDT => {
            RedrawWindow(hwnd, null(), null_mut(), RDW_INVALIDATE | RDW_ERASE);
        }
        WM_PAINT => {
            let t = TEXT.lock().unwrap().clone();
            let mut ps: PAINTSTRUCT = std::mem::zeroed();
            let hdc: HDC;
            hdc = BeginPaint(hwnd, &mut ps);
            let mut rec: RECT = std::mem::zeroed();
            GetClientRect(hwnd, &mut rec);
            rec.top += 4;
            rec.left += 6;
            rec.bottom -= 4;
            rec.right -= 6;
            let txt = wide_null(&t);
            DrawTextW(
                hdc,
                txt.as_ptr(),
                txt.len().try_into().unwrap(),
                &mut rec,
                DT_TOP | DT_LEFT,
            );
            EndPaint(hwnd, &ps);
        }
        _ => return DefWindowProcW(hwnd, msg, wparam, lparam),
    }
    return 0;
}

// Declare class and instantiate window
fn create_main_window(name: &str, title: &str) -> Result<HWND, Box<dyn Error>> {
    let name = wide_null(name);
    let title = wide_null(title);

    unsafe {
        let hinstance = GetModuleHandleW(null_mut());
        let wnd_class = WNDCLASSEXW {
            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
            style: CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: Some(window_proc),
            cbClsExtra: 0,
            cbWndExtra: 0,
            hInstance: hinstance,
            hIcon: LoadIconW(null_mut(), IDI_APPLICATION),
            hCursor: LoadCursorW(null_mut(), IDC_ARROW),
            hbrBackground: COLOR_WINDOWFRAME as HBRUSH,
            lpszMenuName: null_mut(),
            lpszClassName: name.as_ptr(),
            hIconSm: LoadIconW(null_mut(), IDI_APPLICATION),
        };
        // Register window class
        if RegisterClassExW(&wnd_class) == 0 {
            MessageBoxW(
                null_mut(),
                wide_null("Window Registration Failed!").as_ptr(),
                wide_null("Error").as_ptr(),
                MB_ICONEXCLAMATION | MB_OK,
            );
            return Err("Window Registration Failed".into());
        };
        // Create a window based on registered class
        let handle = CreateWindowExW(
            0,                                // dwExStyle
            name.as_ptr(),                    // lpClassName
            title.as_ptr(),                   // lpWindowName
            WS_OVERLAPPEDWINDOW | WS_VISIBLE, // dwStyle
            710,                              // Int x
            580,                              // Int y
            700,                              // Int nWidth
            400,                              // Int nHeight
            null_mut(),                       // hWndParent
            null_mut(),                       // hMenu
            hinstance,                        // hInstance
            null_mut(),                       // lpParam
        );

        if handle.is_null() {
            MessageBoxW( null_mut(),
                wide_null("Window Creation Failed!").as_ptr(),
                wide_null("Error!").as_ptr(), MB_ICONEXCLAMATION | MB_OK,
            );
            return Err("Window Creation Failed!".into());
        }
        Ok(handle)
    }
}

// Message handling loop
fn run_message_loop(hwnd: HWND) -> WPARAM {
    unsafe {
        let mut msg: MSG = std::mem::zeroed();
        loop {
            // Get message from message queue
            if GetMessageW(&mut msg, hwnd, 0, 0) > 0 {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            } else {
                // Return on error (<0) or exit (=0) cases
                return msg.wParam;
            }
        }
    }
}

fn main() {
    let mut int_text = 1234567;
    *TEXT.lock().unwrap() = int_text.to_string() + "\n";

    let hwnd = create_main_window("Main Window", "Main Window").expect("Window creation failed!");
    unsafe {
        ShowWindow(hwnd, SW_SHOW);
        UpdateWindow(hwnd);
    }

    let hwnd2 = hwnd;
    thread::spawn(move || loop {
        thread::sleep(time::Duration::from_millis(2000));
        int_text += 1234567;
        *TEXT.lock().unwrap() += &(int_text.to_string() + "\n");
        unsafe {
            PostMessageW (hwnd2, WM_WEBUPDT, 0 as WPARAM, 0 as LPARAM);
        }
    });
    run_message_loop(hwnd);
}

This program would not compile. Rustc would not allow passing hwnd2: HWND between threads.

But you can easily fool rustc by casting hwnd2 to usize in MT and casting back usize to HWND in WT.

fn main() {
    let mut int_text = 1234567;
    *TEXT.lock().unwrap() = int_text.to_string() + "\n";

    let hwnd = create_main_window("Main Window", "Main Window").expect("Window creation failed!");
    unsafe {
        ShowWindow(hwnd, SW_SHOW);
        UpdateWindow(hwnd);
    }

    let hwnd2 = hwnd as usize;
    thread::spawn(move || loop {
        thread::sleep(time::Duration::from_millis(2000));
        int_text += 1234567;
        *TEXT.lock().unwrap() += &(int_text.to_string() + "\n");
        unsafe {
            PostMessageW (hwnd2 as HWND, WM_WEBUPDT, 0 as WPARAM, 0 as LPARAM);
        }
    });
    run_message_loop(hwnd);
}

Should it work this way? What would be the proper way to handle this situation?

1 Like

Definitely. You are using unsafe for circumventing the type system – poor compiler doesn't stand a chance of detecting that you are in fact sending a non-Send/Sync type across threads.

If your question is "should HWND be not-Send/Sync", then the answer is "probably". Typically, GUI frameworks require to be used exclusively from the main thread, and anything else is an error.

If you want to communicate between threads safely and correctly, use channels.

1 Like

You haven’t “fooled” rustc, you are using unsafe code. Unsafe code means that all you can do is fool yourself. Or, and that’s the case here where passing HWND between threads should probably be fine, as you noted, you’ve simply used an overly “tricky” way to achieve the same thing you could’ve just told the compiler directly as an unsafe code user:

A pointer not being Send in Rust is mostly a precaution that unsafe Rust authors don’t accidentally make their non-threadsafe data structures implement Send. Since you’re using unsafe anyways, you can just as well create a struct containing the pointer, mark that struct Send unsafely, and thus arrive at a better solution than the one involving the usize case, since ptr->int->ptr casts can (at least in principle) have negative effects on what optimizations are possible.

Without using unsafe, a raw pointer is useless anyways; you can’t really do anything (useful) to it, in particular nothing that might dereference it.

Talking about safe Rust… I’m not familiar with win-API so I can’t judge the crate, but I did come across https://crates.io/crates/winsafe just googling for “HWND” in Rust, and their HWND in winsafe - Rust type is a safe wrapper around that pointer that does implement Send. (I.e. essentially following the “create a struct containing the pointer, mark that struct Send” approach I mentioned above.)

15 Likes

There is no way to use channels. When you have started the message loop, the application will stay in it forever. You cannot receive signals in MT other than signals to the windows procedure, but then you have to use hwnd in WT.
The whole winapi is unsafe, you cannot help it. But it works fine. W11 is very stable.
By the way. I have this program in C# developed in Visual Studio. Microsoft really knows how to handle it. It does work this way. You are posting signals to the windows procedure, including a pointer to a function that should be executed when the signal is received.

It should actually be ok to pass a HWND between threads, it's not a pointer (It's the equivalent of a file descriptor on *nix)

I think a better way to communicate would be to use PostMessage with a custom message, and then when handling that message you could read from a channel/mutex/etc.

That's interesting – and incidentally, I don't think this is right. If you start two independent threads, the second one should not block the main thread (and thus the UI). You should be able to send messages from it to a receiver inside the main thread.

Yes, you are able to send messages to a receiver inside the main thread. The receiver is the window procedure. You can use either PostMessage, which is kind of asynchronous, or SendMessage, which waits for the action to complete. In both cases, you are using hwnd: HWND to address the message, so you must have hwnd it in the second thread.

Microsoft explains this as follows: "The system passes all input for an application to the various windows in the application. Each window has a function, called a window procedure, that the system calls whenever it has input for the window."

This operation is perfectly visible when you develop a C# program for Windows.

Oh, I copied the wrong program. I do use PostMessage with a custom message. But PostMessage must have hwnd: HWND. Using RedrawWindow() is wrong, this function may not be thread-safe. I think, I¨ll replace this program with the correct one when I have time.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.