What is the Rust way to write callback using heapless::?

Hello there!
I'm trying to translate a library from C to Rust which is parsing USART data and calling different callbacks (depending on its state).
The code is for microcontroller so heapless:: is going to be used. I'm using std:: here for playground only.

Translated code is generating an error:

`(dyn for<'r> std::ops::FnMut(&'r [u8]) + 'static)` cannot be shared between threads safely
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct Parser {
    cb: Option<&'static FnMut(&[u8])>,
}
impl Parser {
    fn feed(&mut self, st: &[u8]) {
        if let Some(f) = self.cb {
            (f)(&st);
        };
    }
}

fn time_is_out() -> bool {
    false
}

fn main() {
    let mut parser: Parser = Parser { cb: None };
    
    let p = Arc::new(Mutex::new(parser));

    let p_parser = Arc::clone(&p);
    let app_handle = thread::spawn(move || {
        let state = 0;
        let mut _parser = p_parser.lock().unwrap();
        _parser.cb = Some(&|s| {
            if s == b"CONNECT OK\r\n" {
                state = 1
            }
        });
        while state == 0 || !time_is_out() {
            thread::sleep(Duration::from_millis(250));
        }
			
        _parser.cb = Some(&|s| {
            if s == b"CONNECT FAIL\r\n" {
                state = 1
            }
        });
        while state == 0 || !time_is_out() {
            thread::sleep(Duration::from_millis(250));
			
        }
    });
	
    let p = Arc::new(Mutex::new(parser));
    let p_parser = Arc::clone(&p);
    let usart_handle = thread::spawn(move || {
        let mut _parser = p_parser.lock().unwrap();
		loop {
			thread::sleep(Duration::from_millis(250));
			_parser.feed(b"CONNECT OK\r\n");
		}
    });
	

    usart_handle.join().unwrap();
    app_handle.join().unwrap();
}

This post probably won't be too helpful. I don't know much about design patterns for heapless code. But I do know what doesn't work in Rust, and maybe this information can be helpful despite the lack of working solutions?


The reason this is failing is that dyn Trait things are by default non-Send, non-Sync, and they need to be declared Send to be sent between threads (and Sync to be shared). &'static FnMut(...) is deprecated syntax for &'static dyn FnMut(...), so this is effected.

To declare your callback as send/sync, you can use the type &'static dyn FnMut(&[u8]) + Send + Sync.

However, I think that's probably the least of your problems. &'static FnMut(..) seems quite suspicious for multiple reasons. First, it's declared as a reference to something in static memory. Those can only be created either by leaking memory, or by storing it in a static variable. Second, you've declared it as an FnMut, which is only usable from a mutable reference, but you only store a read-only reference to it.

Lastly, you reference thread-local variables in the callback, but then put it back into data which outlasts the thread. This won't, in general work, since those stack variables could die and the data stored in the Arc would live on. Rust won't allow you to write code which does this.

For fixing the first two issue, I'd normally suggest boxing it, but I guess this isn't feasible in your case. And it won't fix the third issue.

The third issue could potentially be fixed by putting state in an Arc<Mutex<>> wrapper itself, but I suspect that'll be iffy to work with too.

Overall, I think you probably need to refactor your design. You can't in general be modifying local variables from a callback passed around between threads, and Rust will stop you from doing that. For regular Rust code, I would probably recommend switching to a channel-based design here - something where you get back data via channel messages rather than setting local variables.

Another alternative might be to replace your closures with a more C-style function-and-data approach? If you can find a way to store the data your closures need, like local variables or channels if you switch to that, then the functions can just be stored as fn(&[u8]) - note the lowercase fn.

Unlike dyn FnMut(&[u8], fn(&[u8]) stores no extra data and is a simple function pointer. It should be possible to send these around between threads no problem.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.