Grand Plan for ABI Safety

Published: May 24, 2021, 9:31 a.m.

Rust does not have ABI safety by default. Meaning, any Rust (non extern "C") code that is called on your dynamically loaded libraries relies on an explicitly unstable ABI. It will work fine if the library is compiled with the same compiler version, but could break if they differ the slightest bit. But wait, it's 2021, how is it still an unsolved problem?

spoiler alert - it is, I just didn't know at the time of writing this post... But, let's not make this go to waste, shall we?

Meet CGlue

CGlue is still in its infancy - it is my attempt at ABI safety from all the lessons learnt building a scalable plugin API for memflow. The one in memflow is not yet released on crates.io, because I want to replace most of the code there with a couple of CGlue annotations. The idea of CGlue is to take traits, and generate C-compatible virtual function tables (vtables) for those traits. Then, generate generic extern "C" wrappers around trait functions that are not FFI-safe, and finally trait implementations on wrapped objects. Once an object is paired with its vtable, it is safe to destroy any type information and use it without knowing the underlying type. Take a look at this code:

use cglue_macro::*;

#[cglue_trait]
pub trait InfoPrinter {
    fn print_info(&self);
}

struct Info {
    value: usize
}

impl InfoPrinter for Info {
    fn print_info(&self) {
        println!("Info struct: {}", self.value);
    }
}

fn use_info_printer(printer: &impl InfoPrinter) {
    println!("Printing info:");
    printer.print_info();
}

fn main() {
    let mut info = Info {
        value: 5
    };

    let obj = cglue_obj!(info as InfoPrinter);

    use_info_printer(&obj);
}

All we do is annotate the trait:

#[cglue_trait]
pub trait InfoPrinter {
    fn print_info(&self);
}

And then use a macro to build an opaque CGlue object:

let obj = cglue_obj!(info as InfoPrinter);

This cglue_obj macro comes in very handy, because raw syntax is not too pretty:

let obj = 
    crate::trait_group::CGlueTraitObj::<_, CGlueVtblInfoPrinter<_>>::from(&mut info)
        .into_opaque();

You can see CGlueVtblInfoPrinter being used, which is being generated by the meat of the project - cglue_trait macro. Here is what it all expands to:

pub trait InfoPrinter {
    fn print_info(&self);
}

/// CGlue vtable for trait InfoPrinter.
///
/// This virtual function table contains ABI-safe interface for the given trait.
#[repr(C)]
pub struct CGlueVtblInfoPrinter<T> {
    pub print_info: extern "C" fn(this: &T) -> (),
}

impl<'a, T: InfoPrinter> Default for &'a CGlueVtblInfoPrinter<T> {
    /// Create a static vtable for the given type.
    fn default() -> Self {
        &CGlueVtblInfoPrinter {
            print_info: cglue_wrapped_print_info,
        }
    }
}

extern "C" fn cglue_wrapped_print_info<T: InfoPrinter>(this: &T) -> () {
    let ret = this.print_info();
    ret
}

/// Opaque CGlue vtable for trait InfoPrinter.
///
/// This virtual function table has type information destroyed, is used in CGlue objects
/// and trait groups.
pub type OpaqueCGlueVtblInfoPrinter = CGlueVtblInfoPrinter<core::ffi::c_void>;

unsafe impl<T: InfoPrinter> crate::trait_group::CGlueBaseVtbl for CGlueVtblInfoPrinter<T> {
    type OpaqueVtbl = OpaqueCGlueVtblInfoPrinter;
}

impl<T: InfoPrinter> crate::trait_group::CGlueVtbl<T> for CGlueVtblInfoPrinter<T> {}

impl<
        T: AsRef<OpaqueCGlueVtblInfoPrinter> + crate::trait_group::CGlueObj<core::ffi::c_void>,
    > InfoPrinter for T
{
    #[inline(always)]
    fn print_info(&self) -> () {
        (self.as_ref().print_info)(self.cobj_ref())
    }
}

As you can see, there is quite a bit of manual work taking place, so doing automatic code generation becomes incredibly handy. Especially since it involves a lot of generics - we use them to disallow illegal objects from becoming opaque ones implementing the trait. That would not be fun.

Road ahead

What was shown above is just the beginning of the road ahead. The next steps will be to implement grouping of the traits, which shouldn't be all that much. It will enable us to create owned heap allocated objects that can be dropped at any point. In addition, I was thinking a lot of so called "optional" traits. I think it's one of the key features such code generator should have. I would imagine the final workflow with grouped traits would be something like this:

// Define a group in a module
cglue_trait_group!(MyTraitGroup, { RequiredA, RequiredB }, { OptionalA, OptionalB, OptionalC});

// Build a group object from an object implementing a trait
let group = MyTraitGroup::from(obj);

// Validate optional features needed for use in a function
let group = cglue_require!(group impl OptionalB + OptionalC)?;

Finally, there are a lot of code gen edge cases that need handling, which will take quite a bit of time. I will explore this topic further, but if there is anything else that would be great to have, don't hesitate to shoot me a message! I want this to be a comprehensive ABI safety solution.