How to make custom UEFI Protocol
Tags: uefi, dxe, edk2, tutorial
Published: 2022-07-31 09:13:02

DXE Driver's main task is to create and install Protocols. In this post, I will explain how to create Protocol in EDK2 and how to use that protocol from other UEFI module.

## UEFI Protocol Explanation
DXE Driver abstract the access to the device by creating **Protocols**. For example, you can use Tcg2Protocol to access TPM2.0 functionality rather than accessing it via things like MMIO or Port IO. Protocols are **data structures containing function pointers** and the actual function's contents are in the DXE Driver which installed that Protocol. The Protocol (data structure) itself is also in that DXE Driver (usually in .data section).
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q0jh93g1a4qzcxgy7gzl.png)
(Image from [EDK2 UEFI Driver Writer's Guide 3.6.2](https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/3_foundation/36_protocols_and_handles/362_protocol_interface_structure)) 

Each Protocol has its own GUID and this is used for other UEFI modules to locate this Protocol. Several Protocols can be grouped and this group is identified by Handle. Therefore, you can use `gBS->LocateHandle` first to locate Handle and then locate the Protocol inside. However, it might be more simpler to use `gBS->LocateProtocol` to locate Protocol by GUID directly.

Protocols are stored into a single database because it has to be accessible via any UEFI modules. In order to store the protocol to this database, the DXE Driver which create the protocol has to use `gBS->InstallMultipleProtocolInterfaces` inside.


## Implementation

### MyDxe2.inf
Let's start with the inf file. The main point is to write `gEfiDummyProtocol` to the `[Protocols]`. We have to write the Protocols this DXE Driver produce and also consume.

```
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = MyDxe2
  FILE_GUID                      = 9d46dccd-ea11-4808-93e9-9b7021889b2d
  MODULE_TYPE                    = DXE_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = MyDxeEntry

[Sources]
	MyDxe2.h
	MyDxe2.c

[Packages]
	MdePkg/MdePkg.dec
	MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
	UefiDriverEntryPoint
	UefiRuntimeServicesTableLib
	UefiBootServicesTableLib
	UefiLib
	PrintLib

[Protocols]
	gEfiDummyProtocolGuid   ## PRODUCES

[Depex]
	TRUE
```

### MyDxe2.h
This is the header file. This mainly defines *types* like `EFI_DUMMY_PROTOCOL` or `DUMMY_FUNC1`, so that you can use it in MyDxe2.c. Definition of `gEfiDummyProtocolGuid` will be defined in the separate place.
```c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/UefiBootServicesTableLib.h>

typedef EFI_STATUS (EFIAPI *DUMMY_FUNC1)();
typedef EFI_STATUS (EFIAPI *DUMMY_FUNC2)();

// e9811f61-14bd-4637-8d17-aec540d8f508
#define EFI_DUMMY_PROTOCOL_GUID \
	{ 0xe9811f61, 0x14bd, 0x4637, { 0x8d, 0x17, 0xae, 0xc5, 0x40, 0xd8, 0xf5, 0x08 } }

extern EFI_GUID gEfiDummyProtocolGuid;
	
typedef struct _EFI_DUMMY_PROTOCOL {
	DUMMY_FUNC1 DummyFunc1;
	DUMMY_FUNC2 DummyFunc2;
} EFI_DUMMY_PROTOCOL;
```

### MyDxe2.c
This is the DXE Driver's contents. `mDummy` is the actual data structure of this Protocol and this Protocol is installed by `gBS->InstallMultipleProtocolInterfaces`. The content of `DummyFunc1` and `DummyFunc2` can be any, but in this example, I'm storing the message "DummyFuncN called" in UEFI variable called `MyDxeStatus`, so that, you can check if the Protocols is really running correctly.

```c
#include "MyDxe2.h"
	
UINT32 myvarSize      = 30;
CHAR8  myvarValue[30] = {0};
CHAR16 myvarName[30]  = L"MyDxeStatus";

// eefbd379-9f5c-4a92-a157-ae4079eb1448
EFI_GUID myvarGUID = { 0xeefbd379, 0x9f5c, 0x4a92, { 0xa1, 0x57, 0xae, 0x40, 0x79, 0xeb, 0x14, 0x48 }};

EFI_HANDLE mDummyHandle = NULL;

EFI_STATUS EFIAPI DummyFunc1() {
	AsciiSPrint(myvarValue, 18, "DummyFunc1 called");
	gRT->SetVariable(
			myvarName,
			&myvarGUID,
			EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
			myvarSize,
			myvarValue);
	return EFI_SUCCESS;
}

EFI_STATUS EFIAPI DummyFunc2() {
	AsciiSPrint(myvarValue, 18, "DummyFunc2 called");
	gRT->SetVariable(
			myvarName,
			&myvarGUID,
			EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
			myvarSize,
			myvarValue);
	return EFI_SUCCESS;
}

EFI_DUMMY_PROTOCOL mDummy = {
	DummyFunc1,
	DummyFunc2
};

EFI_STATUS EFIAPI MyDxeEntry(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
	gBS->InstallMultipleProtocolInterfaces(&mDummyHandle, &gEfiDummyProtocolGuid, &mDummy, NULL);

	EFI_TIME time;
	gRT->GetTime(&time, NULL);
	AsciiSPrint(myvarValue, 12, "%2d/%2d %2d:%2d", time.Month, time.Day, time.Hour, time.Minute);

	gRT->SetVariable(
			myvarName,
			&myvarGUID,
			EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
			myvarSize,
			myvarValue);

	return EFI_SUCCESS;
}
```

### MdeModulePkg.dec
We should define `gEfiDummyProtocolGuid` to MdeModulePkg.dec in order to use `gEfiDummyProtocolGuid` in the other UEFI module. Inside MdeModulePkg.dec, add the definition like below.
```
[Protocols]
...
  ## MyDxe/MyDxe2.h
  gEfiDummyProtocolGuid = { 0xe9811f61, 0x14bd, 0x4637, { 0x8d, 0x17, 0xae, 0xc5, 0x40, 0xd8, 0xf5, 0x08 } }
```

### MdeModulePkg.dsc
```
[Components]
  MdeModulePkg/MyDxe/MyDxe2.inf
  ...
```

### Build it
Make sure you have `ACTIVE_PLATFORM = MdeModulePkg/MdeModulePkg.dsc` in `Conf/target.txt` and let's build it. Then, construct FFS file like I explained in the [previous post](https://dev.to/machinehunter/how-to-build-dxe-driver-by-edk2-11h8), add the FFS file to the BIOS image, and flash the SPI flash chip with that image.


## Using DummyProtocol
Next, let's write a module that uses this Protocol. I will be writing a UEFI module that runs from a USB memory in BDS phase, so that I can print the output to the screen easily.
Create a folder named `AppPkg/` under `edk2/` and will be creating these files.
```
AppPkg/
├── AppPkg.dec
├── AppPkg.dsc
└── Applications/
    └── UseDummyProtocol/
        ├── UseDummyProtocol.c
        └── UseDummyProtocol.inf
```

### AppPkg.dec
`FirstPkg` can be any name.
```
[Defines]
  DEC_SPECIFICATION              = 0x00010005
  PACKAGE_NAME                   = FirstPkg
  PACKAGE_GUID                   = 75281f80-8657-4cf2-837f-04d73179c590
  PACKAGE_VERSION                = 0.1
```

### AppPkg.dsc
```
[Defines]
  PLATFORM_NAME                  = FirstPkg
  PLATFORM_GUID                  = 7b86cb02-9fbb-4f77-8d93-35c8c90f2488
  PLATFORM_VERSION               = 0.1
  DSC_SPECIFICATION              = 0x00010005
  OUTPUT_DIRECTORY               = Build/FirstPkg$(ARCH)
  SUPPORTED_ARCHITECTURES        = IA32|X64
  BUILD_TARGETS                  = DEBUG|RELEASE|NOOPT
  DEFINE DEBUG_ENABLE_OUTPUT     = TRUE

[LibraryClasses]
  # Entry point
  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
  ShellCEntryLib|ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.inf

  # Common Libraries
  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf
  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  !if $(DEBUG_ENABLE_OUTPUT)
    DebugLib|MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf
    DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf
  !else   ## DEBUG_ENABLE_OUTPUT
    DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf
  !endif  ## DEBUG_ENABLE_OUTPUT

  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf
  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf
  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf
  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf
  RegisterFilterLib|MdePkg/Library/RegisterFilterLibNull/RegisterFilterLibNull.inf

[Components]
  AppPkg/Applications/UseDummyProtocol/UseDummyProtocol.inf
```

### UseDummyProtocol.inf
```
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = UseDummyProtocol
  FILE_GUID                      = 5765e8dd-c97a-4ce2-891e-9e24b94d654b
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = UefiMain

[Sources]
  UseDummyProtocol.c

[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
  UefiLib
  ShellCEntryLib
  UefiBootServicesTableLib
  UefiRuntimeServicesTableLib

[Protocols]
  gEfiDummyProtocolGuid  ## CONSUME
```

### UseDummyProtocol.c
This is checking UEFI variable `MyDxeStatus` by `GetVariable` before and after calling `DummyFunc1` (the DummyProtocol's function). `DummyFunc1` will change the value of `MyDxeStatus` to a `DummyFunc1 called` which was originally the current time set when this DXE Driver was executed.
```c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <MyDxe/MyDxe2.h>
#include <Library/BaseMemoryLib.h>

EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
	EFI_DUMMY_PROTOCOL *DummyProtocol;
	gBS->LocateProtocol(&gEfiDummyProtocolGuid, NULL, (VOID**)&DummyProtocol);

	CHAR8 myvarValue[30];
	UINT64 myvarSize = 30;
	UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;
	EFI_GUID myvarGUID = { 0xeefbd379, 0x9f5c, 0x4a92, { 0xa1, 0x57, 0xae, 0x40, 0x79, 0xeb, 0x14, 0x48 }};

	Print(L"[Before Calling Protocol] MyDxeStatus:\r\n");
	gRT->GetVariable(
			L"MyDxeStatus",
			&myvarGUID,
			&Attributes,
			&myvarSize,
			myvarValue);

	AsciiPrint("%a\n\n", myvarValue);

	DummyProtocol->DummyFunc1();

	Print(L"[After Calling Protocol] MyDxeStatus:\r\n");
	gRT->GetVariable(
			L"MyDxeStatus",
			&myvarGUID,
			&Attributes,
			&myvarSize,
			myvarValue);

	AsciiPrint("%a\n", myvarValue);

	while(1);
	return 0;
}
```

### Build and Execute it
Before build, change to `ACTIVE_PLATFORM = AppPkg/AppPkg.dsc` in the `Conf/target.txt`. Then, build it.
This time, I will store the generated UseDummyProtocol.efi to the USB memory. In USB memory, make a directory `EFI/BOOT/` and rename UseDummyProtocol.efi into bootx64.efi and put that into the folder.

Plug the USB into the PC you flashed the BIOS before, and boot that machine. If you see below output, then it's a success.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xd1kxhi05m673pd9qzaz.jpg)
(I missed specifing the string length...)