The problem is that there are some scenarios where we need to break these rules. This is where smart pointers help us.
Smart pointers are structs that manage some internal data.
They are called pointers because they implement the Deref trait, so they can be used like pointers (Using the &
and *
syntax).
They are called smart, because they manage the lifecycle of their internal data. This typically means, among other things, implementing the Drop trait so resources are released correctly when the pointer goes out of scope.
I’ve found the Box
smart pointer to be useful to do cheap transfers of ownership.
In rust, when we transfer ownership of a variable, the whole variable is copied to a different memory location:
1
2
3
4
5
6
7
fn main() {
let text = String::from("Some text");
println!("The text is at: {:p}", &text); // This prints a memory location
let new_text = text; // This invalidates text variable
println!("The text is at: {:p}", &new_text); // This prints a different memory location
}
In the example above, the line let new_text = text;
moves all the data from text
to a different location. If text
contained a very large text, moving the data could be costly.
If we use a Box
, the move happens in constant time, since only the pointer needs to be moved:
1
2
3
4
5
6
fn box_move() {
let b1 = Box::new(String::from("Another text"));
println!("b1 is at {:p}, b1 points to: {:p}. The text is: {}", &b1, b1, b1);
let b2 = b1;
println!("b2 is at {:p}, b2 points to: {:p}. The text is: {}", &b2, b2, b2);
}
In the example above, the address for the boxes changes, but the address for the string remains the same.
There are scenarios where we need a variable to have multiple owners. Rc (Reference counted) smart pointers allow us to achieve this without sacrificing safety:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn rc_pointer() {
let pointer = Rc::new(String::from("More text"));
// Reference count here is 1
println!("pointer is at {:p}, pointer points to: {:p}, reference count is: {}", &pointer, pointer, Rc::strong_count(&pointer));
{
let pointer_clone = Rc::clone(&pointer);
// pointer_clone is at a different address than pointer, but the underlying
// data is at the same address. Reference count here is 2
println!("pointer is at {:p}, pointer points to: {:p}, reference count is: {}", &pointer_clone, pointer_clone, Rc::strong_count(&pointer_clone));
// Reference count here is 2
println!("pointer is at {:p}, pointer points to: {:p}, reference count is: {}", &pointer, pointer, Rc::strong_count(&pointer));
}
// Since pointer_clone has gone out of scope, reference count was decreased to 1
println!("pointer is at {:p}, pointer points to: {:p}, reference count is: {}", &pointer, pointer, Rc::strong_count(&pointer));
}
The example above shows how by cloning an Rc
we can have multiple variables owning the same underlying data. Rc
automatically decreases the reference count when an owner goes out of scope so resources are correctly freed when there are no more owners.
Smart pointers are very easy to use and help us achieve some things that are often necessary.
Find runnable versions of the examples above in my code samples repo.
]]>borrow checker
that makes sure references are not used when they are not valid anymore. The borrow checker uses lifetimes to do its job internally.
Let’s look at a simple example where the borrow checker detects a possibly invalid reference:
1
2
3
4
5
6
7
8
9
10
fn main() {
let r;
{
let i = 1;
r = &i;
}
println!("{}", r);
}
If we compile this, we’ll get the following error:
1
2
3
4
5
6
7
8
9
10
11
12
error[E0597]: `i` does not live long enough
--> src/main.rs:6:13
|
5 | let i = 1;
| - binding `i` declared here
6 | r = &i;
| ^^ borrowed value does not live long enough
7 | }
| - `i` dropped here while still borrowed
8 |
9 | println!("{}", r);
| - borrow later used here
The message says that r = &i
is “borrowing” from i
. Later in the code, we try to use r
, but i
is invalid (because it’s gone out of scope), which makes r
invalid too. This shows that a reference is not allowed to outlive the variable it borrows from.
So far, it’s pretty simple. It becomes trickier when the compiler is unable to infer the lifetime of a reference without the programmer’s help.
When we write a function that returns a reference, the returned reference is always one of the received arguments. The reason for this is that all variables created inside the function will go out of scope when the function execution finishes.
This means, the lifetime of the returned reference is the same as the lifetime of one of the arguments. When we have a single argument, the compiler knows to use that arguments lifetime.
We can see that this is true by compiling this code:
1
2
3
4
5
6
7
8
9
10
fn ditto(input: &str) -> &str {
input
}
fn main() {
let str = String::from("Ditto");
let str2 = ditto(&str);
drop(str);
println!("{}", str2);
}
We will get the following error message:
1
2
3
4
5
6
7
8
9
10
11
error[E0505]: cannot move out of `str` because it is borrowed
--> src/main.rs:8:10
|
6 | let str = String::from("Ditto");
| --- binding `str` declared here
7 | let str2 = ditto(&str);
| ---- borrow of `str` occurs here
8 | drop(str);
| ^^^ move out of `str` occurs here
9 | println!("{}", str2);
| ---- borrow later used here
The error message is telling us that str2
(the reference returned by ditto) is a borrow of str
, so it can’t be used after str
has been dropped.
If a function has more than one argument, things become a little more interesting. Probably surprisingly, the following code will fail to compile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn return_best(input1: &str, input2: &str) -> &str {
if input1 > input2 {
input1
} else {
input2
}
}
fn main() {
let str1 = "One";
let str2 = "Two";
let str3 = return_best(&str1, &str2);
println!("{}", str3);
}
This is the error we’ll get:
1
2
3
4
5
6
7
8
9
10
11
error[E0106]: missing lifetime specifier
--> src/main.rs:1:47
|
1 | fn return_best(input1: &str, input2: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `input1` or `input2`
help: consider introducing a named lifetime parameter
|
1 | fn return_best<'a>(input1: &'a str, input2: &'a str) -> &'a str {
| ++++ ++ ++ ++
The compiler is telling us: “I’m getting two references as arguments and I don’t know what’s the lifetime of the returned reference. Please tell me”. The last part of the error message suggests a way to fix it:
1
2
3
4
5
6
7
fn return_best<'a>(input1: &'a str, input2: &'a str) -> &'a str {
if input1 > input2 {
input1
} else {
input2
}
}
The code above uses lifetime annotations to tell the compiler our intentions. Let’s take a closer look at the function signature:
1
fn return_best<'a>(input1: &'a str, input2: &'a str) -> &'a str {
First of all, we can see that there are some angle brackets (<>
) between the function name and the arguments list. Inside the angle brackets we are specifying a generic lifetime ('a
). Generic lifetimes start with a single quote '
and are generally a single letter.
Then, we can see that we changed our arguments to use &'a str
instead of &str
. This basically means: input1
and input2
are string references and since both use the lifetime 'a
, it will be the overlap between the lifetimes of both arguments.
Finally, we changed the returned type from &str
to &'a str
. We already mentioned that the 'a
is the overlap of the lifetime of both arguments, so that will be the returned lifetime.
We can see that the lifetime of 'a
is equal to the overlap of both arguments, by trying to compile this code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn return_best<'a>(input1: &'a str, input2: &'a str) -> &'a str {
if input1 > input2 {
input1
} else {
input2
}
}
fn main() {
let str1 = String::from("One");
let str3;
{
let str2 = String::from("Two");
str3 = return_best(&str1, &str2);
}
println!("{}", str3);
}
We get the following error message, which tells us that we are trying to use str3
when it might already be invalid:
1
2
3
4
5
6
7
8
9
10
11
error[E0597]: `str2` does not live long enough
--> src/main.rs:14:35
|
13 | let str2 = String::from("Two");
| ---- binding `str2` declared here
14 | str3 = return_best(&str1, &str2);
| ^^^^^ borrowed value does not live long enough
15 | }
| - `str2` dropped here while still borrowed
16 | println!("{}", str3);
| ---- borrow later used here
If we want to create structs that hold references we need to annotate them:
1
2
3
struct HoldReference<'a> {
something: &'a str,
}
This helps the compiler figure out the lifetime of the whole struct.
This article helps us understand the relationships between the lifetimes of different variables and how the compiler uses them to help us avoid mistakes.
This article helped me get through some compiler errors I was getting while writing some code; but I can’t help but wonder “Could the compiler figure out the lifetimes without the need for annotations?”. I tried to find some information about why the annotations are required in cases where the lifetime might be obvious, and from my understanding, these are the reasons:
Find runnable versions of the examples above in my code samples repo.
]]>traits
are Rust’s alternative to Interfaces. They allow us to use polymorphism in Rust. We can create a trait like this:
1
2
3
trait Calculator {
fn add(&self, left: i32, right: i32) -> i32;
}
To implement the trait we use the impl
keyword on a struct:
1
2
3
4
5
6
7
struct GoodCalculator {}
impl Calculator for GoodCalculator {
fn add(&self, left: i32, right: i32) -> i32 {
left + right
}
}
Traits (like interfaces) are useful as function parameters, so different types can be passed. The simplest way to receive a trait as a parameter is using the impl
keyword like this:
1
2
3
fn add_using_calculator(calculator: &impl Calculator) {
println!("The result of adding {} and {} is: {}", 10, 5, calculator.add(10, 5));
}
When we need a type to implement multiple interfaces, we can use this syntax:
1
2
3
4
fn add_and_print(the_thing: &(impl Calculator + Printer)) {
the_thing.print();
println!("The result of adding {} and {} is: {}", 10, 5, the_thing.add(10, 5));
}
Another syntax:
1
2
3
4
fn add_and_print<T: Calculator + Printer>(the_thing: &T) {
the_thing.print();
println!("The result of adding {} and {} is: {}", 10, 5, the_thing.add(10, 5));
}
When we have multiple arguments using the following syntax is preferred:
1
2
3
4
5
6
7
fn add_and_print<T>(the_thing: &T)
where
T: Calculator + Printer
{
the_thing.print();
println!("The result of adding {} and {} is: {}", 10, 5, the_thing.add(10, 5));
}
Sometimes we want to provide default implementations for some methods on our traits. This can easily be done:
1
2
3
4
5
trait SoundMaker {
fn print(&self) {
println!("Default implementation")
}
}
Traits in Rust, work similarly to interfaces in Golang. I added some code using the above examples to Github so you can see it in action.
]]>Rust made the interesting decision that unit tests should be written in the same files as the code under test. Let’s imagine we have a module with a function named add
:
1
2
3
pub fn add(left: i64, right: i64) -> i64 {
left + right
}
If we want to test that function, we would modify the file to look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub fn add(left: i64, right: i64) -> i64 {
left + right
}
#[cfg(test)]
mod tests {
use super::{
add
};
#[test]
fn first_test() {
let result = add(2, 2);
assert!(result == 4);
}
}
The #[cfg(test)]
parameter tells the compiler to only compile that code when we are running tests. The #[test]
parameter is used to specify a function as being a test.
We can run our tests using cargo
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.01s
Running unittests src/lib.rs (target/debug/deps/testing-d359a3188418919d)
running 1 test
test tests::first_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests testing
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
We can see in the example above, the use of assert!
. This is a macro that panics if the passed argument is false. Currently, there are three native assertions in Rust: assert
, assert_eq
, assert_ne
. Here are some examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pub fn add(left: i64, right: i64) -> i64 {
left + right
}
#[cfg(test)]
mod tests {
use super::{
add
};
#[test]
fn first_test() {
let result = add(2, 2);
assert!(result == 4);
}
#[test]
fn assert_eq() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn assert_ne() {
let result = add(2, 2);
assert_ne!(result, 3);
}
}
Assertions can contain custom messages to make it easier to understand what the problem is. For example:
1
2
3
4
5
#[test]
fn assert_with_message() {
let result = add(2, 2);
assert!(result == 4, "Expected 4, actual value was {}", result);
}
Another thing worth noting is that, since we are writing tests within the module, it’s possible to access private members. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub fn add(left: i64, right: i64) -> i64 {
private_add(left, right)
}
fn private_add(left: i64, right: i64) -> i64 {
left + right
}
#[cfg(test)]
mod tests {
use super::{
private_add
};
#[test]
fn test_private_fn() {
let result = private_add(2, 2);
assert_eq!(result, 4);
}
}
Sometimes we want to test that our code panics given certain conditions. We can test that with should_panic
. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub fn divide(left: i64, right: i64) -> i64 {
left / right
}
#[cfg(test)]
mod tests {
use super::{
divide
};
#[test]
#[should_panic]
fn test_panic() {
divide(2, 0);
}
#[test]
#[should_panic(expected="divide by zero")]
fn test_panic_with_message_matching() {
divide(2, 0);
}
}
In the second example we specified the divide by zero
message. In this case, the test will pass if the panic message contains the given string.
If for some reason, we want to ignore a test (maybe it’s flaky and we don’t want to block all builds while we fix it), we can use the #[ignore]
parameter. For example:
1
2
3
4
5
6
#[test]
#[ignore]
fn ignored_failing_test_with_message() {
let result = add(2, 2);
assert_eq!(result, 3, "Expected 3, actual value was {}", result);
}
If we are troubleshooting a specific test, we can filter the tests we want to run by specifying a string to match against the name of the test. For example:
1
cargo test test_private_fn
Rust has a nice feature, where it automatically runs code that is defined using ///
comments. This has the benefit that if our code changes and the examples in our comments are not valid anymore, we will be alerted about it.
An example of how to write doc tests:
1
2
3
4
5
6
7
8
9
10
/// Divides the first number by the second number. Panics if second number is 0
///
/// # Example
///
/// ```
/// assert_eq!(2, testing::divide(8, 4));
/// ```
pub fn divide(left: i64, right: i64) -> i64 {
left / right
}
The output of running cargo test
will look something like this:
1
2
3
4
Doc-tests testing
running 1 test
test src/lib.rs - divide (line 5) ... ok
When we want to write tests that spawn multiple functions, files, or modules, we can create integration tests. Integration tests live in a tests
folder at the same level of the src
folder:
1
2
3
4
5
6
7
project_folder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
└── tests
└── some_test.rs
When we run cargo test
, cargo will automatically run all tests inside the tests
folder.
Since all code under the tests
folder is a test, we don’t need to use #[cfg(test)]
. An example integration test looks like this:
1
2
3
4
5
6
7
use testing;
#[test]
fn integration_test() {
let result = testing::add(2, 2);
assert_eq!(result, 4);
}
Mocking is a commonly used tool when writing tests. In Rust, the most popular tool for doing this is Mockall.
The simplest way to use it, is applying the #[automock]
parameter to a trait. After doing this, a mock named Mock<Name of the trait>
will be made available. With the mock we can do things like expect a method to be called or return a specified value when calling a function. An example usage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use mockall::{
automock,
};
#[automock]
trait Calculator {
fn add(&self, left: i64, right: i64) -> i64;
fn subtract(&self, left: i64, right: i64) -> i64;
}
#[cfg(test)]
mod tests {
use super::{
Calculator,
MockCalculator,
};
use mockall::predicate::eq;
#[test]
fn test_mock_expectations_are_met() {
let mut mock = MockCalculator::new();
mock.expect_add()
.with(eq(1), eq(2))
.return_const(3);
assert_eq!(mock.add(1, 2), 3);
}
}
For scenarios where we need a mock that implements multiple traits, we can use the mock!
macro. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use mockall::{
mock
};
trait Calculator {
fn add(&self, left: i64, right: i64) -> i64;
fn subtract(&self, left: i64, right: i64) -> i64;
}
trait Beeper {
fn beep(&self);
}
mock! {
BeeperCalculator {}
impl Calculator for BeeperCalculator {
fn add(&self, left: i64, right: i64) -> i64;
fn subtract(&self, left: i64, right: i64) -> i64;
}
impl Beeper for BeeperCalculator {
fn beep(&self);
}
}
#[cfg(test)]
mod tests {
use super::{
Beeper,
Calculator,
MockBeeperCalculator,
};
#[test]
fn test_mock_multiple_traits() {
let mut mock = MockBeeperCalculator::new();
mock.expect_add().return_const(3);
mock.expect_beep().return_const(());
mock.add(1, 2);
mock.beep();
}
}
I think test code living in the same file as the code it’s testing is not great, because it can make it harder to navigate code files. Most likely Rust chose that standard because it makes tests more visible.
You can find a runnable version of the examples in this article in my examples repo in github.
]]>Rust compiles directly to machine code, so it doesn’t require a virtual machine. This makes it faster than languages like Java or Python. It also doesn’t use a garbage collector, which makes it faster and more predictive than other compiled languages like Golang.
On top of speed and predictability, Rust also promises a programming model that ensures memory and thread safety, which makes it great for complex applications.
The recommended way to install rust in Linux and Mac is using this command:
1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
We will be greeted by this prompt asking to choose an option:
1
2
3
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
We can choose the default (option 1
).
When the installation finishes, we will need to close our terminal and open a new one. Then we can use this command to verify the installation was successful:
1
rustc --version
Now that we have rust installed, let’s create a file named hello_world.rs
, with this content:
1
2
3
fn main() {
println!("Hello, world!");
}
We can then compile and run the program with:
1
rustc hello_world.rs && ./hello_world
One thing to notice from our hello world code is that there is an exclamation mark (!
) after println
. For now, we just need to know that println
is a macro, and we need to use that notation (!
) when calling macros.
When we installed Rust, Cargo was automatically installed:
1
cargo --version
Cargo allows us to create and interact with Rust projects in a standard way.
We can create a new project and run it with:
1
2
3
cargo new cargo_demo
cd cargo_demo
cargo run
If we inspect the cargo_demo
folder we will see a few files that were created for us.
cargo.toml
is a configuration file for our project. It let’s us set some properties, and it also keeps track of the project’s dependencies:
1
2
3
4
5
6
7
8
[package]
name = "cargo_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cargo.lock
is used to manage dependencies. This file should never be edited manually.
By default cargo generates a project that uses git as version control. For this reason a .git
folder and .gitignore
file are created.
Finally, the code lives inside the src
folder. The generator creates a single file named main.rs
.
Variables in Rust have some peculiar behaviors. We’ll explore those in this section.
They are immutable and their type is implied based on the context:
1
2
3
fn main() {
let x = 5;
}
The code above will create an immutable variable named x
of type i32
and value 5
.
Immutable variables can’t be changed. To make a variable mutable, we can use the mut
keyword:
1
2
3
4
fn main() {
let mut x = 5;
x = 10;
}
By using the mut
keyword, we can modify the value of x
.
An immutable variable might sound like a constant, but there are a few differences between constants and immutable variables in Rust. These statements are true for constants:
const
keywordAn example of a constant declaration:
1
const MY_NUMBER: u32 = 23;
Another interesting aspect of variables in Rust is how shadowing works. The following code is valid in Rust as well as in many other programming languages:
1
2
3
4
5
6
7
8
9
10
fn main() {
let x = 1;
if true {
let x = 2;
println!("x is {x}");
}
println!("x is {x}");
}
The output is:
1
2
x is 2
x is 1
The re-declaration of x
in the inner scope shadows
the previous declaration of x
.
On the other hand, rust allows re-declaring variables in the same scope:
1
2
3
4
5
6
7
fn main() {
let x = 1;
println!("x is {x}");
let x = 2;
println!("x is {x}");
}
The output is:
1
2
x is 1
x is 2
This works in Rust. In other programming languages we would probably get an error telling us that a variable with the name x
has already been declared in that scope.
We’ve already seen the fn
keyword used in the definition of the main
function. We can also create our own functions that receive arguments and return a value. For example:
1
2
3
fn add(x: i32, y: i32) -> i32 {
x + y
}
As is normal in compiled languages, we need to define the types of the arguments and the return value.
The most surprising thing (in my opinion) is the implicit return of the last statement inside a function (with the caveat that it shouldn’t be ended with a semicolon (;
)). We can still use the return
keyword for early returns when necessary. For example:
1
2
3
4
5
6
7
fn add(x: i32, y: i32) -> i32 {
if x == 0 || y == 0 {
return 0
}
x + y
}
When it comes to Object Orientation, Rust is more similar to Golang than it is to C++.
This is an example of how we can create and use a simple struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Animal {
number_of_legs: u8,
color: String,
}
fn main() {
let dog = Animal {
number_of_legs: 4,
color: String::from("brown"),
};
println!("Animal color: {}, number of legs {}", dog.color, dog.number_of_legs);
}
Similar to Golang, method definition is done outside the struct. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Animal {
number_of_legs: u8,
color: String,
sound: String,
}
impl Animal {
fn talk(&self) {
println!("{}", self.sound)
}
}
fn main() {
let dog = Animal {
number_of_legs: 4,
color: String::from("brown"),
sound: String::from("Woof"),
};
dog.talk();
}
Note how we use impl
to start defining methods for a struct.
We can create a method that acts as a constructor by returning a Self
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Animal {
number_of_legs: u8,
color: String,
sound: String,
}
impl Animal {
fn talk(&self) {
println!("{}", self.sound)
}
fn create_dog(color: String) -> Self {
Self {
number_of_legs: 4,
color: color,
sound: String::from("Woof"),
}
}
}
fn main() {
let dog = Animal::create_dog(String::from("brown"));
dog.talk();
}
Privacy in Rust is managed at the package / crate / module level, not at the struct level. Structs, methods and attributes are always locally accessible and by default not accessible by other modules. To illustrate this, let’s dig into packages.
A Package
is a cargo project that can contain multiple binaries and at most, one library. Binaries and libraries are called Crates
in Rust. A crate can contain multiple Modules
, which are defined by a directory structure in the file system.
We created a package in the past with this command:
1
cargo new cargo_demo
A package contains a cargo.toml
file that defines the crates inside of it. Cargo also created the file src/main.rs
. By convention, this means we are creating a binary crate. If we wanted to create a library, we would put it in src/lib.rs
. If we wanted to create a package with multiple binaries, we would create the folder src/bin/
and add our binaries in that folder.
For a newly created project, we will get this folder structure:
1
2
3
4
5
cargo_demo
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
Let’s create a new module by creating the folder src/zoo
and the files src/zoo/animal.rs
and src/zoo.rs
:
1
2
3
4
5
6
7
8
cargo_demo
├── Cargo.lock
├── Cargo.toml
└── src
├── zoo
│ └── animal.rs
├── main.rs
└── zoo.rs
We need to include the module animal
inside zoo.rs
:
1
pub mod animal
Notice the usage of pub
to make the module accessible by other modules.
We can then add this to animal.rs
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct Animal {
number_of_legs: u8,
color: String,
sound: String,
}
impl Animal {
pub fn talk(&self) {
println!("{}", self.sound)
}
pub fn create_dog(color: String) -> Self {
Self {
number_of_legs: 4,
color: color,
sound: String::from("Woof"),
}
}
}
Here, we make the Animal
struct public, as well as the talk
and create_dog
methods. Otherwise, we wouldn’t be able to access them from outside this module.
Finally, we can use the newly defined module in main.rs
:
1
2
3
4
5
6
7
8
9
use crate::zoo::animal::Animal;
pub mod zoo;
fn main() {
let dog = Animal::create_dog(String::from("brown"));
dog.talk();
}
If we tried to access a field from Animal
. For example:
1
2
3
4
5
6
7
8
use crate::zoo::animal::Animal;
pub mod zoo;
fn main() {
let dog = Animal::create_dog(String::from("brown"));
println!("{}", dog.sound)
}
We would get an error:
1
2
3
4
5
error[E0616]: field `sound` of struct `Animal` is private
--> src/main.rs:7:24
|
7 | println!("{}", dog.sound)
| ^^^^^ private field
The way Rust manages resource ownership is probably its main selling point. At the cost of some initially unintuitive rules, it provides a safer way to manage resources.
Rust is similar to C++ in that it has the ability to create destructors that can be used to free resources when a variable goes out of scope. When we are talking about collections or structs, the destructor will be run in all of the children recursively before it’s run in the parent.
A destructor can be defined for a struct like so:
1
2
3
4
5
impl Drop for Animal {
fn drop(&mut self) {
println!("Cleaning animal's poop")
}
}
drop
will be called automatically when an Animal
goes out of scope.
To be able to call destructors safely, Rust has some strict ownership rules. Consider this code:
1
2
3
4
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
It will surprisingly, return an error:
1
2
3
4
5
6
7
8
9
10
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:12:28
|
9 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
10 | let s2 = s1;
| -- value moved here
11 |
12 | println!("{}, world!", s1);
| ^^ value borrowed here after move
What happens here is that String is a type of varying size that goes on the heap. This means that s1
is actually a pointer. When we assign s1
to s2
, the data in the heap doesn’t move, s2
is assigned the same data as s1
(another pointer to the data in the heap), and s1
is invalidated. This is similar to move semantics in C++, but done automatically.
What we want to do instead is assign s2
a reference to s1
:
1
2
3
4
let s1 = String::from("hello");
let s2 = &s1;
println!("{}, world!", s1);
Or clone the string:
1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, world!", s1);
This default behavior becomes trickier when dealing with functions. Look at this scenario:
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
print_string(s1);
println!("{}", s1);
}
fn print_string(input: String) {
println!("{}", input);
}
If we try to run it, we get this error:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | print_string(s1);
| -- value moved here
4 | println!("{}", s1);
| ^^ value borrowed here after move
|
note: consider changing this parameter type in function `print_string` to borrow instead if owning the value isn't necessary
--> src/main.rs:7:24
|
7 | fn print_string(input: String) {
| ------------ ^^^^^^ this parameter takes ownership of the value
| |
| in this function
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | print_string(s1.clone());
| ++++++++
When we call print_string
, we are moving s1
into the function, so it’s invalidated and can’t be used anymore.
The right way to achieve what we intended is to use a reference instead:
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
print_string(&s1);
println!("{}", s1);
}
fn print_string(input: &String) {
println!("{}", input);
}
If we want to modify a passed reference, we need to make it mutable. This example works as expected:
1
2
3
4
5
6
7
8
9
fn main() {
let mut s1 = String::from("hello");
modify_string(&mut s1);
println!("{}", s1);
}
fn modify_string(input: &mut String) {
input.push_str("-bye");
}
One thing to keep in mind about mutable references is that, when we have a mutable reference to a variable, we can’t have any other reference at the same time. This will almost never be a problem for a single threaded program, but it becomes important when working with multiple threads. That’s something we’ll cover in more detail in another article.
I’ve been wanting to take a look at Rust for while and I’m happy I finally got the time. At a first glance it feels to me like a mix of C++ and Golang.
Personally I prefer the way C++ and Java do Object Orientation, but it seems like Rust went with the Golang way.
Although I’m just getting started, I can see how Rust can make programming safer with the way it prevents modifications of variables by forcing the user to be explicit about their intentions.
In general, I dislike garbage collection, so I’m happy that Rust went with destructors instead.
]]>ArduinoHttpClient
library:
1
arduino-cli lib install ArduinoHttpClient
Once we have the library, we can use it to make requests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <WiFiS3.h>
#include <ArduinoHttpClient.h>
const char SSID[] = "NETWORK_NAME";
const char PASS[] = "NETWORK_PASSWORD";
const char HOST_NAME[] = "echo.free.beeceptor.com";
const int HTTP_PORT = 443;
// Use WiFiSSLClient when using https and WiFiClient when using http
WiFiSSLClient wifi;
HttpClient client = HttpClient(wifi, HOST_NAME, HTTP_PORT);
int status = WL_IDLE_STATUS;
String PATH_NAME = "/";
void setup() {
Serial.begin(9600);
// Verify WiFi module is available
while (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
delay(2000);
}
// Connect to WiFi
while (status != WL_CONNECTED) {
char buffer[50];
sprintf(buffer, "Connecting to network: %s", SSID);
Serial.println(buffer);
status = WiFi.begin(SSID, PASS);
// Give some time for connection to be stablished
delay(5000);
}
Serial.println("Connected!");
}
void loop() {
Serial.println("\n");
Serial.println("Making request");
client.get(PATH_NAME);
// read the status code and body of the response
int statusCode = client.responseStatusCode();
String response = client.responseBody();
char statusBuffer[30];
sprintf(statusBuffer, "\nStatus code: %i", statusCode);
Serial.println(statusBuffer);
Serial.println("Response: ");
Serial.println(response);
Serial.println("\n");
Serial.println("Waiting...");
delay(20000);
}
We can upload the code to our board with:
1
sudo chmod a+rw /dev/ttyACM0 && arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi . && arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:renesas_uno:unor4wifi .
Neovim comes with an LSP client included, nvim-lspconfig is a plugin that helps us configure the client so it can talk to LSP servers.
This configuration should be enough to get started with Arduino:
1
2
3
4
5
6
return {
"neovim/nvim-lspconfig",
config = function()
require('lspconfig').arduino_language_server.setup {}
end
}
Neovim runs the LSP client, but it needs to communicate with an LSP server to do its job. We can manually install LSP servers for the languages we are interested in and manually start them and stop them as needed, or we can use Mason and Mason-lspconfig to take care of installing and starting the servers when necessary.
For the mason configuration, this will suffice:
1
2
3
4
5
6
7
return {
'williamboman/mason.nvim',
build = ":MasonUpdate",
config = function()
require("mason").setup()
end
}
We will also need the following mason-lspconfig configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return {
"williamboman/mason-lspconfig.nvim",
dependencies = {
'williamboman/mason.nvim',
},
config = function()
require("mason-lspconfig").setup({
ensure_installed = {
'arduino_language_server',
-- We need to install clangd for arduino_language_server to work
'clangd'
}
})
end
}
Next time we start Neovim, we should get these messages (Use the :messages
command to print the latest messages in Neovim):
1
2
3
4
[mason-lspconfig.nvim] installing arduino_language_server
[mason-lspconfig.nvim] arduino_language_server was successfully installed
[mason-lspconfig.nvim] installing clangd
[mason-lspconfig.nvim] clangd was successfully installed
Although Mason takes care of installing Arduino Language Server, we also need arduino-cli
in our system. Instructions for installing it can be found in the Arduino CLI documentation
We are also required to have an arduino-cli
config file in ~/.arduino15/arduino-cli.yaml
. We can create it with this command:
1
arduino-cli config init
And install the correct core for our board. For example:
1
arduino-cli core install arduino:renesas_uno
Let’s create a new Sketch to test our configuration:
1
arduino-cli sketch new TestSketch
To help the language server understand our project we need to create a sketch.yaml
file. We can do it with this command:
1
2
cd TestSketch
arduino-cli board attach -p /dev/ttyACM0 -b arduino:renesas_uno:unor4wifi TestSketch.ino
To see Arduino LSP in action, open TestSketch.ino
and delete the last bracket in the file. We’ll get an error telling us that there is a missing bracket:
We already saw that Arduino Language Server can tell us when we have syntax errors in our code. In Neovim, we call this diagnostics. In this section we are going to explore what other things we can do with Arduino LSP.
If we want to see the definition of a symbol under our cursor, we can use this command:
1
:lua vim.lsp.buf.definition()
This will replace the current buffer with the definition of the symbol that was under our cursor.
Another way to achieve the same, is to use C-]
(Control + ]). After doing this, we can return to where we were by using C-t
.
Since these key bindings are a little hard to type, I like to add these shortcuts to my Neovim configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'LSP actions',
callback = function(event)
-- Go to definition
vim.keymap.set('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>', {buffer = event.buf})
-- Return to previous location after going to definition
vim.api.nvim_set_keymap('n', 'gb', '<C-t>', {})
-- Go to definition in new tab
vim.api.nvim_set_keymap('n', 'gdt', '<C-w><C-]><C-w>T', {})
end
})
This way, I can use gd
to go to a definition in the same buffer, gb
to return and gdt
to go to the definition in a new tab.
We can trigger code completion with the combination: C-x C-o
.
For example, if we type matrix.
and then C-x C-o
, we will get a pop-up with the available options:
We can navigate the options with the up
(or Ctrl-p
) and down
(or Ctrl-n
) arrows and select the one we want with enter
.
I’m not sure why, but by default, a new buffer is opened whenever an completion is triggered. We can add this to our configuration to avoid this behavior:
1
vim.o.completeopt = 'menu'
Since C-x C-o
is a little hard to type, I prefer to use C-Space
to trigger the completion. We can use this configuration for that:
1
vim.api.nvim_set_keymap('i', '<C-Space>', '<C-x><C-o>', {})
We can get a pop up with documentation for the symbol under our cursor with this command:
1
:lua vim.lsp.buf.hover()
The result looks like this:
We can set a key map so the documentation shows when pressing K
:
1
vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', {buffer = event.buf})
To format our code, we can use:
1
:lua vim.lsp.buf.format()
To map F
to code formatting, we can use this configuration:
1
vim.keymap.set('n', 'F', '<cmd>lua vim.lsp.buf.format()<cr>', {buffer = event.buf})
We can rename a symbol among our project using:
1
:lua vim.lsp.buf.rename()
We will get a prompt similar to:
1
New Name: matrix
Where we can change the name of the symbol and press enter.
We can map this to the number 3 like so:
1
vim.keymap.set('n', '3', '<cmd>lua vim.lsp.buf.rename()<cr>', {buffer = event.buf})
In the previous section we learned how we can do code completion on demand, but most IDEs do code completion automatically as we type. To get this kind of functionality we need another plugin: nvim-cmp.
We can use this configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
return {
'hrsh7th/nvim-cmp',
dependencies = {
'hrsh7th/cmp-nvim-lsp'
},
config = function()
local cmp = require("cmp")
cmp.setup({
mapping = cmp.mapping.preset.insert({
['<C-o>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
}),
snippet = {
expand = function(args)
require('luasnip').lsp_expand(args.body)
end,
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' },
}, {
{ name = 'buffer' },
}),
})
end
}
Now, we will automatically get suggestions as we type:
We can also navigate the options with the up
(or Ctrl-p
) and down
(or Ctrl-n
) arrows and select the one we want with enter
.
After following these instructions we will be able to enjoy most of the features offered by Arduino Language Server within our already familiar Neovim.
]]>For our sketch to be able to use the serial monitor, we need to use Serial.begin
and specify a baud rate. For example:
1
Serial.begin(9600);
The valid baud rates vary depending on the board we are using. 9600
is a safe value that works on most boards.
The first thing we want to do is print to the serial port. For example:
1
Serial.println("Hello");
This simple sketch shows how we can print a Hello
message every 2 seconds:
1
2
3
4
5
6
7
8
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello");
delay(2000);
}
If we upload this sketch to our board and connect the board to our PC, we can use these commands to see the output of our sketch:
1
2
3
sudo chmod a+rw /dev/ttyACM0
sudo stty 9600 -F /dev/ttyACM0 raw -echo
sudo cat /dev/ttyACM0
Note that 9600
matches the baud rate specified in our sketch.
We can also use our board’s serial port to receive input from users. There are multiple functions available for reading data; I’ll use parseInt()
in my example, since it’s very easy to use:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int inputNumber;
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Enter a number:");
while (Serial.available() == 0) {
// Loop until there is data to be read
}
inputNumber = Serial.parseInt();
Serial.print("You entered: ");
Serial.println(inputNumber);
}
We use Serial.available
to wait for the user to input some data. To input said data, we can use these commands:
1
2
3
sudo chmod a+rw /dev/ttyACM0
sudo stty 9600 -F /dev/ttyACM0 raw -echo
echo "45" > /dev/ttyACM0
Arduino IDE includes a Serial Monitor that can be used to interact with a board’s serial port. In Linux environments we can use commands available in most distributions to interact with the serial port even if Arduino IDE is not available.
]]>I personally, use neovim for coding, which makes it a necessity for me to be able to compile and upload my code from my terminal.
If you prefer the IDE, this article might not be for you, but, understanding the CLI could be useful in the future to automate repetitive tasks or run things in a CI environment.
We can install the Arduino CLI on our system with these command:
1
2
3
4
5
6
7
8
# Create a folder to install the CLI
mkdir ~/bin/arduino-cli
# Move to the folder we created
cd ~/bin/arduino-cli
# Install
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
Then we need to add arduino-cli
to our path. We can do this by adding this line to our ~/.bashrc
file:
1
export PATH="$PATH:/home/myself/bin/arduino-cli/bin"
And sourcing ~/.bashrc
:
1
. ~/.bashrc
If everything goes well, the following command will print the installed version:
1
arduino-cli version
We can create a sketch with this command:
1
arduino-cli sketch new NconaSketch
A new folder named NconaSketch
will be created with a file NconaSketch.ino
inside. The file will have this content:
1
2
3
4
5
void setup() {
}
void loop() {
}
Let’s replace the content with the code from my previous article:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
// Each byte with value different to 0 will turn on the corresponding LED
byte Time[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
void setup() {
matrix.begin();
}
void loop() {
// We loop all rows and all columns
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 12; j++) {
// Toggle the LED state
if (Time[i][j] == 0) {
Time[i][j] = 1;
} else {
Time[i][j] = 0;
}
// Re-render the matrix
matrix.renderBitmap(Time, 8, 12);
// Sleep for 100 miliseconds
delay(100);
}
}
}
Connect the board and use this command to verify it’s recognized:
1
arduino-cli board list
The output looks like this for me:
1
2
Port Protocol Type Board Name FQBN Core
/dev/ttyACM0 serial Serial Port (USB) Arduino UNO R4 WiFi arduino:renesas_uno:unor4wifi arduino:renesas_uno
The part we care about is arduino:renesas_uno
, which serves as the identifier for the board type. We can check if that board is installed by listing all the installed types:
1
arduino-cli board listall
If the board is not included in the output we can install it with this command:
1
arduino-cli core install arduino:renesas_uno
To compile the sketch, we can use this command:
1
arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi NconaSketch
To upload it to the board:
1
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:renesas_uno:unor4wifi NconaSketch
In this article we learned how to compile and install a sketch without using the IDE. Using the CLI is useful for people that prefer to work from a terminal or for environments where a GUI is not available (like most CI environments).
]]>In order to compile and install our programs into our Arduino, we need to download the Arduino IDE. We can get it from the Arduino Software Page.
The installation instructions might vary depending on your OS. I use Ubuntu, so I downloaded the AppImage
file.
In order to run AppImage
files we need FUSE:
1
2
sudo add-apt-repository universe
sudo apt install libfuse2
Then we can just run the AppImage
file:
1
2
chmod +x arduino-ide_2.2.1_Linux_64bit.AppImage
./arduino-ide_2.2.1_Linux_64bit.AppImage
The IDE will open:
Arduino programs are called sketches
. In this section we are going to take advantage of the LED matrix included in Arduino UNO R4 to create our first sketch.
Our sketch will simply turn on all the LEDs in the matrix 1 by 1 and then turn them off 1 by 1. The code we are going to use is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
// Each byte with value different to 0 will turn on the corresponding LED
byte Time[8][12] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
void setup() {
matrix.begin();
}
void loop() {
// We loop all rows and all columns
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 12; j++) {
// Toggle the LED state
if (Time[i][j] == 0) {
Time[i][j] = 1;
} else {
Time[i][j] = 0;
}
// Re-render the matrix
matrix.renderBitmap(Time, 8, 12);
// Sleep for 100 miliseconds
delay(100);
}
}
}
The first step is to copy this code on our IDE:
Then we need to connect our Arduino board to our computer using a USB cable, and select it on our IDE:
Finally, click the Upload
button:
If you encounter any issues at this step, look at the troubleshooting
section below.
It’s possible that we get a message asking us to install a board:
We just need to click YES
.
If everything goes well, we’ll get a message saying that the upload is done:
As soon as this happens, our Arduino will start running the program:
I got this error when I tried to upload my sketch into my Arduino. I was able to fix it with this command:
1
sudo chmod a+rw /dev/ttyACM0
This error means that there is some problem communicating with the Arduino board. To fix it, try disconnecting the USB cable and connecting it again. If that doesn’t work, using a different cable might help.
In this article we learned how to use the Arduino IDE to write a program, upload it to our board and have the board run it.
]]>