Embedded Interrupts: 'can't capture dynamic environment in a fn item'

I'm writing some embedded code for an RP2040 custom board, but am having issues with this block giving me a cryptic 'can't capture dynamic environment in a fn item' compiler error, despite using the exact syntax the compiler is suggesting.

This code block is supposed to be "listening" for UART signals on UART0 and then passing any received data into a message queue, MSG_Q.

Relevant code block:

use rp2040_hal::{
    ...
    pac::interrupt,
    ...
};


    #[interrupt]
    fn UART0_IRQ() {
        let mut buffer = [0u8; 64];
        let _bytes_read =  move || { uart_s.read_raw(&mut buffer); };

        if _bytes_read.into().is_ok() {
            unsafe {
                let s: &str = core::str::from_utf8(&buffer).unwrap();
                cortex_m::interrupt::free(|cs| MSG_Q.borrow(cs).borrow_mut().push(s.to_string()));
            }
        }
    }

Compiler error:

error[E0434]: can't capture dynamic environment in a fn item
   --> src\main.rs:150:38
    |
150 |         let _bytes_read =  move || { uart_s.read_raw(&mut buffer); };
    |                                      ^^^^^^
    |
    = help: use the `|| { ... }` closure form instead

Any suggestions would be much appreciated!

The problem is not with _bytes_read, but with the interrupt handler itself. uart_s is a local variable defined outside of it, so handler can't refer to it. How it is defined at all?

1 Like

In Rust, the compiler will implement a closure by creating a struct that will hold onto any state it closes over and implementing the relevant Fn*() trait.

To help with ergonomics, when the closure doesn't capture anything it is assignable to the corresponding bare function (e.g. || println!("Hello, World") is assignable to a fn() variable). This works because everything is dictated by the function implementation and we don't need to carry any state around.

However, once your closure captures some state from the outside (uart_s in this case), we need some place to store that state so the closure can't be assigned to a fn() variable any more (a fn() is literally just a pointer to some machine code). That's what the "can't capture dynamic environment in a fn item" error is trying to tell you.

The typical solution is to make sure any state your callback needs is either stored as a global variable or to make sure your caller somehow passes the state in as an argument.

2 Likes

Here's the uart_s definition.

#[entry]
fn main() -> ! {
    ...
    let uart_s = UartPeripheral::new(periphs.UART0, s_uart_pins, &mut periphs.RESETS)
        .enable(uart::common_configs::_115200_8_N_1, uart_clocks)
        .unwrap();

@Michael-F-Bryan I'm fairly new to Rust, so I'm trying to parse what you're saying, but when you say "state" are you just referring to a temporal snapshot of a variable and its current value?

Does this mean that uart_s needs to be defined before fn main() as a global in order to make it accessible inside fn UART0_IRQ() even though fn UART0_IRQ() is defined inside main()? Why does the compiler suggest a closure as a possible fix? This seems to have nothing to do with the core problem of the variable being out of scope.

I'd like to pass uart_s in as an argument, but I don't think that's possible with an interrupt function.

Function placement has an impact only on visibility of the function itself, nothing more.

If UART0_IRQ didn't have to be the function item, you could fix the error by making it a closure.

1 Like

Which is to say, what the compiler suggests you to do is change

fn UART0_IRQ() { 
    ...
}

itself into

let UART0_IRQ = move || {
    ...
}

which unfortunately is not possible in this case, but the compiler doesn't know that.

1 Like

Thank you all, that really is helpful.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.