winapirustwindows-rs

Comparing a WPARAM and VIRTUAL_KEY in Rust


I'm trying to use the WPARAM parameter in wndproc to get keyboard inputs using the "windows" crate. I found a github repo that tries to achieve the same thing (link to the specific code here) and they've managed to cast a WPARAM to an i32. When I try to do the exact same thing, I get the error Non-primitive cast: `WPARAM` as `i32` [E0605]. Since the repo I found is pretty old I imagine there's been changes made to the crate and I'm not really sure what I should be doing differently. Here's a snippet of my code if it helps:

extern "system" fn wnd_proc(window: HWND, message: u32, w_param: WPARAM, l_param: LPARAM)
    -> LRESULT {
    unsafe {
        match message {
            ...
            WM_SYSKEYDOWN => {
                let vk_code: i32 = w_param as i32;
            }
            ...
        }
    }
}

Solution

  • TL;DR: Replace w_param as i32 with w_param.0 as i32.


    The windows crate makes liberal use of the new type idiom. This is a powerful tool leveraging the type system to guard against misinterpreting data of identical binary types. It does this by inventing wrapper types whose sole purpose is to capture and persist semantic information.

    This is incredibly useful in general. It opens the opportunity to turn what would have been a run-time error into a compile-time error. For example, you cannot pass the HFONT returned from CreateFontW into CloseHandle. While HFONTs and HANDLEs essentially are just isize values at the binary interface, the new type wrappers prevent Rust code from confusing one for the other.

    This is how the WPARAM wrapper is defined in the windows crate:

    #[repr(transparent)]
    pub struct WPARAM(pub usize);
    

    This is a tuple struct with a single field that can be accessed by index using the .0 syntax. The effect of the #[repr(transparent)] attribute is that the WPARAM struct and its contained field have the same binary layout. This is crucial so that WPARAM can appear in an FFI in place of a usize. After all, the wnd_proc above is called by the system (written in C and C++) that doesn't (need to) know what a tuple struct is.

    All due praises aside, the new type idiom doesn't provide much value here. The WPARAM (usize) and LPARAM (isize) values passed to the window procedure are essentially type-erased. Their meaning has to be recovered on a message-by-message basis, with frequent wrapping and unwrapping in between. To illustrate this, here is how a WM_SYSKEYDOWN handler can be implemented:

    WM_SYSKEYDOWN => {
        // Extract raw `usize` value
        let vk_code = w_param.0;
        // Wrap it up in a more appropriate wrapper
        let vk = VIRTUAL_KEY(vk_code as u16);
        // Compare it against `VK_` constants
        if vk == VK_ESCAPE {
            // ...
        }
    }
    

    This may seem overly verbose, but that is in part due to re-encoding the (u16) value inside a VIRTUAL_KEY wrapper. That can be trimmed down to a one-liner, e.g., let vk = VIRTUAL_KEY(w_param.0 as _);, leaving us with a sanity-checked new type that cannot accidentally get confused for an arbitrary u16 value.

    With this, we've come full circle: While the new type idiom doesn't provide much value to the WPARAM wrapper, it sure does once the usize payload has been repacked into a VIRTUAL_KEY struct, unleashing the new type idiom's full potential again.


    A note on why the code you've found on GitHub works without the added verbosity: It uses the winapi crate that models ABI types as type aliases. A type alias merely introduces a new name for an existing type. WPARAM is thus an alternative spelling for the usize type.

    This is very similar to what the windows-sys crate does: Its WPARAM type alias is identical to that of the winapi crate.

    Neither of these low-level binding crates makes much use of Rust's type system beyond the ABI guarantees.