写在前面
UEFI有自己的用户交互界面,它的实现基础被称为HII(Human Interface Infrastructure),本文是一系列介绍HII实现的第一篇。这里从开源EDK代码中的界面(称为Front Page)入手,介绍它的实现,并进一步说明整个HII。
之前已经写过一系列跟Setup相关的文档,内容有重复及补充。
入口说明
Front Page的实现代码可以在https://gitee.com/jiangwei0512/edk2-beni)找到,编译执行之后的结果大致如下:
它对应的是一个启动项程序UiApp,模块代码是MdeModulePkg\Application\UiApp\UiApp.inf,该模块的入口函数是InitializeUserInterface()(位于MdeModulePkg\Application\UiApp\FrontPage.c)。
初始化的大致流程如下:
进入UI
退出UI之后的清理
初始化图形模式参数
关闭软件看门狗
清屏
安装字体
初始化HII字符串
设置终端模式,参数就来自第一步
进入UI入口
UI入口
退出Front Page之后的清理
UI入口
如果有必要就初始化启动设备
刷新启动项
Logo操作
初始化Front Page
显示Front Page
上述流程中最主要的是两个部分,”初始化Front Page“和”显示Front Page“,对应到两个函数InitializeFrontPage()和CallFrontPage(),它们可以连起来看,主要做的事情就是两个:一个是准备素材,这里的素材指的是uni文件、vfr文件等表示的HII数据;另一个是显示这些素材,它通过一个EFI_FORM_BROWSER2_PROTOCOL来完成,这个Protocol提供两个接口,SendForm()用来显示配置好的HII,BrowserCallback()会被回调函数调用,用来获取和设置界面元素。
界面元素
在HII中构成一个界面的元素大致有四种,分别是字符串(Strings)、结构(Forms)、字体(Fonts)和图像(Images),如下图所示(最右边的Questions等是结构的组成部分,这里可以暂时不管,后续会介绍):
字符串通过uni文件产生,结构通过vfr文件产生,字体暂不介绍,而图像则没有特别好介绍的。本例的Front Page中使用了结构、字符串和字体,图像则没有使用到。
元素更新
元素更新主要发生在InitializeFrontPage()函数中,对应的代码:
//
//Updata Front Page banner strings
//
UpdateFrontPageBannerStrings ();
//
// Update front page menus.
//
UpdateFrontPageForm();
前者UpdateFrontPageBannerStrings()主要是获取标记①(Token)对应的字符串的值并设置到对应的标记②中,它初始化Front Page的上半区静态部分:
以代码为例的话就像下面的那样:
NewString = HiiGetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_COMPUTER_MODEL), NULL);
UiCustomizeFrontPageBanner (1, TRUE, &NewString);
HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_COMPUTER_MODEL), NewString, NULL);
FreePool (NewString);
这里的标记STR_FRONT_PAGE_COMPUTER_MODEL出现了两次,虽然名字相同,但是是来自不同的文件,分别是uni文件中的:
#string STR_FRONT_PAGE_COMPUTER_MODEL #language en-US “”
#language fr-FR “”
和来自vfr文件中的:
bannertitle = STRING_TOKEN(STR_FRONT_PAGE_COMPUTER_MODEL),line 1,align left;
标记①的操作通过HiiGetString()获取字符串,标记②的操作通过HiiSetString()设置字符串。不过两者本来就需要是一致的,所以也不需要特意去区分。
后者UpdateFrontPageForm()更新其它动态的部分,而动态的部分通过vfr文件查看可以看到它们位于两个操作码之间:
label LABEL_FRANTPAGE_INFORMATION;
//
// This is where we will dynamically add a Action type op-code to show
// the platform information.
//
label LABEL_END;
对应的代码:
VOID
UpdateFrontPageForm (
VOID
)
{
VOID *StartOpCodeHandle;
VOID *EndOpCodeHandle;
EFI_IFR_GUID_LABEL *StartGuidLabel;
EFI_IFR_GUID_LABEL *EndGuidLabel;
//
// Allocate space for creation of UpdateData Buffer
//
StartOpCodeHandle = HiiAllocateOpCodeHandle ();
ASSERT (StartOpCodeHandle != NULL);
EndOpCodeHandle = HiiAllocateOpCodeHandle ();
ASSERT (EndOpCodeHandle != NULL);
//
// Create Hii Extend Label OpCode as the start opcode
//
StartGuidLabel = (EFI_IFR_GUID_LABEL *) HiiCreateGuidOpCode (StartOpCodeHandle, &gEfiIfrTianoGuid, NULL, sizeof (EFI_IFR_GUID_LABEL));
StartGuidLabel->ExtendOpCode = EFI_IFR_EXTEND_OP_LABEL;
StartGuidLabel->Number = LABEL_FRANTPAGE_INFORMATION; // 对应vfr中的LABEL_FRANTPAGE_INFORMATION
//
// Create Hii Extend Label OpCode as the end opcode
//
EndGuidLabel = (EFI_IFR_GUID_LABEL *) HiiCreateGuidOpCode (EndOpCodeHandle, &gEfiIfrTianoGuid, NULL, sizeof (EFI_IFR_GUID_LABEL));
EndGuidLabel->ExtendOpCode = EFI_IFR_EXTEND_OP_LABEL;
EndGuidLabel->Number = LABEL_END; // 对应vfr中的LABEL_END
//
//Updata Front Page form
//
UiCustomizeFrontPage (
gFrontPagePrivate.HiiHandle,
StartOpCodeHandle
);
HiiUpdateForm (
gFrontPagePrivate.HiiHandle,
&mFrontPageGuid,
FRONT_PAGE_FORM_ID,
StartOpCodeHandle,
EndOpCodeHandle
);
HiiFreeOpCodeHandle (StartOpCodeHandle);
HiiFreeOpCodeHandle (EndOpCodeHandle);
}
这里的两个部分(StartOpCodeHandle和EndOpCodeHandle)组合构成了Front Page剩余的部分,其过程如下:
通过函数HiiAllocateOpCodeHandle()创建两个OpCodeHandle,它们对应的是同一个结构体:
typedef struct {
UINT8 *Buffer;
UINTN BufferSize;
UINTN Position;
} HII_LIB_OPCODE_BUFFER;
创建好之后的结构体中Buffer是一个大小为0x200字节的空间;BufferSize就是0x200;Position初始化为0,它相当于存放其它操作码的容器。
创建两个OpCode,会使用到前面创建的OpCodeHandle:
StartGuidLabel = (EFI_IFR_GUID_LABEL *) HiiCreateGuidOpCode (StartOpCodeHandle, &gEfiIfrTianoGuid, NULL, sizeof (EFI_IFR_GUID_LABEL));
StartGuidLabel->ExtendOpCode = EFI_IFR_EXTEND_OP_LABEL;
StartGuidLabel->Number = LABEL_FRANTPAGE_INFORMATION;
1
2
3
HiiCreateGuidOpCode()的第一个参数就是OpCodeHandle;第二个参数是一个GUID;第三个参数是可选的,可以是NULL;第四个参数是组件元素结构体的大小。本例创建了两个EFI_IFR_GUID_LABEL结构体(跟vfr文件中的label对应),它也是HiiCreateGuidOpCode()的返回值,其结构如下:
///
/// Label opcode.
///
typedef struct _EFI_IFR_GUID_LABEL {
EFI_IFR_OP_HEADER Header;
///
/// EFI_IFR_TIANO_GUID.
///
EFI_GUID Guid;
///
/// EFI_IFR_EXTEND_OP_LABEL.
///
UINT8 ExtendOpCode;
///
/// Label Number.
///
UINT16 Number;
} EFI_IFR_GUID_LABEL;
后面的两句代码用来初始化EFI_IFR_GUID_LABEL结构体的后两个参数,其中Number就对应到vfr文件中的label名,本例中分别是LABEL_FRANTPAGE_INFORMATION和LABEL_END。
对OpCode的自定义,其实就是在StartOpCodeHandle和EndOpCodeHandle之间创建自定义的元素。
更新Front Page结构,更新的部分就是前面代码中增加的自定义元素。
释放资源。
这里最重要的是第3步,对应如下的函数:
VOID
UiCustomizeFrontPage (
IN EFI_HII_HANDLE HiiHandle,
IN VOID *StartOpCodeHandle
)
{
//
// Create “Select Language” menu with Oneof opcode.
//
UiCreateLanguageMenu (HiiHandle, StartOpCodeHandle);
//
// Create empty line.
//
UiCreateEmptyLine(HiiHandle, StartOpCodeHandle);
//
// Find third party drivers which need to be shown in the front page.
//
UiListThirdPartyDrivers (HiiHandle, &gEfiIfrFrontPageGuid, NULL, StartOpCodeHandle);
//
// Create empty line.
//
UiCreateEmptyLine(HiiHandle, StartOpCodeHandle);
//
// Create “Continue” menu.
//
UiCreateContinueMenu(HiiHandle, StartOpCodeHandle);
//
// Create reset menu.
//
UiCreateResetMenu(HiiHandle, StartOpCodeHandle);
}
基本上图中显示的部分都有对应的函数,但是底部黑色部分并没有,它们是根据特定情况而产生的,比如= Select Entry是因为UiCreateLanguageMenu()而显示的,而↑↓=Move Highlight只要有创建上述的元素就会有显示。动作显示的组件有一个包含的关系,如下图所示:
起始标签
单选框
若干单选项
结束标签
空白行
第三方驱动提供的界面
空白行
Continue动作条
Reset动作条
后面的小节介绍如何创建图中的动态显示部分。
创建菜单
UiCreateLanguageMenu()的具体的代码:
VOID
UiCreateLanguageMenu (
IN EFI_HII_HANDLE HiiHandle,
IN VOID *StartOpCodeHandle
)
{
CHAR8 *LangCode;
CHAR8 *Lang;
UINTN LangSize;
CHAR8 *CurrentLang;
UINTN OptionCount;
CHAR16 *StringBuffer;
VOID *OptionsOpCodeHandle;
UINTN StringSize;
EFI_STATUS Status;
EFI_HII_STRING_PROTOCOL *HiiString;
Lang = NULL;
StringBuffer = NULL;
//
// Init OpCode Handle and Allocate space for creation of UpdateData Buffer
//
OptionsOpCodeHandle = HiiAllocateOpCodeHandle ();
ASSERT (OptionsOpCodeHandle != NULL);
GetEfiGlobalVariable2 (L"PlatformLang", (VOID**)&CurrentLang, NULL);
//
// Get Support language list from variable.
//
GetEfiGlobalVariable2 (L"PlatformLangCodes", (VOID**)&gLanguageString, NULL);
if (gLanguageString == NULL) {
gLanguageString = AllocateCopyPool (
AsciiStrSize ((CHAR8 *) PcdGetPtr (PcdUefiVariableDefaultPlatformLangCodes)),
(CHAR8 *) PcdGetPtr (PcdUefiVariableDefaultPlatformLangCodes)
);
ASSERT (gLanguageString != NULL);
}
if (gLanguageToken == NULL) {
//
// Count the language list number.
//
LangCode = gLanguageString;
Lang = AllocatePool (AsciiStrSize (gLanguageString));
ASSERT (Lang != NULL);
OptionCount = 0;
while (*LangCode != 0) {GetNextLanguage (&LangCode, Lang);OptionCount ++;
}//
// Allocate extra 1 as the end tag.
//
gLanguageToken = AllocateZeroPool ((OptionCount + 1) * sizeof (EFI_STRING_ID));
ASSERT (gLanguageToken != NULL);Status = gBS->LocateProtocol (&gEfiHiiStringProtocolGuid, NULL, (VOID **) &HiiString);
ASSERT_EFI_ERROR (Status);LangCode = gLanguageString;
OptionCount = 0;
while (*LangCode != 0) {GetNextLanguage (&LangCode, Lang);StringSize = 0;Status = HiiString->GetString (HiiString, Lang, HiiHandle, PRINTABLE_LANGUAGE_NAME_STRING_ID, StringBuffer, &StringSize, NULL);if (Status == EFI_BUFFER_TOO_SMALL) {StringBuffer = AllocateZeroPool (StringSize);ASSERT (StringBuffer != NULL);Status = HiiString->GetString (HiiString, Lang, HiiHandle, PRINTABLE_LANGUAGE_NAME_STRING_ID, StringBuffer, &StringSize, NULL);ASSERT_EFI_ERROR (Status);}if (EFI_ERROR (Status)) {LangSize = AsciiStrSize (Lang);StringBuffer = AllocatePool (LangSize * sizeof (CHAR16));ASSERT (StringBuffer != NULL);AsciiStrToUnicodeStrS (Lang, StringBuffer, LangSize);}ASSERT (StringBuffer != NULL);gLanguageToken[OptionCount] = HiiSetString (HiiHandle, 0, StringBuffer, NULL);FreePool (StringBuffer);OptionCount++;
}
}
ASSERT (gLanguageToken != NULL);
LangCode = gLanguageString;
OptionCount = 0;
if (Lang == NULL) {
Lang = AllocatePool (AsciiStrSize (gLanguageString));
ASSERT (Lang != NULL);
}
while (*LangCode != 0) {
GetNextLanguage (&LangCode, Lang);
if (CurrentLang != NULL && AsciiStrCmp (Lang, CurrentLang) == 0) {HiiCreateOneOfOptionOpCode (OptionsOpCodeHandle,gLanguageToken[OptionCount],EFI_IFR_OPTION_DEFAULT,EFI_IFR_NUMERIC_SIZE_1,(UINT8) OptionCount);gCurrentLanguageIndex = (UINT8) OptionCount;
} else {HiiCreateOneOfOptionOpCode (OptionsOpCodeHandle,gLanguageToken[OptionCount],0,EFI_IFR_NUMERIC_SIZE_1,(UINT8) OptionCount);
}OptionCount++;
}
if (CurrentLang != NULL) {
FreePool (CurrentLang);
}
FreePool (Lang);
HiiCreateOneOfOpCode (
StartOpCodeHandle,
FRONT_PAGE_KEY_LANGUAGE,
0,
0,
STRING_TOKEN (STR_LANGUAGE_SELECT),
STRING_TOKEN (STR_LANGUAGE_SELECT_HELP),
EFI_IFR_FLAG_CALLBACK,
EFI_IFR_NUMERIC_SIZE_1,
OptionsOpCodeHandle,
NULL
);
}
这里的大部分代码只是创建选项时需要使用的数据,可以不同特别关注,这里的重要步骤起始只有两个:
创建一个新的HII_LIB_OPCODE_BUFFER,用来存放单选项,单选项通过HiiCreateOneOfOptionOpCode()函数放到HII_LIB_OPCODE_BUFFER中;
将存放了单选项的HII_LIB_OPCODE_BUFFER,构建成一个单选框,存放到上一层的HII_LIB_OPCODE_BUFFER中。
创建空行
空行对应到这里的是一个EFI_IFR_SUBTITLE_OP(Subtitle statement),对应的结构体:
typedef struct _EFI_IFR_SUBTITLE {
EFI_IFR_OP_HEADER Header;
EFI_IFR_STATEMENT_HEADER Statement;
UINT8 Flags;
} EFI_IFR_SUBTITLE;
但是没有实际的数据,就变成了一个空行,注意创建时的参数都是0:
VOID
UiCreateEmptyLine (
IN EFI_HII_HANDLE HiiHandle,
IN VOID *StartOpCodeHandle
)
{
HiiCreateSubTitleOpCode (StartOpCodeHandle, STRING_TOKEN (STR_NULL_STRING), 0, 0, 0);
}
创建第三方驱动使用的界面
执行流程:
找到系统中安装的所有HII选项
为每个选项分配空间
初始化每个选项
为每个选项创建操作码
最终的操作也是创建操作码:
HiiCreateGotoExOpCode (StartOpCodeHandle,0,gHiiDriverList[Index].PromptId,gHiiDriverList[Index].HelpId,0,(EFI_QUESTION_ID) (Index + FRONT_PAGE_KEY_DRIVER),0,&gHiiDriverList[Index].FormSetGuid,gHiiDriverList[Index].DevicePathId
);
对应的操作码可能是EFI_IFR_REF_OP、EFI_IFR_REF2_OP、EFI_IFR_REF3_OP和EFI_IFR_REF4_OP,这依赖于传入的参数值。
创建Continue/reset菜单
两者对应的都是EFI_IFR_ACTION_OP操作码,对应的实现:
/**
Create continue menu in the front page.
@param[in] HiiHandle The hii handle for the Uiapp driver.
@param[in] StartOpCodeHandle The opcode handle to save the new opcode.
**/
VOID
UiCreateContinueMenu (
IN EFI_HII_HANDLE HiiHandle,
IN VOID *StartOpCodeHandle
)
{
HiiCreateActionOpCode (
StartOpCodeHandle,
FRONT_PAGE_KEY_CONTINUE,
STRING_TOKEN (STR_CONTINUE_PROMPT),
STRING_TOKEN (STR_CONTINUE_PROMPT),
EFI_IFR_FLAG_CALLBACK,
0
);
}
/**
Create Reset menu in the front page.
@param[in] HiiHandle The hii handle for the Uiapp driver.
@param[in] StartOpCodeHandle The opcode handle to save the new opcode.
**/
VOID
UiCreateResetMenu (
IN EFI_HII_HANDLE HiiHandle,
IN VOID *StartOpCodeHandle
)
{
HiiCreateActionOpCode (
StartOpCodeHandle,
FRONT_PAGE_KEY_RESET,
STRING_TOKEN (STR_RESET_STRING),
STRING_TOKEN (STR_RESET_STRING),
EFI_IFR_FLAG_CALLBACK,
0
);
}
从代码中可以看到,差别只在显示和QuestionId,后者在回调函数中判断其值并执行不同的操作,其代码在UiSupportLibCallbackHandler(),部分代码如下:
switch (QuestionId) {
case FRONT_PAGE_KEY_CONTINUE://// This is the continue - clear the screen and return an error to get out of FrontPage loop//*ActionRequest = EFI_BROWSER_ACTION_REQUEST_EXIT;break;case FRONT_PAGE_KEY_LANGUAGE:*Status = LanguageChangeHandler(Value);break;case FRONT_PAGE_KEY_RESET://// Reset//gRT->ResetSystem (EfiResetCold, EFI_SUCCESS, 0, NULL);*Status = EFI_UNSUPPORTED;default:break;
}