< Return to Blog

Embedded Rust: Adding a Type-safe SNTP callback (to esp-idf-svc)

From my work on rued, I wanted to use the callback defined in esp-idf-sys, but these are the C bindings. The esp-idf-sys crate implements type-safe Rust wrappers.
My initial use and testing in rued involved manipulating the raw-C bindings themselves, along the lines of unsafe extern "C" fn sync_cb(tv: *mut esp_idf_sys::timeval). While I did this just to "get things to work", there is a reason why there is a HAL (hardware-abstraction layer) provided in Rust.
So, it was time to contribute back to the community effort of a Rust-based HAL. To this effort, I created a PR called Add wrapper to specify callback for SNTP.
The first challenge, especially to me as I haven't worked on this project before, was to get a sense of how we should create this type-safe interface; heck what does this even mean, right?
Well, it just boils down to defining the interface in an idiomatic Rust way.  What confused me at the start was how would we describe this unsafe extern "C" fn.
The solution was to specify a trait bound FnMut(Duration) + Send + 'static, and to keep things clean this was defined as a boxed type. Note that we are using a version of Mutex that is type Mutex<T> = embedded_svc::utils::mutex::Mutex<RawMutex, T>, which means it can be used in a no_std environment.
#[cfg(feature = "alloc")] type SyncCallback = alloc::boxed::Box<dyn FnMut(Duration) + Send + 'static>; #[cfg(feature = "alloc")] static SYNC_CB: mutex::Mutex<Option<SyncCallback>> = mutex::Mutex::wrap(mutex::RawMutex::new(), None); impl EspSntp { pub fn new_with_callback<F>(conf: &SntpConf, callback: F) -> Result<Self, EspError> where F: FnMut(Duration) + Send + 'static, { //... } }
We store the callback in a static Box behind a Mutex and the trick is to refer to this data from the existing FFI call that's already present in this module.  I've marked the added code below:
unsafe extern "C" fn sync_cb(tv: *mut esp_idf_sys::timeval) { debug!( " Sync cb called: sec: {}, usec: {}", (*tv).tv_sec, (*tv).tv_usec, ); // This addition fetches the callback from the static Box held in a Mutex #[cfg(feature = "alloc")] if let Some(cb) = &mut *SYNC_CB.lock() { let duration = Duration::from_secs((*tv).tv_sec as u64) + Duration::from_micros((*tv).tv_usec as u64); cb(duration); } }
This prevents the user of this module having to deal with constructs such as esp_idf_sys::timeval.