Biko's House of Horrors

Narrowing Rust dyn pointers

Motivation

Suppose we are using some C library that has a callback interface with the following signature:

void handle_event(int event, void * ctx);

That is, handle_event is invoked with an event ID and an arbitrary ctx pointer.

The C library also provides some functions:

void do_something(void * ctx);      // -> handle_event(1, ctx)
void do_another_thing(void * ctx);  // -> handle_event(2, ctx)

We could implement handle_event in Rust like this:

extern "C" fn handle_event(event: c_int, ctx: *mut c_void) {
    match event {
        1 => {
            // Do something
        }
        2 => {
            // Do another thing
        }
        _ => {}
    }
}

But wouldn't it be nicer to have something like this?

trait EventHandler {
    fn handle(&self);
}

extern "C" fn handle_event(_event: c_int, ctx: *mut c_void) {
    unsafe {
        let handler: *mut dyn EventHandler = ctx.cast();
        (*handler).handle();
    }
}

The problem

Unfortunately, this doesn't compile. In Rust, dyn pointers and references are "wide" pointers: in addition to the object pointer, they also contain a pointer to the vtable1. This can be easily verified:

fn main() {
    dbg!(std::mem::size_of::<*mut c_void>());
    dbg!(std::mem::size_of::<*mut dyn EventHandler>());
}

Which prints out (on a 64-bit machine):

[src/main.rs:8] std::mem::size_of::<*mut c_void>() = 8
[src/main.rs:9] std::mem::size_of::<*mut dyn EventHandler>() = 16

This is unlike C++, where most compilers (or, at the very least, GCC, Clang, and MSVC) put the vtable pointer directly inside the object, so the actual pointer to the object remains the same size.

So, as much as we'd like to, we can't fit a 16-byte peg into an 8-byte hole.

Or can we?

Solution № 1 - Two allocations

A dyn pointer is larger than a regular pointer, but what about a pointer to a dyn pointer? Let's check:

fn main() {
    assert_eq!(
        std::mem::size_of::<*mut *mut dyn EventHandler>(),
        std::mem::size_of::<*mut c_void>()
    );
}

Seems ok. Now for the callback function:

extern "C" fn handle_event(_event: c_int, ctx: *mut c_void) {
    unsafe {
        let handler: *mut *mut dyn EventHandler = ctx.cast();
        (*(*handler)).handle();  // <-- Double dereference
    }
}

Which means we can allocate our handler as Box<Box<dyn EventHandler>>, extract the inner pointer using Box::into_raw, and pass that to the C library.

This works, but requires two memory allocations. Can we do better?

Solution № 2 - A single allocation

Let's try putting the handler object together with the dyn pointer:

#[repr(C)]
struct InlineDyn<T: EventHandler> {
    ptr: *mut dyn EventHandler, // <-- Points to `value`
    value: T,
}

#[repr(C)]
struct InlineDynHeader {
    ptr: *mut dyn EventHandler,
}

Here, InlineDyn is the full structure we'll allocate, and InlineDynHeader is the type-erased version. The idea is that after manually allocating an InlineDyn we'll cast the pointer to an InlineDynHeader, getting a type-erased regular-sized pointer.

Note that both structures are #[repr(C)], so that the members are laid out in the exact order we specify. This is necessary for safely casting between them.

Let's create a wrapper type for this:

use std::alloc::Layout;
use std::ptr::NonNull;

#[repr(C)]
struct InlineDyn<T: EventHandler> {
    ptr: *mut dyn EventHandler,
    value: T,
}

#[repr(C)]
struct InlineDynHeader {
    ptr: *mut dyn EventHandler,
}

struct Dynarrow {
    ptr: NonNull<InlineDynHeader>,
}

impl Dynarrow {
    fn new<T: EventHandler>(value: T) -> Self {
        unsafe {
            // Allocate correctly-aligned memory for the full
            // structure.
            let layout = Layout::new::<InlineDyn<T>>();
            let inline: *mut InlineDyn<T> =
                std::alloc::alloc(layout).cast();
            if inline.is_null() {
                std::alloc::handle_alloc_error(layout);
            }

            // Initialize the memory block by writing to it.
            // We can't use *inline = InlineDyn { ... }
            // because it's illegal to dereference uninitialized
            // memory.
            inline.write(InlineDyn {
                ptr: std::ptr::null_mut(),
                value,
            });

            // Now that the memory is initialized, and the value
            // is in place, make the pointer point to it.
            (*inline).ptr = &mut (*inline).value;

            Self {
                ptr: NonNull::new(inline.cast()).unwrap(),
            }
        }
    }
}

This doesn't quite compile, however:

error[E0271]: type mismatch resolving `<dyn EventHandler as Pointee>::Metadata == ()`
  --> src/lib.rs:40:22
   |
40 |                 ptr: std::ptr::null_mut(),
   |                      ^^^^^^^^^^^^^^^^^^ expected `()`, found `DynMetadata<dyn EventHandler>`
   |
   = note: expected unit type `()`
                 found struct `DynMetadata<(dyn EventHandler + 'static)>`
   = note: required for `(dyn EventHandler + 'static)` to implement `Thin`
note: required by a bound in `null_mut`
  --> /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/ptr/mod.rs:543:1

Looks like std::ptr::null_mut() doesn't work for dyn pointers. No matter, we can work around that:

use std::alloc::Layout;
use std::ptr::NonNull;

#[repr(C)]
struct InlineDyn<T: EventHandler> {
    ptr: Option<NonNull<dyn EventHandler>>, // <-- !!!
    value: T,
}

#[repr(C)]
struct InlineDynHeader {
    ptr: Option<NonNull<dyn EventHandler>>, // <-- !!!
}

struct Dynarrow {
    ptr: NonNull<InlineDynHeader>,
}

impl Dynarrow {
    fn new<T: EventHandler>(value: T) -> Self {
        unsafe {
            // Allocate correctly-aligned memory for the full
            // structure.
            let layout = Layout::new::<InlineDyn<T>>();
            let inline: *mut InlineDyn<T> =
                std::alloc::alloc(layout).cast();
            if inline.is_null() {
                std::alloc::handle_alloc_error(layout);
            }

            // Initialize the memory block by writing to it.
            // We can't use *inline = InlineDyn { ... }
            // because it's illegal to dereference uninitialized
            // memory.
            inline.write(InlineDyn {
                ptr: None, // <-- !!!
                value,
            });

            // Now that the memory is initialized, and the value
            // is in place, make the pointer point to it.
            (*inline).ptr = NonNull::new(&mut (*inline).value);

            Self {
                ptr: NonNull::new(inline.cast()).unwrap(),
            }
        }
    }
}

The idea is that we replace the raw dyn pointer with an Option<NonNull>, which is guaranteed to have the same size.

But it still doesn't compile:

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:46:42
   |
46 |             (*inline).ptr = NonNull::new(&mut (*inline).value);
   |                                          ^^^^^^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
24 |     fn new<T: EventHandler + 'static>(value: T) -> Self {
   |                            +++++++++

This error is because we defined the pointer as Option<NonNull<dyn EventHandler>>, which is equivalent to Option<NonNull<dyn EventHandler + 'static>>2. So let's do what the compiler suggests.

This finally compiles, but we're missing one crucial bit: Drop. Since Dynarrow holds a raw pointer we allocated, we also need to free it.

Let's do that:

#[repr(C)]
struct InlineDyn<T: EventHandler> {
    ptr: Option<NonNull<dyn EventHandler>>,
    layout: Layout, // <-- (1)
    value: T,
}

#[repr(C)]
struct InlineDynHeader {
    ptr: Option<NonNull<dyn EventHandler>>,
    layout: Layout, // <-- (1)
}

// ... Snip ...

impl Drop for InlineDynHeader { // <-- (2)
    fn drop(&mut self) {
        unsafe {
            if let Some(ptr) = self.ptr {
                ptr.as_ptr().drop_in_place();
            }
        }
    }
}

impl Drop for Dynarrow { // <-- (3)
    fn drop(&mut self) {
        unsafe {
            let layout = self.ptr.as_ref().layout; // <-- (1)
            self.ptr.as_ptr().drop_in_place();
            std::alloc::dealloc(self.ptr.as_ptr().cast(), layout);
        }
    }
}

There are several things in play here:

  1. Since std::alloc::dealloc requires that we pass in the same Layout that was used for allocating the memory, we need to store it inside InlineDyn.
  2. The InlineDynHeader drop implementation is responsible for dropping the stored value.
  3. Finally, when Dynarrow is dropped, it will drop the InlineDynHeader then deallocate the memory.

To make the whole thing useful, we can implement Deref for Dynarrow to get a reference to the underlying object, as well as functions for getting the raw pointer:

struct Dynarrow {
    ptr: Option<NonNull<InlineDynHeader>>,
}

impl Dynarrow {
    // ... Snip ...

    unsafe fn from_raw(ptr: NonNull<InlineDynHeader>) -> Self {
        Self { ptr: Some(ptr) }
    }

    fn into_raw(mut self) -> NonNull<InlineDynHeader> {
        // Take the raw pointer and replace it will None, to avoid
        // dropping it when self goes out of scope at the end of
        // the function.
        self.ptr.take().unwrap()
    }
}

impl Deref for Dynarrow {
    type Target = dyn EventHandler;

    fn deref(&self) -> &Self::Target {
        unsafe { self.ptr.unwrap().as_ref().ptr.unwrap().as_ref() }
    }
}

impl DerefMut for Dynarrow {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { self.ptr.unwrap().as_ref().ptr.unwrap().as_mut() }
    }
}

Note that DerefMut is perfectly safe here. The caller gets a &mut dyn EventHandler, which can only be used for calling methods. You can't std::mem::swap or std::mem::replace through the reference since it doesn't have a static size.

On the other hand, if we call a method on the trait that accepts &mut self, and that method decides to swap or replace, it's perfectly fine too. The pointer in InlineDyn will still point to a valid object of the same type, and with the same vtable.

And that's it! To call our C library we can create a Dynarrow, get a raw pointer out of it, pass it to the library, then reconstruct the Dynarrow inside the callback.

The cost? 32 extra bytes (on a 64-bit machine):

And, the Layout can be omitted when using an allocator that doesn't require it for deallocation.

Closing remarks

This solution should work for any object-safe trait, but there's one minor issue: it's not that easy to make it generic. AFAIK Rust doesn't support making types generic over traits. That is, we can't say something like:

struct MyStruct<T: G, G> where G: dyn {
    // ...
}

Which means that to make use of Dynarrow for another trait we'll either have to copy-paste it, or write a macro.

Full code

use std::alloc::Layout;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;

trait EventHandler {
    fn handle(&self);
}

#[repr(C)]
struct InlineDyn<T: EventHandler> {
    ptr: Option<NonNull<dyn EventHandler>>,
    layout: Layout,
    value: T,
}

#[repr(C)]
struct InlineDynHeader {
    ptr: Option<NonNull<dyn EventHandler>>,
    layout: Layout,
}

impl Drop for InlineDynHeader {
    fn drop(&mut self) {
        unsafe {
            if let Some(ptr) = self.ptr {
                ptr.as_ptr().drop_in_place();
            }
        }
    }
}

struct Dynarrow {
    ptr: Option<NonNull<InlineDynHeader>>,
}

impl Dynarrow {
    fn new<T: EventHandler + 'static>(value: T) -> Self {
        unsafe {
            // Allocate correctly-aligned memory for the full
            // structure.
            let layout = Layout::new::<InlineDyn<T>>();
            let inline: *mut InlineDyn<T> =
                std::alloc::alloc(layout).cast();
            if inline.is_null() {
                std::alloc::handle_alloc_error(layout);
            }

            // Initialize the memory block by writing to it.
            // We can't use *inline = InlineDyn { ... }
            // because it's illegal to dereference uninitialized
            // memory.
            inline.write(InlineDyn {
                ptr: None,
                layout,
                value,
            });

            // Now that the memory is initialized, and the value
            // is in place, make the pointer point to it.
            (*inline).ptr = NonNull::new(&mut (*inline).value);

            Self {
                ptr: NonNull::new(inline.cast()),
            }
        }
    }

    unsafe fn from_raw(ptr: NonNull<InlineDynHeader>) -> Self {
        Self { ptr: Some(ptr) }
    }

    fn into_raw(mut self) -> NonNull<InlineDynHeader> {
        // Take the raw pointer and replace it will None, to avoid
        // dropping it when self goes out of scope at the end of
        // the function.
        self.ptr.take().unwrap()
    }
}

impl Drop for Dynarrow {
    fn drop(&mut self) {
        unsafe {
            if let Some(ptr) = self.ptr {
                let layout = ptr.as_ref().layout;
                ptr.as_ptr().drop_in_place();
                std::alloc::dealloc(ptr.as_ptr().cast(), layout);
            }
        }
    }
}

impl Deref for Dynarrow {
    type Target = dyn EventHandler;

    fn deref(&self) -> &Self::Target {
        unsafe { self.ptr.unwrap().as_ref().ptr.unwrap().as_ref() }
    }
}

impl DerefMut for Dynarrow {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { self.ptr.unwrap().as_ref().ptr.unwrap().as_mut() }
    }
}