cwoellner.com ~ personal website & blog

Rust: First Impressions

Published on: Tuesday, Dec 19, 2023

Intro

Last week I had the chance to take part in a training about initial access techniques. While trying out some of the techniques, I came across the issue of having to create some executables, usually I would write the code in nim and then compile, but during the training I also learned that compiled nim executables tend to have certain patterns and since the only things made in nim, that I have seen so far, are offensive tools, I would expect EDR solutions to pick up on my binaries.

This left me with two alternatives C and rust. While I am already familiar with C, in my experience coding in C, while being fun, takes a lot of time to get to something useable, so this week I decided to give rust a try.

What I Have Build So Far

I used the ongoing Advent of Code to get some exercises and so far completed the first three days. The code was written in VS Code, with syntax highlighting and without linting or error checking.

While this is not nearly enough time to create a fair review, I think it is enough time to talk about my first impressions. In my opinion, first impressions are valuable feedback, since you tend to become blind to these beginner issues once you dive deeper into the topic and become a contributor.

What I Liked

Variable Management

While initializing a variable a let is required, if the value of the variable will change during execution a mut needs to be added after the let, on reassignment of the variable value the variable must be named without a let. This prevents accidental variable overrides and makes the code easier to understand.

Easy Casting of Datatypes

What really surprised from a C competitor is the easy of casting data from one type to another. While most methods return some obscure datatypes that can barely be worked with, the datatypes can usually be converted to a Vector by using .collect::<Vec<_»(). The vector can then be easily worked with. For classic datatypes like strings, chars or integers methods for conversion are available without any additional inputs.

Readable Code

The two points above as well as the simple control structures create very readable code, here is my solution for task two of day two:

use std::fs;
use std::process::exit;

fn main() {
    let readfile = fs::read_to_string("./src/input.txt").expect("Should have been able to read the file");
    let contents = readfile.split('\n');
    let mut result = 0;
    for content in contents {
        let game = content.split(": ").collect::<Vec<_>>()[1];
        let draws = game.split("; ");
        let mut maximums: [i32; 3] = [0,0,0];
        for draw in draws {
            let colors = draw.split(", ");
            for color in colors {
                let color_split = color.split(" ").collect::<Vec<_>>();
                let number = color_split[0].parse::<i32>().unwrap();
                let mut max_index = 0;
                match color_split[1] {
                    "red" => max_index = 0,
                    "green" => max_index = 1,
                    "blue" => max_index = 2,
                    &_ => exit(1),
                };
                if number > maximums[max_index] {
                    maximums[max_index] = number;
                }
            }
        }
        result += maximums[0] * maximums[1] * maximums[2]
    }
    println!("{}",result.to_string());
}

Even without being proficient in rust the code is understandable (even tough my variable naming might need some work).

Useful Compiler Errors

So far compiler errors have always been helpful. Very impressive.

What I Disliked

No Classic For-Loop

The first programming languages I learned where Java and C in both languages you can create a for loop like this:

for (int i = 0; i < 5; i++) {
    //code
}

In rust you are apparently not able to do this instead you are supposed to use:

for draw in draws {

This is fine for simple use cases, but what if you need to know on which element of draws you currently are on? Well then you have to an Iterator for your data and then enumerate over it:

for (i, c) in line_chars.iter().enumerate() {

So far this has been working, but, in my opinion, it creates a pretty weird flow for what should be a basic task.

The Option Datatype

In order to transform the character ‘1’ into the integer 1, the .to_digit() method looks like it will do the job. My assumption was that this method would return an integer, but the method returns an option datatype. This type then needs to be cast to an integer using the .unwrap() method. I assume that this a safety mechanism in case the character is not a digit, but this can already be checked using the .is_digit() method, so to me, it looks like in proper code the character can be guaranteed to be a digit making the option to unwrap path obsolete.

if c.is_digit(10) {
    let i = c.to_digit(10).unwrap();
}

The same thing happens when casting a string to an int with .parse::().

Conclusion

My first impressions have been very positive, I ended with working code in no time and both of the issues I disliked have a solution, so they really are just annoyances.

I will try to use rust in some of my future projects, one of them being a stage two loader (as suggested in the intro) and try to get more fluent in it.

If it continues to hold up, rust might become the go-to language for me.