Introduction to Rust

Rust is starting to become the go-to language for developing system-level code. It is fast, guarantees memory safety and thread safety and comes with a good package manager. It has also recently made inroads into the linux kernel, allowing the development of kernel programs in rust.

Extra-cute Ferris
Ferris, the un-official rust mascot.

The ownership model of rust which guarantees memory safety without the use of a garbage collector is astounding. It means that you can write rust code and forget about remembering references or worrying about memory safety. So, let’s understand the rust ownership model briefly.

Ownership oversees how a rust program uses memory. During compile time, the rust compiler uses a set of rules to verify if the ownership model is used properly in the rust code. A program following this defined set of rules is guaranteed to be memory safe. As ownership is checked at compile time, the speed of the program is not affected during runtime while maintaining memory safety.

The ownership rules are as follows,

  • Each value in rust has an owner
  • There can only be one owner
  • When the owner gets out of scope, the value will be dropped.

Let’s see an example of how this works,

First, Let’s take an example c code that requires us to call free everytime we allocate memory from the heap.

// A simple program that allocates memory dynamically
#include<stdio.h>
#include<stdlib.h>

// Free a pointer memory
void free_memory(int *ptr) {
        free(ptr);
}

int main()
{
        int* ptr; // A pointer
        int n = 5;
        printf("ptr location before malloc: %p\n", ptr); // This should be null/uninitialized
        ptr = (int*)malloc(n*sizeof(int)); // Allocate memory from the heap
        printf("ptr location after malloc: %p\n", ptr); // Prints memory location
        free_memory(ptr); // Free the memory
        printf("ptr location after free: %p\n", ptr); // ptr still holds the old memory location
        printf("This should not be possible, %d\n", ptr[0]); // this is trouble
        return 0;
}

which when compiled with options -ansi using gcc compiler outputs,

ptr location before malloc: (nil)
ptr location after malloc: 0x5558fcabd6b0
ptr location after free: 0x5558fcabd6b0
This should not be possible, 0

In such a scenario, it becomes tidious and difficult to track memory allocation leading to a memory leaks and vulnerabilities. By using the ownership model rust avoids the entire class of memory related issues.

Here are some rust examples demonstrating how the ownership model is helpful.

/// Function to print a string
fn print(msg: String) {
    println!("{msg}");
}

/// The main function
fn main() {
    let hello = String::from("hello"); // Assign a string "hello" to hello variable
    print(hello); // Call the print function on the string
    println!("{hello}"); // Compile error, as the string has been consumed by the print function.
}

Let’s go through the lines to better understand what’s happening.

At line 2, we define a function print which takes in a String and prints it. It essentially takes ownership of that string to print it. So, when we define a string and call print on it, its ownership is transferred from main, and it cannot be accessed afterward. As the execution of the print completes, the passed string goes out of scope, rust calls drop, and the memory is freed. So, the compiler would prevent any attempts to access that memory region after drop.

Let’s rewrite the code so that print doesn’t take ownership of our string, and we can use it afterward.

/// Function to print a string
fn print(msg: &String) {
    println!("{msg}");
}

/// The main function
fn main() {
    let hello = String::from("hello"); // Assign a string "hello" to hello variable
    print(&hello); // Call the print function on the string
    println!("{hello}"); // Compiles
}

If you look closely, there is only a minor difference in the code. It’s the use of & in lines 2 and 9. & means that we pass a reference to the object, and the ownership is not transferred. The ownership stays with main, so it can be accessed at line 10.

Let’s look at another example.

let hello = String::from("hello"); // Assign a string "hello" to hello variable
let hellotoo = hello; // Assign hello to hellotoo

println!("{hello}"); // Compile error as memory ref by hello has been transferred to hellotoo

Here, the rust compiler considers hello to be invalid once line 2 has been called. So, no call to drop occurs for variable hello when it goes out of scope i.e. rust effectively performs a move operation where hello is moved into hellotoo.

So, a natural question comes into mind, how does rust do shallow or deep copy? To perform this rust provides a clone trait. A trait is a function that defines functionality for a type and can be shared/implemented by multiple types. For stack only data structures, it provides a copy trait that trivially copies data on the stack. Any data structure where drop has been implemented (on the heap) must use clone instead of copy. clone trait can do either shallow or deep copy based on its implementation.

// Copy on the heap
let hello = String::from("hello"); // Assign a string "hello" to hello variable
let hellotoo = hello.clone(); // Create a deep copy of hello and assign it to hellotoo

println!("{hello}, {hellotoo}"); // is valid

// Copy on the stack
let x = 7; // Assign 7 to x variable
let y = x; // here x is copied into y

println!("{x}, {y}"); // is valid

This simple ownership model effectively prevents an entire class of memory-related vulnerabilities. You can read more about rust’s ownership model in the rust book. The idea of having memory safety guaranteed by the rust compiler makes it easier to focus on the core logic of the app and has been one of my major attractions towards writing rust code.

The idea

Initially, I didn’t have any basic ideas that I would have liked to try out with rust. I was searching for things that would allow me to try out concepts such as cli, logging, and file i/o for my first rust app. While browsing old python code that can be converted into rust code, I stumbled across a script that generates a kml file for a set of given images. I thought this would be the perfect example code to write out in rust. This would require me to take user input using cli, find images in the provided locations, read gps information using exif metadata for those images and then write a kml file for all those images. The result being for the given set of images, you can generate a kml file that can be opened in google earth to visualize where the images were captured.

Sounds interesting!

Writing the app

Initializing the app with cargo

cargo is the official package manager for rust. It provides a simple command, cargo new to init new rust packages.

To create a new binary application demo,

cargo new demo

This would create a new basic layout for a rust app that would look like this,

demo
 β”œβ”€Cargo.toml
 └─src
    └─main.rs

Cargo.toml is a manifest file that stores metadata information needed to compile the package. Any dependencies or package metadata go here.

The file would look something like,

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

[dependencies]

Writing a basic CLI

So, the first step was to start with a basic cli using rust. I found crate clap. This provided a simple and easy way to write a cli.

To add this new package as a dependency, use

cargo add clap

This will add clap as a dependency and able you to use it inside your package.

A basic cli looks like this,


// Import clap
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};

// Define commands
// Derive allows us to use traits. Traits can be used to provide basic implementations such as Clone (Allows cloning)
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long, default_value_t = ("output.kml").to_string())]
    /// The output kml location
    kml_file: String, // This is an optional argument
    /// The list of images locations can be a combination of files and directories
    images: Vec<String>, // This is a positional argument
}

fn main() {
    let cli = Cli::parse(); // Parse the inputs

    // Verify that at least one image is provided
    if cli.images.is_empty() {
        let mut cmd = Cli::command();
        cmd.error(
            ErrorKind::MissingRequiredArgument,
            "You must provide multiple images path or a directory of images or both",
        )
        .exit()
    }

    println!("Provided images arg: {:?}", cli.images); // Print the provided paths 
}

Here, we expect image locations as positional inputs, and if nothing is provided, an error is thrown. This more or less summarizes the minimal cli we need for this app.

Adding logging

So, next, let’s add logging. I tried a few crates namely, env_logger, simple_logger, flexi_logger and simplelog. I settled for simplelog as it provided me the flexibility of using both console and file logging at the same time while being extremely simple (true to its name πŸ™‚).

For logging, I use the log library crate from rust. It is only a facade (it doesn’t output anything πŸ€·β€β™€οΈ) so we need an external logging library that can output these log messages.

Writing the logic

Finally, I worked on the logic. The logic is pretty simple; I need to find all image files in the provided locations. For that, I enumerate through the provided locations and check if a given path has a valid image extension. All paths with a valid extension are stored as a part of the vector. Once I have the paths, I enumerate all the paths and read the EXIF metadata using kamadak-exif. I found it straightforward to use and provide support for different file formats and EXIF tags. Once I have the metadata, I only extract the gps information. Once, I have the GPS information I use quick-xml to write the kml file as kml is xml data with special tags.

For the complete logic please refer to the code

That’s it. The app is ready. 😎

Checking the results

We can use cargo run to build and run our app in debug mode. For compiling an optimized version you can build it using cargo build --release.

Running is as simple as,

cargo run -- DSCN0010.jpg

which would generate a kml file called output.kml. A sample image, DSCN0010.jpg was taken for which the kml looks like,

CLI output visualized using google earth
KML visualized using google earth

For all the CLI options, you can run cargo run -- --help.

When building release binary, you can find the resulting binary at target/release.

Packaging and publishing the app using crates.io πŸš€

Now that the app is ready we can package it and upload it to crates.io. Packaging the app is very simple. Cargo.toml makes it extremely easy to specify additional metadata information about the package. You need some basic metadata such as authors, description, readme, license, keywords and categories for app to be publishable to crates.io.

To test your app packaging you can run,

cargo build --release

This would build a release and optimized version of your app which you can run and test the functionality. We can also write tests but maybe I would cover that in another post.

To publish to cargo you need an account in crates.io which would give you an API key. Once you have your API key publishing is effortless. Just run,

cargo publish

and your app would be visible in the crates index.

Conclusion

It was enjoyable and sometimes frustrating to code in rust. As I mostly use python for my regular programming projects and my job it’s extremely difficult initially to specify types for everything. But to my help, the rust compiler is verbose and helped me fix my errors without searching everything on the internet. clippy also provides valuable suggestions that can help improve rust code. Once you have written some functions and they compile, you will find that rust code is straighforward to understand, and you get the hang of the language. Also, the rust-analyzer is a lifesaver when starting out with rust as it is outstanding in suggesting errors and almost always helped me write faster code.

The code for the app is available at GitHub and is also installable through cargo using crates.io. Star this project on github if you like it, and do follow me on GitHub and this blog for updates.

If you’re considering starting coding is rust, I hope this blog motivates you to write your first app in rust.

Cheers! 🍻