本项目是一个用于深度实践 Rust unsafe 编程和 FFI (Foreign Function Interface) 的指导性实验。
项目目标:使用 Rust 构建一个简单的内存字符串键值 (KV) 存储,并将其编译为 C 动态库 (.so/.dll)。我们将为这个库提供一个内存安全、Panic 安全的 C ABI 接口,最后编写一个 C 程序来加载和使用这个 Rust 库。
本项目旨在覆盖 Rust unsafe 和 FFI 编程中的所有关键概念:
unsafe编程基础:- 解引用裸指针 (Dereferencing raw pointers):
*mut T和*const T。 - 调用
unsafe函数:Box::from_raw,CString::from_raw,CStr::from_ptr。 - 访问
static mut: 实现 C 风格的全局错误码。
- 解引用裸指针 (Dereferencing raw pointers):
- FFI 接口导出:
#[no_mangle]: 防止函数名混淆,确保 C 链接器可以找到函数。extern "C" fn: 使用 C ABI(调用约定)来编译函数。#[repr(C)]: 为enum(错误码)和struct(如果需要)指定 C 兼容的内存布局。
- 跨语言资源管理 (Opaque Pointers):
- 使用
Box::into_raw将 Rust 堆上对象的所有权转移给 C(表现为void*)。 - 使用
Box::from_raw从 C 接收所有权并安全地销毁 Rust 对象。
- 使用
- 跨语言字符串处理:
- C -> Rust: 使用
std::ffi::CStr安全地借用 C 传入的*const c_char。 - Rust -> C: 使用
std::ffi::CString::into_raw将 RustString的所有权转移给 C(表现为*mut c_char)。 - 实现 C 端的配套
free函数,用于释放 Rust 分配的字符串。
- C -> Rust: 使用
- Panic 安全性:
- 使用
std::panic::catch_unwind包装所有导出的 C API 函数,确保 Rust 的panic绝不会跨越 FFI 边界(这会导致未定义行为)。
- 使用
- 错误处理:
- 使用
#[repr(C)] enum定义 C 兼容的状态码(如Ok,NullArg,NotFound)。 - 通过
static mut LAST_ERROR_CODE提供一个 C 风格的get_last_error()函数。
- 使用
- 链接和调用 Rust 编译的动态库。
- 使用
cbindgen自动生成的 C 头文件 (.h) 来进行类型安全调用。 - 正确管理从 Rust 获得的资源(如
KeyValueStore*指针和char*字符串)。 - 调用 Rust 提供的
destroy和free函数来防止内存泄漏。
- 调用 C 函数:
- 使用
extern "C" { ... }块声明外部 C 函数(例如libc::printf)。 - 在
unsafe块中调用 C 函数,用于在 Rust 库内部进行日志记录。
- 使用
.
├── Cargo.toml # 项目配置,包含 [lib] crate-type = ["cdylib"]
├── build.rs # 构建脚本,使用 cbindgen 自动生成 C 头文件
├── main.c # C 测试程序,用于调用 Rust 库
├── rust_kv_store.h # [自动生成] C 头文件,由 build.rs 生成
├── src
│ └── lib.rs # Rust 库的 FFI 接口和 `unsafe` 实现
└── target
└── debug
├── librust_kv_store.so # (或 .dll) 编译好的 Rust 动态库
└── test_app # [自动生成] 编译好的 C 可执行文件
这是我们的 Rust 库需要暴露给 C 语言的函数接口:
/* ------------------------------------------------- */
/* 类型定义 */
/* ------------------------------------------------- */
/**
* C 兼容的 API 返回状态码
*/
typedef enum Status {
Ok = 0,
NullArg = -1,
InvalidUtf8 = -2,
GetErrorNotFound = -3,
Panic = -100,
} Status;
/**
* KeyValueStore 的不透明指针。
* C 代码只持有这个指针,从不访问其内部。
*/
typedef struct KeyValueStore KeyValueStore;
/* ------------------------------------------------- */
/* 核心 API */
/* ------------------------------------------------- */
/**
* 创建一个新的 KeyValueStore 实例。
*
* @return 返回一个指向新 Store 的不透明指针。
* 如果创建失败(极少情况),可能返回 NULL。
*/
KeyValueStore *kv_store_create(void);
/**
* 销毁由 `kv_store_create` 创建的 Store 实例。
*
* @param store_ptr 必须是一个有效的、由 `kv_store_create` 返回的指针。
* 如果传入 NULL,此函数安全地不执行任何操作。
*/
void kv_store_destroy(KeyValueStore *store_ptr);
/**
* 向 Store 中插入一个键值对。
* 如果 Key 已存在,Value 将被覆盖。
*
* @param store_ptr 指向 Store 实例的指针。
* @param key_ptr 指向 C 字符串 (UTF-8, null-terminated) 的指针。
* @param value_ptr 指向 C 字符串 (UTF-8, null-terminated) 的指针。
* @return 操作状态码 (Status::Ok 表示成功)。
*/
Status kv_store_put(KeyValueStore *store_ptr,
const char *key_ptr,
const char *value_ptr);
/**
* 从 Store 中获取一个值。
*
* @param store_ptr 指向 Store 实例的指针。
* @param key_ptr 指向 C 字符串 (UTF-8, null-terminated) 的指针。
* @return
* - 成功: 返回一个 *新的*、*堆分配* 的 C 字符串 (char*)。
* 调用者 *必须* 在使用完毕后调用 `kv_store_free_string` 来释放它。
* - 失败 (如未找到): 返回 NULL。
*/
char *kv_store_get(KeyValueStore *store_ptr, const char *key_ptr);
/**
* 释放由 `kv_store_get` 分配的字符串。
*
* @param s_ptr 必须是一个由 `kv_store_get` 返回的指针。
* 如果传入 NULL,此函数安全地不执行任何操作。
*/
void kv_store_free_string(char *s_ptr);
/**
* 获取上一次 FFI 调用中发生的错误码。
*
* @return C 兼容的 Status 枚举值。
*/
Status kv_store_get_last_error(void);1. 编译 Rust 库 & 生成 C 头文件
cargo build这将在 target/debug/ 目录下生成 librust_kv_store.so (或 .dylib),并在根目录生成 rust_kv_store.h。
2. 编译 C 测试程序
# gcc [C源文件] -I [头文件目录] -L [库文件目录] -l [库名] -o [输出程序名]
gcc main.c \
-I . \
-L ./target/debug \
-l rust_kv_store \
-o test_app3. 运行 C 程序
我们必须告诉操作系统去哪里查找我们的 .so 动态库。
# (Linux)
export LD_LIBRARY_PATH=./target/debug:$LD_LIBRARY_PATH
# (macOS)
export DYLD_LIBRARY_PATH=./target/debug:$DYLD_LIBRARY_PATH
# 运行
./test_app