Rust: Getting rusty

Rust is a relatively new system programming language. This article will attempt to cover some of the basic features of Rust that are common to languages of identical paradigms.

A "hello world"

A Rust hello world can be as simple as,

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

Like many other languages that use def or function, Rust uses fn to indicate a function definition. The function name follows fn, then empty parenthesis pair () indicates that the function doesn't accept any argument. The region enclosed by brace pair {} is function body, as usual. But, probably you noticed something weird in the line println!("Hello world");, there's an exclamation sign (!) following by the function name. This is one of Rust's many unique features, and interestingly this is a macro rather than a function.

Fire it up

In order to work with Rust in our local machine, we need Rust installed. This can be achieved by running a ready made script,

curl -sSf https://static.rust-lang.org/rustup.sh | sh

Although this is a one time step you will have to take, but if you're not feeling like investing that time, then you may use the online editor available here.

Remember, Rust is a compiled language, so we have to face the same painful process of compilation before running the code. Let's do that. First, create a project directory and cd into it.

$ mkdir rusty
$ cd rusty

Now create a source File with rs extension (all the rust sources have any of rs, rlib, rafto extension),

touch main.rs

and insert our little hello world program in it. Save it. And, now the part we are waiting for. Let's compile it.

rustc main.rs

rustc is the rust compiler which, as usual, generates an executable (with the same name as the source, but excluding any extension). Now run the executable

./main

Basics

Rust is like a king in it's own domain. There are too much Rust specific unique features that are to be honest, overwhelming. So, let's discuss the basic features we are generally used to and has wide cross language availability.

Variable

Variables are interesting in Rust. Let's see it through. A variable can be declared like this,

let x = 777;

But, don't be deceived by it's naive look. If you try something like this,

let x = 777;
x = 44;

this will fail. And, that is because variable bindings are immutable by default. In order to define a mutable variable binding we will some something like

let mut x = 777;

Since, it's a statically typed language, you may be wondering, shouldn't I declare a data type? Rust can infer data types and so far we are using the inference feature of Rust. Now, let's define the same variable but, this time, like C/C++ we'll explicitly mention the type.

let x: i32 = 777;

Here we declared x as an immutable 32-bit integer. Now let's have a quick peek to some basic (primitive) types that we may use, Signed integer: i8, i16, i32, i64 Unsigned integer: u8, u16, u32, u64 Float: f32, f64 Boolean: bool Character: char

An important thing to note here, char is by default, four bytes.

Rust has it's referencing mechanism that can be used like,

let x = 777;
let y = &x;

The & here implying that y is indicating to the reference of x. It's more complicated than it appears here. In order to explain it all we'll have to cover several Rust specific concepts like Ownership, Borrowing and Lifetime. We are safe leaving those concepts untouched as long as we are not modifying the referenced variable. So, let's skip it for now.

Let's get familiar with another type which handles sequential data. Yeah, I'm talking about array. An array declaration is made like this,

let x = [666, 777, 888];

Where some basic operations like selection by index and slicing is performed by doing this,

let x = [666, 777, 888];

// Selection by index
x[1]  // returns second entry

// Slicing
let y = &x[..]; // Returns the entire array

The new operator here .. is to indicate range. We could write,

let y = &x[2..];

which would have returned an array with only the last element of x in it.

Code units

Rust's coding units are functions. Functions have following structure.

fn function_name(arg1, arg2, ...) -> return_type {

}

Interesting, isn't it? Let's take a look at a function that takes an integer and returns it's square

fn square(x: i32) -> i32 {
  x*x
}

How about some string?

fn print_merged_str(x: &str, y: &str) {
  println!("{}{}", x, y);
}

Did you notice something? In our string example, there is no return type specified. This, is a default. Functions that doesn't return any value doesn't need the return section. Only exceptions are divergents, but they are whole different things. 😃

Flow control

Rust got it's own version of if.

if expression {
  // ...
} else if expression {

} else {
  // ...
}

The only distinguishable thing here from a C if, is that, it has got not parenthesis to enclose the switching expression. There is no other significant differences.

Iteration

It has the basic while,

while expression {
  // ...
}

and a wonderful for, which is very much like the python's in.

for iterated_value in expression {
  // ...
}

There is also an infinite looping support through

loop {
  // ..
}

which eliminates the need for modifying while or for to achieve this.

A premature escape can be performed through a break statement. I guess you already presumed the existence of a continue statement? Yeah, it exists and functions as expected. Additionally, Rust supports loop labeling that can be used with while or for to achieve controlled loop escaping. For details, see [#link]

Lambda

In Rust, lambda's are called Closures. A closure can be defined as follows,

let x = |p: i32| p*p;

Here, x is the variable binding. The region enclosed by vertical bars contains arguments, the region succeeded by the vertical bars is the lambda definition. It is quite easy, isn't it? 😃

Modularization

So far we worked on a single file source, and didn't use any libraries, but nearly all of the projects will require many dependency libraries. Also, when the project will get bigger, a single file based model will soon become unrealistic. Let's deal with these issues.

Dependency management

Rust provides a tool named cargo that deals with the dependency. All the libraries are called crates. Let's build a cargo enabled binary project (project that can be utilized as an executable).

cargo new rusty --bin

This creates a project folder structure where rusty is the root folder. Let's explore inside,

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

Cargo.toml is the configuration file that cargo uses during dependency management and compilation operations. If we need to add a dependency, we'll append the cratename and it's associated semantic versioning that we want to use in the [dependencies] section of Cargo.toml like this,

...

[dependencies]
package_name = "1.0.0"

Crate generation

Crate building is very much identical to binary project generation that we mentioned in the previous section. In order to build a crate skeleton,

cargo new rusty_crate

This will generate,

  rusty_crate
  ├── Cargo.toml
  └── src
      └── lib.rs

The only difference in a crate is that it doesn't have a fn main() entry point.

In order to utilize a crate, in the consumer rust source file, we'll prepend the following line

extern crate crate_name;

Then we can utilize the crate contents (with the consideration that, it contains a square function) as follows,

println!("Square of four: {}", crate_name::square(4));

Or, we could import the function names into our consumer source's scope by utilizing use like this,

extern crate rusty;

use rusty::square;

println!("Square of four: {}", square(4));

Let's consider another scenario, where rusty crate has a submodule math, which contains square function. We could handle this scenario like this,

extern crate rusty;

use rusty::math::square;

println!("Square of four: {}", square(4));

Performance

Rust performance is very much identical to the performance of C. In some cases it outperforms C and in some other cases it's slower than C. But this should not be considered a bad thing, since Rust is relatively very new and the issues are getting resolved fast and the community is pretty strong.

A tour to the github repository for Rust compiler and standard libraries show that, there's about ~450 issues tagged for it's slow performance, where about ~350 issues are already closed [#link]. The issue reporting and resolving frequency suggests that, we can expect an exactly equivalent performance to C very soon.

Cross platform support

Rust is supported across most of the major platforms. The platform wise support in Rust is divided into three different tiers. A tier indicates the support, reliability and build and execution guarantee.

Tier 1 can be summarized as fully supported platforms.

Tier 2 platform codes will build reliably but test are not always available.

Tier 3 platform support is mostly unofficial and reliability is a big concern here.

Usage

Since Rust is a system programming language, not to mention, it is used mostly in cases where critical performance is required, like embedded applications, game engine development, sophisticated algorithm implementation and many more. Organizations like Mozilla, Dropbox, Samsung, Skylight are contributing and/or consuming Rust in their products. A good example could be Mozilla's Servo browser engine. Rust has over 1200 contributors. Pretty strong, nah?

Possibilities

Many considers Rust as a modern alternative to C. While it is still lacking behind in terms of performance, but the margins are expected to converge soon enough.