Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- move to edition 2024, MSRV 1.88
- `CArray`, `CStringArray` and `CRange` have moved to the new `ffi-convert-extra-ctypes` crate. The core `ffi-convert` crate now only contains the conversion traits.
- rewrote most of the documentation

### Fixed
- memory leak on array conversion error and performance / correctness improvement on array conversion
Expand Down
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,62 @@
# ffi-convert
![ffi-convert](https://github.com/snipsco/snips-utils-rs/workflows/Rust/badge.svg?branch=master&event=push)

![ffi-convert](https://github.com/sonos/ffi-convert-rs/workflows/Rust/badge.svg?branch=main&event=push)
[![ffi-convert on crates.io](https://img.shields.io/crates/v/ffi-convert.svg)](https://crates.io/crates/ffi-convert)
[![ffi-convert documentation](https://docs.rs/ffi-convert/badge.svg)](https://docs.rs/ffi-convert/)

**A collection of utilities (functions, traits, data structures, etc ...) to ease conversion between Rust and C-compatible data structures.**
You can learn more about open source projects at Sonos [here](https://tech-blog.sonos.com/posts/three-open-source-sonos-projects-in-rust/) and find the documentation for `ffi-convert` [here](https://docs.rs/ffi-convert).
**Convert between idiomatic Rust values and C-compatible data structures with
a minimum of unsafe ceremony.**

`ffi-convert` provides two conversion traits — `CReprOf` (Rust → C) and
`AsRust` (C → Rust) — plus `CDrop` and `RawPointerConverter` to handle
ownership of pointer fields, and derive macros that take care of the
boilerplate.

```rust
use ffi_convert::{AsRust, CDrop, CReprOf};
use libc::{c_char, c_float};

pub struct Pizza {
pub name: String,
pub weight: f32,
}

#[repr(C)]
#[derive(CReprOf, AsRust, CDrop)]
#[target_type(Pizza)]
pub struct CPizza {
pub name: *const c_char,
pub weight: c_float,
}

let pizza = Pizza { name: "Margarita".to_owned(), weight: 450.0 };
let c_pizza = CPizza::c_repr_of(pizza).unwrap(); // Rust -> C
let again: Pizza = c_pizza.as_rust().unwrap(); // C -> Rust
```

## Workspace layout

| Crate | What's in it |
|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| [`ffi-convert`](./ffi-convert) | The conversion traits (`CReprOf`, `AsRust`, `CDrop`, `RawPointerConverter`, `RawBorrow`). |
| [`ffi-convert-derive`](./ffi-convert-derive) | `#[derive(...)]` macros for all four conversion traits. Re-exported from `ffi-convert`. |
| [`ffi-convert-extra-ctypes`](./ffi-convert-extra-ctypes) | Optional C-compatible containers: `CArray<T>` (`Vec<U>`), `CStringArray` (`Vec<String>`), `CRange<T>`. |
| [`ffi-convert-tests`](./ffi-convert-tests) | Workspace tests, including C round-trip tests with AddressSanitizer / MemorySanitizer. |

Full documentation lives on [docs.rs/ffi-convert](https://docs.rs/ffi-convert),
including the type-mapping table, attribute reference, and caveats that apply
to the derives.

More on open source projects at Sonos
[here](https://tech-blog.sonos.com/posts/three-open-source-sonos-projects-in-rust/).

## License

Licensed under either of

* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)

at your option.

### Contribution
Expand Down
108 changes: 107 additions & 1 deletion ffi-convert-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
//! This crate provides ffi_convert derive macros for CReprOf, AsRust and CDrop traits.
//! Derive macros for the conversion traits provided by the
//! [`ffi-convert`](https://docs.rs/ffi-convert) crate.
//!
//! The four derives are also re-exported from `ffi-convert`, so users of
//! `ffi-convert` typically do not need to depend on this crate directly.
//!
//! See the top-level [`ffi-convert`](https://docs.rs/ffi-convert) documentation
//! for the overall design, the type-mapping table, and the caveats that apply
//! to all four derives. The per-macro documentation below lists the supported
//! helper attributes.

extern crate proc_macro;

Expand All @@ -14,6 +23,34 @@ use creprof::impl_creprof_macro;
use proc_macro::TokenStream;
use rawpointerconverter::impl_rawpointerconverter_macro;

/// Derive [`CReprOf<T>`](../ffi_convert/trait.CReprOf.html) for a struct or unit enum.
///
/// Generates a consuming conversion from the idiomatic Rust type named in
/// `#[target_type(...)]` to `Self`. C-string fields (pointers to `c_char`) are
/// re-allocated as `CString`s, other pointer fields are boxed via
/// [`RawPointerConverter::into_raw_pointer`](../ffi_convert/trait.RawPointerConverter.html),
/// and remaining fields go through their own `CReprOf` impl.
///
/// # Struct-level attributes
///
/// - `#[target_type(Path)]` — **required**. The idiomatic Rust type this
/// `#[repr(C)]` struct mirrors.
///
/// # Field-level attributes
///
/// - `#[nullable]` — required on every pointer field whose Rust counterpart is
/// an [`Option`]. A `None` value is written as a null pointer.
/// - `#[target_name(ident)]` — name of the matching field on the Rust side
/// when it differs from the C-side name.
/// - `#[c_repr_of_convert(expr)]` — replace the generated conversion with a
/// custom expression. The owned Rust value `input: TargetType` is in scope.
/// A field marked with this attribute is also skipped by the `AsRust`
/// derive — if the reverse direction is needed, provide it with
/// `#[as_rust_extra_field(...)]` on the struct.
///
/// # Enums
///
/// Enums are supported only if every variant is a unit variant.
#[proc_macro_derive(
CReprOf,
attributes(target_type, nullable, c_repr_of_convert, target_name)
Expand All @@ -23,6 +60,39 @@ pub fn creprof_derive(token_stream: TokenStream) -> TokenStream {
impl_creprof_macro(&ast)
}

/// Derive [`AsRust<T>`](../ffi_convert/trait.AsRust.html) for a struct or unit enum.
///
/// Generates a non-consuming conversion that returns a freshly-allocated value
/// of the type named in `#[target_type(...)]`. C-string fields are decoded as
/// UTF-8 and copied; other pointer fields are borrowed via
/// [`RawBorrow`](../ffi_convert/trait.RawBorrow.html) and then converted with
/// their own `AsRust` impl; remaining fields go through their own `AsRust`
/// impl directly.
///
/// The derived `AsRust` reads pointer fields under the same layout assumptions
/// as `CReprOf` / `CDrop`; deriving all three together keeps them in sync.
///
/// # Struct-level attributes
///
/// - `#[target_type(Path)]` — **required**.
/// - `#[as_rust_extra_field(name = expr)]` — initialise an extra field on the
/// Rust side that has no C counterpart. The attribute can be repeated; `self`
/// (the C-compatible value) is in scope inside `expr`, allowing
/// reconstruction from unrelated C-side fields.
///
/// # Field-level attributes
///
/// - `#[nullable]` — map a null pointer to [`None`] instead of failing.
/// - `#[target_name(ident)]` — name of the matching field on the Rust side
/// when it differs from the C-side name.
///
/// A field annotated with `#[c_repr_of_convert(...)]` (see [`CReprOf`]) is
/// skipped by this derive; pair it with `#[as_rust_extra_field]` if the Rust
/// struct still has a matching field.
///
/// # Enums
///
/// Enums are supported only if every variant is a unit variant.
#[proc_macro_derive(
AsRust,
attributes(
Expand All @@ -38,12 +108,48 @@ pub fn asrust_derive(token_stream: TokenStream) -> TokenStream {
impl_asrust_macro(&ast)
}

/// Derive [`CDrop`](../ffi_convert/trait.CDrop.html) and (by default) [`Drop`]
/// for a struct or unit enum.
///
/// The generated `do_drop` releases every owning pointer field by calling
/// [`RawPointerConverter::drop_raw_pointer`](../ffi_convert/trait.RawPointerConverter.html),
/// which takes the value back from the raw pointer and lets it drop. Non-pointer
/// fields are left to Rust's regular drop glue.
///
/// Deriving [`CDrop`] assumes the struct owns its pointer fields and was initialized
/// via `CReprOf::c_repr_of`. Derive `CReprOf` and `CDrop` together to keep their
/// assumptions in sync.
///
/// The default output also emits a `Drop` impl that calls `do_drop`, so that
/// letting a value go out of scope releases its pointer fields. A `CDrop`
/// impl without a matching `Drop` leaks every pointer field on scope exit.
///
/// # Struct-level attributes
///
/// - `#[no_drop_impl]` — generate only the `CDrop` impl and skip the blanket
/// `Drop` impl. Use this when you need a manual `Drop`; that manual `Drop`
/// should call `do_drop`, otherwise the pointer fields leak.
///
/// # Field-level attributes
///
/// - `#[nullable]` — skip the free when the pointer is null. This is the same
/// attribute that `CReprOf` and `AsRust` read on the field; annotate the
/// field once and all three derives stay in sync.
#[proc_macro_derive(CDrop, attributes(no_drop_impl, nullable))]
pub fn cdrop_derive(token_stream: TokenStream) -> TokenStream {
let ast = syn::parse(token_stream).unwrap();
impl_cdrop_macro(&ast)
}

/// Derive [`RawPointerConverter<Self>`](../ffi_convert/trait.RawPointerConverter.html)
/// for a struct.
///
/// The derived implementation boxes `self` into `*const Self` / `*mut Self`
/// (and conversely). It is needed on any C-compatible struct that is reached
/// through a raw pointer field in another C-compatible struct, because the
/// derived [`CReprOf`] of the parent calls `into_raw_pointer()` on it.
///
/// No helper attributes.
#[proc_macro_derive(RawPointerConverter)]
pub fn rawpointerconverter_derive(token_stream: TokenStream) -> TokenStream {
let ast = syn::parse(token_stream).unwrap();
Expand Down
39 changes: 29 additions & 10 deletions ffi-convert-extra-ctypes/src/c_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,58 @@ use ffi_convert::{
take_back_from_raw_pointer, take_back_from_raw_pointer_mut,
};

/// A utility type to represent arrays of the parametrized type.
/// Note that the parametrized type should have a C-compatible representation.
/// A `#[repr(C)]` mirror of [`Vec<U>`] with impls of [`CReprOf`], [`CDrop`]
/// and [`AsRust`].
///
/// Layout is a `(data_ptr, size)` pair. An empty array is represented with a
/// null `data_ptr` and `size == 0`.
///
/// When `U` is a primitive numeric type (`u8`, `i8`, `u16`, `i16`, `u32`,
/// `i32`, `f32`, or `f64`) the conversion takes a fast path: `c_repr_of`
/// reuses the input `Vec`'s buffer directly, and `as_rust` does a bulk
/// `ptr::copy` into a new `Vec`. Otherwise each element is converted
/// individually through its `CReprOf` / `AsRust` implementation.
///
/// `CArray` owns the backing buffer and frees it via its [`Drop`] impl (by
/// way of [`CDrop`]). Do not reconstruct a `CArray` from a pointer you do not
/// own.
///
/// # Example
///
/// ```
/// use ffi_convert::{CReprOf, AsRust, CDrop};
/// use ffi_convert::{AsRust, CDrop, CReprOf};
/// use ffi_convert_extra_ctypes::CArray;
/// use libc::c_char;
///
/// pub struct PizzaTopping {
/// pub ingredient: String,
/// }
///
/// #[derive(CDrop, CReprOf, AsRust)]
/// #[derive(CReprOf, AsRust, CDrop)]
/// #[target_type(PizzaTopping)]
/// pub struct CPizzaTopping {
/// pub ingredient: *const c_char
/// pub ingredient: *const c_char,
/// }
///
/// let toppings = vec![
/// PizzaTopping { ingredient: "Cheese".to_string() },
/// PizzaTopping { ingredient: "Ham".to_string() } ];
/// PizzaTopping { ingredient: "Cheese".into() },
/// PizzaTopping { ingredient: "Ham".into() },
/// ];
///
/// let ctoppings = CArray::<CPizzaTopping>::c_repr_of(toppings);
/// // Rust -> C (the `CArray` now owns the C strings it allocated).
/// let c_toppings = CArray::<CPizzaTopping>::c_repr_of(toppings).unwrap();
/// assert_eq!(c_toppings.size, 2);
///
/// // C -> Rust (deep copy; `c_toppings` stays valid).
/// let round_tripped: Vec<PizzaTopping> = c_toppings.as_rust().unwrap();
/// assert_eq!(round_tripped[0].ingredient, "Cheese");
/// ```
#[repr(C)]
#[derive(Debug)]
pub struct CArray<T> {
/// Pointer to the first element of the array
/// Pointer to the first element, or null when `size == 0`.
pub data_ptr: *const T,
/// Number of elements in the array
/// Number of elements in the array.
pub size: usize,
}

Expand Down
7 changes: 5 additions & 2 deletions ffi-convert-extra-ctypes/src/c_range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ use std::ops::Range;

use ffi_convert::{AsRust, AsRustError, CDrop, CDropError, CReprOf, CReprOfError};

/// A utility type to represent range.
/// Note that the parametrized type T should have `CReprOf` and `AsRust` trait implemented.
/// A `#[repr(C)]` mirror of [`std::ops::Range<U>`] with impls of [`CReprOf`],
/// [`CDrop`] and [`AsRust`].
///
/// Contains a plain `(start, end)` pair — no allocation involved. Use it as
/// a field type when a struct needs to expose a range through FFI.
///
/// # Example
///
Expand Down
32 changes: 26 additions & 6 deletions ffi-convert-extra-ctypes/src/c_string_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,42 @@ use ffi_convert::{
AsRust, AsRustError, CDrop, CDropError, CReprOf, CReprOfError, RawBorrow, RawPointerConverter,
};

/// A utility type to represent arrays of string
/// A `#[repr(C)]` mirror of `Vec<String>` with impls of [`CReprOf`], [`CDrop`]
/// and [`AsRust`].
///
/// Layout is a `(data, size)` pair where `data` is a pointer to a
/// heap-allocated array of `*const c_char`, each pointing to its own
/// nul-terminated C string allocated by the Rust side. The whole structure
/// is freed via [`CDrop`](ffi_convert::CDrop) / [`Drop`].
///
/// Strings containing interior NUL bytes cannot be represented and will
/// cause [`CReprOf::c_repr_of`](ffi_convert::CReprOf::c_repr_of) to fail
/// with [`CReprOfError::StringContainsNullBit`](ffi_convert::CReprOfError).
///
/// # Example
///
/// ```
/// use ffi_convert::CReprOf;
/// use ffi_convert::{AsRust, CReprOf};
/// use ffi_convert_extra_ctypes::CStringArray;
/// let pizza_names = vec!["Diavola".to_string(), "Margarita".to_string(), "Regina".to_string()];
/// let c_pizza_names = CStringArray::c_repr_of(pizza_names).expect("could not convert !");
///
/// let pizza_names = vec![
/// "Diavola".to_string(),
/// "Margarita".to_string(),
/// "Regina".to_string(),
/// ];
/// let c_pizza_names = CStringArray::c_repr_of(pizza_names.clone()).unwrap();
/// assert_eq!(c_pizza_names.size, 3);
///
/// // Deep-copy back into owned Rust strings.
/// let round_tripped: Vec<String> = c_pizza_names.as_rust().unwrap();
/// assert_eq!(round_tripped, pizza_names);
/// ```
#[repr(C)]
#[derive(Debug, RawPointerConverter)]
pub struct CStringArray {
/// Pointer to the first element of the array
/// Pointer to the first `*const c_char` element of the array.
pub data: *const *const libc::c_char,
/// Number of elements in the array
/// Number of elements in the array.
pub size: usize,
}

Expand Down
25 changes: 20 additions & 5 deletions ffi-convert-extra-ctypes/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
//! Extra C-compatible utility types for [`ffi_convert`].
//! C-compatible mirrors of a few standard Rust containers, for use with
//! [`ffi-convert`](https://docs.rs/ffi-convert).
//!
//! These types mirror common stdlib types ([`Vec<String>`], [`Vec<T>`], [`std::ops::Range`])
//! with C-compatible representations. They are provided as a convenience but are not
//! required to use the `ffi-convert` core crate: users who prefer to define their own
//! layouts can skip this crate entirely.
//! The three types exposed here cover the containers most FFI boundaries
//! inevitably need:
//!
//! | Rust type | C-compatible mirror |
//! |------------------|--------------------------------|
//! | `Vec<T>` | [`CArray<T>`] |
//! | `Vec<String>` | [`CStringArray`] |
//! | `std::ops::Range<T>` | [`CRange<T>`] |
//!
//! Each mirror implements [`CReprOf`](ffi_convert::CReprOf),
//! [`AsRust`](ffi_convert::AsRust), and [`CDrop`](ffi_convert::CDrop), so it
//! can be embedded in any struct you derive the conversion traits on — see
//! the top-level [`ffi-convert`](https://docs.rs/ffi-convert) documentation
//! for how the pieces fit together.
//!
//! This crate is optional: if none of these types fit your layout, depend on
//! `ffi-convert` alone and define your own `#[repr(C)]` container with the
//! conversion trait impls you need.

mod c_array;
mod c_range;
Expand Down
Loading
Loading