refactor: refactor c-friendly ffi design doc

This commit is contained in:
2026-08-09 21:41:08 +08:00
parent bad72120f9
commit 0c21166e53
5 changed files with 229 additions and 81 deletions
@@ -0,0 +1,50 @@
# Allowed Data Types
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. The
correspondence between these types and their C/C++ counterparts is covered in
[C/C++ Headers](c-cpp-headers.md).
## Primitive Type Conversion
This subsection covers the conversion of primitive types. The rules described here
apply to both parameter and return value positions.
### Types Allowed to Cross the Boundary Directly
The following Rust types are allowed to be passed directly across the FFI boundary:
- `bool`
- `f32`
- `f64`
- `i8`
- `i16`
- `i32`
- `i64`
- `u8`
- `u16`
- `u32`
- `u64`
- `usize`
- `isize`
### The u128 and i128 Problem
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.
To transfer data of these types, refer to the later section about passing opaque
structs.
### The char Problem
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.
The solution is to use the Rust `u32` type for passing. Under this scheme:
- The function signature uses `u32` as the parameter type.
- For input parameters, use `char::try_from` to perform a fallible conversion that
validates the incoming character.
- For output parameters, use `u32::from` for the conversion.
@@ -0,0 +1,72 @@
# C/C++ Headers
Under this section we discuss how to write the C/C++ header files that accompany the
Rust FFI library. The Rust-side counterpart of each topic is covered in
[FFI Function Signatures](ffi-function-signatures.md) and
[Allowed Data Types](allowed-data-types.md).
## Function Declaration Style
FFI functions must be exported under C names, that is, they must not go through the
C++ name mangling mechanism. In the generated C header file, you need to use the
`extern "C"` specifier, the `__cplusplus` macro and macro conditionals to import the
declarations correctly, for example:
```c++
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
CError WFStartup(void);
// More FFI functions omitted...
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
```
## Required Header Files
The generated C/C++ header files must include the appropriate header files for the
types used in the declarations.
- In C, the `bool` type is provided by `<stddef.h>`. C++ supports `bool` natively
and does not need any header file.
- In C, the integer types are provided by `<stdint.h>`. In C++, they are provided
by `<cstdint>`.
- In C, the `uintptr_t` and `intptr_t` types are provided by `<stdint.h>`. In C++,
they are provided by `<cstdint>`.
- The floating-point types do not require any header file.
## Type Correspondences
The following table shows the correspondence between the Rust types that can cross
the FFI boundary directly (listed in [Allowed Data Types](allowed-data-types.md))
and their C/C++ counterparts. The left column lists the Rust type and the right
column lists the corresponding C/C++ type.
| Rust | C/C++ |
|------|-------|
| `bool` | `bool` |
| `f32` | `float` |
| `f64` | `double` |
| `i8` | `int8_t` |
| `i16` | `int16_t` |
| `i32` | `int32_t` |
| `i64` | `int64_t` |
| `u8` | `uint8_t` |
| `u16` | `uint16_t` |
| `u32` | `uint32_t` |
| `u64` | `uint64_t` |
| `usize` | `uintptr_t` |
| `isize` | `intptr_t` |
## The char Problem
For the Rust `char` type, the C/C++ header side uses `char32_t` as the parameter
type. Note that `char32_t` can hold any 32-bit unsigned integer, which is broader
than the set of valid Unicode scalar values carried by the Rust `char` type. The
Rust side is responsible for validating the incoming value; see
[The char Problem](allowed-data-types.md#the-char-problem) in
[Allowed Data Types](allowed-data-types.md).
@@ -0,0 +1,94 @@
# FFI Function Signatures
Under this section we discuss the signature requirements of the exported FFI
functions.
## C-style Name Export
FFI functions must be exported under C names, that is, they must not go through the
C++ name mangling mechanism.
In Rust, you need to decorate the function with the `#[unsafe(no_mangle)]` attribute
together with `extern "C"`, for example:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn WFStartup() -> CError {
// Function body omitted...
}
// More FFI functions omitted...
```
## Export Name Style
In this subsection, we discuss the naming style of exported functions.
In this design, there are exactly two styles to choose from. We call them the
Windows style and the POSIX style. Either style can be chosen freely, but within a
single library the two styles must not be mixed.
We recommend the following selection rule: if your library is Windows-only, choose
the Windows style; otherwise, choose the POSIX style.
### Windows Style
The Windows style format is `<PREFIX><FUNC>` for ordinary functions, and
`<PREFIX><STRUCT><FUNC>` for opaque struct functions.
In the format, `<STRUCT>` is the name of the opaque struct and `<FUNC>` is the name
of the function. Both of them must use PascalCase naming.
For `<PREFIX>`, it is a prefix identifier that is specific to this library. As we
all know, C has no namespace concept, and all functions are exported into the same
space. Therefore, avoiding name collisions between functions is an important task.
By prepending `<PREFIX>` to the function name, name duplication can be avoided as
much as possible. For this reason, you need to choose `<PREFIX>` carefully and try
to pick a prefix that is unlikely to collide.
In the Windows style, there are two spellings for `<PREFIX>`. One is the
all-uppercase style and the other is the style in which only the first letter is
uppercase and all remaining letters are lowercase. Choose according to your own
needs. In addition, the length of `<PREFIX>` must not exceed 5 characters, and
it must not contain anything other than letters and digits (that is, underscores are
not allowed).
The following examples show some legal and illegal Windows style function names:
| Name | Verdict | Reason |
|------|---------|--------|
| `WFStartup` | Legal | An ordinary (non opaque struct) function. |
| `WFIconGetHandle` | Legal | An opaque struct function whose `<STRUCT>` is `Icon`. |
| `WFWordHandleGetHandle` | Legal | An opaque struct function whose `<STRUCT>` is `WordHandle`. |
| `FlGetMessage` | Legal | Uses a `<PREFIX>`, which is `Fl`, that capitalizes only its first letter. |
| `MyRustGetMessage` | Illegal | `<PREFIX>`, which is `MyRust`, does not keep all remaining letters lowercase. |
| `My_RustGetMessage` | Illegal | `<PREFIX>` contains an underscore. |
| `IconGetHandle` | Illegal | Missing `<PREFIX>`. |
| `QwertyuiopGetData` | Illegal | `<PREFIX>`, which is `Qwertyuiop`, is too long. |
| `WFget_message` | Illegal | `<FUNC>`, which is `get_message`, is not PascalCase. |
| `WFicon_handleGetMessage` | Illegal | `<STRUCT>`, which is `icon_handle`, is not PascalCase. |
### POSIX Style
The POSIX style format is `<PREFIX>_<FUNC>` for ordinary functions, and
`<PREFIX>_<STRUCT>_<FUNC>` for opaque struct functions.
The `<PREFIX>`, `<STRUCT>` and `<FUNC>` components in the format have the same
meaning as in the Windows style, but the format is different. `<STRUCT>` and `<FUNC>`
must use snake_case naming. `<PREFIX>` must be all lowercase, and it must not contain
anything other than letters and digits (that is, underscores are not allowed). Its
length and selection requirements are the same as in the Windows style.
The following examples show some legal and illegal POSIX style function names:
| Name | Verdict | Reason |
|------|---------|--------|
| `wf_startup` | Legal | An ordinary (non opaque struct) function. |
| `wf_icon_get_handle` | Legal | An opaque struct function whose `<STRUCT>` is `icon`. |
| `wf_word_handle_get_handle` | Legal | An opaque struct function whose `<STRUCT>` is `word_handle`. |
| `Fl_get_message` | Illegal | `<PREFIX>`, which is `Fl`, is not all lowercase. |
| `my_rust_get_message` | Illegal | `<PREFIX>`, which is `my_rust`, contains an underscore. |
| `icon_get_handle` | Illegal | Missing `<PREFIX>`. |
| `qwertyuiop_get_data` | Illegal | `<PREFIX>`, which is `qwertyuiop`, is too long. |
| `wf_GetMessage` | Illegal | `<FUNC>`, which is `GetMessage`, is not snake_case. |
| `wf_Icon_get_message` | Illegal | `<STRUCT>`, which is `Icon`, is not snake_case. |
+13
View File
@@ -0,0 +1,13 @@
# C-friendly FFI Design
This document is a guide for authoring Rust libraries that expose a C-friendly FFI
and distributing them as ordinary CMake/pkg-config packages. It covers how to design
C-friendly FFI signatures, how to pass primitive types, enums, strings and
Rust-specific constructs such as `Option` and `Result` across the boundary, and how
to write the accompanying C/C++ header files.
## Contents
- [FFI Function Signatures](ffi-function-signatures.md)
- [Allowed Data Types](allowed-data-types.md)
- [C/C++ Headers](c-cpp-headers.md)