Conversion traits
ezffi defines four traits that describe how to translate a value between its Rust-type and its C-type representation. #[ezffi::export] implements them automatically for every type it touches, so most users never see them but they are the heart of this project. They are public so you can implement them by hand when you need a specific layout for your types that the macro doesn't produce.
#![allow(unused)] fn main() { pub trait RustRefIntoC<T> { type C: CRefIntoRust<Self>; unsafe fn ref_into_c(&self) -> Self::C; } pub trait RustOwnedIntoC<T>: Sized { type C: COwnedIntoRust<Self>; unsafe fn owned_into_c(self) -> Self::C; } pub trait CRefIntoRust<T: ?Sized> { unsafe fn into_rust(&self) -> &T; unsafe fn into_rust_mut(&mut self) -> &mut T; } pub trait COwnedIntoRust<T> { unsafe fn into_rust_owned(self) -> T; } }
What each one does
The four traits come in two pairs, splitting the conversion by direction and ownership:
| Trait | Direction | Ownership |
|---|---|---|
RustRefIntoC | Rust → C | borrowed (&T → C-type) |
RustOwnedIntoC | Rust → C | owned (T → C-type, moves into C) |
CRefIntoRust | C → Rust | borrowed (C-type → &T) |
COwnedIntoRust | C → Rust | owned (C-type → T, moves into Rust) |
The conversion flow
So, how am I using these traits?? Basically, when you want to export a Rust fn, the macro creates a wrapper that receives and returns the C-types of the Rust-types used in your Rust fn, and, depending on the type of argument/return, the wrapper picks the trait that makes the desired conversion, it will be easier to understand with an example.
Let's suppose we want to wrap pub fn hire(user: &mut User, company: &Company, salary: u64, idk: Idk) -> &Contract, the macro would generate something similar to the following wrapper:
pub extern "C" fn c_hire(user: *mut CUser, company: *const CCompany, salary: u64, idk: CIdk) -> *const CContract {
use ezffi::RustRefIntoC;
use ezffi::RustOwnedIntoC;
use ezffi::CRefIntoRust;
use ezffi::COwnedIntoRust;
// Bcs User is a mut ref, the macro receives a pointer and
// asks for the rust mut ref using the right trait.
let user = &mut *user;
let user = user.into_rust_mut();
// Here almost the same, but this time we want an immutable ref
let company = &*company;
let company = company.into_rust();
// Idk passes by value so we consume it and obtain an owned
// Rust-type that will be dropped at the end of this function.
let idk = idk.into_rust_owned();
// For simplicity, primitive types also apply the trait
let salary = salary.into_rust_owned();
// Now that we have the Rust-types we can call the Rust function
let result = hire(user, company, salary, idk);
// And, since the function returns a reference to something, we
// convert that reference into a reference to the right C-type
// by borrowing the data. Dont free borrowed data!!!
result.ref_into_c();
}
If you pay attention, the way this library achieves the 0% overhead is by providing 0-cost conversion when code is compiled so this entire wrapper gets replaced with the Rust call.
Why two pairs (ref vs owned)
This is pretty simple to answer, some types only make sense as a reference (eg: slices), so it doesn't make sense to implement the owned logic for them, it may even be impossible
Implementing them by hand
The macro decides layout automatically: C-compatible Rust-types stay as #[repr(C)] values; everything else is wrapped in an opaque *mut c_void handle (see the macro chapter). That covers most cases, but sometimes you want a different shape, a custom inline struct, a tagged-union, a pointer with extra metadata, a borrowing form that aliases an existing buffer, etc. In that case you take over the four impls yourself.
The pattern is always the same: declare your C-type, then write the four impls so they round-trip, write a free function if you allocate data, and you no longer need the #[ezffi::export] on your type to be used in an exported function.
#![allow(unused)] fn main() { // Suppose we want a custom C-type for `String` that exposes // pointer + length without going through an opaque handle. #[repr(C)] #[derive(Clone, Copy)] pub struct MyStr { ptr: *const u8, len: usize, } impl ::ezffi::RustRefIntoC<()> for String { type C = MyStr; unsafe fn ref_into_c(&self) -> MyStr { MyStr { ptr: self.as_ptr(), len: self.len() } } } impl ::ezffi::CRefIntoRust<String> for MyStr { unsafe fn into_rust(&self) -> &String { // Up to you how to reconstruct the borrow safely unimplemented!() } unsafe fn into_rust_mut(&mut self) -> &mut String { unimplemented!() } } // And the owned pair if you also need to move strings across. }
One rule of thumb:
- Mind the ownership semantics. If
owned_into_cheap-allocates,into_rust_ownedmust consume that allocation. Ifref_into_caliases borrowed memory,into_rustmust not extend its lifetime past the original borrow.
If you find yourself reaching for a custom impl frequently, that's usually a sign the macro is missing a feature, open an issue, ty.