Rust has been gaining some popularity with systems programming. I keep hearing about how the compiler itself can protect you from common programming mistakes which should make the overall program more reliable. As an experienced C developer, I have been curious about this relatively new language. Let’s compare the two languages and see how they differ. Also, I’m going into this blind. I haven’t read any real information on rust syntax before now.

First Impressions with Hello World!

I am reading through The Rust Programming Language and the first example is the traditional “Hello World” program which just prints a message to the console. Let’s compare the two.

fn main() {
    println!("Hello, Rust!");
}

So as my first impression of Rust, the fn is a bit odd. And the exclamation point is double odd, but no need to include standard libraries is quite nice. Besides those two things, it’s pretty typical syntax.

The ‘!’ for macros

The ‘!’ is actually how Rust differentiates a macro from an actual function. This is quite interesting, coming from C we will typically define macros in ALL_CAPS or maybe even disguise them as a FunctionName depending on our use case. It seems nice to not have to sacrifice style by making macros all caps, yet still be able to differentiate between real functions and macros.

Introducing Cargo

Cargo is rust’s built in package manager. This allows the community to share libraries, which makes for foundations that empower building bigger and bigger projects. Python has pip. Javascript has npm. Windows C++ has NuGet. ANSI C Has… nothing?

A Brief (and probably inaccurate) History of Package Managers

I did a bit of research and found that pip was introduced to Python in 2008 and was first named pyinstall. NPM for javascript packages was initially released in 2010. Rust was first introduced in 2010 as well. So I would make the assumption that during this time, sharing community code through packages was quite trendy. Today, when starting new projects, developers may search for packages that they can leverage to save time on their project. What’s interesting about Python is that Python 2.0 was released in 2000 and they still introduced a package manager when it was recognized as a great way to organize and share code. A quick google search will bring up a variety of package managers for C, but as far as I know there is no “Standard” package manager. In my experience, C packages tend to be released by manually going to a website, downloading a zip of header files and library files, and figuring out for yourself how best to organize this into your project. Having a standard package manager I think is a great improvement over C.

Please comment if you know of any C package manager that is widely used.

Programming Concepts

At the end of the day, code is code, so what makes Rust so special?

Variables, Immutability, and… Shadowing?

So typically in a program, you may declare a variable, assign a value to it, change the value in it, use it in calculations, etc. However, in Rust, all variables are immutable by default. So it’s basically like if I prefixed all my variables in C with const.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}
error[E0384]: cannot assign twice to immutable variable `x`

The Rust compiler actually doesn’t allow you to write code where you modify your variable! I don’t think I fully understand why you would want that, but Rust is really about safety. I suppose you can’t have a race condition on a variable if the compiler guarantees the variable is constant. It could prevent some basic concurrency errors like that.

That’s not to say you can’t make variables mutable, you just need to be explicit.

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

But this language feature also introduces a new concept to me called “Shadowing.” That looks like this:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    let x = 6;
    println!("The value of x is: {}", x);
}

In C, or most other languages I’ve used, this would be a compiler error telling you that you’ve already declared x! However in Rust, the second declaration of x shadows the first one. Behind the scenes, these two x’s are two unique variables with different memory allocated for each, and they could even be different types. The idea here is that x is still immutable after you’ve assigned a new value to it.

Visual Separators in Integer Literals

This one blew me away! In Rust, you can put underscores in your integer literals! On embedded systems I’m working with hex all day. In our logging prints we always separate 64 bit values like 0x12345678_87654321, but in Rust you can write that directly as the actual value. Absolutely a game changer.

fn main() {
    let x = 9_000;
    println!("X is {}", x);
}
x is 9000

32-bit char type

In Rust, char‘s inherently support unicode! What!? The one time I’ve really had to deal with unicode characters was when writing some homebrew for the Nintendo Switch. I was writing in C, but needed to render 16-bit unicode characters to the screen. Well C doesn’t support 16 bit unicode so there was all this other extra code for dealing with chars and strings with unicode. It’s nice to know there is a language that just supports it by default.

But the real question is can I use unicode characters as variable names….? I’m gonna try it.

fn main() {
    let 😻 = "Cat with heart eyes";
    println!("My variable is {}", 😻);
}
error: unknown start of token: \u{1f63b}

The answer is no. You can’t. I’m a little sad about it. But you can use unicode in your strings.

fn main() {
    let unicode_string = "Cat! 😻";
    println!("My variable is {}", unicode_string);
}
My variable is Cat! 😻

Out of Bounds Indexing

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}
error: this operation will panic at runtime
 --> main.rs:5:19
  |
5 |     let element = a[index];
  |                   ^^^^^^^^ index out of bounds: the length is 5 but the index is 10
  |

Mhmm. Good job Rust. Easy catch, I’m pretty sure C has similar warnings…

#include <stdio.h>

int main(void) {
    int a[] = {1, 2, 3, 4, 5};
    int index = 10;

    int element = a[index];

    printf("The value of element is: %d\n", element);
}

Quick note, I’m using the windows compiler here:

cl main.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.28.29336 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

main.c
Microsoft (R) Incremental Linker Version 14.28.29336.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj

Hmm… No warnings there. Let’s try adding the Wall flag.

cl /Wall main.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.28.29336 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

main.c
C:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\ucrt\stdio.h(948): warning C4710: 'int printf(const char *const ,...)': function not inlined
C:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\ucrt\stdio.h(948): note: see declaration of 'printf'
Microsoft (R) Incremental Linker Version 14.28.29336.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj

Umm.. what? Am I using printf wrong somehow? Well there were no warnings about just going out of bounds. Let’s run it.

The value of element is: 9766960

Ok, C, sure. If you say so. I really thought there would be a warning though.

Returning values from Functions with Ugly Syntax

Let me start by saying I have not looked at any proper Rust code yet. Just the examples from the book. I’m going to have to take a look around to see some common conventions, but most likely I’ll stick to the conventions written in the book… Except for just this one…

fn main() {
    let x = plus_one(5);
    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Just looking at this code makes the C developer in me cringe a bit… The above code is correct and compiles and runs. But first of all, plus_one has no return statement. In Rust the last expression is returned for the function implicitly. Second, it has no semicolon at the end. Up until now Rust has been pretty typical, it was quite similar to C besides the behavior differences. Adding a semicolon like x + 1; would actually be an error because it changes the line from an expression to a statement.

I don’t know if this is typically written this way in Rust projects, or if the Rust book is just showing off, but it does support proper return statements and I plan on sticking to them.

But there is one convenient use for this unpleasant syntax. Rust has gone away with that confusing ternary statement, and instead leverages the fact that blocks of code to return a value, as long as the final line is an expression without a semicolon. So you can do this:

fn main() {
    let value = if true { 7 } else { 4 };
    println!("The value of value is: {}", value);
}
The value of value is: 7

So here, the if block acts as an expression. Since I just ran if true it goes into the first block. Since there’s no semicolon, it returns the expression as the value of the if block. There’s no ternary operator in Rust, and this syntax is actually substantially more readable than int value = true ? 7 : 4;

Result Types

C developers know that C has this arcane global errno concept for tracking errors in the standard library. Basically in case a function returns a bad value, it sets the global errno value and returns a bad value. For malloc it looks like this:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

typedef char byte;
int main() {
    byte* data = malloc(999999999999999);
    if (data == NULL) {
        printf("Failed to allocate memory. Got error: %d\n", errno);
    } else {
        // Use allocated data
        printf("Successfully allocated memory %p\n", data);
        free(data);
    }
}
Failed to allocate memory. Got error: 12

It works! But it’s quite ugly… Rust introduces a Result type in its standard library that provides the same functionality as errno, but with a much cleaner interface. This example is straight from The Book.

fn get_user_input() -> String {
    let mut user_input = String::new();
    let result = io::stdin().read_line(&mut user_input);
    result.expect("Failed to read line");

    return user_input;
}

The read_line function returns one of these result types. Result is an enum with some methods added onto it. It can either be Ok or Err. The method expect will check if the result was an Err. If it was an Err it will exit the program with the given error message.

In C, even without using errno, the language overrides the meaning of your variables. They double as both a proper result that you wanted and an error status. By separating these two meanings by giving the output through an ouptut parameter and returning a status as a result, Rust makes the overall program more readable, and it makes handling errors much more intuitive.

Wrapping Up

I’m only on Chapter 3 in the book, and I’m already quickly becoming a Rustacean. The language does look and feel C-like, but it definitely feels like the language pushes you towards better code. In C, all bets are off, it’s up the developer to have the knowledge not to make many mistakes like the ones I’ve written about here. As I learn more about Rust, I’ll post more details about the language. I’m hoping the language starts getting more traction!

Categories:

Tags:

One response

Leave a Reply

Your email address will not be published.