FFI-safe dyn Trait
s / 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:
-
the
#[derive_ReprC(dyn)]
annotation on a givenTrait
definition (this makes it sodyn Trait : ReprCTrait
) -
the
VirtualPtr<dyn Trait + …>
FFI-safeBox
-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 specialrelease_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:
-
#define CALL(obj, method, ...) \ obj.vtable method(obj.ptr ##__VA_ARGS__)
-
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);
-
as well as the special retain_vptr
, in the case of #[derive_ReprC(dyn, Clone)]