doc: update ffi design

This commit is contained in:
2026-08-17 22:04:56 +08:00
parent b9e9039c83
commit da56800265
2 changed files with 212 additions and 22 deletions
@@ -1,18 +1,12 @@
# Allowed Data Types # Allowed Data Types
Under this section we discuss which data types are allowed to appear in an FFI Under this section we discuss which data types are allowed to appear in an FFI interface and how to convert Rust data types into these allowed types.
interface and how to convert Rust data types into these allowed types. The
correspondence between these types and their C/C++ counterparts is covered in
C/C++ header writing chapter.
## Primitive Type Conversion Please note that all "type" word are referred to the argument of `in_param_ty!` or `out_param_ty!`. These "type"s are not the final types in FFI signature. They will be decorated by these macros.
This subsection covers the conversion of primitive types. The rules described here ## Directly Passed Primitive Types
apply to both parameter and return value positions.
### Types Allowed to Cross the Boundary Directly The following Rust primitive types are allowed to be passed directly across the FFI boundary:
The following Rust types are allowed to be passed directly across the FFI boundary:
- `bool` - `bool`
- `f32` - `f32`
@@ -28,23 +22,196 @@ The following Rust types are allowed to be passed directly across the FFI bounda
- `usize` - `usize`
- `isize` - `isize`
### The u128 and i128 Problem ## `u128` and `i128` Types
The `u128` and `i128` types have very poor support in the C/C++ standard libraries. The `u128` and `i128` types have very poor support in the C/C++ standard libraries. Therefore they cannot be passed directly across the FFI boundary.
Therefore they cannot be passed directly across the FFI boundary.
To transfer data of these types, refer to the later section about passing opaque To transfer data of these types, refer to the later section about passing opaque structs.
structs.
### The char Problem ## `char` Type
The Rust `char` type only holds valid Unicode scalar values. The Rust official The Rust `char` type only holds valid Unicode scalar values. The Rust official documentation explicitly states that the `char` type does not have FFI safety, so it cannot be used as a value passed across the FFI boundary.
documentation explicitly states that the `char` type does not have FFI safety, so it
cannot be used as a value passed across the FFI boundary.
The solution is to use the Rust `u32` type for passing. Under this scheme: The solution is to use the Rust `u32` type for passing. Under this scheme:
- The function signature uses `u32` as the parameter type. - The function signature uses `u32` as the parameter type.
- For input parameters, use `char::try_from` to perform a fallible conversion that - For input parameters, use `char::try_from` to perform a fallible conversion that validates the incoming character.
validates the incoming character.
- For output parameters, use `u32::from` for the conversion. - For output parameters, use `u32::from` for the conversion.
## Enum Types
Enum types in Rust are quite diverse, so we need to adopt different strategies for different variants.
### Plain Enums
The plain enum type is the simplest kind, as shown below:
```rust
enum Plain {
Entry1,
Entry2,
}
```
For a plain enum type, we first need to choose an underlying type for it, just like the `enum class` in C++. The chosen underlying type needs to be specified on this enum type via the `repr` attribute. Then, when passing across the FFI boundary, we use this underlying type. Specifically:
- Use the chosen underlying type as the parameter type in the function signature.
- For input parameters, convert from the chosen underlying type to our enum, and verify that it is a valid enum field value.
- For output parameters, convert the enum to the chosen underlying type, and then pass it out.
These conversion and validation methods for input and output parameters can be written by hand, but we recommend using an existing package to accomplish this more conveniently. Choose the library suitable for your FFI project according to the following situations:
- Use the `num_enum` package (lightweight; use it when your project does not use `strum`).
- Use the `strum` and `strum_macros` packages (heavier; use them when your project already uses `strum`).
#### Using `num_enum`
An example of using the `num_enum` package to conveniently perform conversion and validation is as follows:
```rust
#[derive(Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
enum Number {
Zero,
One,
}
fn foobar() {
// Convert the enum to the chosen underlying type.
let zero: u8 = Number::Zero.into();
assert_eq!(zero, 0u8);
// Convert from the chosen underlying type to the enum (success case).
let zero = Number::try_from(0u8);
assert_eq!(zero, Ok(Number::Zero));
// Convert from the chosen underlying type to the enum (failure case).
let three = Number::try_from(3u8);
assert!(three.is_err());
}
```
#### Using `strum` and `strum_macros`
An example of using the `strum` and `strum_macros` packages to conveniently perform conversion and validation is as follows:
```rust
#[derive(Debug, PartialEq, FromRepr)]
#[repr(u8)]
enum Number {
One = 1,
Three = 3,
}
fn foobar() {
// Convert the enum to the chosen underlying type.
let one: u8 = Number::One as u8;
assert_eq!(one, 1u8);
// Convert from the chosen underlying type to the enum (success case).
assert_eq!(Some(Number::One), Number::from_repr(1));
// Convert from the chosen underlying type to the enum (failure case).
assert_eq!(None, Number::from_repr(0));
}
```
#### Enum Types from Other Crates
In most cases, the enum type comes from another crate (because we develop with the two-project structure). In this case, Rust does not allow you to attach attribute tags to a type from outside your own crate. The solution is to create a wrapper type in your own crate, decorate it with attributes, and implement the bidirectional `From` traits. As shown below:
```rust
use another_crate::Example as InternExample;
#[repr(u8)]
enum Example {
// Entries omitted...
}
impl From<Example> for InternExample {
// Implementation omitted...
}
impl From<InternExample> for Example {
// Implementation omitted...
}
```
### Flag Enums
The best practice for flag-style enums in Rust is `bitflags`, as shown below. We assume that you are using `bitflags` to implement all flag-style enums in your project.
```rust
bitflags! {
pub struct Flags: u32 {
const A = 0b00000001;
const B = 0b00000010;
const C = 0b00000100;
}
}
```
Similarly, we also need to choose an underlying type for it. The difference is that when using `bitflags` we have already specified this underlying type, so there is no need to specify it additionally. When passing across the FFI boundary, follow the same requirements as the plain enum types.
We also need to establish the conversion and validation connection between this enum type and the chosen underlying type. Fortunately, `bitflags` already provides these functions for us, for example the automatically generated `from_bits` method and the `bits` method from the `Flags` trait. For example:
```rust
bitflags! {
pub struct Flags: u32 {
const A = 0b00000001;
const B = 0b00000010;
const C = 0b00000100;
}
}
fn foobar() {
// Convert the enum to the chosen underlying type.
let flags = Flags::A | Flags::B;
let bits: u32 = flags.bits();
assert_eq!(bits, 0b00000011u32);
// Convert from the chosen underlying type to the enum (success case).
let success = Flags::from_bits(0b00000101u32);
assert_eq!(success, Some(Flags::A | Flags::C));
// Convert from the chosen underlying type to the enum (failure case).
let failure = Flags::from_bits(0b00001000u32);
assert_eq!(failure, None);
}
```
### Payload Enums
The payload enum is the most complex kind of enum type, for example:
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
```
We require that payload enums be converted into opaque structs for access. Put the payload enum into a struct, use a plain enum type to distinguish the different payload entries, and provide different methods to access the different entries. For example:
```rust
use some_crate::Message as InternMessage;
struct Message {
inner: InternMessage,
}
enum MessageKind {
// Entries omitted...
}
impl Message {
pub fn get_kind(&self) -> MessageKind {
// Contents omitted...
}
pub fn get_write_entry(&self) -> &str {
// Contents omitted...
}
// The access functions for the following entries are omitted...
}
```
+24 -1
View File
@@ -5,7 +5,7 @@
When writing FFI, we require you to develop with two projects using the Rust workspace mechanism. For example: When writing FFI, we require you to develop with two projects using the Rust workspace mechanism. For example:
- An ordinary Rust library project named `foobar`. In this project you write your library logic in a pure Rust way. - An ordinary Rust library project named `foobar`. In this project you write your library logic in a pure Rust way.
- An FFI-specific library project named `foobar-ffi` (the name is not mandatory). This project references `foobar` within the workspace, writes the relevant code according to this design, wraps `foobar` into a form suitable for FFI export, and finally exports it in FFI form. - An FFI-specific library project named `foobar-ffi` (the name does not have to be exactly this; you may choose any name that fits your need). This project references `foobar` within the workspace, writes the relevant code according to this design, wraps `foobar` into a form suitable for FFI export, and finally exports it in FFI form.
## Panic Policy ## Panic Policy
@@ -28,3 +28,26 @@ The build artifact of the FFI project should meet the requirements of the C lang
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
``` ```
## Module Organization
In the FFI project, do not try to pile everything into `lib.rs`. You need to make reasonable use of the Rust module mechanism. For example:
- When combining multiple Rust types to create an opaque struct dedicated to FFI, you can write it in a module named like `wrapper` (the name does not have to be exactly this; you may choose any name that fits your need).
- When re-wrapping Rust types, you can write it in a module named like `ffi_types` (the name does not have to be exactly this; you may choose any name that fits your need).
`lib.rs` should contain only the following:
- User-defined error types, `Result` types, `CError` constants and so on.
- The constants and types to export, including those defined directly with `pub type` and `pub const`, and those defined indirectly with `pub use` from other modules.
- All the FFI functions to export.
The re-wrapped contents can be written in a separate module `wrapper.rs`. `lib.rs` only contains the error definitions, the types to export, the constants and the functions.
## Dependency Specification
When adding `sarasacw-omrf` to the dependencies of the FFI project, we require specifying it as a git repository, supplemented with a tag and the `version` field, to ensure the version is correct. As shown below:
```toml
sarasacw-omrf = { version="1.0.0", git = "https://github.com/SarasasChipWorkshop/sarasacw-omrf.git", tag = "omrf/1.0.0" }
```