OS in Rust: Incorporate VGA buffer: Part-6

Knoldus Blog Audio
Reading Time: 5 minutes

This series pertains to create a basic Operating System using Rust Programming Language. This series aims to learn and understand the basics of the Operating System.
Through this, you will get some ideas about the internal components of the Operating System and how they interact with each other.

This article pertains to the incorporation of VGA text mode in our kernel. Here we’ll create an interface through which we can use its functionality to print something on a screen.

This image has an empty alt attribute; its file name is os.jpeg

If you are new to this series, then it’d be great if you have a look at the previous posts so that you can get an idea of where we started and how much we have covered till now.

To print something on the screen in a VGA text mode, we need to write our text in the VGA text buffer.

VGA text buffer

VGA text buffer is a two-dimensional array with typically 25 rows and 80 columns, which is directly rendered to the screen. Each character on screen is represented by two bytes or by 16 bits.
8 bits are used to represent the attribute of character and remaining 8 represents code point for the character set. .Every character will have a code point defined as per Unicode standards.
For example ‘A’ will be ‘U+0041’ There are UTF-8, UTF-16 etc.
In short, first byte represents the character and second byte defines how a character should be displayed, in which the first 4 bits defined the foreground color, next three bits define background color and the last bit defines the blink property.

We can read and write VGA text buffer through normal memory operations in a particular address because it is accessible via memory-mapped I/O to the address 0xb8000.

Now, let’s implement this in our kernel.
To implement this we need to follow few steps:

  • Create Rust module and define colors
  • Provide color code
  • Provide text buffer
  • Create Writer
  • Code in Action

Create Rust module and define colors

As we all know, to create a module in Rust we need to use the mod keyword. So let’s create a module that will be handling the printing of text on a screen.
We’ll create a binary module by adding this line to the main.rs file.

mod vga_buffer;

Now let’s define different colors in this module.

#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
    Black = 0,
    Blue = 1,
    Green = 2,
    Cyan = 3,
    Red = 4,
    Magenta = 5,
    Brown = 6,
    LightGray = 7,
    DarkGray = 8,
    LightBlue = 9,
    LightGreen = 10,
    LightCyan = 11,
    LightRed = 12,
    Pink = 13,
    Yellow = 14,
    White = 15,
}


We’ve used enum to define colors and because of repr(u8) attribute, each enum variant will store as an u8.
We’ve provided the #[allow(dead_code)] attribute, which will restrict the compiler to throw a warning for unused variant and dervied Copy, clone, Debug, PartialEq & Eq traits, which will enable copy semantics for the type.

Now, let’s provide the implementation for color code that specified foreground and background color.

Provide color code

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);

impl ColorCode {
    fn new(foreground: Color, background: Color) -> ColorCode {
        ColorCode((background as u8) << 4 | (foreground as u8))
    }
}

This structure contains foreground as well as background color and we used repr(transparent) attribute to ensure it has the same data.

Okay, we provided the color code now it’s time to provide a structure that represents a screen character and the text buffer.

Provide text buffer

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
    character: u8,
    color_code: ColorCode,
}

const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;

#[repr(transparent)]
struct Buffer {
    chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}

Okay, we’ve provided a structure that represents screen character and the next structure is for text buffer.
The first line is self-explanatory, and the repr(c) attribute guarantees that the struct’s fields are laid out exactly like in a C struct and thus guarantees the correct field ordering. And in the ScreenChar we provided “character” & “color_code” which represent the screen character.
As we know VGA text buffer is a two-dimensional array with 25 rows and 80 columns and to represent this we provided BUFFER_HEIGHT & BUFFER_WIDTH.
Now, if we talk about Buffer struct, we’ve used the repr(transparent) attribute just to ensure that it has the same memory layout as its single field. And inside this structure, we’ve provided one field only which talks about the data.

Now, let’s create a writer which writes the character on the screen.

Create Writer

Before creating a writer, let’s understand whats the role of the writer, and as its name suggested it prints or writes the value on the screen. So one question that arises here is what the properties or attributes we need in our writer?

As it will write something so the first thing we need is the data, the next thing will be the color of the data and the last will be the column position like where we need to write it we don’t need the row position because the writer will always write to the last line.
Let’s create it.

pub struct Writer {
    column_position: usize,
    color_code: ColorCode,
    buffer: &'static mut Buffer,
}

The above snippet defines the Writer structure which has column_position, color_code, & buffer.
Column_position is usize, color_code is the structure that we defined above and the buffer is also a structure and it is the actual data that we need to write. Okay, we’ve defined the writer structure now it’s time to provide the implementation of the writer.

pub fn write_byte(&mut self, byte: u8) {
        let row = BUFFER_HEIGHT - 1;
        let col = self.column_position;

        let color_code = self.color_code;
        self.buffer.chars[row][col].write(ScreenChar {
            character: byte,
            color_code,
        });
        self.column_position += 1;
    }

We’ve provided the implementation of the writer structure by adding one method for now which is write_byte. This method pertains to writing byte into the screen.
Initially, we are decreasing buffer height by 1 because the writer will always write to the last line, then we are fetching the column position, text color, after that we are assigning byte and color to the buffer.chars to print the data, and at last, we are incrementing column position to shift the pointer.

Now we are set to run our code, but before running our code we need to provide some inputs to the writer which will be displayed on the screen.

Code in Action

pub fn print_data() {
    let mut writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
    };

    writer.write_byte(b'K');
    writer.write_byte(b'N');
    writer.write_byte(b'O');
    writer.write_byte(b'L');
    writer.write_byte(b'D');
    writer.write_byte(b'U');
    writer.write_byte(b'S');
}

This function first creates a new Writer that points to the VGA buffer at 0xb8000.
Then we cast the integer 0xb8000 as a mutable raw pointer.
Then we convert it to a mutable reference by dereferencing it (through *) and immediately borrowing it again (through &mut).
This conversion requires an unsafe block since the compiler can’t guarantee that the raw pointer is valid.
And at last, we called the write_byte() method by providing a byte.

Okay, let’s call the print_data() function from _start() function.

#[no_mangle]
pub extern "C" fn _start() -> ! {
    vga_buffer::print_data();

    loop {}
}

After running this we got:

This image has an empty alt attribute; its file name is screenshot-from-2021-03-03-18-23-17.png

That’s all for this article, thanks for reading…

In the next article, we’ll try to add some more functionalities to our buffer writer.

Stay tuned!!!

References:

Blogs of this series:

If you want to read more content like this?  Subscribe Rust Times Newsletter and receive insights and latest updates, bi-weekly, straight into your inbox. Subscribe Rust Times Newsletter: https://bit.ly/2Vdlld7 .

This image has an empty alt attribute; its file name is rust-times.png

Knoldus-blog-footer-image

Written by 

Pawan Singh Bisht is a Software Consultant at Knoldus Software LLP, having a strong experience of more than two years in the technology field. He has been well versed in the core implementation of Rust and Java. He loves to contribute to the community which he attained from the community.

2 thoughts on “OS in Rust: Incorporate VGA buffer: Part-67 min read

  1. First of all, great article!

    Are you sure there is not anything missing?

    error: no method named `write` found for struct `ScreenChar` in the current scope
    label: method `write` not found for this

    Seems like we need this write implementation in the write_byte in Writer.

    Did you forget to add to the article or am I missing something?

    Thanks anyway.

Comments are closed.

%d bloggers like this: