You don't need any sort of large toolchain. In short, a UEFI application works like this (from memory so some details may be wrong): The system calls your main function with a parameter for the address of a struct. That struct contains a function pointer which you can call to get other structs known as protocols. Those protocols contain function pointers which you use for everything else (putting images on the screen, setting callbacks for keyboard input, making network requests, etc.).
All you need is the function signatures and the constants to lookup the protocols, it's no more than you need to use any other C library. I can tell you from experience that if all you want to do is textual IO you only need a few hundred lines of header files: https://github.com/liampwll/zig-efi-os
Getting the GNU EFI toolchain to work is somewhat involved. The result is also convoluted for reasons I don’t understand. For example, it uses a perfectly normal GCC that is perfectly capable of defining and invoking __stdcall functions, but instead it has assembly thunks you’re supposed to use to translate to and from __cdecl. AFAICT it shouldn’t be that much harder than a bunch of header files, a freestanding compiler invocation with a custom crt0 and an objcopy to make the PE file (OK, yeah, PE relocations are different from ELF ones, but still), yet the standard C setup you’ll find on OSDev Wiki or similar is, in fact, quite annoying to use.
I've never actually used gcc to target UEFI, but under Clang and other LLVM based compilers it really is just a few flags and header files. If I recall correctly my little demo linked above predates any sort of UEFI support in zig and just uses what LLVM provides, you can see how straightforward the whole thing is in the makefile (the little bit of assembly is unrelated to UEFI): https://github.com/liampwll/zig-efi-os/blob/master/Makefile
That struct contains a function pointer which you can call to get other structs known as protocols. Those protocols contain function pointers which you use for everything else
That's what I mean by "obtusely indirect".
if all you want to do is textual IO you only need a few hundred lines of header files
"only need a few hundred lines"? That's huge in comparison to MS-DOS.
They could've made UEFI almost like MS-DOS in API too, just with a flat address space. Yet they came up with a monstrosity far more complex.
> Especially when AFAIK you need to use a rather large toolchain to access the obtusely indirect APIs.
Weight that (literally only a couple macros) versus the idea of having to dig out a 16-bit toolchain and/or a DOS extender which also brings its own obtusely indirect APIs with it. In case you decide to do the former, you'll probably have to rewrite big chunks of your program in order to rearchitect it around XMS. In case you decide to do the latter, you'll be able to use a 32-bit toolchain, but you'll have to write your own drivers for high-resolution graphics, sound, and depending on your luck, mouse. All of this UEFI brings for free, and even for recent hardware.
I'm not so sure about that. Especially when AFAIK you need to use a rather large toolchain to access the obtusely indirect APIs.