Example: FFI-safe Futures and executors

By the way, all this is actually already directly featured by safer-ffi itself if you enable the futures or tokio features.

FfiFuture

  1. Future Trait:

    trait Future {
        type Output;
    
        fn poll(self: Pin<&mut Self>, &'_ mut Context<'_>)
          -> Poll<Self::Output>
        ;
    }
    
  2. Simplify it: Future<Output = ()>

    trait SimpleFuture {
        fn dyn_poll(self: Pin<&mut Self>, &'_ mut Context<'_>)
          -> Poll<()>
        ;
    }
    
    • Renamed to dyn_poll for more readable semantics down the line.
  3. Make it FFI-compatible: define an FFI-safe Poll<()> equivalent

    
    #![allow(unused)]
    fn main() {
    #[derive_ReprC]
    #[repr(u8)]
    enum FfiPoll {
        Completed,
        Pending,
    }
    
    #[derive_ReprC(dyn)]
    trait FfiFuture {
        fn dyn_poll(self: Pin<&mut Self>, &'_ mut Context<'_>)
          -> FfiPoll
        ;
    }
    }
    
    • We have reached a point where #[derive_ReprC(dyn)] can be used!

    • This means we have now gotten the impl -> dyn "coërcions":

      • impl<'f, F : 'f + FfiFuture> From<
            Pin<Box<F>>
        > for // ↓
            VirtualPtr<dyn 'f + FfiFuture>
        
      • impl<'f, F : 'f + FfiFuture> From<
            Pin<&'f mut F>
        > for // ↓
            VirtualPtr<dyn 'f + FfiFuture>
        
  4. Convenience conversions to/from Rust Futures.

    • From:

      
      #![allow(unused)]
      fn main() {
      impl<F : Future<Output = ()>> FfiFuture for F {
          fn dyn_poll(self: Pin<&mut Self>, ctx: &'_ mut Context<'_>)
            -> FfiPoll
          {
              match self.poll(ctx) {
                  | Poll::Pending => FfiPoll::Pending,
                  | Poll::Ready(()) => FfiPoll::Completed,
              }
          }
      }
      }
      
    • Into:

      
      #![allow(unused)]
      fn main() {
      impl<'fut> VirtualPtr<dyn 'fut + FfiFuture> {
          fn into_future(self)
            -> impl 'fut + Future<Output = ()>
          {
              let mut vptr = self;
              ::core::future::poll_fn(move /* vptr */ |cx| {
                  match Pin::new(&mut vptr).dyn_poll(cx) {
                      | FfiPoll::Pending => Poll::Pending,
                      | FfiPoll::Completed => Poll::Ready(()),
                  }
              })
          }
      }
      }
      

Bonus: offering Wake ups to the FFI:

Remember: Context is an opaque FFI type, which makes it unusable there. FFI users won't be able to provide their own custom dyn FfiFuture objects (other than trivial Futures or Futures busy-looping the polls).

We can palliate this by exposing virtual methods / accessors specific to the opaque Context type:


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! ffi_export_future_helpers {() => (
    const _: () = {
        use $crate::ඞ::std::{sync::Arc, task::Context, prelude::v1::*};

        #[ffi_export]
        fn rust_future_task_context_wake (
            task_context: &'static Context<'static>,
        )
        {
            task_context.waker().wake_by_ref()
        }

        #[ffi_export]
        fn rust_future_task_context_get_waker (
            task_context: &'static Context<'static>,
        ) -> Arc<dyn 'static + Send + Sync + Fn()>
        {
            let waker = task_context.waker().clone();
            Arc::new(move || waker.wake_by_ref()).into()
        }
    };
)}
}

FFI-safe Executor / Runtime Handle

Note: ::tokio does not expose an Executor API, but rather, a Runtime, which packages/bundles both the Executor part1 as well as the Reactor part2. The latter is the reason some of ::tokio's leaf Futures (such as File I/O and sleeps) cannot be polled, by default, by other executors… See https://docs.rs/async-compat for more info.

1

the block_on() runtime in charge of calling .poll() to make a future progress, with concurrent .poll()s to spawed Futures.

2

background thread(s)/threadpool to which certain Waker::wake() calls are scheduled, needed by certain leaf futures.

  1. A trait abstraction thereof:

    
    #![allow(unused)]
    fn main() {
    trait Executor : Send + Sync {
        fn block_on<T>(
            self: &'_ Self,
            future: impl '_ + Future<Output = T>,
        ) -> T
        ;
        fn spawn(
            self: &'_ Self,
            future: impl 'static + Send + Future<Output = ()>
        ) -> Pin<Box<dyn Send + Future<Output = ()>>>
        ;
    }
    }
    
  2. Making it dyn-safe:

    
    #![allow(unused)]
    fn main() {
    trait Executor : Send + Sync {
        fn dyn_block_on(
            self: &'_ Self,
            future: Pin<Box<dyn '_ + Future<Output = ()>>>,
        )
        ;
        fn dyn_spawn(
            self: &'_ Self,
            future: Pin<Box<dyn Send + Future<Output = ()>>>
        ) -> Pin<Box<dyn Send + Future<Output = ()>>>
        ;
    }
    }
    
  3. Making it FFI-safe:

    
    #![allow(unused)]
    fn main() {
    #[derive_ReprC(dyn, Clone)]
    trait FfiFutureExecutor : Send + Sync {
        fn dyn_block_on(
            self: &'_ Self,
            future: VirtualPtr<dyn '_ + FfiFuture>,
        )
        ;
        fn dyn_spawn(
            self: &'_ Self,
            future: VirtualPtr<dyn Send + FfiFuture>
        ) -> VirtualPtr<dyn Send + FfiFuture>
        ;
    }
    }
    
  4. From a ::tokio::Handle:

    impl FfiFutureExecutor for ::tokio::runtime::Handle {
        fn dyn_block_on (
            self: &'_ Self,
            ffi_fut: VirtualPtr<dyn '_ + FfiFuture>,
        )
        {
            self.block_on(ffi_fut.into_future())
        }
    
        fn dyn_spawn (
            self: &'_ Self,
            future: VirtualPtr<dyn Send + FfiFuture>,
        ) -> VirtualPtr<dyn Send + FfiFuture>
        {
            let handle_result = self.spawn(future.into_future());
            let handle_unwrapped = async {
                handle
                    .await
                    .unwrap_or_else(|caught_panic| {
                        ::std::panic::resume_unwind(caught_panic.into_panic())
                    })
            };
            Box::pin(handle_unwrapped)
                .into()
        }
    }
    
  5. Usable as an Executor:

    
    #![allow(unused)]
    fn main() {
    /// Notice how we got the generics back!
    impl VirtualPtr<dyn FfiFutureExecutor> {
        fn block_on<R> (
            self: &'_ Self,
            fut: impl Future<Output = R>
        ) -> R
        {
            // We use `Option<R>` as a simple non-`'static` channel
            // to get the generic payload back.
            let mut ret = None;
            self.dyn_block_on(
                // From< Pin<&mut F> >
                ::core::pin::pin!(async {
                    ret = Some(fut.await);
                })
                .into()
            );
            ret.expect("`.dyn_block_on()` did not complete")
        }
    
        fn spawn<R : 'static + Send> (
            self: &'_ Self,
            fut: impl 'static + Send + Future<Output = R>,
        ) -> impl 'static + Future<Output = R>
        {
            // Channel to be able to get the generic payload back.
            let (tx, rx) = ::futures::channel::oneshot::channel();
            let ffi_handle = self.dyn_spawn(
                // From< Pin<Box<F>> >
                Box::pin(async move {
                    tx.send(fut.await).ok();
                })
                .into()
            );
            let handle = async move {
                ffi_handle.into_future().await;
                rx  .await
                    .expect("\
                        executor dropped the `spawn`ed task \
                        before its completion\
                    ")
            };
            handle
        }
    }
    }