Skip to content

Instantly share code, notes, and snippets.

@jmeggitt
Last active August 13, 2023 21:08
Show Gist options
  • Save jmeggitt/a4a3284752c1daec2ffee7ed1b138b57 to your computer and use it in GitHub Desktop.
Save jmeggitt/a4a3284752c1daec2ffee7ed1b138b57 to your computer and use it in GitHub Desktop.
Notes on Common C FFI Patterns in Idiomatic Rust

Idiomatic FFI in Rust

Pointer concepts

Due to niche optimization, Rust will guarentee that the following types will all have the same layout in memory. However, each option comes with different guarentees and should be selected accordingly.

Non-null

  • &T
  • &mut T
  • NonNull<T>
    • *mut T, but non-zero and covariant
  • Box<T>
    • Same aliasing rules as &mut T

Nullable

  • *const T
  • *mut T
  • Option<&T>
  • Option<&mut T>
  • Option<NonNull<T>>
  • Option<Box<T>>

Types

When using a wrapper struct, annotate it with #[repr(transparent)] to guarentee that niche optimizations can stil be applied to the wrapped type.

#[repr(transparent)]
pub struct Foo<'a> (&'a i32);
pub struct Bar<'a> (&'a i32);

// Always true
assert_eq!(size_of::<*const Foo>(), size_of::<Option<Foo>>());
// Probably true, but no guarentees are given
assert_eq!(size_of::<*const Foo>(), size_of::<Option<Bar>>());

Common Patterns

Opaque Malloced Singletons

Lets say we need to implement the functions exported by this header.

struct Foo;

struct Foo *foo_alloc(void);
void foo_free(struct Foo *);

int foo_get_x(struct Foo *);
void foo_set_x(struct Foo *, int x);

In this case, we know that C code will never attempt to read/write/move Foo so we can let Rust manage the memory. This can all be mirrored in safe Rust using the following approach. One big benefit of this is thta it does not require any unsafe and will also work when called from Rust with no added complications.

use std::ffi::c_int;

pub struct Foo {
    x: i32,
}

impl Foo {
    #[export_name = "foo_alloc"]
    pub extern "C" fn new_boxed() -> Box<Self> {
       Box::new(Foo {
          x: 5,
       })
    }
    
    // Use an option here because many libraries support passing null. We don't have to implement
    // anything because by taking ownership of the box, it will be freed at the end of the function.
    #[export_name = "foo_free"]
    pub extern "C" fn _drop_boxed(_: Option<Box<Self>>) {}
    
    #[export_name = "foo_get_x"]
    pub extern "C" fn get_x(&self) -> c_int {
        // Since we use i32 internally, we need to convert to c_int incase the types do not match
        self.x as c_int
    }
    
    #[export_name = "foo_set_x"]
    pub extern "C" fn set_x(&mut self, x: c_int) {
        self.x = x as i32;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment