Implementing Stdio for UEFI
Hello everyone. In this post, I will go through how I implemented the stdio module for UEFI. The implementation code can be found here.
UEFI Protocols
These are the protocols we will be using for implementing stdio. According to the spec, at least a blank implementation of these protocols should be present.
Simple Text Input Protocols
Interface Structure
typedef struct _EFI_SIMPLE_TEXT_INPUT_PROTOCOL EFI_SIMPLE_TEXT_INPUT_PROTOCOL;
Description
This protocol allows taking console input one character at a time.
Simple Text Output Protocol
Interface Structure
typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
Description
This protocol allows writing UCS-2 strigs to console. This protocol will be used to implement both Stdout and Stderr.
SystemTable
Interface Structure
typedef struct EFI_SYSTEM_TABLE;
Description
SystemTable is passed at the entry function. It provides the pointers to access the Stdin, Stdout, and Stderr.
Stdout
I am using the ConOut
pointer from the SystemTable for Stdout implementation. The signature of io::Write::write
:
;
As you can see, we are provided with a buffer containing a UTF-8 bytes slice. I have added some functions as a part of std::sys_common::ucs2
that make the conversion of UTF-8 to UCS-2 pretty easy. It also replaces all the unsupported characters (4-byte UTF-8 characters) with the Unicode unsupported character (U+fffd).
Currently, I am using a fixed-length slice (length = 4096) for the UCS-2 string since this is what Windows and other OS seem to do. Technically, it is possible to use heap allocation here as well; I have just decided not to use allocation inside the standard library until I need to.
Finally, I am also converting LF (Rust default) to CRLF. This allows having the same behavior when using Rust printing macros as in other operating systems.
Stderr
The implementation of Stderr is the same as Stdout. The only difference is that I use the StdErr
pointer here.
Stdin
I am using the ConIn
pointer from SystemTable for Stdin implementation. The signature for io::Read::read
:
;
As you can see, we are supplied with the buffer in which we have to write the UTF-8 formatted bytes. This function should return the number of bytes read. Since the Standard Input Protocol only provides reading a single keystroke, I simply read a single UCS-2 character each time (which can be multiple bytes) and return. This works fine for the most part. However, it does have one drawback; there is line editing support. This means currently, while reading, you cannot edit any mistakes. While it might seem like line editing works, that’s just because it reads and stores the backspace
symbol while reading. I might try implementing line editing at some point. However, for now, I think this single-character read implementation is acceptable.
Note: Reading and returning a single character from this method does not mean that the Rust std::io
functions will stop after reading a single character. All the methods like readline
, etc. work how they are supposed to.
I am also printing every character read to Stdout to provide feedback (since most OSs do this).
Finally, this function converts CR (enter
seems to be read as CR in UEFI) to LF. This is needed because all the Rust io functions are designed around LF.
Conclusion
This is just a brief overview of the implementation of stdio for UEFI. I tried not to put too much code here since I am making many changes, and the code becomes obsolete as soon as I publish the post. All my work for UEFI std can be found here. Feel free to test it out for yourself.
Consider supporting me if you like my work.