動機
- Win32API を使ってみたい
- Rust を使って何かしたい
RustでWindows用キー入力表示ソフトを作ってみるか。
環境
エディション Windows 10 Pro
バージョン 21H1
インストール日 2021/02/02
OS ビルド 19043.1526
エクスペリエンス Windows Feature Experience Pack 120.2212.4170.0
nightly-x86_64-pc-windows-msvc (default)
rustc 1.60.0-nightly (e646f3d2a 2022-02-10)
1. windows
クレートを使ったウィンドウ有りサンプルを実行する
Win32APIをRustから使用する際、GitHugの microsoft/windows-rs で管理されている windows
クレートが使えます。
samples
ディレクトリがあるので、良さげな windows-rs/crates/samples/create_window/ を手元で実行してみます。
cargo new
してプロジェクトを作ります。
cargo new keyboard-input
サンプルの Cargo.toml はクレートを相対パスで指定しているので、現時点の最新 version 0.32.0 を指定します。
[package]
name = "keyboard-input"
version = "0.1.0"
edition = "2021"
[dependencies.windows]
version = "0.32.0"
features = [
"alloc",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
]
src/main.rs はそのままコピペします。
use windows::{core::*, Win32::Foundation::*, Win32::Graphics::Gdi::ValidateRect, Win32::System::LibraryLoader::GetModuleHandleA, Win32::UI::WindowsAndMessaging::*};
fn main() -> Result<()> {
unsafe {
let instance = GetModuleHandleA(None);
debug_assert!(instance.0 != 0);
let window_class = "window";
let wc = WNDCLASSA {
hCursor: LoadCursorW(None, IDC_ARROW),
hInstance: instance,
lpszClassName: PSTR(b"window\0".as_ptr()),
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wndproc),
..Default::default()
};
let atom = RegisterClassA(&wc);
debug_assert!(atom != 0);
CreateWindowExA(Default::default(), window_class, "This is a sample window", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, None, None, instance, std::ptr::null_mut());
let mut message = MSG::default();
while GetMessageA(&mut message, HWND(0), 0, 0).into() {
DispatchMessageA(&message);
}
Ok(())
}
}
extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match message as u32 {
WM_PAINT => {
println!("WM_PAINT");
ValidateRect(window, std::ptr::null());
LRESULT(0)
}
WM_DESTROY => {
println!("WM_DESTROY");
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcA(window, message, wparam, lparam),
}
}
}
cargo run
するとウィンドウが出現します。windows
クレートを使用することができました。
ウィンドウをドラッグ・リサイズするとターミナルに WM_PAINT
が出力され、ウィンドウを閉じると WM_DESTROY
が出力されます。
下の関数 extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT
がコールバック関数で、ウィンドウの描写と削除を監視しています。
2. フォアグラウンドでキーボード入力を取得する
C++ win32 get keyboard
で検索すると、Microsoft Docs の キーボード入力 (Win32 および C++ でのはじめに) - Win32 apps が見つかり、LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
から始まるコールバック部分に適用できそうなサンプルコードがあるので移植します。
// 略
extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match message as u32 {
WM_PAINT => {
println!("WM_PAINT");
ValidateRect(window, std::ptr::null());
return LRESULT(0);
}
WM_DESTROY => {
println!("WM_DESTROY");
PostQuitMessage(0);
return LRESULT(0);
}
WM_KEYDOWN => {
println!("WM_KEYDOWN: {}", wparam.0);
}
WM_KEYUP => {
println!("WM_KEYUP: {}", wparam.0);
}
_ => ()
}
DefWindowProcA(window, message, wparam, lparam)
}
}
cargo run
してからキーボードで abc
と入力すると、
PS P:\program\rust\keyboard-input> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target\debug\keyboard-input.exe`
WM_PAINT
WM_KEYDOWN: 65
WM_KEYUP: 65
WM_KEYDOWN: 66
WM_KEYUP: 66
WM_KEYDOWN: 67
WM_KEYUP: 67
WM_DESTROY
PS P:\program\rust\keyboard-input>
対応する文字コードの値が表示されます。
ここで問題があり、ウィンドウからフォーカスを外す(別のウィンドウにフォーカスする)と、ターミナル上に出力がされません。これではキー入力表示ソフトには成りません。
3. バックグラウンドからキーボード入力を取得する
C++ win32 background get keyboard
で検索します。
C++/Win32: Keyboard input to a non-foreground window - Stack Overflow
有りました。貼ってあるGIF を見る限り実装したい機能そのものです。
シンプルなコードなので、以下の方針で移植します。
- C++でNULLとなっていて、Rust上では構造体が要求される箇所は
T::default()
にする - Rust上の構造体とプリミティブ型の比較をする際は構造体の中身を使う
use windows::{core::*, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*};
fn main() -> Result<()> {
unsafe {
let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
let mut message = MSG::default();
while GetMessageA(&mut message, HWND::default(), 0, 0).into() {
DispatchMessageA(&message);
}
if !k_hook.is_invalid() {
UnhookWindowsHookEx(k_hook);
}
Ok(())
}
}
extern "system" fn k_callback1(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
if wparam.0 as u32 == WM_KEYDOWN && ncode as u32 == HC_ACTION {
let vk_code_inner = &*(lparam.0 as *const u16) as &u16;
dbg!(vk_code_inner);
}
CallNextHookEx(HHOOK::default(), ncode, wparam, lparam)
}
}
移植時に悩んだ部分
extern
k_callback1 の extern
部分をC++の __stdcall
に対応するよう extern "stdcall"
にすると
PS P:\program\rust\keyboard-input> cargo run
Compiling keyboard-input v0.1.0 (P:\program\rust\keyboard-input)
error[E0308]: mismatched types
--> src\main.rs:5:61
|
5 | let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
| ^^^^^^^^^^^ expected "system" fn, found "stdcall" fn
|
= note: expected fn pointer `unsafe extern "system" fn(_, windows::Win32::Foundation::WPARAM, windows::Win32::Foundation::LPARAM) -> windows::Win32::Foundation::LRESULT`
found fn item `extern "stdcall" fn(_, windows::Win32::Foundation::WPARAM, windows::Win32::Foundation::LPARAM) -> windows::Win32::Foundation::LRESULT {k_callback1}`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `keyboard-input` due to previous error
このように型が合わなくなります。
External blocks - The Rust Reference に以下のように書いてあります。
extern "system" -- Usually the same as extern "C", except on Win32, in which case it's "stdcall", or what you should use to link to the Windows API itself
// 略
extern "stdcall" -- The default for the Win32 API on x86_32.
x86_64
なら、expected されている extern "system"
として問題ないはずです。
構造体のキャスト
C++の PKBDLLHOOKSTRUCT key = (PKBDLLHOOKSTRUCT)lParam;
に対応させて、以下のようにするとコンパイルできません。
let hookStruct = &*(lparam as *const KBDLLHOOKSTRUCT) as &KBDLLHOOKSTRUCT;
PS P:\program\rust\keyboard-input> cargo run
Compiling keyboard-input v0.1.0 (P:\program\rust\keyboard-input)
error[E0605]: non-primitive cast: `windows::Win32::Foundation::LPARAM` as `*const windows::Win32::UI::WindowsAndMessaging::KBDLLHOOKSTRUCT`
--> src\main.rs:19:28
|
19 | let hookStruct = &*(lparam as *const KBDLLHOOKSTRUCT) as &KBDLLHOOKSTRUCT;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object
For more information about this error, try `rustc --explain E0605`.
error: could not compile `keyboard-input` due to previous error
as
はプリミティブ型間か特定のトレイトオブジェクトに変換する場合にしか使えないと叱られます。
KBDLLHOOKSTRUCT
にキャストしていますが、使用しているのは vkCode
だけです。KBDLLHOOKSTRUCT (winuser.h) - Win32 apps を読むと vkCode
は 構造体の先頭 DWORD
(符号なし32ビット整数) なので、以下のようにしました。
let vk_code_inner = &*(lparam.0 as *const u16) as &u16;
(2022-02-18 追記)
@benki さんに コメント で std::mem::transmute を使用すればキャストできると教えていただきました。
let kb_hook = std::mem::transmute::<isize, &KBDLLHOOKSTRUCT>(lparam.0);
dbg!(kb_hook.vkCode);
ドキュメントには信じられないほど unsafe とあるので、気をつけて使うと良さそうです。
transmute is incredibly unsafe. There are a vast number of ways to cause undefined behavior with this function. transmute should be the absolute last resort.
4. バックグラウンドからキーボード入力を取得して表示する
上記のコードでは wparam.0 as u32 == WM_KEYDOWN
としているので、キーが離れたときの検知ができていません。
現在のキー入力状態を保存して表示するように修正します。
use windows::{core::*, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*};
fn main() -> Result<()> {
unsafe {
let k_hook = SetWindowsHookExA(WH_KEYBOARD_LL, Some(k_callback1), HINSTANCE::default(), 0);
let mut message = MSG::default();
while GetMessageA(&mut message, HWND::default(), 0, 0).into() {
DispatchMessageA(&message);
}
if !k_hook.is_invalid() {
UnhookWindowsHookEx(k_hook);
}
Ok(())
}
}
static mut INPUTS_ARRAY: [bool; 256] = [false; 256];
unsafe fn set_and_show(vk_code: &u16, tf: bool) {
INPUTS_ARRAY[*vk_code as usize] = tf;
let mut s = String::with_capacity((b'Z' - b'A' + 1) as usize);
for i in (b'A' as usize)..=(b'Z' as usize) {
s.push(if INPUTS_ARRAY[i] { 'T' } else { 'F' });
}
println!("{s}");
}
extern "system" fn k_callback1(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
if ncode as u32 == HC_ACTION {
match wparam.0 as u32 {
WM_KEYDOWN => set_and_show(&*(lparam.0 as *const u16) as &u16, true),
WM_KEYUP => set_and_show(&*(lparam.0 as *const u16) as &u16, false),
_ => (),
}
}
CallNextHookEx(HHOOK::default(), ncode, wparam, lparam)
}
}
Aを押して離す→Aを押す→Bを押す→Cを押す→ABCを離す とした場合のGIFです。
キーの入力開始と終了がバックグラウンドでも分かるようになりました。あとは 頑張って 表示を作れば良いはずです。別の記事で書くかもしれません。
あとがき
- Win32APIは(C++の)サンプルコードが多くあるので、Rustで使うにしても結構どうにかなるかもしれない
- 型があると型に従えば良いのでやりやすい
- ユーザーが編集可能なキー入力表示ソフトにしようとした場合、かなり面倒そう
- Win32APIのSetWindowsHookExAを使って入力を取れるRustのGUIライブラリがあるなら、それで作ったほうが楽そう