Sharing values between functions in rust

328 views Asked by At

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.

1

There are 1 answers

0
cameron1024 On BEST ANSWER

The basic gist of the error can be reproduced with a simpler example:

struct Foo {
  bar: String
}

impl Foo {
  fn baz(&self) {
    let s = self.bar;
  }
}

What's happening is:

  • self has type &Foo, because the parameter is declared with &self, which is a shorthand for self: &Self
  • self.bar has type String, because bar is declared to be a String
  • this leads to an issue, we're trying to *move s out of self, but we only have a reference to self, not owned access to self

You'll need to find a way to make that work without owned access. Disclaimer, I've not used this crate before (and the setup instructions are a bit crazy), but here's what I could piece together from the docs for PinDriver::output

  • PinDriver::output takes an impl Peripheral, i.e. any type that implements the trait Peripheral
  • looking at the docs for Peripheral, we can see a list of structs that implement it.
  • Gpio8 does implement it, but this is no good, since we'd run into the "cannot move out of shared reference" issue again
  • in most Rust code, you might want to .clone() the gpio pin, however, there's no clone method (because it represents a unique handle to a physical resource, unless you've got some board that can grow new pins :p)
  • however, right at the bottom, there's this impl:
impl<T: DerefMut> Peripheral for T
where
    T::Target: Peripheral { ... }
  • i.e. any type which implements DerefMut<Target = T> implements Periperal, as long as T also implements Peripheral
  • this means you can use &mut Gpio8. This makes sense, since you're mutating the state of the light, so you need a mutable handle to it. If you change blink() to take a &mut self, you should be able to write PinDriver::output(&mut self.peripherals.pins.gpio8)

FWIW, embedded Rust often uses clever type system tricks to verify more behavior at compile time (either to save precious CPU cycles or for better reliability). If you're new to Rust, it can be quite confusing, since it's arguably more "advanced" than most CLI apps, for example