Rustler

Rust is a high-level language for lower-level development and is quite popular in the blockchain scene. Today we’ll be getting our hands dirty and get through the basics.

Why?

Rust is a modern programming language that aims to provide a safer way of programming without having to do manual memory management (e.g. C or C++) or need for a garbage collector (e.g. Java, Python, Javascript).

The issues with manual memory management are well known and until today, still a major source of concern and software vulnerabilities.

Garbage collection in contrast, proved to be a great solution and even improved developer productivity but it’s not meant for system programming (e.g. not ideal for bare metal development, audio applications, real-time / timing critical applications, etc).

Rust’s design aims to achieve the safety of garbage collected languages without compromising the speed we get with non-GC languages. This is achieved through the concept of Owernship, the borrow-checker and other design choices introduced briefly in the next few topics.

Install?

Install Rust with Rustup , a tool to manage Rust versions, equivalent to NVM for node.

Visit Rustup and follow the instructions to install!

For example, if a new version of Rust is available, you’d simply execute:

rustup update

Once installed, you should have the following (e.g. the commands and the flag --version should output the version of each command line tool):

cargo --version

rustc --version

rustdoc --version

Cargo is Rust’s package manage, and a general tool to manage Rust (compilation, docs).

The use-cases are:

  • Initialise a new project
  • Run
  • Build
  • Manage external library dependencies

Rustc is the Rust compiler, generally invoked through Cargo.

Rustdoc is the Rust documentation tool that generates documentation from comments written in a particular format, outputting it to HTML files. Generally invoked through Cargo, our general purpose tool.

Cargo?

We use Cargo to initialise new projects by executing the command:

cargo new PROJECT-NAME

It’ll generate a basic project file structure for us, that includes:

  • Cargo.toml, includes all the project metadata required to compile, of example if the project has dependencies we’d put it in this file (Find more keys and definitions

  • .git, .gitignore, git version control repository

  • src the actual project source files.

├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

To run you can execute the following command from any directory in the project:

cargo run

As we have previously mentioned about Cargo, the command invokes the compiler, and then run the executable it produced for us.

The executable that Cargo produced is placed in the target/debug directory in the project, among other related files.

You can run the executable:

./target/debug/hello

Clear the generated files:

cargo clean

Most common Cargo commands are:

  • cargo new - creates a new project
  • cargo init - creates a new project in an existing directory
  • cargo build - builds the project via Rustc
  • cargo run - runs the project
  • cargo update - project dependency updates
  • cargo test - run tests
  • cargo bench - run benchmarks
  • cargo doc - generate the project documentation via Rustdoc
  • cargo check - analyze the project to check for errors without building

How to tell which toolchain Rust compiler’s using?#

Use rustup show to see your active toolchain which contains the name of the platform you’re using. For example:

rustup show

If rustup did not successfully install and configure the stable toolchain, you can do it manually:

There are cases where rustup doesn’t succesfully install and configure the stable toolchain, you can do it manually by:

rustup install stable
rustup default stable

Learn more about toolchains here .

Hello world?#

Rust syntax is similar to C, C++ or Javascript. Functions in Rust use the keyword fn, where the body of a function is written inside {} brackets.

The println! is a macro invocation, and not a function call.

The Rust book explains Macros by saying that “Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming”.

Here’s a simple Hello world in Rust:

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

An example of a factorial function, including a test written in the same file:

fn factorial(n: u32) -> u32 {
    assert!(n != 0);
    if n > 1 {
        return n * factorial(n - 1);
    }
    return n;
}

#[test]
fn test_factorial() {
    assert_eq!(factorial(3), 6);
}

Running tests?#

When running tests, the println’s are ignored. To see the output, you might be interested in the flag --show-output:

cargo test -- --show-output

There’s a Cargo watch that watches your project source code for changes and runs commands when they occur.

Here’s an example of how to use it:

cargo install cargo-watch
cargo watch -x 'test -- --show-output'

Debugging#

As explained previously, output from print statements are not printed to stdout on cargo test.

To see the output from print statements, you can also run the tests with the nocapture flag.

cargo test -- --nocapture

Note: --show-output only prints stdout of successful tests. Also –nocapture doesn’t capture the output, so your tests get the same stdout that you see in your terminal otherwise it’s captured, and if you pass --show-output the captured output is shown.

Make the output colourful by using the Colored , or the Termion crate(s).

To see the colours, you’d run with the flag --color:

cargo test -- --color always --nocapture

Add, remove, upgrade dependencies?#

If you are familiar with Javascript package manager npm or yarn, the cargo-edit provides add, remove and upgrade dependency functionalities to cargo.

Install by:

cargo install cargo-edit

Add a dependency:

cargo add regex@0.1.41

Remove a dependency:

cargo rm regex

Or, remove a --dev dependency (there’s also --build):

cargo rm regex --dev

To upgrade dependencies:

cargo upgrade

Format Rust code?#

Rustfmt is a tool for formatting Rust code according to style guidelines.

Start by adding to toolchain:

rustup component add rustfmt

To run:

cargo fmt

You might notice that the process to install rustfmt differs from cargo-watch, for example. They’re all cargo subcommands (which means that if you call cargo foo, cargo looks for cargo-foo on $PATH).

That means that Rustfmt is an official Rust project and part of the toolchain, the others are from crates .

Strings vs &str?#

String is the dynamic heap string type, like Vec<> use it when you need to own or modify your string data.

str is an immutable1 sequence of UTF-8 bytes of dynamic length somewhere in memory. Since the size is unknown, can only be handled behind a pointer. You’ll commonly see this as as &str a reference to some UTF-8 data, normally called a “String slice” or just a “Slice”.

Let’s say you have a String and you’d like to concate &str:

format!("[{}]: {}", a_string, a_str)

Or, by using Add +:

String::from("Some text") + a_str
let a_string: String = "foo".into();

a_string + a_str

💡 The into uses the type definition to transform to, learn more about it here

Or, append to string:

let mut s = String::from("foo");

s.push_str("bar")

Learn more about it here

Syntax examples?#

An example of a factor of number function, uses a Vector (a resizable array):

fn factor_of_number(n: u32) -> Vec<u32> {
    let mut result: Vec<u32> = Vec::new();
    let mut i = 1;
    while i <= n {
        if n % i == 0 {
            result.push(i);
        }
        i += 1;
    }
    return result;
}

#[test]
fn test_factor_of_number() {
    assert_eq!(
      factor_of_number(60),
      [
        1,
        2,
        3,
        4,
        5,
        6,
        10,
        12,
        15,
        20,
        30,
        60
      ]
    );
}

An example of how to reverse a string:

pub fn reverse(input: &str) -> String {
    input.chars().rev().collect::<String>()
}

fn test_reverse() {
    assert_eq!(reverse("I'm hungry!"), "!yrgnuh m'I");
}

An example of how to calculate time:

use chrono::{DateTime, Duration, Utc};
use std::ops::Add;

// Returns a Utc DateTime one billion seconds after start.
pub fn after(start: DateTime<Utc>) -> DateTime<Utc> {
    start.add(Duration::seconds(1_000_000_000))
    // We don't need to use the trait
    // and could simply use the + operator
    // start + Duration::seconds(1_000_000_000)
}

fn test_after() {
    let start_date = Utc.ymd(2011, 4, 25).and_hms(0, 0, 0);

    assert_eq!(
        gigasecond::after(start_date),
        Utc.ymd(2043, 1, 1).and_hms(1, 46, 40)
    );
}

An example of match, similar to switch statements:

// on every year that is evenly divisible by 4
// except every year that is evenly divisible by 100
// unless the year is also evenly divisible by 400
pub fn is_leap_year(year: u64) -> bool {    
    if year % 4 != 0 {
        return false
    }

    if year % 100 == 0 && year % 400 != 0{
        return false
    }

    true
}

pub fn is_leap_year(year: u64) -> bool {    
    match (year % 4, year % 100, year % 400) {
        (0, 0, 0) => true,
        (0, 0, _) => false,
        (0, _, _) => true,
        (_, _, _) => false,
    }
}

An example of string concatonation:

pub fn raindrops(n: u32) -> String {
    let mut sound = String::new();

    let pling = "Pling";
    let plang = "Plang";
    let plong = "Plong";


    if n % 3 == 0 {
        sound.push_str(pling)
    }

    if n % 5 == 0 {
        sound.push_str(plang)
    }

    if n % 7 == 0 {
        sound.push_str(plong)
    }

    if sound.len() == 0 {
        return n.to_string();
    }

    sound
}

Rust has two string types, String and &str (actually, there are more).

  • String is an owned string and can grow and shrink dynamically
  • &str is a borrowed string and is immutable

Rust supports closures, let’s rewrite the previous example with mut closures:

pub fn raindrops(n: u32) -> String {
    let mut sound = String::new();

    let mut on_factor_push = |factor, term| {
        if n % factor == 0 {
            sound.push_str(term)
        }
    };

    on_factor_push(3, "Pling");
    on_factor_push(5, "Plang");
    on_factor_push(7, "Plong");

    if sound.is_empty() {
        sound = n.to_string();
    }

    sound
}

Here’s an example of filter, closure and method chaining:

pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 {
    (0..limit)
        .filter(|n| factors.iter().filter(|&&f| f != 0).any(|f| n % f == 0))
        .sum()
}

References

The Rust Programming language

Rust by example

Rust standard library

Rust reference book

Learning Rust

Programming idioms

Rust Language Cheat Sheet

comments powered by Disqus