Daniel Drywa

Game 0 - Part 2: Hello, world!

Printing text on the screen

The progress so far

In my previous post, I set-up the Cargo package for the video game. At the moment, it only contains a single function – the entry point of my program. This function prints the line Hello, world! to the terminal.

fn main() {
    println!( "Hello, world!" );
}

In the end, I was left with the question of what it means to print to the terminal. In order to answer this question I have to start by taking a look at History.

The Terminal

In the early computing days, electromechanical teleprinters/teletypewriters provided a user interface to mainframe computers. Unlike keyboards today, these terminals were custom build for each type of mainframe computer and worked only with that specific type of computer. The user could send instructions via typed commands or paper slips to the mainframe computer, which would then print the result on paper.

ASR-33 Teletype terminal IMG 1658.jpg
Photograph by Rama, Wikimedia Commons, Cc-by-sa-2.0-fr, CC BY-SA 2.0 fr, Link

As time advanced, teleprinters evolved into computer terminals with a Video Display Unit (VDU). These computer terminals looked much closer to what desktop computers look like today. The main difference to the older terminals was that instead of printing the result of a computation on paper (albeit still being possible), the VDU would be used to display the results on the screen. From a programmers perspective, displaying characters on a screen was very similar to printing on paper. No matter where the output would be displayed in the end, it was still called printing.

IBM 2260 video display terminal.jpg
By David L. Mills, PhD - http://www.eecis.udel.edu/~mills/gallery/gallery8.html, Public Domain, Link

Text-based user interfaces were eventually replaced by graphical user interfaces allowing users to interact with electronic devices through graphical icons and visual indicators. However, the need to communicate with older machines or use older programs remained, so terminal emulators were created to emulate text-based user interfaces within a graphical user interface. The default GNOME Terminal used in Fedora is such an emulator.

A running instance of the GNOME Terminal emulator on Fedora
Screenshot by Daniel Drywa

Printing "Hello, world!"

The print in println! means that a given sequence of characters will be printed to the terminal. In my case this means the text output of the GNOME Terminal emulator. ln stands for newline, meaning Hello, world! will be printed with a LINE FEED character at the end. Thus, the next print instruction will print the text on the next line, below the previous Hello, world! message. If I were to use print! instead, which doesn't add a newline character to the end of my message, the output would look like this:

[daniel@satopc game0]$ cargo run
   Compiling game0 v0.1.0 (/home/daniel/Projects/game0)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/game0`
Hello, world![daniel@satopc game0]$

As a reminder, the output with println! looks like this:

[daniel@satopc game0]$ cargo run
   Compiling game0 v0.1.0 (/home/daniel/Projects/game0)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/game0`
Hello, world!
[daniel@satopc game0]$

But how does println! print my message to the terminal? Does it not need to know what kind of terminal emulator I am using? To answer this question, I start by taking a look at the documentation of println! that states:

Macro for printing to the standard output, with a newline.

The clue I am looking for here is standard output. What is the standard output?

Standard streams

As computers evolved, so did input/output devices. Various printers, terminals, and video display units could be attached to a computer, and a computer program had to be written for each combination of devices. A program written for a specific combination of devices would not work for another. To spare programmers this tedious task of writing specialised programs, Unix introduced the concept of abstract devices. A program written for Unix didn't need to know what kind of devices it was communicating with. To read input from a device, such as a keyboard, a program would read the input from a abstract device connection. To print characters on paper or the video display, a program would write its output to another abstract device connection. These connections were called Standard streams.

Unix introduced three standard streams:

  1. Standard input (stdin) - The data sent to a process.
  2. Standard output (stdout) - The data returned by a process.
  3. Standard error (stderr) - The error data returned by a process.
Stdstreams-notitle.svg
By Danielpr85 based on Graphviz source of TuukkaH - (Own work), Public Domain, Link

On modern Linux distributions, the standard streams are still available and used for I/O in terminal emulators. In typical Unix fashion, they are file descriptors – an abstract indicator or handle used to access a file or other I/O resources. In Unix based systems, file descriptors are simple integer values used as identifiers for an opened I/O resources. These values correspond to the ones listed above.

Dissecting println!

Now I know that println! is printing a given sequence of characters to standard output. This small function seems to be doing a lot for me. Time to take a look at what is happening inside.

First thing to understand is that println! is not a function, but a macro. A macro will be replaced by a block of code during compile time. In the case of println!, the macro definition is as follows:

macro_rules! println {
    () => (print!("\n"));
    ($($arg:tt)*) => ({
        $crate::io::_print(format_args_nl!($($arg)*));
    })
}

The details of how macro_rules! works are not important right now, but what I can see is the code that println! will be replaced with. Similar to the Rust match syntax, I can see two branches. The first one is for an empty call, such as println!(), and the second is for any other non-empty call. Thus, println!( "Hello, world!" ); will be replaced by the second branch:

::io::_print( format_args_nl!( "Hello, world!" ) );

$crate and $($arg)* are macro variables resolved during compile time. However, I can't use the above line of code directly because _print and format_args_nl! are for internal use only. Trying to do so will result in the following errors:

error[E0658]: `format_args_nl` is only for internal language use and is subject to change
 --> src/main.rs:2:22
  |
2 |     std::io::_print( format_args_nl!( "Hello, world!" ) );
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0658]: use of unstable library feature 'print_internals': implementation detail which may disappear or be replaced at any time
 --> src/main.rs:2:5
  |
2 |     std::io::_print( format_args_nl!( "Hello, world!" ) );
  |  

format_args_nl! is a macro responsible for converting all arguments into a single string, and append it with a LINE FEED character. Going into the details of this macro is out of scope for now. However, _print is a function:

pub fn _print(args: fmt::Arguments) {
    print_to(args, &LOCAL_STDOUT, stdout, "stdout");
}

_print calls a more complex function, named print_to, which tries to write the given args to the LOCAL_STDOUT instance. If it is not able to do so, it will use the global stdout instance instead. Both instances implement the std::fmt::Write trait, which includes the write method responsible for writing to the stream.

The write implementation depends on the operating system I am using to compile my program. Since I am using a Unix-like operating system, it will use the Unix implementation of this method:

pub fn write(&self, data: &[u8]) -> io::Result<usize> {
    let fd = FileDesc::new(libc::STDOUT_FILENO);
    let ret = fd.write(data);
    fd.into_raw();
    ret
}

Here I can see that a file descriptor is being accessed and data is written to it. The STDOUT_FILENO constant is an integer with the value 1 referring to the Standard output (stdout). The data will be my Hello, world! message. fd.into_raw() removes the file descriptor from the fd instance without closing it. The standard stream file descriptors should be kept open for the entire runtime of the operating system.

The println! macro is doing a lot for me, but I could write to the standard output without using it. There are two different ways of doing this:

Printing without a Macro (Standard I/O)

One way of printing text to standard output without the use of the println! macro is to use the same standard I/O types used internally by the macro.

Calling stdout() returns the std::io::Stdout instance I need. Stdout implements the std::io::Write trait containing the write method able to write a slice of bytes to the output stream. The final program would look like this:

use std::io::{ self, Write };

fn main() {
    let mut stdout = io::stdout();

    stdout.write( b"Hello, world!\n" ).unwrap();
}

This is a very simplified form of what the println! macro is doing. stdout.write will retrieve the file descriptor and write to it internally. On other operating systems, it will use a different method for writing to the standard output stream.

Printing without a Macro (file descriptor)

Another way of printing text to standard output without the use of the println! macro is to access the file descriptor directly. This however only works on Unix-like operating systems.

For this way of printing, I can't reuse the code from the Rust implementation. Given most types and functions are for internal use only, my alternative is to use the provided Rust APIs to access file descriptors via the std::os::unix::io module.

First, I have to specify a constant for the file descriptor I want to access. A file descriptor is just an integer and the standard streams have fixed file descriptors. To access stdout, I need a constant with the value 1:

const FD_STDOUT: io::RawFd = 1;

Next, I have to open a std::fs::File handle to write to the file descriptor. However, I can't just open any file handle. I have to open a handle for the stdout file descriptor 1. This can be done via the std::os::unix::io::FromRawFd trait containing the from_raw_fd method. This method is marked as unsafe, meaning I have to put it in an unsafe block:

unsafe {
    stdout = io::FromRawFd::from_raw_fd( FD_STDOUT );
}

Now I can write my message with the std::io::Write trait just like before. The whole program looks like this:

use std::os::unix::io;
use std::io::{ Write };

const FD_STDOUT: io::RawFd = 1;

fn main() {
    let mut stdout: std::fs::File;

    unsafe {
        stdout = io::FromRawFd::from_raw_fd( FD_STDOUT );
    }

    stdout.write( b"Hello, World!\n" ).unwrap();
}

Summary

Printing text to the terminal is done by writing characters to a standard stream. The stream is responsible for talking to the hardware to print the characters on the screen. Rust provides macros that abstract all work to be done for this. It is possible to achieve the same outcome without the use of the macro, but it implies more work and less flexibility.

Next time, I will be looking at the graphical capabilities of the computer and how the standard streams relate to them.

Related Posts

Resources


31 March 2019Talk to me