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 repositorysrc
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()
}