# 13.ShellAppMain Entry point

이전 장에서는 `EFI_SHELL_PARAMETERS_PROTOCOL`을 사용하여 애플리케이션에 매개변수를 받는 방법을 알아보았다. 이것도 맞는 방법이지만 더욱 간단한 방법이 있다.

더 쉽게 하기 위해서는 다음 형식의 Entry point를 사용하면 된다.

{% embed url="https://github.com/tianocore/edk2/blob/master/ShellPkg/Include/Library/ShellCEntryLib.h" %}

```c
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  );
```

이런 방식을 쓰면 일반적인 C프로그래밍에서 동작하는 것처럼 Argc/Argv가 애플리케이션 Entry point로 바로 전달된다.

우리가 만들었던 `HelloWorld`앱을 기반으로 간단한 `SimpleShellApp`을 만들어보자.

수정을 거친 `inf`파일은 다음과 같다.

```
[Defines]
  INF_VERSION                    = 1.25
  BASE_NAME                      = SimpleShellApp
  FILE_GUID                      = 2afd1202-545e-4f8d-b8fb-bc179e84ddc8
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = ShellCEntryLib

[Sources]
  SimpleShellApp.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiLib
  ShellCEntryLib
```

주요 변경 사항:

* `ENTRY_POINT = ShellCEntryLib` 가 `[Defines]` 섹션에 추가 되었다.
* `UefiApplicationEntryPoint`대신에 `ShellCEntryLib`가 `[LibraryClasses]`섹션에 추가되었다.

Shell C 라이브러리는 표준 Entry point가 있는 기본 UEFI 애플리케이션이다.

```c
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
```

우리가 했던 것처럼 `EFI_SHELL_PARAMETERS_PROTOCOL`로 수신된 매개변수를 파싱한 다음 애플리케이션의 Entry point를 호출함과 동시에 파싱된 모든 매개변수를 전달한다.

```c
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  );
```

다음 링크에서 ShellAppMain의 소스코드를 확인 가능하다.

{% embed url="https://github.com/tianocore/edk2/blob/master/ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.c" %}

소스코드를 확인하면 우리가 썼던 `HandleProtocol`대신에,

{% code title="우리가 썼던 코드" %}
```c
Status = gBS->HandleProtocol(
  ImageHandle,
  &gEfiShellParametersProtocolGuid,
  (VOID **) &ShellParameters
);
```
{% endcode %}

OpenProtocol이라는 API를 사용한다. (우리가 썼던 것과 비교 가능하게 코드를 약간 수정하였다.)

```c
Status = gBS->OpenProtocol(
  ImageHandle,
  &gEfiShellParametersProtocolGuid,
  (VOID **)&ShellParameters,
  ImageHandle,
  NULL,
  EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
```

UEFI 스펙에 따르면 `HandleProtocol` API는 오래되었으며 대신 `OpenProtocol`을 사용해야 한다고 적혀있다. `OpenProtocol` API는 더 많은 경우의 수를 다룰 수 있는 보다 일반적인 호출이다.

UEFI 드라이버 개발을 시작할 때 이런 모든 것들이 중요하다. 더 많은 정보를 얻기 위해서는 UEFI 스펙을 읽어 보는 것이 좋다. 현재는 UEFI 애플리케이션에 대해서 이 두 호출 방법이 동일하다는 사실을 알고 넘어가면 된다.

다시 코드로 돌아가서 우리가 필요한 `ShellCEntryLib`를 검색해서 찾아본다.

```shell
$ grep ShellCEntryLib -r ./ --include=*.inf | grep LIBRARY_CLASS
./ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.inf:  LIBRARY_CLASS                  = ShellCEntryLib|UEFI_APPLICATION UEFI_DRIVER
```

찾은 라이브러리 클래스를 `UefiLessonsPkg/UefiLessonsPkg.dsc`에 추가한다.

```
ShellCEntryLib|ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.inf
```

새로 만든 애플리케이션을 `[Components]`섹션에 추가하는 것도 잊지말자.

```
UefiLessonsPkg/SimpleShellApp/SimpleShellApp.inf
```

이제 `.c` 파일을 살펴보자.

이제 `SystemTable`을 사용할 수 없으므로 원래의 출력 방법을 사용할 수 없지만 `gST` 포인터의 도움으로 간단히 `SystemTable`을 사용할 수 있다.

```c
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>

INTN EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
{
  Print(L"Argc=%d\n", Argc);
//  SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World!\n");
  gST->ConOut->OutputString(gST->ConOut, L"Hello again!\n");
  Print(L"Bye!\n");
  return 0;
}
```

이제 매개변수를 처리하는 코드를 작성해보자.

```c
for (UINTN i=Argc; i>0; i--) {
  Print(L"Arg[%d]=%s\n", Argc-i, Argv[Argc-i]);
}
```

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

```shell
FS0:\> SimpleShellApp.efi kkk ggg
Hello again!
Bye!
Arg[0]=FS0:\SimpleShellApp.efi
Arg[1]=kkk
Arg[2]=ggg
FS0:\> SimpleShellApp.efi
Hello again!
Bye!
Arg[0]=FS0:\SimpleShellApp.efi
```
