Rust Web Development with Rocket

By Karuna Murti
    What do you get with a Packt Subscription?

  • Instant access to this title and 7,500+ eBooks & Videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Free Chapter
    Chapter 1: Introducing the Rust Language
About this book

Looking for a fast, powerful, and intuitive framework to build web applications? This Rust book will help you kickstart your web development journey and take your Rust programming skills to the next level as you uncover the power of Rocket - a fast, flexible, and fun framework powered by Rust.

Rust Web Development with Rocket wastes no time in getting you up to speed with what Rust is and how to use it. You’ll discover what makes it so productive and reliable, eventually mastering all of the concepts you need to play with the Rocket framework while developing a wide set of web development skills. Throughout this book, you'll be able to walk through a hands-on project, covering everything that goes into making advanced web applications, and get to grips with the ins and outs of Rocket development, including error handling, Rust vectors, and wrappers. You'll also learn how to use synchronous and asynchronous programming to improve application performance and make processing user content easy.

By the end of the book, you'll have answers to all your questions about creating a web application using the Rust language and the Rocket web framework.

Publication date:
June 2022
Publisher
Packt
Pages
420
ISBN
9781800561304

 

Chapter 1: Introducing the Rust Language

Almost every programmer has heard about the Rust programming language or even tried or used it. Saying "the Rust programming language" every time is a little bit cumbersome, so let's just call it Rust, or the Rust language from this point forward.

In this chapter, we will talk a little bit about Rust to help you if you are new to this language or as a refresher if you have tried it already. This chapter might also help seasoned Rust language programmers a bit. Later in the chapter, we will learn how to install the Rust toolchain and create a simple program to introduce the features of the Rust language. We will then use third-party libraries to enhance one of our programs, and finally, we will see how we can get help for the Rust language and its libraries.

In this chapter, we're going to cover the following main topics:

  • An overview of the Rust language
  • Installing the Rust compiler and toolchain
  • Writing Hello World
  • Exploring Rust crates and Cargo
  • Exploring other tools and where to get help
 

Technical requirements

To follow the content of this book, you will need a computer running a Unix-like operating system such as Linux, macOS, or Windows with Windows Subsystem for Linux (WSLv1 or WSLv2) installed. Don't worry about the Rust compiler and toolchain; we will install it in this chapter if you don't have it installed already.

The code for this chapter can be found at https://github.com/PacktPublishing/Rust-Web-Development-with-Rocket/tree/main/Chapter01.

 

An overview of the Rust language

To build web applications using the Rocket framework, we must first learn a bit about the Rust language since Rocket is built using that language. According to https://www.rust-lang.org, the Rust language is "a language empowering everyone to build reliable and efficient software." It began as a personal project for a programmer named Graydon Hoare, an employee at Mozilla, around 2006. The Mozilla Foundation saw the potential of the language for their product; they started sponsoring the project in 2009 before announcing it to the public in 2010.

Since its inception, the focus of Rust has always been on performance and safety. Building a web browser is not an easy job; an unsafe language can have very fast performance, but programmers working with system languages without adequate safety measures in place can make a lot of mistakes, such as missing pointer references. Rust was designed as a system language and learned many mistakes from older languages. In older languages, you can easily shoot yourself in the foot with a null pointer, and nothing in the language prevents you from compiling such mistakes. In contrast, in the Rust language, you cannot write a code that resulted in null pointer because it will be detected during compile time, and you must fix the implementation to make it compile.

A lot of the Rust language design is borrowed from the functional programming paradigm, as well as from the object-oriented programming paradigm. For example, it has elements of a functional language such as closures and iterators. You can easily make a pure function and use the function as a parameter in another function; there are syntaxes to easily make closures and data types such as Option or Result.

On the other hand, there are no class definitions, but you can easily define a data type, for example, a struct. After defining that data type, you can create a block to implement its methods.

Even though there is no inheritance, you can easily group objects by using traits. For example, you can create a behavior and name it the MakeSound trait. Then, you can determine what methods should be in that trait by writing the method signatures. If you define a data type, for example, a struct named Cow, you can tell the compiler that it implements a MakeSound trait. Because you say the Cow struct implements the MakeSound trait, you have to implement the methods defined in the trait for the Cow struct. Sounds like an object-oriented language, right?

The Rust language went through several iterations before a stable version was released (Rust 1.0) on May 15, 2015. Some of the early language design was scrapped before releasing the stable release. At one point, Rust had a class feature but this was scrapped before the stable release because Rust design was changed to have data and behavior separation. You write data (for example, in the form of a struct or enum type), and then you write a behavior (for example, impl) separately. To categorize those impl in the same group, we can make a trait. So, all the functionality you would want from an object-oriented language can be had thanks to that design. Also, Rust used to have garbage collection, but it was then scrapped because another design pattern was used. When objects get out of scope, such as exiting a function, they are deallocated automatically. This type of automatic memory management made garbage collection unnecessary.

After the first stable release, people added more functionalities to make Rust more ergonomic and usable. One of the biggest changes was async/await, which was released in version 1.39. This feature is very useful for developing applications that handle I/O, and web application programming handles a lot of I/O. Web applications have to handle database and network connections, reading from files, and so on. People agree that async/await was one of the most needed features to make the language suitable for web programming, because in async/await, the program doesn't need to make a new thread, but it's also not blocking like a conventional function.

Another important feature is const fn, a function that will be evaluated at compile-time instead of runtime.

In recent years, many large companies have started to build a talent pool of Rust developers, which highlights its significance in business.

Why use the Rust language?

So, why should we use the Rust language for web application development? Aren't existing established languages good enough for web development? Here are a few reasons why people would want to use the Rust language for creating web applications:

  • Safety
  • No garbage collection
  • Speed
  • Multithreading and asynchronous programming
  • Statically typed

Safety

Although writing applications using a system programming language is advantageous because it's powerful (a programmer can access the fundamental building block of a program such as allocating computer memory to store important data and then deallocating that memory as soon as it is not in use), it's very easy to make mistakes.

There's nothing in a traditional system language to prevent a program from storing data in memory, creating a pointer to that data, deallocating the data stored in memory, and trying to access the data again through that pointer. The data is already gone but the pointer is still pointing to that part of the memory.

Seasoned programmers might easily spot such mistakes in a simple program. Some companies force their programmers to use a static analysis tool to check the code for such mistakes. But, as programming techniques become more sophisticated, the complexity of the application grows, and these kinds of bugs can still be found in many applications. High-profile bugs and hacks found in recent years, such as Heartbleed, can be prevented if we use a memory-safe language.

Rust is a memory-safe language because it has certain rules regarding how a programmer can write their code. For example, when the code is compiled, it checks the lifetime of a variable, and the compiler will show an error if another variable still tries to access the already out-of-scope data. Ralf Jung, a postdoctoral researcher, already made the first formal verification in 2020 that the Rust language is indeed a safe language. Built-in data types, such as Option or Result, handle null-like behavior in a safe manner.

No garbage collection

Many programmers create and use different techniques for memory management due to safety problems. One of these techniques is garbage collection. The idea is simple: memory management is done automatically during runtime so that a programmer doesn't have to think about memory management. A programmer just needs to create a variable, and when the variable is not used anymore, the runtime will automatically remove it from memory.

Garbage collection is an interesting and important part of computing. There are many techniques such as reference counting and tracing. Java, for example, even has several third-party garbage collectors besides the official garbage collector.

The problem with this language design choice is that garbage collection usually takes significant computing resources. For example, a part of the memory is still not usable for a while because the garbage collector has not recycled that memory yet. Or, even worse, the garbage collector is not able to remove used memory from the heap, so it will accumulate, and most of the computer memory will become unusable, or what we usually call a memory leak. In the stop-the-world garbage collection mechanism, the whole program execution is paused to allow the garbage collector to recycle the memory, after which the program execution is resumed. As such, some people find it hard to develop real-time applications with this kind of language.

Rust takes a different approach called resource acquisition is initialization (RAII), which means an object is deallocated automatically as soon as it's out of scope. For example, if you write a function, an object created in the function will be deallocated as soon as the function exits. But obviously, this makes Rust very different compared to programming languages that deallocate memory manually or programming languages with garbage collection.

Speed

If you are used to doing web development with an interpreted language or a language with garbage collection, you might say that we don't need to worry about computing performance as web development is I/O bound; in other words, the bottleneck is when the application accesses the database, disk, or another network, as they are slower than a CPU or memory.

The adage might be primarily true but it all depends on application usage. If your application processes a lot of JSON, the processing is CPU-bound, which means it is limited by the speed of your CPU and not the speed of disk access or the speed of network connection. If you care about the security of your application, you might need to work with hashing and encryption, which are CPU-bound. If you are writing a backend application for an online streaming service, you want the application to work as optimally as possible. If you are writing an application serving millions of users, you want the application to be very optimized and return the response as fast as possible.

The Rust language is a compiled language, so the compiler will convert the program into machine code, which a computer processor can execute. A compiled language usually runs faster than an interpreted language because, in an interpreted language, there is an overhead when the runtime binary interprets the program into native machine code. In modern interpreters, the speed gap is reduced by using modern techniques such as a just-in-time (JIT) compiler to speed up the program execution, but in dynamic languages such as Ruby, it's still slower than using a compiled language.

Multithreading and asynchronous programming

In traditional programming, synchronous programming means the application has to wait until CPU has processed a task. In a web application, the server waits until an HTTP request is processed and responded to; only then does it go on to handle another HTTP request. This is not a problem if the application just directly creates responses such as simple text. It becomes a problem when the web application has to take some time to do the processing; it has to wait for the database server to respond, it has to wait until the file is fully written on the server, and it has to wait until the API call to the third-party API service is done successfully.

One way to overcome the problem of waiting is multithreading. A single process can create multiple threads that share some resources. The Rust language has been designed to make it easy to create safe multithreaded applications. It's designed with multiple containers such as Arc to make it easy to pass data between threads.

The problem with multithreading is that spawning a thread means allocating significant CPU, memory, and OS resources, or what is colloquially known as being expensive. The solution is to use a different technique called asynchronous programming, where a single thread is reused by different tasks without waiting for the first task to finish. People can easily write an async program in Rust because it's been incorporated into the language since November 7, 2019.

Statically-typed

In programming languages, a dynamically-typed language is one where a variable type is checked at runtime, while a statically-typed language checks the data type at compile time.

Dynamic typing means it's easier to write code, but it's also easier to make mistakes. Usually, a programmer has to write more unit tests in dynamically-typed languages to compensate for not checking the type at compile time. A dynamically-typed language is also considered more expensive because every time a function is called, the routine has to check the passed parameters. As a result, it's difficult to optimize a dynamically-typed language.

Rust, on the other hand, is statically-typed, so it's very hard to make mistakes such as passing a string as a number. The compiler can optimize the resulting machine code and reduce programming bugs significantly before the application is released.

Now that we have provided an overview of the Rust language and its strengths compared to other languages, let's learn how to install the Rust compiler toolchain, which will be used to compile Rust programs. We'll be using this toolchain throughout this book.

 

Installing the Rust compiler toolchain

Let's start by installing the Rust compiler toolchain. Rust has three official channels: stable, beta, and nightly. The Rust language uses Git as its version control system. People add new features and bug fixes to the master branch. Every night, the source code from the master branch is compiled and released to the nightly channel. After six weeks, the code will be branched off to the beta branch, compiled, and released to the beta channel. People will then run various tests in the beta release, most often in their CI (Continuous Integration) installation. If a bug is found, the fix will be committed to the master branch and then backported to the beta branch. Six weeks after the first beta branch-off, the stable release will be created from the beta branch.

We will use the compiler from the stable channel throughout the book, but if you feel adventurous, you can use the other channels as well. There's no guarantee the program we're going to create will compile if you use another channel though because people add new features and there might be regression introduced in the new version.

There are several ways to install the Rust toolchain in your system, such as bootstrapping and compiling it from scratch or using your OS package manager. But, the recommended way to install the Rust toolchain in your system is by using rustup.

The definition on its website (https://rustup.rs) is very simple: "rustup is an installer for the systems programming language Rust." Now, let's try following these instructions to install rustup.

Installing rustup on the Linux OS or macOS

These instructions apply if you are using a Debian 10 Linux distribution, but if you are already using another Linux distribution, we're going to assume you are already proficient with the Linux OS and can adapt these instructions suitable to your Linux distribution:

  1. Open your terminal of choice.
  2. Make sure you have cURL installed by typing this command:
    curl
  3. If cURL is not installed, let's install it:
    apt install curl

If you are using macOS, you will most likely already have cURL installed.

  1. After that, follow the instructions on https://rustup.rs:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. It will then show a greeting and information, which you can customize; for now, we're just going to use the default setup:
    ...
    1) Proceed with installation (default)
    2) Customize installation
    3) Cancel installation
    >
  3. Type 1 to use the default installation.
  4. After that, reload your terminal or type this in the current terminal:
    source $HOME/.cargo/env
  5. You can confirm whether the installation was successful or not by typing rustup in the Terminal and you should see the usage instruction for rustup.
  6. Now, let's install the stable Rust toolchain. Type the following in the terminal:
    rustup toolchain install stable
  7. After the toolchain has been installed into your OS, let's confirm whether we can run the Rust compiler. Type rustc in the terminal and you should see the instructions on how to use it.

Installing a different toolchain and components

Right now, we have the stable toolchain installed, but there are two other default channels that we can install: nightly and beta.

Sometimes, you might want to use a different toolchain for various reasons. Maybe you want to try a new feature, or maybe you want to test regression in your application against an upcoming version of Rust. You can simply install it by using rustup:

rustup toolchain install nightly

Each toolchain has components, some of which are required by the toolchain, such as rustc, which is the Rust compiler. Other components are not installed by default, for example, clippy, which provides more checks not provided by the rustc compiler and gives code style suggestions as well. To install it is also very easy; you can use rustup component add <component> as shown in this example:

rustup default stable
rustup component add clippy

Updating the toolchain, rustup, and components

The Rust toolchain has a regular release schedule of around every three months (six weeks plus six weeks), but sometimes there's an emergency release for a major bug fix or a fix for a security problem. As a result, you sometimes need to update your toolchain. Updating is very easy. This command will also update the components installed in the toolchain:

rustup update

Besides the toolchain, rustup itself might also be updated. You can update it by typing the following:

rustup self update

Now that we have the Rust compiler toolchain installed in our system, let's write our first Rust program!

 

Writing Hello World!

In this section, we are going to write a very basic program, Hello World!. After we successfully compile that, we are going to write a more complex program to see the basic capabilities of the Rust language. Let's do it by following these instructions:

  1. Let's create a new folder, for example, 01HelloWorld.
  2. Create a new file inside the folder and give it the name main.rs.
  3. Let's write our first code in Rust:
    fn main() { 
        println!("Hello World!");
    }
  4. After that, save your file, and in the same folder, open your terminal, and compile the code using the rustc command:
    rustc main.rs
  5. You can see there's a file inside the folder called main; run that file from your terminal:
    ./main
  6. Congratulations! You just wrote your first Hello World program in the Rust language.

Next, we're going to step up our Rust language game; we will showcase basic Rust applications with control flow, modules, and other functionalities.

Writing a more complex program

Of course, after making the Hello World program, we should try to write a more complex program to see what we can do with the language. We want to make a program that captures what the user inputted, encrypts it with the selected algorithm, and returns the output to the terminal:

  1. Let's make a new folder, for example, 02ComplexProgram. After that, create the main.rs file again and add the main function again:
    fn main() {}
  2. Then, use the std::io module and write the part of the program to tell the user to input the string they want to encrypt:
    use std::io;
    fn main() {
        println!("Input the string you want to encrypt:");
        let mut user_input = String::new();
        io::stdin()
            .read_line(&mut user_input)
            .expect("Cannot read input");
        println!("Your encrypted string: {}", user_input);
    }

Let's explore what we have written line by line:

  1. The first line, use std::io;, is telling our program that we are going to use the std::io module in our program. std should be included by default on a program unless we specifically say not to use it.
  2. The let... line is a variable declaration. When we define a variable in Rust, the variable is immutable by default, so we must add the mut keyword to make it mutable. user_input is the variable name, and the right hand of this statement is initializing a new empty String instance. Notice how we initialize the variable directly. Rust allows the separation of declaration and initialization, but that form is not idiomatic, as a programmer might try to use an uninitialized variable and Rust disallows the use of uninitialized variables. As a result, the code will not compile.
  3. The next piece of code, that is, the stdin() function, initializes the std::io::Stdin struct. It reads the input from the terminal and puts it in the user_input variable. Notice that the signature for read_line() accepts &mut String. We have to explicitly tell the compiler we are passing a mutable reference because of the Rust borrow checker, which we will discuss later in Chapter 9, Displaying User's Post. The read_line() output is std::result::Result, an enum with two variants, Ok(T) and Err(E). One of the Result methods is expect(), which returns a generic type T, or if it's an Err variant, then it will cause panic with a generic error E combined with the passed message.
  4. Two Rust enums (std::result::Result and std::option::Option) are very ubiquitous and important in the Rust language, so by default, we can use them in the program without specifying use.

Next, we want to be able to encrypt the input, but right now, we don't know what encryption we want to use. The first thing we want to do is make a trait, a particular code in the Rust language that tells the compiler what functionality a type can have:

  1. There are two ways to create a module: create module_name.rs or create a folder with module_name and add a mod.rs file inside that folder. Let's create a folder named encryptor and create a new file named mod.rs. Since we want to add a type and implementation later, let's use the second way. Let's write this in mod.rs:
    pub trait Encryptable {
        fn encrypt(&self) -> String;
    }
  2. By default, a type or trait is private, but we want to use it in main.rs and implement the encryptor on a different file, so we should denote the trait as public by adding the pub keyword.
  3. That trait has one function, encrypt(), which has self-reference as a parameter and returns String.
  4. Now, we should define this new module in main.rs. Put this line before the fn main block:
    pub mod encryptor;
  5. Then, let's make a simple type that implements the Encryptable trait. Remember the Caesar cipher, where the cipher substitutes a letter with another letter? Let's implement the simplest one called ROT13, where it converts 'a' to 'n' and 'n' to 'a', 'b' to 'o' and 'o' to 'b', and so on. Write the following in the mod.rs file:
    pub mod rot13;
  6. Let's make another file named rot13.rs inside the encryptor folder.
  7. We want to define a simple struct that only has one piece of data, a string, and tell the compiler that the struct is implementing the Encryptable trait. Put this code inside the rot13.rs file:
    pub struct Rot13(pub String);
    impl super::Encryptable for Rot13 {}

You might notice we put pub in everything from the module declaration, to the trait declaration, struct declaration, and field declaration.

  1. Next, let's try compiling our program:
    > rustc main.rs 
    error[E0046]: not all trait items implemented, missing: `encrypt`
     --> encryptor/rot13.rs:3:1
      |
    3 | impl super::Encryptable for Rot13 {}
      | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing 
      `encrypt` in implementation
      | 
     ::: encryptor/mod.rs:6:5
      |
    6 |     fn encrypt(&self) -> String;
      |     ----------------------------------------------
      ------ `encrypt` from trait
    error: aborting due to previous error
    For more information about this error, try `rustc --explain E0046`.

What is going on here? Clearly, the compiler found an error in our code. One of Rust's strengths is helpful compiler messages. You can see the line where the error occurs, the reason why our code is wrong, and sometimes, it even suggests the fix for our code. We know that we have to implement the super::Encryptable trait for the Rot13 type.

If you want to see more information, run the command shown in the preceding error, rustc --explain E0046, and the compiler will show more information about that particular error.

  1. We now can continue implementing our Rot13 encryption. First, let's put the signature from the trait into our implementation:
    impl super::Encryptable for Rot13 {
        fn encrypt(&self) -> String {
        }
    }

The strategy for this encryption is to iterate each character in the string and add 13 to the char value if it has a character before 'n' or 'N', and remove 13 if it has 'n' or 'N' or characters after it. The Rust language handles Unicode strings by default, so the program should have a restriction to operate only on the Latin alphabet.

  1. On our first iteration, we want to allocate a new string, get the original String length, start from the zeroeth index, apply a transformation, push to a new string, and repeat until the end:
    fn encrypt(&self) -> String {
        let mut new_string = String::new();
        let len = self.0.len();
        for i in 0..len {
            if (self.0[i] >= 'a' && self.0[i] < 'n') || 
            (self.0[i] >= 'A' && self.0[i] < 'N') {
                new_string.push((self.0[i] as u8 + 13) as 
                char);
            } else if (self.0[i] >= 'n' && self.0[i] < 
            'z') || (self.0[i] >= 'N' && self.0[i] < 'Z') 
            {
                new_string.push((self.0[i] as u8 - 13) as 
                char);
            } else {
                new_string.push(self.0[i]);
            }
        } 
        new_string
    }
  2. Let's try compiling that program. You will quickly find it is not working, with all errors being `String` cannot be indexed by `usize`. Remember that Rust handles Unicode by default? Indexing a string will create all sorts of complications, as Unicode characters have different sizes: some are 1 byte but others can be 2, 3, or 4 bytes. With regard to index, what exactly are we saying? Is index means the byte position in a String, grapheme, or Unicode scalar values?

In the Rust language, we have primitive types such as u8, char, fn, str, and many more. In addition to those primitive types, Rust also defines a lot of modules in the standard library, such as string, io, os, fmt, and thread. These modules contain many building blocks for programming. For example, the std::string::String struct deals with String. Important programming concepts such as comparison and iteration are also defined in these modules, for example, std::cmp::Eq to compare an instance of a type with another instance. The Rust language also has std::iter::Iterator to make a type iterable. Fortunately, for String, we already have a method to do iteration.

  1. Let's modify our code a little bit:
    fn encrypt(&self) -> String {
        let mut new_string = String::new();
        for ch in self.0.chars() {
            if (ch >= 'a' && ch < 'n') || (ch >= 'A' &&
            ch < 'N') {
                new_string.push((ch as u8 + 13) as char);
            } else if (ch >= 'n' && ch < 'z') || (ch >= 
            'N' && ch < 'Z') {
                new_string.push((ch as u8 - 13) as char);
            } else {
                new_string.push(ch);
            }
        }
        new_string
    }
  2. There are two ways of returning; the first one is using the return keyword such as return new_string;, or we can write just the variable without a semicolon in the last line of a function. You will see that it's more common to use the second form.
  3. The preceding code works just fine, but we can make it more idiomatic. First, let's process the iterator without the for loop. Let's remove the new string initialization and use the map() method instead. Any type implementing std::iter::Iterator will have a map() method that accepts a closure as the parameter and returns std::iter::Map. We can then use the collect() method to collect the result of the closure into its own String:
    fn encrypt(&self) -> Result<String, Box<dyn Error>> {
        self.0
            .chars()
            .map(|ch| {
                if (ch >= 'a' && ch < 'n') || (ch >= 'A' 
                && ch < 'N') {
                    (ch as u8 + 13) as char
                } else if (ch >= 'n' && ch < 'z') || (
                ch >= 'N' && ch < 'Z') {
                    (ch as u8 - 13) as char
                } else {
                    ch
                }
            })
            .collect()
    }

The map() method accepts a closure in the form of |x|.... We then use the captured individual items that we get from chars() and process them.

If you look at the closure, you'll see we don't use the return keyword either. If we don't put the semicolon in a branch and it's the last item, it will be considered as a return value.

Using the if block is good, but we can also make it more idiomatic. One of the Rust language's strengths is the powerful match control flow.

  1. Let's change the code again:
    fn encrypt(&self) -> String {
        self.0
            .chars()
            .map(|ch| match ch {
                'a'..='m' | 'A'..='M' => (ch as u8 + 13) 
                as char,
                'n'..='z' | 'N'..='Z' => (ch as u8 - 13) 
                as char,
                _ => ch,
            })
            .collect()
    }

That looks a lot cleaner. The pipe (|) operator is a separator to match items in an arm. The Rust matcher is exhaustive, which means that the compiler will check whether all possible values of the matcher are included in the matcher or not. In this case, it means all characters in Unicode. Try removing the last arm and compiling it to see what happens if you don't include an item in a collection.

You can define a range by using .. or ..=. The former means we are excluding the last element, and the latter means we are including the last element.

  1. Now that we have implemented our simple encryptor, let's use it in our main application:
    fn main() {
        ...
        io::stdin()
        .read_line(&mut user_input)
        .expect("Cannot read input");
        println!(
            "Your encrypted string: {}",
            encryptor::rot13::Rot13(user_input).encrypt()
        );
    }

Right now, when we try to compile it, the compiler will show an error. Basically, the compiler is saying you cannot use a trait function if the trait is not in the scope, and the help from the compiler is showing what we need to do.

  1. Put the following line above the main() function and the compiler should produce a binary without any error:
    use encryptor::Encryptable;
  2. Let's try running the executable:
    > ./main
    Input the string you want to encrypt:
    asdf123
    Your encrypted string: nfqs123
    > ./main
    Input the string you want to encrypt:
    nfqs123
    Your encrypted string: asdf123

We have finished our program and we improved it with real-world encryption. In the next section, we're going to learn how to search for and use third-party libraries and incorporate them into our application.

 

Packages and Cargo

Now that we know how to create a simple program in Rust, let's explore Cargo, the Rust package manager. Cargo is a command-line application that manages your application dependencies and compiles your code.

Rust has a community package registry at https://crates.io. You can use that website to search for a library that you can use in your application. Don't forget to check the license of the library or application that you want to use. If you register on that website, you can use Cargo to publicly distribute your library or binary.

How do we install Cargo into our system? The good news is Cargo is already installed if you install the Rust toolchain in the stable channel using rustup.

Cargo package layout

Let's try using Cargo in our application. First, let's copy the application that we wrote earlier:

cp -r 02ComplexProgram  03Packages
cd 03Packages
cargo init . --name our_package

Since we already have an existing application, we can initialize our existing application with cargo init. Notice we add the --name option because we are prefixing our folder name with a number, and a Rust package name cannot start with a number.

If we are creating a new application, we can use the cargo new package_name command. To create a library-only package instead of a binary package, you can pass the --lib option to cargo new.

You will see two new files, Cargo.toml and Cargo.lock, inside the folder. The .toml file is a file format commonly used as a configuration file. The lock file is generated automatically by Cargo, and we don't usually change the content manually. It's also common to add Cargo.lock to your source code versioning application ignore list, such as .gitignore, for example.

Let's check the content of the Cargo.toml file:

[package]
name = "our_package"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at
https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[[bin]]
name = "our_package"
path = "main.rs"

As you can see, we can define basic things for our application such as name and version. We can also add important information such as authors, homepage, repository, and much more. We can also add dependencies that we want to use in the Cargo application.

One thing that stands out is the edition configuration. The Rust edition is an optional marker to group various Rust language releases that have the same compatibility. When Rust 1.0 was released, the compiler did not have the capability to know the async and await keywords. After async and await were added, it created all sorts of problems with older compilers. The solution to that problem was to introduce Rust editions. Three editions have been defined: 2015, 2018, and 2021.

Right now, the Rust compiler can compile our package perfectly fine, but it is not very idiomatic because a Cargo project has conventions on file and folder names and structures. Let's change the files and directory structure a little bit:

  1. A package is expected to reside in the src directory. Let's change the Cargo.toml file [[bin]] path from "main.rs" to "src/main.rs".
  2. Create the src directory inside our application folder. Then, move the main.rs file and the encryptor folder to the src folder.
  3. Add these lines to Cargo.toml after [[bin]]:
    [lib]
    name = "our_package"
    path = "src/lib.rs"
  4. Let's create the src/lib.rs file and move this line from src/main.rs to src/lib.rs:
    pub mod encryptor;
  5. We can then simplify using both the rot13 and Encryptable modules in our main.rs file:
    use our_package::encryptor::{rot13, Encryptable};
    use std::io;
    fn main() {
        ...
        println!(
            "Your encrypted string: {}",
            rot13::Rot13(user_input).encrypt()
        );
    }
  6. We can check whether there's an error that prevents the code from being compiled by typing cargo check in the command line. It should produce something like this:
    > cargo check
        Checking our_package v0.1.0 
        (/Users/karuna/Chapter01/03Packages)
        Finished dev [unoptimized + debuginfo] target(s) 
        in 1.01s
  7. After that, we can build the binary using the cargo build command. Since we didn't specify any option in our command, the default binary should be unoptimized and contain debugging symbols. The default location for the generated binary is in the target folder at the root of the workspace:
    $ cargo build
       Compiling our_package v0.1.0 
       (/Users/karuna/Chapter01/03Packages)
        Finished dev [unoptimized + debuginfo] target(s) 
        in 5.09s

You can then run the binary in the target folder as follows:

./target/debug/our_package

debug is enabled by the default dev profile, and our_package is the name that we specify in Cargo.toml.

If you want to create a release binary, you can specify the --release option, cargo build --release. You can find the release binary in ./target/release/our_package.

You can also type cargo run, which will compile and run the application for you.

Now that we have arranged our application structure, let's add real-world encryption to our application by using a third-party crate.

Using third-party crates

Before we implement another encryptor using a third-party module, let's modify our application a little bit. Copy the previous 03Packages folder to the new folder, 04Crates, and use the folder for the following steps:

  1. We will rename our Encryptor trait as a Cipher trait and modify the functions. The reason is that we only need to think about the output of the type, not the encrypt process itself:
    • Let's change the content of src/lib.rs to pub mod cipher;.
    • After that, rename the encryptor folder as cipher.
    • Then, modify the Encryptable trait into the following:
      pub trait Cipher {
          fn original_string(&self) -> String;
          fn encrypted_string(&self) -> String;
      }

The reality is we only need functions to show the original string and the encrypted string. We don't need to expose the encryption in the type itself.

  1. After that, let's also change src/cipher/rot13.rs to use the renamed trait:
    impl super::Cipher for Rot13 {
        fn original_string(&self) -> String {
            String::from(&self.0)
        }
        fn encrypted_string(&self) -> String {
            self.0
                .chars()
                .map(|ch| match ch {
                    'a'..='m' | 'A'..='M' => (ch as u8 + 
                    13) as char,
                    'n'..='z' | 'N'..='Z' => (ch as u8 – 
                    13) as char,
                    _ => ch,
                })
                .collect()
        }
    }
  2. Let's also modify main.rs to use the new trait and function:
    use our_package::cipher::{rot13, Cipher};
    …
    fn main() {
        …
        println!(
            "Your encrypted string: {}",
            rot13::Rot13(user_input).encrypted_string()
        );
    }

The next step is to determine what encryption and library we want to use for our new type. We can go to https://crates.io and search for an available crate. After searching for a real-world encryption algorithm on the website, we found https://crates.io/crates/rsa. We found that the RSA algorithm is a secure algorithm, the crate has good documentation and has been audited by security researchers, the license is compatible with what we need, and there's a huge number of downloads. Aside from checking the source code of this library, all indications show that this is a good crate to use. Luckily, there's an install section on the right side of that page. Besides the rsa crate, we are also going to use the rand crate, since the RSA algorithm requires a random number generator. Since the generated encryption is in bytes, we must encode it somehow to string. One of the common ways is to use base64.

  1. Add these lines in our Cargo.toml file, under the [dependencies] section:
    rsa = "0.5.0"
    rand = "0.8.4"
    base64 = "0.13.0"
  2. The next step should be adding a new module and typing using the rsa crate. But, for this type, we want to modify it a little bit. First, we want to create an associated function, which might be called a constructor in other languages. We want to then encrypt the input string in this function and store the encrypted string in a field. There's a saying that all data not in processing should be encrypted by default, but the fact is that we as programmers rarely do this.

Since RSA encryption is dealing with byte manipulation, there's a possibility of errors, so the return value of the associated function should be wrapped in the Result type. There's no compiler rule, but if a function cannot fail, the return should be straightforward. Regardless of whether or not a function can produce a result, the return value should be Option, but if a function can produce an error, it's better to use Result.

The encrypted_string() method should return the stored encrypted string, and the original_string() method should decrypt the stored string and return the plain text.

In src/cipher/mod.rs, change the code to the following:

pub trait Cipher {
    fn original_string(&self) -> Result<String, 
    Box<dyn Error>>;
    fn encrypted_string(&self) -> Result<String, 
    Box<dyn Error>>;
}
  1. Since we changed the definition of the trait, we have to change the code in src/cipher/rot13.rs as well. Change the code to the following:
    use std::error::Error;
    pub struct Rot13(pub String);
    impl super::Cipher for Rot13 {
        fn original_string(&self) -> Result<String, 
        Box<dyn Error>> {
            Ok(String::from(&self.0))
        }
        fn encrypted_string(&self) -> Result<String, 
        Box<dyn Error>> {
            Ok(self
                .0
                ...
                .collect())
        }
    }
  2. Let's add the following line in the src/cipher/mod.rs file:
    pub mod rsa;
  3. After that, create rsa.rs inside the cipher folder and create the Rsa struct inside it. Notice that we use Rsa instead of RSA as the type name. The convention is to use CamelCase for type:
    use std::error::Error;
    pub struct Rsa {
        data: String,
    }
    impl Rsa {
        pub fn new(input: String) -> Result<Self, Box<
        dyn Error>> {
            unimplemented!();
        }
    }
    impl super::Cipher for Rsa {
        fn original_string(&self) -> Result<String, ()> {
           unimplemented!();
        }
        fn encrypted_string(&self) -> Result<String, ()> {
            Ok(String::from(&self.data))
        }
    }

There are a couple of things we can observe. The first one is the data field does not have the pub keyword since we want to make it private. You can see that we have two impl blocks: one is for defining the methods of the Rsa type itself, and the other is for implementing the Cipher trait.

Also, the new() function does not have self, mut self, &self, or &mut self as the first parameter. Consider it as a static method in other languages. This method is returning Result, which is either Ok(Self) or Box<dyn Error>. The Self instance is the instance of the Rsa struct, but we'll discuss Box<dyn Error> later when we talk about error handling in Chapter 7, Handling Errors in Rust and Rocket. Right now, we haven't implemented this method, hence the usage of the unimplemented!() macro. Macros in Rust look like a function but with an extra bang (!).

  1. Now, let's implement the associated function. Modify src/cipher/rsa.rs:
    use rand::rngs::OsRng;
    use rsa::{PaddingScheme, PublicKey, RsaPrivateKey};
    use std::error::Error;
    const KEY_SIZE: usize = 2048;
    pub struct Rsa {
        data: String,
        private_key: RsaPrivateKey,
    }
    impl Rsa {
         pub fn new(input: String) -> Result<Self, Box<
        dyn Error>> {
            let mut rng = OsRng;
            let private_key = RsaPrivateKey::new(&mut rng, 
            KEY_SIZE)?;
            let public_key = private_key.to_public_key();
            let input_bytes = input.as_bytes();
            let encrypted_data =
                public_key.encrypt(&mut rng, PaddingScheme
                ::new_pkcs1v15_encrypt(), input_bytes)?;
            let encoded_data = 
            base64::encode(encrypted_data);
            Ok(Self {
                data: encoded_data,
                private_key,
            })
        }
    }

The first thing we do is declare the various types we are going to use. After that, we define a constant to denote what size key we are going to use.

If you understand the RSA algorithm, you already know that it's an asymmetric algorithm, meaning we have two keys: a public key and a private key. We use the public key to encrypt data and use the private key to decrypt the data. We can generate and give the public key to the other party, but we don't want to give the private key to the other party. That means we must store the private key inside the struct as well.

The new() implementation is pretty straightforward. The first thing we do is declare a random number generator, rng. We then generate the RSA private key. But, pay attention to the question mark operator (?) on the initialization of the private key. If a function returns Result, we can quickly return the error generated by calling any method or function inside it by using (?) after that function.

Then, we generate the RSA public key from a private key, encode the input string as bytes, and encrypt the data. Since encrypting the data might have resulted in an error, we use the question mark operator again. We then encode the encrypted bytes as a base64 string and initialize Self, which means the Rsa struct itself.

  1. Now, let's implement the original_string() method. We should do the opposite of what we do when we create the struct:
    fn original_string(&self) -> Result<String, Box<dyn Error>> {
        let decoded_data = base64::decode(&self.data)?;
        let decrypted_data = self
            .private_key
            .decrypt(PaddingScheme::
            new_pkcs1v15_encrypt(), &decoded_data)?;
        Ok(String::from_utf8(decrypted_data)?)
    }

First, we decode the base64 encoded string in the data field. Then, we decrypt the decoded bytes and convert them back to a string.

  1. Now that we have finished our Rsa type, let's use it in our main.rs file:
    fn main() {
        ...
        println!(
            "Your encrypted string: {}",
            rot13::Rot13(user_input).encrypted_
            string().unwrap()
        );
        println!("Input the string you want to encrypt:");
        let mut user_input = String::new();
        io::stdin()
            .read_line(&mut user_input)
            .expect("Cannot read input");
        let encrypted_input = rsa::Rsa::new(
        user_input).expect("");
        let encrypted_string = encrypted_input.encrypted_
        string().expect("");
        println!("Your encrypted string: {}", 
        encrypted_string);
        let decrypted_string = encrypted_input
        .original_string().expect("");
        println!("Your original string: {}", 
        decrypted_string);
    }

Some of you might wonder why we redeclared the user_input variable. The simple explanation is that Rust already moved the resource to the new Rot13 type, and Rust does not allow the reuse of the moved value. You can try commenting on the second variable declaration and compile the application to see the explanation. We will discuss the Rust borrow checker and moving in more detail in Chapter 9, Displaying Users' Post.

Now, try running the program by typing cargo run:

$ cargo run
   Compiling cfg-if v1.0.0
   Compiling subtle v2.4.1
   Compiling const-oid v0.6.0
   Compiling ppv-lite86 v0.2.10
   ...
   Compiling our_package v0.1.0 
   (/Users/karuna//Chapter01/04Crates)
    Finished dev [unoptimized + debuginfo] target(s) 
    in 3.17s
     Running `target/debug/our_package`
Input the string you want to encrypt:
first
Your encrypted string: svefg
Input the string you want to encrypt:
second
Your encrypted string: lhhb9RvG9zI75U2VC3FxvfUujw0cVqqZFgPXhNixQTF7RoVBEJh2inn7sEefDB7eNlQcf09lD2nULfgc2mK55ZE+UUcYzbMDu45oTaPiDPog4L6FRVpbQR27bkOj9Bq1KS+QAvRtxtTbTa1L5/OigZbqBc2QOm2yHLCimMPeZKhLBtK2whhtzIDM8l5AYTBg+rA688ZfB7ZI4FSRm4/h22kNzSPo1DECI04ZBprAq4hWHxEKRwtn5TkRLhClGFLSYKkY7Ajjr3EOf4QfkUvFFhZ0qRDndPI5c9RecavofVLxECrYfv5ygYRmW3B1cJn4vcBhVKfQF0JQ+vs+FuTUpw==
Your original string: second

You will see that Cargo automatically downloaded the dependencies and builds them one by one. Also, you might notice that encrypting using the Rsa type took a while. Isn't Rust supposed to be a fast system language? The RSA algorithm itself is a slow algorithm, but that's not the real cause of the slowness. Because we are running the program in a development profile, the Rust compiler generates an application binary with all the debugging information and does not optimize the resulting binary. On the other hand, if you build the application using the --release flag, the compiler generates an optimized application binary and strips the debugging symbols. The resulting binary compiled with the release flag should execute faster than the debug binary. Try doing it yourself so you'll remember how to build a release binary.

In this section, we have learned about Cargo and third-party packages, so next, let's find out where to find help and documentation for the tools that we have used.

 

Tools and getting help

Now that we have created a pretty simple application, you might be wondering what tools we can use for development, and how to find out more about Rust and get help.

Tools

Besides Cargo, there are a couple more tools we can use for Rust application development:

  • rustfmt

This program is for formatting your source code so it follows the Rust style guide. You can install it by using rustup (rustup component add rustfmt). Then, you can integrate it with your favorite text editor or use it from the command line. You can read more about rustfmt at https://github.com/rust-lang/rustfmt.

  • clippy

Does the name remind you of something? clippy is useful for linting your Cargo application using various lint rules. Right now, there are more than 450 lint rules you can use. You can install it using this command: rustup component add clippy. Afterward, you can use it in the Cargo application by running cargo clippy. Can you try it in the Cargo application that we wrote earlier? You can read more about clippy at https://github.com/rust-lang/rust-clippy.

Text editor

Most likely, the text editor of your choice already supports the Rust language, or at least syntax highlighting Rust. You can install the Rust language server if you want to add important functionalities such as go to definition, go to implementation, symbol search, and code completion. Most popular text editors already support the language server, so you can just install an extension or other integration method to your text editor:

  • The Rust language server

You can install it using the rustup command: rustup component add rls rust-analysis rust-src. Then, you can integrate it into your text editor. For example, if you are using Visual Studio Code, you can install the Rust extension and enable rls.

You can read more about it at https://github.com/rust-lang/rls.

  • Rust analyzer

This application is poised to be the Rust language server 2.0. It's still considered to be in alpha as of the writing of this book, but in my experience, this application works well with regular updates. You can find the executable for this one at https://github.com/rust-analyzer/rust-analyzer/releases, and then configure your editor language server to use this application. You can read more about it at https://rust-analyzer.github.io.

Getting help and documentation

There are a few important documents that you might want to read to find help or references:

  • The Rust programming language book: This is the book that you want to read if you want to understand more about the Rust programming language. You can find it online at https://doc.rust-lang.org/book/.
  • Rust by Example: This documentation is a collection of small examples that show the concepts of the Rust language and its standard library's capabilities. You can read it online at https://doc.rust-lang.org/rust-by-example/index.html.
  • Standard library documentation: As a programmer, you will refer to this standard library documentation. You can read more about standard libraries, their modules, the function signatures, what standard libraries' functions do, read the examples, and more. Find it at https://doc.rust-lang.org/std/index.html.
  • The Cargo book: If you are interested in Cargo and related information such as the Cargo.toml manifest format, you can read more about it at https://doc.rust-lang.org/cargo/index.html.
  • Rust style guidelines: The Rust language, like other programming languages, has style guidelines. These guidelines tell a programmer what the convention for naming is, about whitespaces, how to use constants, and other idiomatic conventions for a Rust program. Read more about it at https://doc.rust-lang.org/1.0.0/style/.
  • Docs.rs: Suppose you are using a third-party crate, such as the rsa crate that we used earlier. To find documentation for that library, you can go to https://crates.io and search for the crate's page, then go to the right pane and go to the documentation section. Or, you can go to https://docs.rs and search for the crate name and find the documentation for it.
  • Rustup doc: This documentation is not online, but you can install it using rustup (rustup component add rust-docs). Then, you can open documentation in your browser while offline using the rustup doc command. If you want to open standard library documentation offline, you can type rustup doc --std. There are other documents you can open; try and see what they are by using rustup doc --help.
  • The Rust user forum: If you want to get help or help other Rust programmers, you can find it all over the internet. There's a dedicated forum to discuss Rust-related topics at https://users.rust-lang.org/.
 

Summary

In this chapter, we had a brief overview of the Rust language. We learned about the Rust toolchain and how to install it as well as the tools required for Rust development. After that, we created two simple programs, used Cargo, and imported third-party modules to improve our program. Now that you can write a small program in the Rust language, explore! Try creating more programs or experimenting with the language. You can try Rust by Example to see what features we can use in our programs. In subsequent chapters, we will learn more about Rocket, a web framework written in the Rust language.

About the Author
  • Karuna Murti

    Karuna Murti started out as a network engineer in Indonesia back in 2005. After that, he continued his career as a web developer, engineering manager, and chief engineer at a consulting company, using various languages such as PHP, Ruby, JavaScript, Fortran, and Python. He also worked with web applications and frameworks such as Magento, WordPress, and Ruby on Rails. In 2015, after establishing his own consulting company, he moved to Japan and now works as a technical lead at Merpay, one of the leading mobile payment apps in Japan. He is helping the company to build a cashless society by leading a team that connects millions of users with thousands of financial institutions in Japan. Even though he uses Go as the main language in his day job, Rust and Rocket have been a passion of his ever since their release.

    Browse publications by this author
Rust Web Development with Rocket
Unlock this book and the full library FREE for 7 days
Start now