Fork me on GitHub

What

What we are trying to achieve

What’s missing in our CustomOS is a graphical terminal. Up until now we used the text mode of the BIOS, but it can’t be used to display image, colours or a graphical user interface.

We’ve seen in Pt.6 how we can access a raw graphics buffer and set invidiual pixels. Using this text can be displayed by displaying a pre-rendered bitmap font.

With that we can write our own graphical terminal that renders the characters to the screen. We will need to handle rendering the font, line breaks, line feed and carriage return ourselves.

When

In the last part of the series we’ve seen how code written in Rust can be built and interfaced with C. We can build on that knowledge now.

Why

Apart from being an interesting new langauge Rust is particularly useful for our use case when it comes to buffer overflow protection. While a bit tedious to develop it actively prevents the developer from accidently writing out of bounds of arrays.

With some programming effort we can display a graphical terminal that accepts user input and calls C functions that come back to Rust and print to the graphical terminal.

Contents

Contents

Background

Available Resolution and Colours

In the BIOS mode I’m using I’ve got 320x200 pixels available. I’m using 16-bit color.

One of the first things to think about is how to use the limited space and the limited number of availble colours.

We only have limited colours available in VGA 16-Bit


(Image taken from [1])

But as a starting point I had a look at the current Gnome Terminal color scheme used in Ubuntu 22.04 that I’ve come to appreciate

and simply aim for most similar colour.

How

Interfacing with C

In order to interface C with Rust we need to do a lot of type conversion. There are many Rust libraries to do this such as the “Foreign Function Interface” with it’s std::ffi::CString class, but the CustomOS is intentionally limited in functionally and lacks exception handling and a heap and most notably dynamic memory allocation (so now malloc, free).

To keep the OS as lean as possible I’m trying to see how much can be implemented without these features. This means many String-Functions built into Rust’s standard library won’t compile due to missing symbols.

Calling Rust from C

In order to safely call a Rust function from C we need to mark it with #[no_mangle] to prevent the Rust compiler from name mangling it. It has to be public pub to be accessable and extern "C" to make it available to C.

Additionally all paramters need to be C compatible. The datatype cty::c_char in Rust is compatible with a C-Char.

#[no_mangle]
pub unsafe extern "C" fn shellKeyboardDown(mut key: cty::c_char, scancode: u8) { 
  if key as u8 == 10 { // newline

Arrays

Arrays seem a bit backward in defintion, but are okay once gotten used to.

Where a two dimensional Array in C looks like this:

char fontSourceCodeProLight10pt_buffer[93][14] = {
{0x00, 0x04, 0x04,

In Rust it becomes something like this:

pub const FONT_SOURCECODEPRO_LIGHT_10PT_BUFFER: &[[u8;14];93] = &[
[0x00, 0x04, 0x04,

Notice how row and column are swapped in order. Rust has a built-in linter that causes warnings if something other than snake-case and capital letters are used. C uses curly braces “{“, for Rust square brackets were chosen “[”. I guess it just had to be made different.

Dealing with Chars from C

C-Chars can be used from Rust by casting them over by as u8 as char.

if key as u8 as char == 'y' {

Dealing with Char Arrays from C

In order to get things working I’ll receive C chars cty::c_char or pointers to C-Char Array buffer: *mut cty::c_char and thoughtfully access them with Rusts built-in pointer::offset()-functions, then cast the result as u8 as char to get a Rust char.

I managed to get around most of that by simply sending character by character from C. Conveniently we already had the _put_char(char ch) function in C that handles all the output from the custom implementations of printf, snprintf, etc in C

Sending Rust str to C

The other way around is a bit more involved.

The following snippet does work for my use cases, but we need to keep in mind that Rust doesn’t null-terminate strings.

let mystring: str = "Testing attention please";
let bytes: &[u8] = mystring.as_bytes();
let name: *const cty::c_char = bytes.as_ptr() as *const i8; // Caution: this C-Char-Array is not null-terminated

ataList(name); // call a C function

The “unsafe” keyword

All of the above, due to raw pointer access and char conversion means that essentially the entire code has to be marked “unsafe” in Rust.

This is not ideal by any means, but is easily done by extending the function defintions with the unsafe keyword.

unsafe fn shellCheckCommand

Out of bounds checking

Where Rust really excels is for Out of bounds checking. For implementing a Terminal there are a lot of low-level pixel buffer/char array manipulations. Especially when iterating over arrays of char pointers it is far too easy to run out of bounds and overwrite other memory locations.

Each time I forgot to manually check the bounds

if SHELL_TEXT_POSITION > SHELL_TEXT_BUFFER.len() - 1 {
if SHELL_TEXT_POSITION < 0 {

Rust would immediatly try to pull in _ZN4core9panicking18panic_bounds_check17h17c53c20976be3aeE and the compiler would fail due to missing symbols.

It’s easy to find the culprit by commenting out code and then adding such out-of-bounds checks. The compiler will then build the code and as a developer I know I haven’t accidently overwritten anything.

Font Rendering

Initially I had used the “Roboto Mono” Font, but it isn’t designed to render to 6x14 pixels.

Later I switched to “Source Code Pro”. That looked a lot better.

I’ve spent quite some time to get my Python pre-rendering script to properly align the characters in the pixel buffer. Due to rounding errors by a single pixel the text was all over the place as seen below.

Eventually I got it right by writing the entire Char-Set into one wide image with Python’s PIL-library and then chopping it up to invidual images that were then convertered to *.xbm format to get C-Arrays and from there rewritten to Rust-Arrays by Python script.

With that I was eventually able to properly display the characters at the right height and without cutting of the bottom half of the letter ‘g’ or top half of ‘T’.

Progress

Conclusion

So far we’ve already achieved a lot. The CustomOS is now getting close in capabilities to an early version of DOS or Commodore DOS.

Looking back at the goals I had defined in Part 1 we’ve reached the most important ones:

I’ve added in

Additionally I had planed to

There are also a number smaller fixes that can be made

And some new ideas


1] https://lospec.com/palette-list/microsoft-vga