From 40fee1819454aae2d0f3c73a758fd8b58e49ff97 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 14 Aug 2026 16:38:09 +0800 Subject: [PATCH] doc: update ffi design --- .../c-cpp-side/c-header.md | 19 ------ .../c-cpp-side/cpp-header.md | 4 -- .../c-cpp-side/docstring.md | 4 +- .../c-friendly-ffi-design/c-cpp-side/misc.md | 46 ++++++++++++++ assets/c-friendly-ffi-design/index.md | 1 + .../rust-side/data-types.md | 2 +- .../rust-side/func-signature.md | 4 +- .../rust-side/library-architecture.md | 63 +++++++++++++++++++ .../c-friendly-ffi-design/rust-side/misc.md | 29 +++++++++ 9 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 assets/c-friendly-ffi-design/rust-side/library-architecture.md diff --git a/assets/c-friendly-ffi-design/c-cpp-side/c-header.md b/assets/c-friendly-ffi-design/c-cpp-side/c-header.md index 2bb9029..093cf95 100644 --- a/assets/c-friendly-ffi-design/c-cpp-side/c-header.md +++ b/assets/c-friendly-ffi-design/c-cpp-side/c-header.md @@ -5,25 +5,6 @@ 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). -## Include Guard - -When writing the include guard of a C header file, we require using both the modern `#pragma once` approach and the traditional approach based on the `#ifndef`, `#define` and `#endif` preprocessing directives. The file should be written like this: - -```c -#pragma once -#ifndef SOME_HEADER_H_ -#define SOME_HEADER_H_ - -// The actual content of the header file... - -#endif // SOME_HEADER_H_ -``` - -The `SOME_HEADER` prefix is usually related to the name of the header file. It must consist of all uppercase characters, but underscores are allowed. The `_H_` suffix is fixed and cannot be changed; this suffix style originates from the Linux kernel. - -For example, when the header file is named `wfassoc.h`, the macro name would be `WFASSOC_H_`. -However, if the name of the header file is too simple and may collide with other header files, it would be better to prepend something to the prefix to remove this ambiguity. For example, for a project called MyRust with a header file named `utils.h`, a good macro name would be `MY_RUST_UTILS_H_`. - ## Function Declaration Style FFI functions must be exported under C names, that is, they must not go through the diff --git a/assets/c-friendly-ffi-design/c-cpp-side/cpp-header.md b/assets/c-friendly-ffi-design/c-cpp-side/cpp-header.md index dc60ceb..5054153 100644 --- a/assets/c-friendly-ffi-design/c-cpp-side/cpp-header.md +++ b/assets/c-friendly-ffi-design/c-cpp-side/cpp-header.md @@ -1,6 +1,2 @@ # C++ Header -## Include Guard - -The include guard requirement in the C++ header files is identical to the one in the C header files. - diff --git a/assets/c-friendly-ffi-design/c-cpp-side/docstring.md b/assets/c-friendly-ffi-design/c-cpp-side/docstring.md index 6bd7a25..27982ce 100644 --- a/assets/c-friendly-ffi-design/c-cpp-side/docstring.md +++ b/assets/c-friendly-ffi-design/c-cpp-side/docstring.md @@ -78,7 +78,7 @@ An example is shown below: * @param[out] out_c Example output parameter. * @return Example return value. */ -int FlFooBar(OMRF_IN(int) in_a, OMRF_IN(int) in_b, OMRF_OUT(int) out_c); +int FlFooBar(OMRF_IN_PARAM_TY(int) in_a, OMRF_IN_PARAM_TY(int) in_b, OMRF_OUT_PARAM_TY(int) out_c); ``` ### Object Docstring @@ -352,7 +352,7 @@ When the following situations occur, additional requirements apply to the corres When a parameter carries a pointer, the `@param` annotation of that parameter must state whether the pointer could be NULL (for output) or whether passing NULL is allowed (for input). -Note that the pointer mentioned here refers to the case where the type decorated by `OMRF_IN` or `OMRF_OUT` is itself a pointer type. Since every output parameter is passed by pointer, the pointer mentioned above does not refer to that pointer created by the output decoration. +Note that the pointer mentioned here refers to the case where the type decorated by `OMRF_IN_PARAM_TY` or `OMRF_OUT_PARAM_TY` is itself a pointer type. Since every output parameter is passed by pointer, the pointer mentioned above does not refer to that pointer created by the output decoration. ### String Lifetime diff --git a/assets/c-friendly-ffi-design/c-cpp-side/misc.md b/assets/c-friendly-ffi-design/c-cpp-side/misc.md index 8416470..fe2c51f 100644 --- a/assets/c-friendly-ffi-design/c-cpp-side/misc.md +++ b/assets/c-friendly-ffi-design/c-cpp-side/misc.md @@ -1 +1,47 @@ # Miscellaneous Stuff + +## Include Guard + +When writing the include guard of a C/C++ header file, we require using both the modern `#pragma once` approach and the traditional approach based on the `#ifndef`, `#define` and `#endif` preprocessing directives. The file should be written like this: + +```c +#pragma once +#ifndef SOME_HEADER_H_ +#define SOME_HEADER_H_ + +// The actual content of the header file... + +#endif // SOME_HEADER_H_ +``` + +The `SOME_HEADER` prefix is usually related to the name of the header file. It must consist of all uppercase characters, but underscores are allowed. The `_H_` suffix is fixed and cannot be changed; this suffix style originates from the Linux kernel. + +For example, when the header file is named `wfassoc.h`, the macro name would be `WFASSOC_H_`. +However, if the name of the header file is too simple and may collide with other header files, it would be better to prepend something to the prefix to remove this ambiguity. For example, for a project called MyRust with a header file named `utils.h`, a good macro name would be `MY_RUST_UTILS_H_`. + +## Block Closing Annotations + +When we use preprocessor directives such as `#if` or `#ifdef` to conditionally enable or disable some code, we require you to annotate the condition after its matching `#else` and `#endif`, as shown below: + +```c +#ifdef __cplusplus +// Contents omitted... +#else // __cplusplus +// Contents omitted... +#endif // __cplusplus +``` + +Besides the preprocessor directives, you also need to write similar closing annotations for `namespace` (C++ only) and `extern "C"` blocks (C++ only), for example: + +```c++ +extern "C" { + // Contents omitted... +} // extern "C" + +namespace foobar { + // Contents omitted... +} // namespace foobar +``` + +Note that the spaces around the `//` comment markers in the examples are required. + diff --git a/assets/c-friendly-ffi-design/index.md b/assets/c-friendly-ffi-design/index.md index e0e1876..d63ddd9 100644 --- a/assets/c-friendly-ffi-design/index.md +++ b/assets/c-friendly-ffi-design/index.md @@ -10,6 +10,7 @@ to write the accompanying C/C++ header files. * [Rust Side](rust-side.md) - [FFI Function Signature](rust-side/func-signature.md) + - [Library Architecture](rust-side/library-architecture.md) - [Allowed Data Types](rust-side/data-types.md) - [FFI Example](rust-side/example.md) - [Miscellaneous Stuff](rust-side/misc.md) diff --git a/assets/c-friendly-ffi-design/rust-side/data-types.md b/assets/c-friendly-ffi-design/rust-side/data-types.md index e716797..7f24aad 100644 --- a/assets/c-friendly-ffi-design/rust-side/data-types.md +++ b/assets/c-friendly-ffi-design/rust-side/data-types.md @@ -3,7 +3,7 @@ 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). +C/C++ header writing chapter. ## Primitive Type Conversion diff --git a/assets/c-friendly-ffi-design/rust-side/func-signature.md b/assets/c-friendly-ffi-design/rust-side/func-signature.md index 0d65e6d..7d74432 100644 --- a/assets/c-friendly-ffi-design/rust-side/func-signature.md +++ b/assets/c-friendly-ffi-design/rust-side/func-signature.md @@ -133,7 +133,7 @@ Input parameters are passed directly by value (for example, integers, floats or pointers). In the example, `in_a` and `in_b` are both of type `u32`. You can use the `in_param_ty!` macro provided by the crate to conveniently mark this. -By convention, input parameters are prefixed with `in_`, but this is not enforced. +By convention, input parameters are prefixed with `in`, but this is not enforced. #### Output Parameters @@ -149,7 +149,7 @@ conveniently dereference an output parameter for assignment, or directly use the need to do that, because in later chapters you will be guided on how to correctly use `cffi_wrapper!` to maximize the efficiency of writing FFI functions. -By convention, output parameters are prefixed with `out_`, but this is not enforced. +By convention, output parameters are prefixed with `out`, but this is not enforced. ### The Return Value diff --git a/assets/c-friendly-ffi-design/rust-side/library-architecture.md b/assets/c-friendly-ffi-design/rust-side/library-architecture.md new file mode 100644 index 0000000..1d32930 --- /dev/null +++ b/assets/c-friendly-ffi-design/rust-side/library-architecture.md @@ -0,0 +1,63 @@ +# Library Architecture + +The functions exported in a library can be roughly divided into two categories: auxiliary functions and functional functions. + +## Auxiliary Functions + +Auxiliary functions do not perform the main functionality of the library. They help the functional functions work properly. + +### Startup/Shutdown Functions + +Startup/shutdown functions are a category of auxiliary functions, as shown in the following example: + +```rust +#[unsafe(no_mangle)] +pub extern "C" fn WFStartup() -> CError { + // Function body omitted... +} + +#[unsafe(no_mangle)] +pub extern "C" fn WFShutdown() -> () { + // Function body omitted... +} +``` + +Here `WF` is the ``, indicating that these are startup/shutdown functions exclusive to this library. Usually the word `startup` is used as the `` of the startup function and the word `shutdown` is used as the `` of the shutdown function. Other names may be chosen according to the preference of the library author. + +These functions usually have no input or output parameters, but this is not mandatory. The library author may add parameters as needed. + +Among these functions, the return value of the startup function is `CError`, and the return value of the shutdown function is `()` (that is, `void` in C/C++). The return value types of these two cannot be changed. The reason for setting the return values this way is: when the startup function runs, it may return some errors to explicitly inform the user that initialization failed, so its return value is `CError`. As for the shutdown function, in order to more closely match the semantics of Rust's `Drop` and C++ destructors, its return value is `()`. + +When implementing these functions on the Rust side and using them in C/C++, you need to guarantee: + +- Any call to a functional function must be after the call to the startup function and before the call to the shutdown function. Otherwise these functional functions must return an error code to indicate this error. +- The startup/shutdown functions can be called repeatedly, but the startup and shutdown functions must appear in pairs. This is similar to Win32 COM's `CoInitialize` and `CoUninitialize`. + +You may use the `library_lifecycle` module provided by the crate to simplify the writing of the startup/shutdown functions. + +Startup/shutdown functions are an optional kind of auxiliary function. If your library will not fail when initializing DLL-level resources, there is no need to add startup/shutdown functions. You can directly use Rust's lazy loading mechanism (for example `LazyLock`) so that the resources are loaded when the DLL is loaded and released when the DLL is released. + +### Error Information Functions + +Error information functions are used to return the error information of the last call. Here is an example: + +```rust +#[unsafe(no_mangle)] +pub extern "C" fn WFGetErrorMessage() -> CStrPtr { + // Function body omitted... +} +``` + +Here `WF` is the ``, indicating that this is the error information function exclusive to this library. Usually the phrase "get last error" or "get error message" is used as the `` of the error information function. Other names may be chosen according to the preference of the library author. + +The return value of the error information function is not `CError` but `CStrPtr`. `CStrPtr` is a type in the `cstr_ffi` module provided by the crate. This is among the few functions in the whole library whose return value is not `CError`. + +When implementing the error information function, you should guarantee that its return value is based on the last call on the current thread rather than the last call on the process. + +You may use the `last_error` module provided by the crate to simplify the writing of the error information function. Furthermore, by properly using `cffi_wrapper!`, you can simplify the writing of the error information function to the greatest extent, without even touching the `last_error` module. + +## Functional Functions + +Functional functions are the main part of the exported functions. They are responsible for actually performing the main functionality of the library. + +Functional functions can be ordinary functions or opaque struct functions, depending on the needs of the function itself. diff --git a/assets/c-friendly-ffi-design/rust-side/misc.md b/assets/c-friendly-ffi-design/rust-side/misc.md index 8416470..463867b 100644 --- a/assets/c-friendly-ffi-design/rust-side/misc.md +++ b/assets/c-friendly-ffi-design/rust-side/misc.md @@ -1 +1,30 @@ # Miscellaneous Stuff + +## Two-Project Workspace Structure + +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 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. + +## Panic Policy + +In the FFI project, we enforce that every panic causes the process to exit immediately, just like executing `std::abort` in C++. So you need to specify the following in the Cargo.toml of the FFI project, so that this feature is enabled only in the production environment (that is, the release mode): + +```toml +[profile.release] +panic = "abort" +``` + +The reason for enabling this feature only in the release mode is that in the debug mode you may need to perform operations such as stack tracing to debug the project, and enabling this feature would make such operations impossible. + +You may think this operation would cause the user's process to crash frequently. But a well-designed library should be able to catch all recoverable errors and try not to trigger any unrecoverable errors. This operation forces developers to design a well-functioning library and eliminate potential errors during the development stage. + +## Artifact Type + +The build artifact of the FFI project should meet the requirements of the C language FFI. So you need to specify the following in the Cargo.toml of the FFI project to set the produced artifact type: + +```toml +[lib] +crate-type = ["cdylib"] +```