Manually calling wake in non-async context

Hi everyone,

I'm writing an EtherCAT master which targets no_std systems.

One part of the EtherCAT protocol involves sending an EtherCAT packet with a given index, then waiting for a response from a peripheral device with that same index.

Because I want to be able to handle parsing for incoming frames separately (and asynchronously) from the sending of the initial packet, I've come up with a design that uses futures and wakes the future from a sync context. Here's a heavily reduced example demonstrating the only working approach I've stumbled upon so far:

use core::task::Poll;
use core::task::Waker;
use core::time::Duration;
use smol::LocalExecutor;
use std::cell::Cell;
use std::rc::Rc;

#[derive(Clone)]
struct Client {
    waker: Rc<Cell<Option<Waker>>>,
    frame: Rc<Cell<Option<u8>>>,
}

impl Default for Client {
    fn default() -> Self {
        Self {
            waker: Rc::new(Cell::new(None)),
            frame: Rc::new(Cell::new(None)),
        }
    }
}

impl Client {
    /// This will send an EtherCAT frame over the PHY and wait for a response.
    /// 
    /// All response handling is omitted for brevity.
    pub async fn brd<T>(&self, _address: u16) -> () {
        println!("Before");

        futures_lite::future::poll_fn(|ctx| {
            println!("poll_fn");

            if let Some(frame) = self.frame.take() {
                println!("poll_fn -> Ready");
                Poll::Ready(frame)
            } else {
                self.waker.replace(Some(ctx.waker().clone()));

                println!("poll_fn -> Pending");

                Poll::Pending
            }
        })
        .await;

        println!("After");
    }

    /// Parse EtherCAT packet(s) from incoming Ethernet frames.
    /// 
    /// Parsing and storage of the frames is omitted for brevity.
    pub fn parse_response_ethernet_frame(&mut self, _ethernet_frame_payload: &[u8]) {
        println!("Frame received");

        // Frame is ready; tell everyone about it
        if let Some(waker) = self.waker.take() {
            println!("I have a waker");
            self.frame.replace(Some(1u8));
            waker.wake()
        }
    }
}

fn main() {
    let local_ex = LocalExecutor::new();

    futures_lite::future::block_on(local_ex.run(async {
        let client = Client::default();

        let mut client2 = client.clone();

        local_ex
            .spawn(async move {
                async_io::Timer::after(Duration::from_secs(1)).await;

                println!("Writing");

                let data_from_ethernet_dma = &[ /* ... */ ];    

                client2.parse_response_ethernet_frame(data_from_ethernet_dma);
            })
            .detach();

        client.brd::<u8>(1010).await;
    }));
}

This solution feels very unclean to me, but it seems to work in the contrived case. Is there a better way to architect my code?

Specifically:

  1. Calling waker.wake() in a sync context in parse_response_ethernet_frame feels very odd. Is this at least safe?
  2. Is there a better way of asynchronously waiting for the response in brd() where parse_response_ethernet_frame() controls when the future completes?

Please note that parse_response_ethernet_frame and brd will be called from different tasks in an embedded context, or potentially different threads if running on a desktop PC, so they can't be combined. I also don't want to block in brd() because this would prevent other packets being sent from other threads.

Your code looks fine to me, though it's a bit low level.

Thanks for taking a look! Glad to hear I'm on the right track :slight_smile: