48. UNI 파일 및 HiiLib를 사용하여 HII String 패키지 게시 및 작업하기

NewPackageList 함수를 직접 사용하여 새 문자열 패키지를 추가하는 것은 꽤 어려운 작업이다. 우리는몇 개의 문자열만 추가했으며 패키지 목록에 필요한 데이터 배열을 동적으로 계산하지도 않았다는 점을 명심해야한다. 또한 글꼴/양식/이미지/등등을 추가하려면 이러한 패키지의 형식을 조사하고 필요한 함수도 작성해야한다.

EDKII가 이러한 작업을 단순화하기 위해 무엇을 제공할 수 있는지 확인해보자. 이번 장에서는 특히 문자열 패키지 생성을 단순화하는 방법에 대해 알아볼 것이다.

애플리케이션 만들기

평소처럼 스크립트를 사용해 새 애플리케이션을 만든다.

./createNewApp.sh HIIStringsUNI

DSC 패키지에 추가해준다.(UefiLessonsPkg/UefiLessonsPkg.dsc)

[Components]
  ...
  UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI.inf

지난 번과 같이 패키지 목록에 대한 GUID가 필요하며 패키지 DEC 파일 (UefiLessonsPkg/UefiLessonsPkg.dec)에서 선언한다.

[Guids]
  ...
  gHIIStringsUNIGuid = { 0x6ee19058, 0x0fe2, 0x44ed, { 0x89, 0x1c, 0xa5, 0xd7, 0xe1, 0x08, 0xee, 0x1a }}

그리고 애플리케이션 INF 파일(UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI.inf)에 이것들을 추가한다.

[Packages]
  ...
  UefiLessonsPkg/UefiLessonsPkg.dec
  
...

[Guids]
  gHIIStringsUNIGuid

UNI 파일

EDKII에서는 특별한 UNI 형식의 파일에서 모든 번역 문자열을 정의할 수 있다. EDKII 빌드 유틸리티는 이러한 파일의 데이터를 구문 분석하고 문자열 패키지 콘텐츠로 배열을 생성해 준다.

애플리케이션 INF 파일 UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI.infSources 섹션에 Strings.uni 파일을 추가해보자.

*.uni 파일에 아무 이름이나 사용할 수 있으며 Source 섹션에 *.uni를 원하는 만큼 포함할 수 있다는 것을 알아두자.

[Sources]
  ...
  Strings.uni

UefiLessonsPkg/HIIStringsUNI/Strings.uni파일을 만들어주고 내용을 써준다.

#langdef en-US "English"
#langdef fr-FR "Francais"

#string STR_HELLO         #language en-US  "Hello!"
                          #language fr-FR  "Bonjour!"

#string STR_BYE           #language en-US  "Bye!"
                          #language fr-FR  "Au revoir!"

이 파일은 2개의 문자열 패키지에 대한 소스가 된다.

1) 'en-US' string package
ID 1: "English"
ID 2: "Hello!"
ID 3: "Bye!"
2) 'fr-FR' string package
ID 1: "Francais"
ID 2: "Bonjour!"
ID 3: "Au revoir!"

UNI 파일 형식 스펙문서에서 UNI 파일 형식에 대한 자세한 내용을 읽을 수 있다. (https://edk2-docs.gitbook.io/edk-ii-uni-specification/)

지금 애플리케이션을 빌드하면 이 파일이 일반 빌드 파일인 Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI/DEBUG/HIIStringsUNIStrDefs.h와 함께 생성된다.

이 파일의 이름은 INF 파일의 BASE_NAME 값으로 구성되며 기본적으로 <BASE_NAME>StrDefs.h이다.

이 파일을 살펴보면,

extern unsigned char HIIStringsUNIStrings[];

이것은 필요한 문자열 패키지 데이터가 있는 배열이다. HiiLib의 HiiAddPackages 함수로 전달하여 HII 패키지를 데이터베이스에 추가하고 새 패키지 목록을 생성한다.

[https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Include/Library/HiiLib.h \

https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/UefiHiiLib/HiiLib.c](https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Include/Library/HiiLib.hhttps://github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/UefiHiiLib/HiiLib.c)

/**
  Registers a list of packages in the HII Database and returns the HII Handle
  associated with that registration.  If an HII Handle has already been registered
  with the same PackageListGuid and DeviceHandle, then NULL is returned.  If there
  are not enough resources to perform the registration, then NULL is returned.
  If an empty list of packages is passed in, then NULL is returned.  If the size of
  the list of package is 0, then NULL is returned.
  The variable arguments are pointers that point to package headers defined
  by UEFI VFR compiler and StringGather tool.
  #pragma pack (push, 1)
  typedef struct {
    UINT32                  BinaryLength;
    EFI_HII_PACKAGE_HEADER  PackageHeader;
  } EDKII_AUTOGEN_PACKAGES_HEADER;
  #pragma pack (pop)
  @param[in]  PackageListGuid  The GUID of the package list.
  @param[in]  DeviceHandle     If not NULL, the Device Handle on which
                               an instance of DEVICE_PATH_PROTOCOL is installed.
                               This Device Handle uniquely defines the device that
                               the added packages are associated with.
  @param[in]  ...              The variable argument list that contains pointers
                               to packages terminated by a NULL.
  @retval NULL   An HII Handle has already been registered in the HII Database with
                 the same PackageListGuid and DeviceHandle.
  @retval NULL   The HII Handle could not be created.
  @retval NULL   An empty list of packages was passed in.
  @retval NULL   All packages are empty.
  @retval Other  The HII Handle associated with the newly registered package list.
**/
EFI_HII_HANDLE
EFIAPI
HiiAddPackages (
  IN CONST EFI_GUID    *PackageListGuid,
  IN       EFI_HANDLE  DeviceHandle  OPTIONAL,
  ...
  )
;

이러한 것들로 패키지 목록 생성은 다음과 같이 간단하게 할 수 있다.

...

#include <Library/HiiLib.h>

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_HII_HANDLE Handle = HiiAddPackages(&gHIIStringsUNIGuid,
                                         NULL,
                                         HIIStringsUNIStrings,
                                         NULL);

  if (Handle == NULL)
  {
    Print(L"Error! Can't perform HiiAddPackages\n");
    return EFI_INVALID_PARAMETER;
  }

  return EFI_SUCCESS;
}

우리의 애플리케이션 INF 파일의 LibraryClasses 섹션에 HiiLib를 포함하는 것을 잊지 말자.

[Packages]
  ...
  MdeModulePkg/MdeModulePkg.dec

...

[LibraryClasses]
  ...
  HiiLib

애플리케이션을 빌드하고 OVMF에서 실행하면 실제로 코드가 2개의 문자열 패키지로 새 패키지 목록을 생성하는 것을 볼 수 있다.

FS0:\> HIIStringsUNI.efi
FS0:\> ShowHII.efi
...
PackageList[20]: GUID=6EE19058-0FE2-44ED-891C-A5D7E108EE1A; size=0xA6
        Package[0]: type=STRINGS; size=0x46
        Package[1]: type=STRINGS; size=0x48
        Package[2]: type=END; size=0x4

HiiGetString

HiiLib 라이브러리가 이미 포함되어 있으므로 HiiGetString 함수를 사용하여 문자열을 인쇄해 보자.

[https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Include/Library/HiiLib.h \

https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/UefiHiiLib/HiiString.c](https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Include/Library/HiiLib.hhttps://github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/UefiHiiLib/HiiString.c)

/**
  Retrieves a string from a string package in a specific language specified in Language
  or in the best lanaguage. See HiiGetStringEx () for the details.
  @param[in]  HiiHandle  A handle that was previously registered in the HII Database.
  @param[in]  StringId   The identifier of the string to retrieved from the string
                         package associated with HiiHandle.
  @param[in]  Language   The language of the string to retrieve.  If this parameter
                         is NULL, then the current platform language is used.  The
                         format of Language must follow the language format assumed in
                         the HII Database.
  @retval NULL   The string specified by StringId is not present in the string package.
  @retval Other  The string was returned.
**/
EFI_STRING
EFIAPI
HiiGetString (
  IN EFI_HII_HANDLE  HiiHandle,
  IN EFI_STRING_ID   StringId,
  IN CONST CHAR8     *Language  OPTIONAL
  );

애플리케이션에 다음 코드를 추가해보자.

Print(L"en-US ID=1: %s\n", HiiGetString(Handle, 1, "en-US"));
Print(L"en-US ID=2: %s\n", HiiGetString(Handle, 2, "en-US"));
Print(L"en-US ID=3: %s\n", HiiGetString(Handle, 3, "en-US"));
Print(L"fr-FR ID=1: %s\n", HiiGetString(Handle, 1, "fr-FR"));
Print(L"fr-FR ID=2: %s\n", HiiGetString(Handle, 2, "fr-FR"));
Print(L"fr-FR ID=3: %s\n", HiiGetString(Handle, 3, "fr-FR"));

빌드 후에 실행해보면 다음과 같은 결과를 볼 수 있다.

FS0:\> HIIStringsUNI.efi
en-US ID=1: English
en-US ID=2: <null string>
en-US ID=3: <null string>
fr-FR ID=1: Francais
fr-FR ID=2: <null string>
fr-FR ID=3: <null string>

뭔가 잘못된 것을 알 수 있다. String 패키지에 언어 문자열(ID=1)만 채워진 것을 볼 수 있다.

Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI/DEBUG/AutoGen.c 파일에 있는 실제 HIIStringsUNIStrings 배열 데이터를 살펴보자.

//
//Unicode String Pack Definition
//
unsigned char HIIStringsUNIStrings[] = {

// STRGATHER_OUTPUT_HEADER
  0x92,  0x00,  0x00,  0x00,

// PACKAGE HEADER

  0x46,  0x00,  0x00,  0x04,  0x34,  0x00,  0x00,  0x00,  0x34,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x01,  0x00,  0x65,  0x6E,
  0x2D,  0x55,  0x53,  0x00,

// PACKAGE DATA

// 0x0001: $PRINTABLE_LANGUAGE_NAME:0x0001
  0x14,  0x45,  0x00,  0x6E,  0x00,  0x67,  0x00,  0x6C,  0x00,  0x69,  0x00,  0x73,  0x00,  0x68,  0x00,  0x00,
  0x00,
  0x00,
// PACKAGE HEADER

  0x48,  0x00,  0x00,  0x04,  0x34,  0x00,  0x00,  0x00,  0x34,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x01,  0x00,  0x66,  0x72,
  0x2D,  0x46,  0x52,  0x00,

// PACKAGE DATA

// 0x0001: $PRINTABLE_LANGUAGE_NAME:0x0001
  0x14,  0x46,  0x00,  0x72,  0x00,  0x61,  0x00,  0x6E,  0x00,  0x63,  0x00,  0x61,  0x00,  0x69,  0x00,  0x73,
  0x00,  0x00,  0x00,
  0x00,

};

몇 가지 주목해야 할 사항들이 있다.

  • 배열에는 문자열 데이터 패키지만 포함되며 패키지 목록 헤더나 End 패키지는 포함되지 않는다.

  • 배열에는 특별한 4바이트 헤더 STRGATHER_OUTPUT_HEADER가 있다. - 이 헤더를 포함하는 배열의 크기를 포함한다.

  • 배열에는 실제로 언어 이름 문자열만 있다.

어떻게 STRGATHER_OUTPUT_HEADER가 패키지 목록을 구성하고 적절한 데이터로 NewPackageList를 호출하는 데 사용되는지 알아보기 위해 함수 구현을 살펴볼 수 있다.

이제 Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI/DEBUG/HIIStringsUNISTrDefs.h를 한 번 더 살펴보자.

//
//Unicode String ID
//
// #define $LANGUAGE_NAME                                       0x0000 // not referenced
// #define $PRINTABLE_LANGUAGE_NAME                             0x0001 // not referenced
// #define STR_HELLO                                            0x0002 // not referenced
// #define STR_BYE                                              0x0003 // not referenced

이것이 우리 문제의 원인이다. 단순히 토큰이참조되지 않았기 때문에 문자열이 배열로 이동하지 않았던 것이다.

배열 데이터 생성을 담당하는 빌드 도구 StrGather.py (https://github.com/tianocore/edk2/blob/master/BaseTools/Source/Python/AutoGen/StrGather.py)는 애플리케이션 코드에서 STRING_TOKEN(...) 매크로를 확인하고 코드에서 참조되는 문자열만 채운다.

STRING_TOKEN = re.compile('STRING_TOKEN *\(([A-Z0-9_]+) *\)', re.MULTILINE | re.UNICODE)

그러나 전체 언어 이름은 필수 필드이므로 항상 채워진다. 이것이 첫 번째 애플리케이션 실행에서 그 필드만 보인 이유이다.

출력코드를 다음과 같이 변경해보자.

Print(L"en-US ID=1: %s\n", HiiGetString(Handle, 1, "en-US"));
Print(L"en-US ID=2: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_HELLO), "en-US"));
Print(L"en-US ID=3: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_BYE), "en-US"));
Print(L"fr-FR ID=1: %s\n", HiiGetString(Handle, 1, "fr-FR"));
Print(L"fr-FR ID=2: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_HELLO), "fr-FR"));
Print(L"fr-FR ID=3: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_BYE), "fr-FR"));

애플리케이션을 빌드하고 생성된 파일을 확인해보자.

Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI/DEBUG/HIIStringsUNIStrDefs.h

//
//Unicode String ID
//
// #define $LANGUAGE_NAME                                       0x0000 // not referenced
// #define $PRINTABLE_LANGUAGE_NAME                             0x0001 // not referenced
#define STR_HELLO                                            0x0002
#define STR_BYE                                              0x0003

Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/HIIStringsUNI/HIIStringsUNI/DEBUG/AutoGen.c

//
//Unicode String Pack Definition
//
unsigned char HIIStringsUNIStrings[] = {

// STRGATHER_OUTPUT_HEADER
  0xD6,  0x00,  0x00,  0x00,

// PACKAGE HEADER

  0x60,  0x00,  0x00,  0x04,  0x34,  0x00,  0x00,  0x00,  0x34,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x01,  0x00,  0x65,  0x6E,
  0x2D,  0x55,  0x53,  0x00,

// PACKAGE DATA

// 0x0001: $PRINTABLE_LANGUAGE_NAME:0x0001
  0x14,  0x45,  0x00,  0x6E,  0x00,  0x67,  0x00,  0x6C,  0x00,  0x69,  0x00,  0x73,  0x00,  0x68,  0x00,  0x00,
  0x00,
// 0x0002: STR_HELLO:0x0002
  0x14,  0x48,  0x00,  0x65,  0x00,  0x6C,  0x00,  0x6C,  0x00,  0x6F,  0x00,  0x21,  0x00,  0x00,  0x00,
// 0x0003: STR_BYE:0x0003
  0x14,  0x42,  0x00,  0x79,  0x00,  0x65,  0x00,  0x21,  0x00,  0x00,  0x00,
  0x00,
// PACKAGE HEADER

  0x72,  0x00,  0x00,  0x04,  0x34,  0x00,  0x00,  0x00,  0x34,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x01,  0x00,  0x66,  0x72,
  0x2D,  0x46,  0x52,  0x00,

// PACKAGE DATA

// 0x0001: $PRINTABLE_LANGUAGE_NAME:0x0001
  0x14,  0x46,  0x00,  0x72,  0x00,  0x61,  0x00,  0x6E,  0x00,  0x63,  0x00,  0x61,  0x00,  0x69,  0x00,  0x73,
  0x00,  0x00,  0x00,
// 0x0002: STR_HELLO:0x0002
  0x14,  0x42,  0x00,  0x6F,  0x00,  0x6E,  0x00,  0x6A,  0x00,  0x6F,  0x00,  0x75,  0x00,  0x72,  0x00,  0x21,
  0x00,  0x00,  0x00,
// 0x0003: STR_BYE:0x0003
  0x14,  0x41,  0x00,  0x75,  0x00,  0x20,  0x00,  0x72,  0x00,  0x65,  0x00,  0x76,  0x00,  0x6F,  0x00,  0x69,
  0x00,  0x72,  0x00,  0x21,  0x00,  0x00,  0x00,
  0x00,

};

이번에는 문자열이 HIIStringsUNIStrings 배열에 들어간 것을 볼 수 있다.

이제 OVMF에서 애플리케이션을 실행해보면 올바른 출력을 얻을 수 있다.

FS0:\> HIIStringsUNI.efi
en-US ID=1: English
en-US ID=2: Hello!
en-US ID=3: Bye!
fr-FR ID=1: Francais
fr-FR ID=2: Bonjour!
fr-FR ID=3: Au revoir!

STRING_TOKEN 매크로를 사용하여 문자열을 참조하는 것이 얼마나 중요한지 알 수 있지만 실제로 이 매크로는 그 값에 아무런 영향을 미치지 않는다는 점을 잘 기억해야 한다. (https://github.com/tianocore/edk2/blob/master/BaseTools/Source /C/Include/Common/UefiInternalFormRepresentation.h)

//
// References to string tokens must use this macro to enable scanning for
// token usages.
//
//
// STRING_TOKEN is not defined in UEFI specification. But it is placed
// here for the easy access by C files and VFR source files.
//
#define STRING_TOKEN(t) t

Best Language

HiiGetString 라이브러리 기능은 직접 프로토콜 사용보다 간단할 뿐만 아니라 또 다른 유용한 기능이 존재한다. 바로 대상 언어를 제공하지 않고 HiiGetString을 호출할 수 있다는 점이다. 이 방법을 쓰면 함수는 어떤 언어를 사용하는 것이 더 나은지 스스로 결정한다. 이렇게 하면 PlatformLang 런타임 변수의 값에 따라 최상의 언어가 선택된다. gRT->GetNextVariableName/gRT->GetVariable 을 통해 런타임 변수 작업을 했던 것이 기억날 것이다. PlatformLang도 그 중 하나였다. ListVariables.efi 애플리케이션을 통해 이 옵션이 gEfiGlobalVariableGuid 아래에 있음을 발견했었다.

8BE4DF61-93CA-11D2-AA0D-00E098032B8C - gEfiGlobalVariableGuid https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Guid/GlobalVariable.h

https://github.com/tianocore/edk2/blob/master/MdePkg/MdePkg.dec

애플리케이션 끝에 다음 문자열 출력문을 추가해보자.

Print(L"Best language ID=1: %s\n", HiiGetString(Handle, 1, NULL));
Print(L"Best language ID=2: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_HELLO), NULL));
Print(L"Best langiage ID=3: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_BYE), NULL));

애플리케이션을 빌드 후 OVMF에서 실행해보면 다음과 같은 결과를 볼 수 있다.

FS0:\> HIIStringsUNI.efi
en-US ID=1: English
en-US ID=2: Hello!
en-US ID=3: Bye!
fr-FR ID=1: Francais
fr-FR ID=2: Bonjour!
fr-FR ID=3: Au revoir!
Best language ID=1: English
Best language ID=2: Hello!
Best language ID=3: Bye!

이제 UEFI Shell에서 exit을 실행하여 BIOS 설정으로 이동한 후 언어를 프랑스어로 변경한다. QEMU를 닫고 다시 실행해보면 이제 최상의 언어에 대한 출력은 fr-FR String 패키지에서 나오는 것을 볼 수 있다.

FS0:\> HIIStringsUNI.efi
en-US ID=1: English
en-US ID=2: Hello!
en-US ID=3: Bye!
fr-FR ID=1: Francais
fr-FR ID=2: Bonjour!
fr-FR ID=3: Au revoir!
Best language ID=1: Francais
Best language ID=2: Bonjour!
Best language ID=3: Au revoir!

또한 보다 일반적인 언어 이름으로 HiiGetString을 호출할 수 있는 기능이 있다. 예를 들어 fr-FR 대신 fr을 써도 여전히 ​​올바른 문자열 패키지를 선택한다.

 Print(L"fr ID=3: %s\n", HiiGetString(Handle, STRING_TOKEN(STR_BYE), "fr"));

위 코드는 다음과 같이 출력된다.

fr ID=3: Au revoir!

이 기능들은 모두 HiiGetString 구현에서 온 것임을 명심해야한다.

EFI_HII_STRING_PROTOCOL.GetString()에 "fr-FR" 대신 "fr"을 넣으면 오류가 발생한다.

Last updated