39. RegisterKeyNotify / UnrigisterKeyNotify 함수를 사용해 단축키 기능을 추가하는 드라이버 만들기

39장에서는 단축키 조합에 대한 callback을 등록하는 드라이버를 만든다

UEFI에서는 EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOLRegisterKeyNotify 함수를 사용할 수 있다.

EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL.RegisterKeyNotify()

Summary:
Register a notification function for a particular keystroke for the input device.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_REGISTER_KEYSTROKE_NOTIFY) (
 IN EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
 IN EFI_KEY_DATA *KeyData,
 IN EFI_KEY_NOTIFY_FUNCTION KeyNotificationFunction,
 OUT VOID **NotifyHandle
 );

Parameters:
This 				A pointer to the EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL instance.
KeyData 			A pointer to a buffer that is filled in with the keystroke information
				for the key that was pressed. If KeyData.Key, KeyData.KeyState.KeyToggleState and
				KeyData.KeyState.KeyShiftState are 0, then any incomplete keystroke will trigger a
				notification of the KeyNotificationFunction.
KeyNotificationFunction		Points to the function to be called when the key sequence is typed
				specified by KeyData.
NotifyHandle 			Points to the unique handle assigned to the registered notification.

Description:
The RegisterKeystrokeNotify() function registers a function which will be called when a specified
keystroke will occur. The keystroke being specified can be for any combination of KeyData.Key or
KeyData.KeyState information.

callback 함수의 프로토타입은 다음과 같다.

typedef
EFI_STATUS
(EFIAPI *EFI_KEY_NOTIFY_FUNCTION) (
 IN EFI_KEY_DATA *KeyData
 );

EFI_KEY_DATA의 경우, 두 개의 필드가 있는 구조이다.

typedef struct {
 EFI_INPUT_KEY Key;            // 입력 장치에서 반환된 EFI scan code 및 UNICODE 값이다.
 EFI_KEY_STATE KeyState;       // input modifier 값 뿐만 아니라, 다양한 토글 속성의 현 상태이다.
} EFI_KEY_DATA

EFI_INPUT_KEY 유형은 SimpleTextIn.h 파일에 정의되어 있다.

$ cat MdePkg/Include/Protocol/SimpleTextIn.h
---
typedef struct {
  UINT16  ScanCode;
  CHAR16  UnicodeChar;
} EFI_INPUT_KEY;

InteractiveApp 애플리케이션에서 한 번 사용했던 적이 있다.

EFI_KEY_STATE 유형이 새로 추가되었으며, SimpleTestInEx.h 파일에서 찾을 수 있다.

///
/// EFI_KEY_TOGGLE_STATE. The toggle states are defined.
/// They are: EFI_TOGGLE_STATE_VALID, EFI_SCROLL_LOCK_ACTIVE
/// EFI_NUM_LOCK_ACTIVE, EFI_CAPS_LOCK_ACTIVE
///
typedef UINT8 EFI_KEY_TOGGLE_STATE;

typedef struct _EFI_KEY_STATE {
  ///
  /// Reflects the currently pressed shift
  /// modifiers for the input device. The
  /// returned value is valid only if the high
  /// order bit has been set.
  ///
  UINT32                KeyShiftState;
  ///
  /// Reflects the current internal state of
  /// various toggled attributes. The returned
  /// value is valid only if the high order
  /// bit has been set.
  ///
  EFI_KEY_TOGGLE_STATE  KeyToggleState;
} EFI_KEY_STATE;

KeyShiftState / KeyToggleState 필드에 코딩되어 있는 정보를 이해하려면, 동일한 파일에서 이 정의를 살펴보자.

//
// Any Shift or Toggle State that is valid should have
// high order bit set.
//
// Shift state
//
#define EFI_SHIFT_STATE_VALID     0x80000000
#define EFI_RIGHT_SHIFT_PRESSED   0x00000001
#define EFI_LEFT_SHIFT_PRESSED    0x00000002
#define EFI_RIGHT_CONTROL_PRESSED 0x00000004
#define EFI_LEFT_CONTROL_PRESSED  0x00000008
#define EFI_RIGHT_ALT_PRESSED     0x00000010
#define EFI_LEFT_ALT_PRESSED      0x00000020
#define EFI_RIGHT_LOGO_PRESSED    0x00000040
#define EFI_LEFT_LOGO_PRESSED     0x00000080
#define EFI_MENU_KEY_PRESSED      0x00000100
#define EFI_SYS_REQ_PRESSED       0x00000200

//
// Toggle state
//
#define EFI_TOGGLE_STATE_VALID    0x80
#define EFI_KEY_STATE_EXPOSED     0x40
#define EFI_SCROLL_LOCK_ACTIVE    0x01
#define EFI_NUM_LOCK_ACTIVE       0x02
#define EFI_CAPS_LOCK_ACTIVE      0x04

또한 이런 EDKII 파일(SimpleTextInEx.h)에는 스캔 코드(위, 아래, 오른쪽, 왼쪽, home, end, delete, f1-f24 등)에 대한 정의가 포함되어 있다. 따라서 단축키에 대한 스캔 코드를 사용하려는 경우, 해당 파일을 살펴보자.

마지막으로 드라이버에 대한 코드를 작성한다.

이 드라이버의 이름은 HotKeyDriver라고 짓는다. entry point 함수에 바로가기 키 조합을 등록하고 언로드 기능에서 등록 취소한다.

$ vi UefiLessonsPkg/HotKeyDriver/HotKeyDriver.inf
---
[Defines]
  INF_VERSION                    = 1.25
  BASE_NAME                      = HotKeyDriver
  FILE_GUID                      = da316635-c66f-477e-9df6-880d2d729f1b
  MODULE_TYPE                    = UEFI_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = HotKeyDriverEntryPoint
  UNLOAD_IMAGE                   = HotKeyDriverUnload
  
[Sources]
  HotKeyDriver.c

[Packages]
  MdePkg/MdePkg.dec

[Protocols]
  gEfiSimpleTextInputExProtocolGuid            # <----- guid for EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL

[LibraryClasses]
  UefiDriverEntryPoint
  UefiLib

먼저 드라이버 entrypoint 함수에서 guid gEfiSimpleTextInputExProtocolGuid(HotKeyDriver.c)로 EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL을 찾아야 한다.

$ vi UefiLessonsPkg/HotKeyDriver/HotKeyDriver.c
---
#include <Protocol/SimpleTextInEx.h>

EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL* InputEx = NULL;

EFI_STATUS
EFIAPI
HotKeyDriverEntryPoint(
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status = gBS->LocateProtocol(
			 &gEfiSimpleTextInputExProtocolGuid,
			 NULL,
			 (VOID**)&InputEx
			 );
  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't locate EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL: %r", Status);
    return Status;
  }

  ...

  return EFI_SUCCESS;
}

언로드 기능에도 EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL이 필요하므로, 변수를 전역으로 선언해준다.

이제 RegisterKeyNotify API를 사용한다. LCtrl + LAlt + Z로 실행되는 callback을 만들어 보자.

  EFI_KEY_DATA HotKey;
  HotKey.Key.ScanCode = 0;
  HotKey.Key.UnicodeChar = L'z';
  HotKey.KeyState.KeyShiftState = EFI_LEFT_CONTROL_PRESSED | EFI_LEFT_ALT_PRESSED | EFI_SHIFT_STATE_VALID;
  HotKey.KeyState.KeyToggleState = 0;

  Status = InputEx->RegisterKeyNotify(InputEx,
			&HotKey,
			MyKeyNotificationFunction,
                        (VOID**)&NotifyHandle);

  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't perform RegisterKeyNotify: %r", Status);
    return Status;
  }

파일 시작 부분에서 NotifyHandle에 대한 전역 변수(드라이버 언로드함수에서 사용되므로)를추가하고, MyKeyNotificationFunction callback 함수를 정의한다.

EFI_HANDLE NotifyHandle;

EFI_STATUS EFIAPI MyKeyNotificationFunction(EFI_KEY_DATA* KeyData)
{
	Print(L"\nHot Key 1 is pressed\n");         // we add '\n' in the begining, so the output wouldn't interfere with the UEFI shell prompt
	return EFI_SUCCESS;
}

지금부턴 numlock이 활성화된 경우 z를 누를 때마다 실행이 되는 또 다른 callback을 작성해 보겠다.

  HotKey.KeyState.KeyShiftState = 0;
  HotKey.KeyState.KeyToggleState = EFI_TOGGLE_STATE_VALID | EFI_NUM_LOCK_ACTIVE | EFI_KEY_STATE_EXPOSED;
  Status = InputEx->RegisterKeyNotify(InputEx,
			&HotKey,
			MyKeyNotificationFunction1,
                        (VOID**)&NotifyHandle1);

  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't perform RegisterKeyNotify: %r", Status);
    return Status;
  }
EFI_HANDLE NotifyHandle1;

EFI_STATUS EFIAPI MyKeyNotificationFunction1(EFI_KEY_DATA* KeyData)
{
	Print(L"\nHot Key 2 is pressed\n");     // we add '\n' in the begining, so the output wouldn't interfere with the UEFI shell prompt
	return EFI_SUCCESS;                       // or another our callback function
}

드라이버 언로드 시, callback 함수의 등록을 취소해야 한다고 위에서 언급했다. 이에 대한 이유가 궁금할 것이다.

MyKeyNotificationFunctionMyKeyNotificationFunction1와 같은 함수는 HotKeyDriver 메모리에 정의되어 있기 때문이다. 메모리에서 HotKeyDriver를 언로드하면 이 메모리는 해제/비활성화 된다.

그리고 바로가기 키 조합에서 push 시스템은 포인터에서 유효하지 않은 영역에 대한 callback 함수를 역참조하고자 시도한다. 이 점이 예외로 이어진다.

등록 취소 실행의 경우, EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOLUnreisterKeyNotify 함수를 정의한다.

EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL.UnregisterKeyNotify()

Summary:
Remove the notification that was previously registered.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_UNREGISTER_KEYSTROKE_NOTIFY) (
 IN EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
 IN VOID *NotificationHandle
 );

Parameters:
This 			A pointer to the EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL instance.
NotificationHandle	The handle of the notification function being unregistered.

Description:
The UnregisterKeystrokeNotify() function removes the notification which was previously registered. 

callback 함수를 등록 취소하는 데 사용

EFI_STATUS
EFIAPI
HotKeyDriverUnload(
  IN EFI_HANDLE        ImageHandle
  )
{
  if (!InputEx)
    return EFI_SUCCESS;

  EFI_STATUS Status;

  if (!NotifyHandle) {
    Status = InputEx->UnregisterKeyNotify(InputEx,
                                   (VOID*)NotifyHandle);

    if (EFI_ERROR(Status)) {
      Print(L"Error! Can't perform RegisterKeyNotify: %r", Status);
      return Status;
    }
  }

  if (!NotifyHandle1) {
    Status = InputEx->UnregisterKeyNotify(InputEx,
                       (VOID*)NotifyHandle1);

    if (EFI_ERROR(Status)) {
      Print(L"Error! Can't perform RegisterKeyNotify: %r", Status);
      return Status;
    }
  }

  return EFI_SUCCESS;
}

이제 다 되었으니 드라이버를 빌드하고 테스트하자.

일반적이지 않은 키를 escape sequence로 터미널 변환하는 것을 방지하려면, -nograpic 옵션을 빼고 QEMU를 실행해야 한다.

기본 그래픽으로 부팅하거나, vnc 서버를 통해 그래픽으로 부팅할 수 있다.

# 기본 그래픽으로 부팅

qemu-system-x86_64 \
  -drive if=pflash,format=raw,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd \
  -drive format=raw,file=fat:rw:~/UEFI_disk
# vnc로 부팅

qemu-system-x86_64 \
  -drive if=pflash,format=raw,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd \
  -drive format=raw,file=fat:rw:~/UEFI_disk
  -vnc :1

QEMU를 부팅하면, 드라이버를 OVMF로 로드한다.

FS0:\> load HotKeyDriver.efi
Image 'FS0:\HotKeyDriver.efi' loaded at 6646000 - Success

numlock이 비활성화된 경우, 하나의 callback만이 가능하다.

  • LCtrl + LAlt + Z

FS0:\>
Hot Key 1 is pressed

numlock이 활성화된 경우,

  • Z

FS0:\>
Hot Key 2 is pressed
  • LCtrl + LAlt + Z

FS0:\>
Hot Key 1 is pressed
Hot Key 2 is pressed

다른 키 입력 조합 사이에 UEFI shell에서 cls 명령으로 화면을 clear하여 새로운 출력 확인에 방해가 안되도록 하자.

또한, QEMU가 -nographic 모드에서 실행되는 경우, 모든 수정 사항이 파싱되지 않는다는 점에 유의하자. 따라서, Z를 누를 때마다 항상 callback이 모두 발생한다.

FS0:\> z
Hot Key 1 is pressed
Hot Key 2 is pressed

Last updated