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;
}
...
}
}
}
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 HFONT
s and HANDLE
s 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.