Strings & memory
Our tiny markdown compiler is only just being born. We’ve got a project setup, and if we run the program we are able to output a banner—a block of helpful text that usually says what the program is, who wrote it, and how to use it. By the end of the last chapter, we were outputting the version of our program.
In this chapter, we will improve our banner by pulling data from the project’s manifest file (Cargo.toml). To do this, we need to understand how Rust deals with strings and the concept of memory ownership.
# Creating a string variable in Rust
Rust has two kinds of string types: strings and string slices, the primary difference being their mutability; a mutable variable can be changed, whereas an immutable variable cannot be changed.
| Name | Type | Mutability |
|---|---|---|
| A string | String | Can be mutable or immutable |
| A string slice | &str | Is only immutable |
Think of a string like a vector; it can grow and shrink, you can push and pop elements into and out of it, and it is automatically freed when it goes out of scope. In Rust, a string is the owner of the memory where the bytes that make up the string are stored.
A string slice, on the other hand, does not own any buffers in memory. Instead, it borrows whatever is at an address from a different owner. Think of a string slice as pointer or borrowed reference to a string owned by either a different variable or the application itself. For this reason, string slices are always immutable. A string can change and a string slice cannot, so in general, use strings when you have values that might change and use string slices for values that won’t. For example, in our markdown compiler, we will be using A More on Strings and String slices
String to hold the value of our compiled HTML until we’re ready to write it to a file. Using a string slice wouldn’t make sense here since the value of the String will change as we add data to it.&str, on the other hand, is a window into another string, whether that string is a string literal (in which case the string slice would be static and stack-allocated) or a String (in which case the string slice would be heap-allocated).
# Using strings
Let’s see how both of these string types can be used effectively by modifying our the_version variable to be a &str—and while we’re at it, let’s change the version of our markdown compiler to be a more traditional early prototype version (0.1) instead of 1000:
fn usage() {
let the_version: &str = "0.1";
println!("tinymd, a markdown compiler written by <YOUR NAME>");
println!("The Version: {}", the_version);
}Here we have turned the_version into a string slice (&str). Rust is going to see the "0.1" and know to compile that value as a static string in the program, and then has the_version borrow that value.
The reason we say it borrows the value there is because technically it’s not the owner of the buffer where "0.1" lives—the application is. (Anytime a string literal like "0.1" is created at compile time, it is created as a static string slice, which means it exists in the binary code that makes up the executable and cannot be changed.)
The Rust compiler is smart enough to infer the_version’s type based on the provided value ("0.1"), so we can rewrite the declaration to omit the type:
let the_version = "0.1";Here we omit the : &str part of the declaration because Rust will infer that, since "0.1" is a string literal and all string literals become static string
slices, the_version needs to be a &str.
When Rust goes to compile our program, the string "0.1" is compiled into the program as a string literal (essentially a static string) and thus gets instantiated in stack memory, and the_version (which is allocated dynamically at runtime in heap memory) borrows the value at the address in stack memory where Rust stored it.
Our program will build and run just fine as long as we comment out our old get_version() function. Ensure your main.rs looks like this now:
// Comment this out for now; we will come back to it soon
/*fn get_version() -> u16 {
1000
}*/
fn usage() {
let the_version = "0.1";
println!("tinymd, a markdown compiler written by <YOUR NAME>");
println!("The Version: {}", the_version);
}
fn main() {
usage();
}Go ahead and build and run it:
$ cargo run -q
tinymd, a markdown compiler written by <YOUR NAME>
The Version: 0.1Though it runs fine, we haven’t improved anything by baking in a string version instead of an integer version. What we want to do instead is to pull the actual version of the application from the project’s manifest file (Cargo.toml). Rust gives us a macro to do just that: env!(), which returns the value associated with a particular key.
Let’s take a look inside Cargo.toml right now, to see what we are working with:
[package]
name = "tinymd"
version = "0.1.0"
authors = ["Jesse Lawson <[email protected]>"]
edition = "2018"Many other languages use manifest files like Rust’s Cargo.toml, such as Node (package.json) and Ruby (Gemfile). The information here is fairly straight-forward. These variables in here are sometimes called environment variables. Rust will provide the key values from the manifest file as environment variables for us during compilation.
Our goal is to pull out variables from this manifest file and stick them into our usage() function. In doing so, we will then be able to display the string values we see in the manifest file as parts of the banner of our application. Neat!
To do this, we will use the env!() macro I mentioned earlier. Remember that macros in Rust are, as far as we are concerned, basically the same as functions–except a macro has an exclamation point after its name to denote that it is a macro. The env!() macro takes one argument: a string key corresponding to the variable we want.
The following are the string keys we are going to be using:
CARGO_PKG_VERSION- The full version of your package.CARGO_PKG_AUTHORS- Colon separated list of authors from the manifest of your package.CARGO_PKG_NAME- The name of your package.CARGO_PKG_DESCRIPTION- The description from the manifest of your package.CARGO_PKG_HOMEPAGE- The home page from the manifest of your package.
As you may have guessed, each of these string keys corresponds to a key from the manifest file. For example, the version key in the manifest file is retrieved by passing CARGO_PKG_VERSION to env!().
You’ll notice that there are some fields in the list of string keys above that are not in the manifest file. The reason they aren’t there is because they are not part of the default scaffolding. Let’s go ahead and add them; you are free to set these to whatever you would like.
Go to the Cargo.toml file and add entries for description and homepage, then modify the name, authors, and version as you see fit:
[package]
name = "tinymd"
version = "0.1.0"
authors = ["Jesse Lawson <[email protected]>"]
edition = "2021"
description = "A tiny markdown compiler from the book Getting Started with Rust."
homepage = "https://jesselawson.org/rust"Note: The edition field lets you target a specific edition of Rust. Don’t change this; use the value that Cargo put in there for now.
Looking good. Next, we’ll create a function that gets us one of the environment variables. Let’s do the version first. Knowing that env!() takes a single string key as an argument and returns the environment variable from the manifest file, how do you think we would do that?
One way is to just set the value of the_version to be the result of a call to env!():
// ...
let the_version = env!("CARGO_PKG_VERSION");
println!("Version: {}", the_version);
// ...A more efficient way of generating the banner is through a single function call. In other words, anytime we would need to print out the tool’s banner, we should be able to do it with a single function call: usage(). To do that, we would need to move the version variable into the usage() function. While we’re at it, let’s go ahead and encapsulate the work of getting the version out into its own function—and replacing get_version() with something a little more helpful.
# Return a string from a function
Recall that there are two types of strings in Rust: there’s the vector-esque String, and the string slice &str. We use a String when we want a string that we can modify like we would an array (pushing and popping characters and/or strings to and from it), and &str when we only need a slice of an existing string.
We can modify get_version() to return a string by modifying the return value of the function. Recall that the return value of the function right now is a u16. How do you think we would change it to a String?
fn get_version() -> String {
//...
}Changing the return value to a String is good, but I want to take this function a different way. Instead of just getting our version—which would mean we would create separate functions for all of the other manifest values we want to retrieve—let’s replace get_version() with a function that can retrieve more than just one value from the manifest file.
When the usage() function prints the banner, I want it to print something like this:
$ tinymd
tinymd (v0.1.0), a tiny and mostly useless markdown compiler.
Written by <Your Name>
Usage: tinymd <somefile.md>You might already see the variables that we need to retrieve in order to produce the above output:
[name] (v[version]), [description]
Written by [author]
Usage: tinymd <somefile.md>Additionally, whenever the tool is doing it’s job, I still want part of the banner to be outputted. For example, if I wanted to compile a file called something.md into something.html, maybe the tool works like this:
$ tinymd something.md
tinymd (v0.1.0), a tiny and mostly useless markdown compiler.
Compiling something.md...
Done! Your new file is something.html.Imagining how we want our tool to behave is a good way to think about what needs to be done to get there.
Since we’re always going to print the first line, let’s create a separate function to prepare it for us.
To do this, we are going to create a String variable and then push the title, the version, and the description into it. This means that the variable needs to be mutable, and brings us to an important concept in Rust: all variables are immutable by default.
Rust makes all variables immutable by default as part of a memory strategy that guarantees no memory leaks. If you want to modify a variable, you must first create a mutable reference to it.
To illustrate how Rust treats all variables as immutable unless told otherwise, let’s try to modify the value of the_version after declaring it:
fn usage() {
let the_version = "0.1";
the_version = "0.2";
println!("tinymd, a markdown compiler written by <YOUR NAME>");
println!("The Version: {}", the_version);
}
fn main() {
usage();
}Go ahead and replace all the contents in main.rs with what you see above.
Try to build this project using cargo build, and look what the Rust compiler tells you:
$ cargo build
Compiling tinymd v0.1.0
warning: value assigned to `the_version` is never read
--> src\main.rs:36:9
|
36 | let the_version = "0.1";
| ^^^^^^^^^^^
|
= note: #[warn(unused_assignments)] on by default
= help: maybe it is overwritten before being read?
error[E0384]: cannot assign twice to immutable variable `the_version`
--> src\main.rs:37:5
|
36 | let the_version = "0.1";
| -----------
| |
| first assignment to `the_version`
| help: make this binding mutable: `mut the_version`
37 | the_version = "0.2";
| ^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
error: Could not compile `tinymd`.
To learn more, run the command again with --verbose.You are now witnessing the power of Rust’s development toolchain: the integrated borrow-checker, Rust’s smart compiler. My favorite part about this is how it will not only tell you when you’re doing something wrong, it also suggests a way to fix it.
Let’s take a closer look at three parts of this output:
warning: value assigned to 'the_version' is never read. Here, Rust is telling us that we assigned a value tothe_versionbut then never used it. Why would we do that? Rust is trying to show us where we can improve the quality of our program, as evident by its message a few lines down:= help: maybe it is overwritten before being read?error[E0384]: cannot assign twice to immutable variable 'the_version'. Rust is telling us thatthe_versionis an immutable variable; it cannot be changed, so when we try to assign a new value to its instantiated value, the Rust compiler aborts.help: make this binding mutable: 'mut the_version'. Rust’s borrow checker is showing us how to turnthe_versioninto a mutable variable. Only mutable variables can have their values changed. Otherwise, treat them like a static.
How do you think we should fix this?
(Go to your code editor and try to fix it using the output from the cargo build command as guidance. I’ll give you a hint: you have to add a three-letter keyword somewhere…)
The answer is to add the mut keyword after let in the declaration of the_version:
fn usage() {
let mut the_version = "0.1";
the_version = "0.2";
println!("tinymd, a markdown compiler written by <YOUR NAME>");
println!("The Version: {}", the_version);
}Any variable that we want to be able to change after declaration needs to have the mut keyword, otherwise the variable will be considered immutable.
With this new knowledge, we can now create a String variable that we will use to build the first line of the banner.
Go ahead and delete everything in main.rs; we’re going to start from scratch, creating a new way to think about our banner that includes dynamically created strings.
Let’s start off with the banner itself. We want to isolate the first line, which includes the title, version, and description, so that we can call it from both usage() and from another function which will be the meat and potatoes of the compiler. Let’s call that function parse_markdown_file().
In our empty main.rs file, go ahead and create these two empty functions along with the same main() function as before:
fn parse_markdown_file() {
}
fn usage() {
}
fn main() {
usage();
}Now, just above usage(), let’s create two more functions: one to print just the first line of the banner, which I’ll call the title, and one to print both the title and the rest of the banner (e.g., the “written by” and “usage” strings). We are going to call them print_short_banner() and print_long_banner(), respectively:
fn parse_markdown_file() {
}
fn print_short_banner() {
}
fn print_long_banner() {
}
fn usage() {
}
fn main() {
usage();
}We’re almost finished mapping out the main functions of our tool. The last one we need–and the one we will spend the rest of this chapter on–goes right at the top and will be called get_title(). This one will be different because it is going to return a String:
fn get_title() -> String {
}Great! Let’s go over all these functions so we know how they’re all related.
parse_markdown_file()will be called when we are passed a markdown file via the command line. We will leave this empty; Chapter 4 is when we will flesh this out.print_short_banner()will output the title, version, and description. We’re going to build this next.print_long_banner()will output the short banner plus a “written by” attribution and “usage” example.
Recall that the env!() macro, when passed a string key (like CARGO_PKG_NAME), will retrieve the value of that key from the manifest file and return it.
Armed with the knowledge we’ve acquired so far, let’s dive into the get_title() function.
The first thing we’ll do is create a local String variable to hold all the data we want to output. Since this will be a variable that we are going to modify by adding strings onto the end of it (like the version and the description), what keyword do we need to have when declaring it?
We need the mut keyword:
fn get_title() -> String {
let mut the_title = String::from(env!("CARGO_PKG_NAME"));Here’s something we haven’t seen before: a String variable can be created from another string–even if that string is a string slice. Here we are creating a mutable variable the_title from the string returned by the call to env!(), which is grabbing the string value associated with the key CARGO_PKG_NAME–which itself is associated with the manifest file’s name key. Neat!
What is the value of the_title right now?
It’s tinymd, which is what we see when we look at the value for the name key in the manifest file.
With the beginnings of the title string started, we can now start to imagine what this string looks like based on the individual chunks of strings we would need to add to it to make it complete:
[TITLE] (v[VERSION]), [DESCRIPTION]
Or, as a list of individual strings:
[TITLE](v[VERSION]),[DESCRIPTION]
Finally, as a list of the actual strings we will be using:
env!("CARGO_PKG_NAME")(venv!("CARGO_PKG_VERSION")),env!("CARGO_PKG_DESCRIPTION")
So the title string itself is composed of five individual strings that need to be pushed onto the_title.
We can push a string onto another string (i.e., concatenate two strings) by using the .push_str() method of a String. Since we already have the title in the_title, the next string we need to add is (v:
fn get_title() -> String {
let mut the_title = String::from(env!("CARGO_PKG_NAME"));
the_title.push_str(" (v");As you can see, a String can be instantiated by building one from another source–like, in this case, the return value of the env!() macro. You’ll also note that the `String::from`` bears resemblance to method calling from other languages.
To add to the end of the string, Rust gives us the push_str() method. At this point, the value of the_title is tinymd (v.
How do you think we will build the rest of the_title?
Try to build the rest of the_title yourself, using the .push_str() examples above, and then open up the Solution below to see if you got it right.
Remember: the goal is for the_title to equal something like this:
tinymd (v0.1.0), A tiny markdown compiler based on Jesse's tutorials.One Possible Solution
fn get_title() -> String {
let mut the_title = String::from(env!("CARGO_PKG_NAME"));
the_title.push_str(" (v");
the_title.push_str(env!("CARGO_PKG_VERSION"));
the_title.push_str("), ");
the_title.push_str(env!("CARGO_PKG_DESCRIPTION"));
return the_title;
}
We now have a function that will get the title string of our program, which is what we call the short banner.
We are going to call You can use the string substitution characters get_title() from the print_short_banner() function by using the println!() macro. Can you guess how we will do that?One Possible Solution
{} (just like how you might use %s in C’s printf), like this:fn print_short_banner() {
println!("{}", get_title());
}
Great! Now, anytime we want to output just the short banner, we have a function dedicated to just that.
Next, let’s flesh out the print_long_banner() function.
We know that the first thing the long banner print’s is the short banner. How might we call the short banner function from inside print_long_banner()?
Exactly the way you would think to do it:
fn print_long_banner() {
print_short_banner();
}The rest of the long banner will contain the other elements from the manifest file that we haven’t retrieved yet, and we will retrieve them exactly the same way we did so for the name, version, and description in the short banner.
Here’s what we want the long banner to look like:
$ tinymd
tinymd (v0.1.0), A tiny markdown compiler based on Jesse's tutorials.
Written by: <your name>
Homepage: https://jesselawson.org/rust
Usage: tinymd <somefile>.mdIf we break this up into four separate strings, it will be easier to plan out how you will tackle this function:
print_short_banner()"Written by: "+env!("CARGO_PKG_AUTHORS")"Homepage: "+env!("CARGO_PKG_HOMEPAGE")"Usage: tinymd <somefile>.md"
Using what you’ve learned so far and the One way to do this is by creating separate strings for each line of the banner: Another way is to just pull all this directly into a single call to the get_title() function as a reference, try constructing the rest of the print_long_banner() function on your own. Here’s a hint: you only need the above env!() examples and some corresponding calls to println!().Two Possible Solutions
fn print_long_banner() {
print_short_banner();
let mut written_by = String::from("Written by: ");
written_by.push_str(env!("CARGO_PKG_AUTHORS"));
let mut homepage = String::from("Homepage: ");
homepage.push_str(env!("CARGO_PKG_HOMEPAGE"));
let mut usage = String::from("Usage: tinymd <somefile>.md");
println!("{}", written_by);
println!("{}", homepage);
println!("{}", usage);
}println!() macro:fn print_long_banner() {
print_short_banner();
println!("Written by: {}\nHomepage: {}\nUsage: tinymd <somefile>.md\n",
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_HOMEPAGE")
);
}
Let’s finish out this chapter by building our tool one last time. What command can we use to build the project and run it quietly all at once? Recall that Cargo’s One Solution
run command will automatically trigger a build if it detects that source files have been changed since the last time the program was built. We can also pass the -q flag to keep the output quiet–that is, not show all the verbose output that comes with a bare cargo build command.$ cargo run -q
In this chapter, we got really familiar with how Rust deals with strings in memory. Next, we’ll begin parsing command line arguments and start working with the filesystem.
“It’s not that I’m so smart, it’s just that I stay with problems longer.”
— Albert Einstein
📦 All the code up to this point is available on GitHub.
Checkpoint
Before moving on, you should be able to confidently:
Create a string variable without errors
Return a string variable from a function without errors
Concatenate two strings without errors
Print a string to the command line without errors