Skip to content

zeroffa233/rust-unsafe-experiment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

Rust FFI & unsafe 实践:构建安全的 C API 键值存储

本项目是一个用于深度实践 Rust unsafe 编程和 FFI (Foreign Function Interface) 的指导性实验。

项目目标:使用 Rust 构建一个简单的内存字符串键值 (KV) 存储,并将其编译为 C 动态库 (.so/.dll)。我们将为这个库提供一个内存安全、Panic 安全的 C ABI 接口,最后编写一个 C 程序来加载和使用这个 Rust 库。


核心实践知识点

本项目旨在覆盖 Rust unsafe 和 FFI 编程中的所有关键概念:

Rust (作为库提供方)

  • unsafe 编程基础:
    • 解引用裸指针 (Dereferencing raw pointers): *mut T*const T
    • 调用 unsafe 函数: Box::from_raw, CString::from_raw, CStr::from_ptr
    • 访问 static mut: 实现 C 风格的全局错误码。
  • 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 将 Rust String 的所有权转移给 C(表现为 *mut c_char)。
    • 实现 C 端的配套 free 函数,用于释放 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() 函数。

C (作为库消费方)

  • 链接和调用 Rust 编译的动态库。
  • 使用 cbindgen 自动生成的 C 头文件 (.h) 来进行类型安全调用。
  • 正确管理从 Rust 获得的资源(如 KeyValueStore* 指针和 char* 字符串)。
  • 调用 Rust 提供的 destroyfree 函数来防止内存泄漏。

Rust (作为库消费方)

  • 调用 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 可执行文件

API 概览 (C-API)

这是我们的 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);

构建与运行 (Linux / macOS)

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_app

3. 运行 C 程序

我们必须告诉操作系统去哪里查找我们的 .so 动态库。

# (Linux)
export LD_LIBRARY_PATH=./target/debug:$LD_LIBRARY_PATH

# (macOS)
export DYLD_LIBRARY_PATH=./target/debug:$DYLD_LIBRARY_PATH

# 运行
./test_app

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published