Windows Hooks는 이상하다
Source: Dev.to
위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역이 필요한 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.
훅 프로그램
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && wParam == WM_KEYDOWN)
{
KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT*)lParam;
unsigned char keyboard_state[256];
wchar_t unicodeData[5] = {0};
GetKeyboardState(keyboard_state);
int unicodeKey = ToUnicodeEx(kb->vkCode,
kb->scanCode,
keyboard_state,
unicodeData,
sizeof(unicodeData) / sizeof(wchar_t),
0,
NULL);
if (unicodeKey > 0)
{
printf("%ls", unicodeData);
}
if (kb->vkCode == VK_RETURN)
{
printf("\n");
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
int main()
{
hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
/* message loop intentionally empty */
}
}
그다지 못하진 않네요, 제 생각엔.
The Problem
조금 재미를 보면서 Caps Lock이나 Shift가 인쇄된 키 로그에 등록되지 않는 것을 발견했습니다. 당황한 저는 자존심 있는 프로그래머라면 당연히 Google을 했습니다. 그러다 이 Stack Overflow 질문을 찾게 되었죠:
댓글에 다음과 같은 해결책이 제시되었습니다:
short caps_held = GetKeyState(VK_CAPITAL) & 0x0001;
short shift_held = GetKeyState(VK_SHIFT) & 0x8000;
if (caps_held) keyboard_state[VK_CAPITAL] |= 0x01;
else keyboard_state[VK_CAPITAL] &= ~0x01;
if (shift_held) keyboard_state[VK_SHIFT] |= 0x80;
else keyboard_state[VK_SHIFT] &= ~0x80;
Note:
GetAsyncKeyState를 사용해야 한다는 것을 알고 있었지만, 반항적인 저는GetKeyState를 사용했습니다. 왜냐하면 작동했으니, 왜 안 되겠어요? 나중에 이 점을 기억하세요.
Caps와 Shift 키는 고정됐지만 새로운 문제가 나타났습니다: Ctrl.
Ctrl + any key를 누르면 이상한 문자들이 나타났습니다. 특히 Ctrl + C를 누르면 ♥가 나왔습니다. 이전에는 이런 현상이 없었기에 조사해 보았습니다.
keyboard_state에서 Ctrl + C는 unicodeData[0]에 3이라는 값을 주었는데, 이는 ♥로 렌더링되는 제어 문자입니다. 높은 비트를 지워 보았습니다:
keyboard_state[VK_CONTROL] &= ~0x80;
문제는 해결된 듯 보였지만… 아직도?
GetKeyboardState가 왜 작동할까?
GetKeyboardState()는 메시지 루프를 펌프하지 않는 애플리케이션에서는 작동하지 않는데, 여기서는 무슨 일이 일어나고 있을까요? 답은 **GetKeyState**입니다.
어떤 이유에 의해, 훅 콜백 내부에서 GetKeyState를 호출하면 키의 이전 상태, 즉 함수가 호출되기 전의 상태를 반환합니다. 이렇게 함으로써 GetKeyboardState가 실제로 업데이트되도록 하는 무언가를 수행합니다.
단순히 GetKeyState(0)을 호출하면 위에서 보여준 caps‑and‑shift 조건문이 필요 없게 된다는 것을 발견했습니다. GetAsyncKeyState(0)은 이러한 부작용이 없습니다. 이것이 의도된 동작일까요? 궁금했습니다.
Raymond Chen on GetAsyncKeyState / GetKeyState
읽는 중에 2004년 레전드인 Raymond Chen이 쓴 글을 발견했습니다:
발췌
[발췌는 간결함을 위해 생략]
하지만 이 설명에 몇 가지 문제가 있습니다.
-
그가 함수가 “입력 큐(input queue)에서 가져온 메시지를 기반으로 키를 반환한다”고 말할 때, 정확히 어떤 입력 큐를 의미합니까?
-
MSDN 문서에는 다음과 같이 적혀 있습니다:
“이 함수가 반환하는 키 상태는 스레드가 메시지 큐(message queue)에서 키 메시지를 읽을 때마다 변경됩니다.”
따라서 입력 큐와 메시지 큐가 동일하다고 추정하게 되지만, GetKeyboardState는 메시지‑큐 상태를 기반으로 값을 반환하고, 내 저수준 훅에서는 이 메시지 큐가 펌프되지 않습니다. 어떻게 가능한 걸까요? 큐가 동일하다면 GetKeyboardState가 처음부터 작동하지 않은 이유가 무엇인가요? 이 큐들은 서로 다른가, 같은가? 문서가 명확하지 않습니다.
내 추측: GetKeyboardState와 GetKeyState는 다른 큐 또는 상태를 읽으며, GetKeyState가 GetKeyboardState가 읽는 상태와 무언가를 동기화하는 방식으로 동작한다는 것입니다.
Simple test program
I decided to test GetKeyState myself:
int main()
{
while (true)
{
short ctrlPressed = GetKeyState('A');
printf("%d\n", ctrlPressed & 0x8000);
}
}
When I press A, it prints 32768 even though this program doesn’t appear to interact with a message queue at all. If we take Chen’s and MSDN’s words literally, this implies that every Windows app has a message queue (which could be true), but where is GetKeyState reading from?
The MSDN entry for GetKeyState says:
“이 함수가 반환하는 키 상태는 스레드가 메시지 큐에서 키 메시지를 읽을 때마다 변경됩니다.”
My simple program never reads from its message queue, nor does my hook program (as far as I’m aware), so something’s not right.
GetKeyboardState 문서 (MSDN)
“응용 프로그램은 이 함수를 호출하여 모든 가상 키의 현재 상태를 가져올 수 있습니다. 상태는 스레드가 메시지 큐에서 키보드 메시지를 제거할 때 변경됩니다. 상태는 키보드 메시지가 스레드의 메시지 큐에 게시될 때는 변경되지 않으며, 다른 스레드의 메시지 큐에 게시되거나 해당 큐에서 가져올 때도 변경되지 않습니다.”
MSDN의 말을 그대로 받아들인다면, 논리적으로 이것은 GetKeyState가 키를 얻기 전에 키보드 메시지를 제거하고 큐를 처리한다는 의미이며, 이는 내 간단한 프로그램이 작동한 이유를 설명합니다. 이는 무게가 실린 가정이며, 저는 그것이 사실이라고 믿지 않습니다.
미해결 질문 요약
GetKeyState는 어떤 큐를 읽나요?- 스레드 자체의 메시지 큐인가, 시스템 전체 큐인가, 아니면 다른 것인가?
- 왜 로우‑레벨 훅 안에서
GetKeyState를 호출하면GetKeyboardState가 “프라임”되는 것처럼 보이나요?- 키보드 상태를 강제로 새로 고치게 하는 문서에 명시되지 않은 부작용이 있나요?
GetKeyState와GetKeyboardState가 서로 다른 내부 키보드 표현을 보고 있나요?- 그렇다면 두 상태는 어떻게 동기화되나요?
- Raymond Chen의 “입력 큐”는 무엇을 의미하며, 이것이 스레드별 메시지 큐와는 어떤 관계가 있나요?
위 질문들이 아직도 제가 맞추어 가고 있는 부분입니다. 명확한 설명(또는 더 깊은 문서에 대한 참고 자료)이 있으면 크게 도움이 될 것입니다.
이 모순을 설명할 수 있는 유일한 방법은 Chen과 MSDN의 글이 과도하게 단순화되었거나, 오래되었거나, 혹은 문서에 명시되지 않은 기능일 가능성이라는 것입니다. Chen의 글이 20년 이상 전에 작성된 점을 고려하면 세 가지 모두 해당될 수 있지만, 솔직히 확신할 수 없습니다.
제가 파고든 결과, GetKeyState(0)가 GetKeyboardState가 읽고 있는 데이터를 새로 고친다는 것은 확실히 확인했습니다. 왜 그런지는 아직 불명확합니다. 이 현상은 제가 사용 중인 Windows 10 및 11 버전에서는 동작했지만, Windows 7에서는 테스트하지 않았습니다. 앞으로 변경될 가능성도 전혀 없지 않으니 주의하시기 바랍니다.
물론, 훅 자체에서 키 상태를 직접 만들 수도 있습니다:
else if (nCode == HC_ACTION && wParam == WM_KEYUP) {
KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT*)lParam;
if (kb->vkCode == VK_LSHIFT || kb->vkCode == VK_RSHIFT) {
shiftHeld = true;
printf("%s\n", "Shift released");
}
}
이 방법도 유효하며 더 세밀하게 제어할 수 있습니다.
이 글이 훅을 사용하는 분들에게 도움이 되길 바랍니다. 제가 틀린 부분이 있거나, 왜 이런 현상이 발생하는지 아시는 분은 댓글을 남겨 주시거나 직접 연락 주세요. 언제든지 답변드릴 준비가 되어 있으며, 상황을 정확히 이해하고 싶습니다.
다음 번엔…