Rust で windows クレートを使ってキーボード入力をバックグラウンドから取得して表示する

# 動機

- 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](https://github.com/microsoft/windows-rs) で管理されている `windows` クレートが使えます。

`samples` ディレクトリがあるので、良さげな [windows-rs/crates/samples/create_window/](https://github.com/microsoft/windows-rs/tree/master/crates/samples/create_window) を手元で実行してみます。

`cargo new` してプロジェクトを作ります。

```powershell
cargo new keyboard-input
```

サンプルの [Cargo.toml](https://github.com/microsoft/windows-rs/blob/master/crates/samples/create_window/Cargo.toml) はクレートを相対パスで指定しているので、現時点の最新 version 0.32.0 を指定します。

```Cargo.toml
[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](https://github.com/microsoft/windows-rs/blob/master/crates/samples/create_window/src/main.rs) はそのままコピペします。

```rs: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` クレートを使用することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186959/13175d07-2b12-3ba5-61ff-279e7673efac.png)

ウィンドウをドラッグ・リサイズするとターミナルに `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](https://docs.microsoft.com/ja-jp/windows/win32/learnwin32/keyboard-input) が見つかり、`LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)` から始まるコールバック部分に適用できそうなサンプルコードがあるので移植します。

```rs:src/main.rs
// 略

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](https://stackoverflow.com/questions/65210101/c-win32-keyboard-input-to-a-non-foreground-window)

有りました。[貼ってあるGIF](https://i.stack.imgur.com/uSbVP.gif) を見る限り実装したい機能そのものです。

シンプルなコードなので、以下の方針で移植します。

- C++でNULLとなっていて、Rust上では構造体が要求される箇所は `T::default()` にする
- Rust上の構造体とプリミティブ型の比較をする際は構造体の中身を使う


```rs:src/main.rs
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)
    }
}
```

![keyboard-input.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186959/034c6327-3eb1-937f-7c08-5cb755a61962.gif)


## 移植時に悩んだ部分

### 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](https://doc.rust-lang.org/reference/items/external-blocks.html#abi )　に以下のように書いてあります。

> 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;` に対応させて、以下のようにするとコンパイルできません。

```rs
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](https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/ns-winuser-kbdllhookstruct) を読むと `vkCode` は 構造体の先頭 `DWORD` (符号なし32ビット整数） なので、以下のようにしました。

```rs
let vk_code_inner = &*(lparam.0 as *const u16) as &u16;
```

**（2022-02-18 追記）**
@benki さんに [コメント](https://qiita.com/horyu/items/12f6cd13ceb217782df3#comment-897380eb2435da6749de) で [std::mem::transmute](https://doc.rust-lang.org/stable/std/mem/fn.transmute.html) を使用すればキャストできると教えていただきました。

```rs
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` としているので、キーが離れたときの検知ができていません。

現在のキー入力状態を保存して表示するように修正します。

```rs:src/main.rs
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です。

![keyboard-input2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186959/25f9536b-6d01-6f4e-c909-4ddc746d385e.gif)

キーの入力開始と終了がバックグラウンドでも分かるようになりました。あとは **頑張って**  表示を作れば良いはずです。別の記事で書くかもしれません。

## あとがき

- Win32APIは（C++の）サンプルコードが多くあるので、Rustで使うにしても結構どうにかなるかもしれない
- 型があると型に従えば良いのでやりやすい
- ユーザーが編集可能なキー入力表示ソフトにしようとした場合、かなり面倒そう
    - Win32APIのSetWindowsHookExAを使って入力を取れるRustのGUIライブラリがあるなら、それで作ったほうが楽そう