FFI-safe dyn Traits / Virtual objects

This is a generalization of callbacks. For instance, while you could model an Iterator as an FnMut(), and then make that closure FFI-compatible based on the previous chapter, you could also be tempted to instead actually use some FFI-safe version of dyn Iterator, right?

Example (click to see)

Rather than doing:


#![allow(unused)]
fn main() {
use ::safer_ffi::prelude::*;

#[ffi_export]
fn fibonacci() -> repr_c::Box<dyn Send + FnMut() -> u32> {
    let mut state = (0, 1);
    Box::new(move || {
        let to_yield = state.0;
        (state.0, state.1) = (state.1, state.0 + state.1);
        to_yield
    })
    .into()
}
}
  • C usage

    Generated header:

    /** \brief
     *  `Box<dyn 'static + Send + FnMut() -> Ret>`
     */
    typedef struct BoxDynFnMut0_uint32 {
        /** <No documentation available> */
        void * env_ptr;
    
        /** <No documentation available> */
        uint32_t (*call)(void *);
    
        /** <No documentation available> */
        void (*free)(void *);
    } BoxDynFnMut0_uint32_t;
    
    /** <No documentation available> */
    BoxDynFnMut0_uint32_t
    fibonacci (void);
    

    Usage:

    // 1. Create it
    auto next = fibonacci();
    // 2. Use it
    for(int i = 0; i < 5; ++i) {
        printf("%u\n", next.call(next.env_ptr));
    }
    // 3. Release it
    next.free(next.env_ptr);
    

You may want to instead do:


#![allow(unused)]
fn main() {
//! PSEUDO CODE

#[somehow_make_it_ffi_dyn_safe]
trait FfiIterator : Send {
    fn next(&mut self) -> u32;
}

#[ffi_export]
fn fibonacci() -> repr_c::Box<dyn FfiIterator> {
    struct Fibo(u32, u32);

    impl FfiIterator<u32> for Fibo {
        fn next(&mut self) -> u32 {
            let to_ret = self.0;
            (self.0, self.1) = (self.1, self.0 + self.1);
            to_ret;
        }
    }

    Box::new(Fibo(0, 1))
        .into()
}
}

This functionality is indeed supported by safer-ffi, thanks to the combination of two things:

  1. the #[derive_ReprC(dyn)] annotation on a given Trait definition (this makes it so dyn Trait : ReprCTrait)

  2. the VirtualPtr<dyn Trait + …> FFI-safe Box-like pointer usage in some function signature.


#![allow(unused)]
fn main() {
//! REAL CODE

use ::safer_ffi::prelude::*;

#[derive_ReprC(dyn)] // 👈 1
trait FfiIterator : Send {
    fn next(&mut self) -> u32;
}

#[ffi_export]
fn fibonacci()
  -> VirtualPtr<dyn FfiIterator> // 👈 2
{
    struct Fibo(u32, u32);

    impl FfiIterator for Fibo {
        fn next(&mut self) -> u32 {
            let to_ret = self.0;
            (self.0, self.1) = (self.1, self.0 + self.1);
            to_ret
        }
    }

    Box::new(Fibo(0, 1))
        .into()
}
}
  • The resulting VirtualPtr then has an opaque data .ptr field, as well as a .vtable field, containing all the (virtual) methods of the trait, as well as the special release_vptr method1.

    Generated header (click to see)
    /** <No documentation available> */
    typedef struct Erased Erased_t;
    
    /** <No documentation available> */
    typedef struct FfiIteratorVTable {
        /** <No documentation available> */
        void (*release_vptr)(Erased_t *);
    
        /** <No documentation available> */
        uint32_t (*next)(Erased_t *);
    } FfiIteratorVTable_t;
    
    /** <No documentation available> */
    typedef struct VirtualPtr__Erased_ptr_FfiIteratorVTable {
        /** <No documentation available> */
        Erased_t * ptr;
    
        /** <No documentation available> */
        FfiIteratorVTable_t vtable;
    } VirtualPtr__Erased_ptr_FfiIteratorVTable_t;
    
    /** <No documentation available> */
    VirtualPtr__Erased_ptr_FfiIteratorVTable_t
    fibonacci (void);
    

    FFI usage:

    // 1. Create it
    auto obj = fibonacci();
    // 2. Use it
    for(int i = 0; i < 5; ++i) {
        printf("%u\n", obj.vtable.next(obj.ptr));
    }
    // 3. Release it
    obj.vtable.release_vptr(obj.ptr);
    

    Trick:

    1. #define CALL(obj, method, ...) \
          obj.vtable method(obj.ptr ##__VA_ARGS__)
      
    2. So as to:

      // 1. Create it
      auto obj = fibonacci();
      // 2. Use it
      for(int i = 0; i < 5; ++i) {
          printf("%u\n", CALL(obj, .next));
      }
      // 3. Release it
      CALL(obj, .release_vptr);
      
1

as well as the special retain_vptr, in the case of #[derive_ReprC(dyn, Clone)]