Files
Sprimo/skills/unsafe-checker/rules/ffi-18-no-trait-objects.md
2026-02-12 22:58:33 +08:00

3.8 KiB

id, original_id, level, impact
id original_id level impact
ffi-18 P.UNS.FFI.18 P HIGH

Avoid Passing Trait Objects to C Interfaces

Summary

Trait objects (dyn Trait) have Rust-specific layout (fat pointers with vtable) that is not compatible with C.

Rationale

  • Trait objects are "fat pointers": data ptr + vtable ptr
  • C expects thin pointers (single pointer)
  • Vtable layout is not stable across Rust versions
  • C cannot call Rust vtable methods

Bad Example

// DON'T: Pass trait objects to C
trait Handler {
    fn handle(&self, data: i32);
}

extern "C" {
    // This won't work - dyn Handler is a fat pointer!
    fn set_handler(h: *const dyn Handler);
}

// DON'T: Store trait objects in FFI structs
#[repr(C)]
struct BadCallback {
    handler: *const dyn Handler,  // Not C-compatible!
}

Good Example

use std::os::raw::{c_int, c_void};

// DO: Use function pointers with user_data (trampoline pattern)
type HandlerFn = extern "C" fn(data: c_int, user_data: *mut c_void);

extern "C" {
    fn set_handler(handler: HandlerFn, user_data: *mut c_void);
}

trait Handler {
    fn handle(&self, data: i32);
}

fn register_handler<H: Handler + 'static>(handler: H) {
    // Box the handler
    let boxed: Box<H> = Box::new(handler);
    let user_data = Box::into_raw(boxed) as *mut c_void;

    extern "C" fn trampoline<H: Handler>(data: c_int, user_data: *mut c_void) {
        let handler = unsafe { &*(user_data as *const H) };
        handler.handle(data as i32);
    }

    unsafe {
        set_handler(trampoline::<H>, user_data);
    }
}

// DO: Use concrete types when possible
struct ConcreteHandler {
    multiplier: i32,
}

impl Handler for ConcreteHandler {
    fn handle(&self, data: i32) {
        println!("{}", data * self.multiplier);
    }
}

// DO: Create C-compatible vtable manually if needed
#[repr(C)]
struct HandlerVtable {
    handle: extern "C" fn(this: *const c_void, data: c_int),
    drop: extern "C" fn(this: *mut c_void),
}

#[repr(C)]
struct CCompatibleHandler {
    data: *mut c_void,
    vtable: *const HandlerVtable,
}

impl CCompatibleHandler {
    fn new<H: Handler + 'static>(handler: H) -> Self {
        extern "C" fn handle_impl<H: Handler>(this: *const c_void, data: c_int) {
            let handler = unsafe { &*(this as *const H) };
            handler.handle(data as i32);
        }

        extern "C" fn drop_impl<H: Handler>(this: *mut c_void) {
            unsafe { drop(Box::from_raw(this as *mut H)); }
        }

        static VTABLE: HandlerVtable = HandlerVtable {
            handle: handle_impl::<ConcreteHandler>,  // Need concrete type
            drop: drop_impl::<ConcreteHandler>,
        };

        Self {
            data: Box::into_raw(Box::new(handler)) as *mut c_void,
            vtable: &VTABLE,
        }
    }

    fn handle(&self, data: i32) {
        unsafe {
            ((*self.vtable).handle)(self.data, data as c_int);
        }
    }
}

impl Drop for CCompatibleHandler {
    fn drop(&mut self) {
        unsafe {
            ((*self.vtable).drop)(self.data);
        }
    }
}

Why Trait Objects Don't Work

Rust trait object (*const dyn Handler):
[data pointer][vtable pointer]  <- 16 bytes on 64-bit

C pointer (void*):
[pointer]  <- 8 bytes on 64-bit

The sizes don't match!

Alternatives to Trait Objects

Instead of Use
dyn Trait Function pointer + user_data
Box<dyn Trait> Boxed concrete type + trampoline
&dyn Trait C-compatible vtable struct
Arc<dyn Trait> Reference counting wrapper

Checklist

  • Am I passing trait objects across FFI?
  • Can I use concrete types instead?
  • Have I used the trampoline pattern for callbacks?
  • If vtable is needed, is it C-compatible?
  • ffi-16: Closure to C with trampoline pattern
  • ffi-14: Types should have stable layout