Reader small image

You're reading from  Practical System Programming for Rust Developers

Product typeBook
Published inDec 2020
PublisherPackt
ISBN-139781800560963
Edition1st Edition
Tools
Right arrow
Author (1)
Prabhu Eshwarla
Prabhu Eshwarla
author image
Prabhu Eshwarla

Prabhu Eshwarla has been shipping high-quality, business-critical software to large enterprises and running IT operations for over 25 years. He is also a passionate teacher of complex technologies. Prabhu has worked with Hewlett Packard and has deep experience in software engineering, engineering management, and IT operations. Prabhu is passionate about Rust and blockchain and specializes in distributed systems. He considers coding to be a creative craft, and an excellent tool to create new digital worlds (and experiences) sustained through rigorous software engineering.
Read more about Prabhu Eshwarla

Right arrow

Chapter 10: Working with Device I/O

In Chapter 6, Working with Files and Directories in Rust, we covered the details of how to perform file I/O operations (such as reading and writing to files) using the Rust Standard Library. In Unix-like operating systems, a file is an abstraction that is used to work not only with regular disk files (which are used to store data) but also with several types of devices that are connected to a machine. In this chapter, we will look at the features of the Rust Standard Library that enable us to perform reads and writes to any type of device (also called device I/O) in Rust. Device I/O is an essential aspect of system programming to monitor and control various types of devices attached to a computer, such as keyboards, USB cameras, printers, and sound cards. You may be curious to know what support Rust provides to a system programmer to handle all these different types of devices. We'll answer this question as we go through the chapter.

In this...

Technical requirements

Verify that rustup, rustc, and cargo have been installed correctly with the following command:

rustup --version
rustc --version 
cargo --version

The Git repo for the code in this chapter can be found at https://github.com/PacktPublishing/Practical-System-Programming-for-Rust-Developers/tree/master/Chapter10/usb.

For running and testing the project in this book, you must have the native libusb library installed where it can be found by pkg-config.

The project in this book has been tested on macOS Catalina 10.15.6.

For instructions on building and testing on Windows, refer: https://github.com/dcuddeback/libusb-rs/issues/20

For general instructions on environmental setup of libusb crate, refer to: https://github.com/dcuddeback/libusb-rs

Understanding device I/O fundamentals in Linux

In previous chapters, we saw how to schedule work on CPUs using processes and threads, and how to manage memory by controlling the memory layout of a program. In addition to the CPU and memory, the operating system also manages the system's hardware devices. Examples of hardware devices include keyboards, mice, hard disks, video adapters, audio cards, network adapters, scanners, cameras, and other USB devices. But the peculiarities of these physical hardware devices are hidden from the user programs by the operating system, using software modules called device drivers. Device drivers are indispensable software components for doing device I/O. Let's take a closer look at them.

What are device drivers?

Device drivers are shared libraries loaded into the kernel that contain functions to perform low-level hardware control. They communicate with the devices through the computer bus or communication subsystem to which the device...

Doing buffered reads and writes

Reads and writes are the fundamental operations performed on I/O types such as files and streams and are very crucial for working with many types of system resources. In this section, we'll discuss different ways to do reads and writes to I/O in Rust. We'll first cover the core traits – Read and Write – which allow Rust programs to perform read and write operations on objects that implement these traits (which are also called readers and writers). Then, we'll see how to do buffered reads and buffered writes, which are more efficient for certain types of read and write operations.

Let's start with the basic Read and Write traits.

In line with the everything-is-a-file philosophy, the Rust Standard Library provides two traits – Read and Write – which provide a general interface for reading and writing inputs and outputs. This trait is implemented for different types of I/O, such as files, TcpStream,...

Working with standard input and output

In Linux/Unix, streams are communication channels between a process and its environment. By default, three standard streams are created for every running process: standard input, standard output, and standard error. A stream is a communication channel that has two ends. One end is connected to the process and the other end to another system resource. For example, a standard input can be used by a process to read characters or text from a keyboard or another process. Similarly, a standard output stream can be used by a process to send some characters to the terminal or to a file. In many modern programs, the standard error of a process is connected to a log file, which makes analyzing and debugging errors easier.

The Rust Standard Library provides methods to interact with standard input and output streams. The Stdin struct in the std::io module represents the handle to the input stream of a process. This handle implements the Read trait, which...

Chaining and iterators over I/O

In this section, we'll look at how to use iterators and chaining with the std::io module.

Many of the data structures provided by the std::io module have built-in iterators. Iterators let you process a series of items, such as lines in a file or incoming network connections on a port. They provide a nicer mechanism compared to while and for loops. Here is an example of using the lines() iterator with the BufReader struct, which is a part of the std::io module. This program reads lines from the standard input stream in a loop:

use std::io::{BufRead, BufReader};
fn main() {
    // Create handle to standard input
    let s = std::io::stdin();
    //Create a BufReader instance to optimize sys calls
    let file_reader = BufReader::new(s);
    // Read from standard input line-by-line
    for single_line in file_reader.lines(...

Handling errors and returning values

In this section, we'll learn about the built-in error handling support in the std::io module. Handling recoverable errors in an appropriate manner makes Rust programs more robust.

In the code examples we've seen so far, we've used the unwrap() function to extract the return value from the std::io module methods and associated functions, such as Read, Write, BufReader, and BufWriter. However, this is not the correct way to handle errors. The std::io module has a specialized Result type that is returned from any function or method in this module that may produce an error.

Let's rewrite the previous example (of chaining readers) using the io::Result type as the return value from the function. This allows us to use the ? operator to directly pass errors back from the main() function, instead of using the unwrap() function:

use std::fs::File;
use std::io::Read;
fn main() -> std::io::Result<()> {
   ...

Getting details of connected USB devices (project)

In this section, we will demonstrate an example of working with devices in Rust. The example chosen is to display details of all connected USB devices of a computer. We'll be using libusb, a C library that helps to interact with USB devices. The libusb crate in Rust is a safe wrapper around the C libusb library. Let's first look at the design.

Designing the project

Here is how this would work:

  • When a USB device is plugged into a computer, the electrical signals on the computer bus trigger the USB controller (hardware device) on the computer.
  • The USB controller raises an interrupt on the CPU, which then executes the interrupt handler registered for that interrupt in the kernel.
  • When a call is made from the Rust program through the Rust libusb wrapper crate, the call is routed to the libusb C library, which in turn makes a system call on the kernel to read the device file corresponding to the USB device...

Summary

In this chapter, we reviewed the foundational concepts of device management in Unix/Linux. We looked at how to do buffered reads and writes using the std::io module. We then learned how to interact with the standard input, standard output, and standard error streams of a process. We also saw how to chain readers together and use iterators for reading from devices. We then looked at the error handling features with the std::io module. We concluded with a project to detect the list of connected USB devices and printed out the details of each USB device both to the terminal and to an output file.

The Rust Standard Library provides a clean layer of abstraction for doing I/O operations on any type of device. This encourages the Rust ecosystem to implement these standard interfaces for any type of device, enabling Rust system programmers to interact with different devices in a uniform manner. Continuing on the topic of I/O, in the next chapter, we will learn how to do network...

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Practical System Programming for Rust Developers
Published in: Dec 2020Publisher: PacktISBN-13: 9781800560963
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Prabhu Eshwarla

Prabhu Eshwarla has been shipping high-quality, business-critical software to large enterprises and running IT operations for over 25 years. He is also a passionate teacher of complex technologies. Prabhu has worked with Hewlett Packard and has deep experience in software engineering, engineering management, and IT operations. Prabhu is passionate about Rust and blockchain and specializes in distributed systems. He considers coding to be a creative craft, and an excellent tool to create new digital worlds (and experiences) sustained through rigorous software engineering.
Read more about Prabhu Eshwarla