I'm trying to learn Rust, but have hit a wall, tying to do something I expected to be relatively straightforward. I'm trying to write a simple blink example for the ESP32c3 MCU. I got a basic example working, but started running into compilation errors when trying to expand/generalize the example.
My project consists of a cargo workspace with two crates - entrypoint
and blink
.
I was able to get the following basic version working without issues:
// entrypoint/src/main.rs
use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported
use blink::blink;
fn main() {
// Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
// or else some patches to the runtime implemented by esp-idf-sys might not link properly.
esp_idf_sys::link_patches();
println!("Hello, world!");
blink();
}
// blink/src/lib.rs
use std::thread;
use std::time::Duration;
use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::gpio;
pub fn blink() {
let peripherals = Peripherals::take().unwrap();
let mut led = gpio::PinDriver::output(peripherals.pins.gpio8).unwrap();
for _ in 0..20 {
led.set_high().unwrap();
thread::sleep(Duration::from_secs(1));
led.set_low().unwrap();
thread::sleep(Duration::from_secs(1));
}
}
Then I wanted to improve error handling (stop using unwrap()
inside the blink
crate) and make the blink()
function reusable (the Peripherals::take()
call panics, if it's executed more than once).
I came up with the following changes to improve error handling. This version also worked fine, I'm only including it to get feedback on how idiomatic my approach is / what would you do differently? I'm guessing it would be better practice to make a custom error type or is it acceptable/common place to return a string slice as an error even in production code?
pub fn blink(count: i32) -> Result<(), &'static str> {
let peripherals = Peripherals::take().ok_or("Failed to take peripherals")?;
let mut led = gpio::PinDriver::output(peripherals.pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
for _ in 0..count {
led.set_high().map_err(|_: EspError| "Failed to set pin high")?;
thread::sleep(Duration::from_secs(1));
led.set_low().map_err(|_: EspError| "Failed to set pin low")?;
thread::sleep(Duration::from_secs(1));
}
Ok(())
}
Next, I attempted to make the blink()
function reusable by separating the Peripherals::take()
call from the rest of the blink()
function, so it could be called only once at boot. I know I could make the call in my entrypoint and pass the peripherals as an argument to blink()
, but I wanted to keep the blink
crate responsible for making the Peripherals::take()
call. This is where I started running into issues.
Attempt nr. 1: My first approach was trying to use a global Peripherals
variable. I quickly found out that won't work unless I wrap the global variable with the thread_local
macro or wrap operations on the global variable into an unsafe
block which I wanted to avoid. I tried a number of things, but couldn't get my code to compile when using thread_local
.
Both with and without RefCell
(I found articles suggesting to use RefCell
, but after trying it and reading the docs, I didn't see a good reason to use it for my use-case), thread_local
seems to wrap my global variable into a LocalKey
. I'm not sure how to use the LocalKey
, besides the with()
function - I'd like to avoid using with()
, if possible, since I need to move my code into a closure, making it harder to read. I'm also not sure how to keep the for loop outside of the closure and only initialize the led
variable from inside the closure - usually I'd move the variable declaration out of the closure, initialized to null
, but null
doesn't seem to be a concept which exists within Rust as far as I can tell.
thread_local! {
static PERIPHERALS: Option<Peripherals> = Peripherals::take();
}
pub fn blink(count: i32) -> Result<(), &'static str> {
PERIPHERALS.with(| p | {
let peripherals = match p {
Some(peripherals) => peripherals,
None => return Err("Failed to take peripherals")
};
let mut led = gpio::PinDriver::output(peripherals.pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
for _ in 0..count {
led.set_high().map_err(|_: EspError| "Failed to set pin high")?;
thread::sleep(Duration::from_secs(1));
led.set_low().map_err(|_: EspError| "Failed to set pin low")?;
thread::sleep(Duration::from_secs(1));
}
Ok(())
})
}
The above code resulted in the following compiler error:
error[E0507]: cannot move out of `peripherals.pins.gpio8` which is behind a shared reference
--> blink/src/lib.rs:19:47
|
19 | let mut led = gpio::PinDriver::output(peripherals.pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
| ^^^^^^^^^^^^^^^^^^^^^^ move occurs because `peripherals.pins.gpio8` has type `Gpio8`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0507`.
error: could not compile `blink` due to previous error
The same error occurs, if I try to dereference peripherals
variable first:
...
let mut led = gpio::PinDriver::output((*peripherals).pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
...
Attempt nr. 2: As my next approach, I tried to write a struct with a couple functions which would act as a class. Unfortunately I ran into the exact same compiler error.
// blink/src/lib.rs
use std::thread;
use std::time::Duration;
use anyhow::Result;
use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::gpio;
use esp_idf_sys::EspError;
pub struct Blink {
peripherals: Peripherals,
}
impl Blink {
pub fn new() -> Result<Blink, &'static str> {
match Peripherals::take() {
Some(peripherals) => Ok(Blink{ peripherals }),
None => return Err("Failed to take peripherals")
}
}
pub fn blink(&self, count: i32) -> Result<(), &'static str> {
let mut led = gpio::PinDriver::output(self.peripherals.pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
for _ in 0..count {
led.set_high().map_err(|_: EspError| "Failed to set pin high")?;
thread::sleep(Duration::from_secs(1));
led.set_low().map_err(|_: EspError| "Failed to set pin low")?;
thread::sleep(Duration::from_secs(1));
}
Ok(())
}
}
// entrypoint/src/main.rs
use std::thread;
use std::time::Duration;
use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported
use blink::Blink;
fn main() {
// Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
// or else some patches to the runtime implemented by esp-idf-sys might not link properly.
esp_idf_sys::link_patches();
println!("Hello, world!");
let blink = Blink::new()?;
loop {
blink.blink(2).unwrap();
thread::sleep(Duration::from_secs(5));
}
}
error[E0507]: cannot move out of `self.peripherals.pins.gpio8` which is behind a shared reference
--> blink/src/lib.rs:23:47
|
23 | let mut led = gpio::PinDriver::output(self.peripherals.pins.gpio8).map_err(|_: EspError| "Failed to set pin to output")?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ move occurs because `self.peripherals.pins.gpio8` has type `Gpio8`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0507`.
error: could not compile `blink` due to previous error
I don't have a good enough understanding of how borrowing, references, and/or variable moving/copying works in Rust just yet to be able to solve this. It seems to be drastically different from other (more traditional) languages I'm familiar with (C, C++, Java, JS/TS, Python, Dart).
Once again, I'd also really appreciate any best practice recommendations/corrections, if you find anything out of the ordinary in my code above.