Skip to content

Rust FFI完整项目:C宿主与同步回调 ​

示例未执行。本页源码与仓库工程同源,不运行Cargo、Rust/C编译器或CMake;静态模板与配置解析不证明工程可编译。先读ABI与所有权两页,再按README未来自行验证。

工程输入与依赖 ​

Rust FFI宿主工程输入固定在C源文件,依赖只有Rust标准库、C11工具链和CMake;没有第三方crate或服务。标签采用UTF-8字节和长度,delta为有符号64位整数,返回码用int32_t。C宿主创建句柄并调用Rust,Rust在成功提交后调用C函数,形成真实双向调用而非输出模拟。

本工程不是通用插件加载器;CMake进行链接,不使用运行时查找符号。源码包含Rust库、C头文件、C宿主、构建配置和说明。输入和预期无需下载额外资源。MSVC和Unix配置差异见同源README,不承诺GNU Windows导入库被覆盖。

创建调用销毁流程 ​

Rust FFI宿主的创建调用销毁流程是counter_new分配、counter_add借用、counter_free释放;Host上下文属于C栈帧,回调属于同步调用。句柄成功创建后C保存地址但不解引用;最后销毁并置null。库必须在句柄与回调都结束后才能卸载,置null只保护这一个变量,不自动清除别名。

头文件为两端共同契约,Rust函数的Safety注释说明同样的前提。接口仅能校验null/长度上界/编码/算术;C保证存储、对齐、唯一释放和无重入。宿主用显式条件检查返回值,不依赖可能被NDEBUG关闭的assert。

错误用例规格 ​

Rust FFI宿主的错误用例规格覆盖空输出槽、空句柄、空指针非零长度、非法UTF-8和算术溢出;错误码分别为ARGUMENT、UTF8和OVERFLOW。7加INT64_MAX必须失败,随后减2的回调总和应为5,证明已定义的输入错误没有修改状态。不会执行悬垂指针或二次释放来“测试校验”,这些属于UB。

成功情况下恰有两次回调;第二次标签为空但长度零,宿主不调用fwrite读取空缓冲区。未提供callback的合法调用可更新总和而不通知,context解释权属于指定的回调;本例错误路径只用null回调避免不必要通知。PANIC分支没有人为触发入口,不能根据这些规格宣称捕获行为经过运行验证。

构建部署与预期 ​

Rust FFI宿主的构建部署预期要求Rust与C使用相同架构,先生成cdylib再构建C宿主;动态加载阶段还需要库可被找到。README给出PowerShell和Bash命令及逐项预期;此处“预期”只是规格,没有实测日志。Windows复制DLL到exe旁,Unix构建设置RPATH,移动部署仍需打包库并重新检查搜索路径。

恢复时,对参数或编码错误修正输入后重试,对panic停止复用句柄;关闭时先结束调用再释放。当前没有后台线程、注册表和持久数据。staticlib产物供另行集成研究,CMake不假装同时完成所有目标的系统静态链接。

完整同源文件 ​

Cargo.toml ​

toml
[package]
name = "ffi_counter"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib", "staticlib"]

[profile.release]
panic = "unwind"

src/lib.rs ​

rust
//! C callers must satisfy ffi_counter.h, including pointer provenance and no reentry.
use std::ffi::c_void;
use std::panic::catch_unwind;

pub struct Counter { total: i64 }
type Callback = unsafe extern "C" fn(*mut c_void, *const u8, usize, i64);
const OK: i32 = 0;
const ARGUMENT: i32 = 1;
const UTF8: i32 = 2;
const OVERFLOW: i32 = 3;
const PANIC: i32 = 4;

fn boundary(f: impl FnOnce() -> i32 + std::panic::UnwindSafe) -> i32 {
    match catch_unwind(f) {
        Ok(status) => status,
        // A custom panic payload can panic in Drop. Never drop it across C.
        Err(payload) => { std::mem::forget(payload); PANIC }
    }
}

/// # Safety
/// out must point to a writable aligned pointer slot, disjoint from live objects.
#[no_mangle]
pub unsafe extern "C" fn counter_new(out: *mut *mut Counter) -> i32 {
    boundary(|| {
        if out.is_null() { return ARGUMENT; }
        unsafe { out.write(std::ptr::null_mut()); }
        let counter = Box::new(Counter { total: 0 });
        unsafe { out.write(Box::into_raw(counter)); }
        OK
    })
}

/// # Safety
/// handle must be live, unique for this call; label is readable for len bytes
/// in one allocation (or null with len 0). Callback/context follow header rules.
#[no_mangle]
pub unsafe extern "C" fn counter_add(
    handle: *mut Counter, label: *const u8, len: usize, delta: i64,
    callback: Option<Callback>, context: *mut c_void,
) -> i32 {
    boundary(|| {
        if handle.is_null() || (label.is_null() && len != 0) || len > isize::MAX as usize {
            return ARGUMENT;
        }
        // Empty slices do not dereference label, and from_raw_parts requires non-null.
        let bytes = if len == 0 { &[][..] } else {
            unsafe { std::slice::from_raw_parts(label, len) }
        };
        if std::str::from_utf8(bytes).is_err() { return UTF8; }
        let next = match unsafe { (*handle).total }.checked_add(delta) {
            Some(value) => value,
            None => return OVERFLOW,
        };
        unsafe { (*handle).total = next; }
        if let Some(notify) = callback {
            // Synchronous borrow only; never store callback/context/bytes.
            unsafe { notify(context, bytes.as_ptr(), bytes.len(), next); }
        }
        OK
    })
}

/// # Safety
/// handle must be null or the original live pointer from counter_new, freed once.
#[no_mangle]
pub unsafe extern "C" fn counter_free(handle: *mut Counter) -> i32 {
    boundary(|| {
        if !handle.is_null() { unsafe { drop(Box::from_raw(handle)); } }
        OK
    })
}

include/ffi_counter.h ​

c
#ifndef FFI_COUNTER_H
#define FFI_COUNTER_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Counter Counter;
enum { COUNTER_OK=0, COUNTER_ARGUMENT=1, COUNTER_UTF8=2,
       COUNTER_OVERFLOW=3, COUNTER_PANIC=4 };
/* cdecl / platform C ABI; no exceptions, longjmp, or same-handle reentry.
 * label is borrowed for this callback only; copy it to retain it.
 * context is owned by C, must be valid for callback's own requirements,
 * and must not overlap handle storage. No concurrent access/free of the handle.
 * Callback runs on caller thread, synchronously; it is never retained. */
typedef void (*counter_callback)(void *context, const uint8_t *label,
                                 size_t len, int64_t total);
/* out: valid aligned writable slot, not an alias of any live counter/buffer.
 * Success transfers one Rust allocation to C as an opaque handle.
 * Allocation failure can abort the process; no OOM status is promised. */
int32_t counter_new(Counter **out);
/* handle: original live pointer, exclusive until return, no concurrent calls.
 * label: one readable allocation of len bytes, disjoint from handle state;
 * null is accepted only with len=0; len <= ISIZE_MAX on the Rust target.
 * label encoding: UTF-8, embedded zero accepted. callback may be NULL.
 * ARGUMENT/UTF8/OVERFLOW: state unchanged, callback not called.
 * PANIC: discard handle via counter_free; do not assume rollback.
 * No function can check dangling pointers, capacity, or double-free. */
int32_t counter_add(Counter *handle, const uint8_t *label, size_t len,
                    int64_t delta, counter_callback callback, void *context);
/* NULL allowed. Otherwise free exactly once using this library instance.
 * After return invalidate all copies; never use free() or unload library first. */
int32_t counter_free(Counter *handle);
#ifdef __cplusplus
}
#endif
#endif

host/main.c ​

c
#include "ffi_counter.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

struct Host { unsigned calls; int64_t last; };
static void on_total(void *context, const uint8_t *label, size_t len, int64_t total) {
    struct Host *host = context;
    host->calls++;
    host->last = total;
    fputs("callback label=", stdout);
    if (len) fwrite(label, 1, len, stdout);
    printf(" total=%" PRId64 "\n", total);
}
static void expect(const char *name, int32_t got, int32_t wanted) {
    printf("%s status=%" PRId32 "\n", name, got);
    if (got != wanted) exit(EXIT_FAILURE);
}
int main(void) {
    Counter *counter = NULL;
    struct Host host = {0, 0};
    const uint8_t first[] = {'f','i','r','s','t'};
    const uint8_t invalid[] = {0xff};
    expect("new-null", counter_new(NULL), COUNTER_ARGUMENT);
    expect("new", counter_new(&counter), COUNTER_OK);
    expect("add", counter_add(counter, first, sizeof first, 7, on_total, &host), COUNTER_OK);
    expect("null-handle", counter_add(NULL, NULL, 0, 0, NULL, NULL), COUNTER_ARGUMENT);
    expect("null-buffer", counter_add(counter, NULL, 1, 0, NULL, NULL), COUNTER_ARGUMENT);
    expect("bad-utf8", counter_add(counter, invalid, sizeof invalid, 0, NULL, NULL), COUNTER_UTF8);
    expect("overflow", counter_add(counter, NULL, 0, INT64_MAX, NULL, NULL), COUNTER_OVERFLOW);
    expect("empty", counter_add(counter, NULL, 0, -2, on_total, &host), COUNTER_OK);
    if (host.calls != 2 || host.last != 5) return EXIT_FAILURE;
    expect("free", counter_free(counter), COUNTER_OK);
    counter = NULL;
    expect("free-null", counter_free(counter), COUNTER_OK);
    puts("host contract cases complete");
    return EXIT_SUCCESS;
}

CMakeLists.txt ​

cmake
cmake_minimum_required(VERSION 3.20)
project(ffi_counter_host LANGUAGES C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(RUST_TARGET_DIR "${CMAKE_CURRENT_SOURCE_DIR}/target/release" CACHE PATH "Cargo release artifacts")
add_library(ffi_counter SHARED IMPORTED)
if(WIN32)
  set_target_properties(ffi_counter PROPERTIES
    IMPORTED_LOCATION "${RUST_TARGET_DIR}/ffi_counter.dll"
    IMPORTED_IMPLIB "${RUST_TARGET_DIR}/ffi_counter.dll.lib")
elseif(APPLE)
  set_target_properties(ffi_counter PROPERTIES IMPORTED_LOCATION "${RUST_TARGET_DIR}/libffi_counter.dylib")
else()
  set_target_properties(ffi_counter PROPERTIES IMPORTED_LOCATION "${RUST_TARGET_DIR}/libffi_counter.so")
endif()
add_executable(ffi_host host/main.c)
target_include_directories(ffi_host PRIVATE include)
target_link_libraries(ffi_host PRIVATE ffi_counter)
if(WIN32)
  add_custom_command(TARGET ffi_host POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
    "$<TARGET_FILE:ffi_counter>" "$<TARGET_FILE_DIR:ffi_host>")
else()
  set_target_properties(ffi_host PROPERTIES BUILD_RPATH "${RUST_TARGET_DIR}")
endif()

README.md ​

md
# Rust计数器与C宿主

示例未执行。未运行Cargo、Rust/C编译器、CMake或宿主;下列命令和输出是供读者以后自行验证的规格,不能视为已编译或运行证据。

依赖:Rust支持edition 2021的stable工具链(无第三方crate),CMake >=3.20、C11编译器及其系统链接器。建议同一主机目标架构。Windows命令假设x64 MSVC Rust工具链与Visual Studio C++开发工具,MSVC Developer PowerShell;GNU Windows导入库名称不同,不由此CMake配置覆盖。Linux使用本机C编译器;macOS使用Xcode Command Line Tools。未验证这些平台的真实兼容性,不安装依赖。

所有输入均在host/main.c:ASCII标签first、字节0xff、空标签、7/-2/INT64_MAX。没有命令行参数、网络、文件或环境机密。Cargo.toml定义cdylib和staticlib;本宿主只动态链接cdylib。没有build.rs、外部包或伪造Cargo.lock。全部文件:Cargo.toml、src/lib.rs、include/ffi_counter.h、host/main.c、CMakeLists.txt、README.md。

## 读者自跑步骤(本轮未执行)

在本目录,Bash(Linux/macOS)执行:

```bash
cargo build --release
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
./build/ffi_host
```

Windows MSVC Developer PowerShell执行:

```powershell
cargo build --release
cmake -S . -B build -A x64
cmake --build build --config Release
.\build\Release\ffi_host.exe
```

若Rust配置了target-dir或交叉目标,需给CMake传绝对的`-DRUST_TARGET_DIR=...`并保证C工具链同架构;默认只覆盖本机target/release。Windows后构建把DLL复制到宿主旁;Unix构建RPATH指向Rust产物目录,移动部署须重新设计安装RPATH并打包库。staticlib不是此CMake的备选开关;静态接入还需按Rust目标所需系统库单独设置链接,不能简单改后缀。

## 预期结果(不是实测日志)

new-null返回1;new返回0;首次回调label=first total=7;add返回0。null-handle和null-buffer返回1,bad-utf8返回2,overflow返回3;这些分支不回调且总和仍为7。empty回调空标签total=5并返回0;两次回调后free/free-null均0,最后打印host contract cases complete,退出0。expect检测到不符会退出失败;回调总数和最后值也必须匹配。宿主在返回前检查结果,不能靠NDEBUG禁用的assert验证。

## 生命周期与恢复

Counter只能由Rust分配和释放,C不能解引用、复制其结构、free或重复释放。host上下文栈对象在counter_add返回前存在,回调只同步借用;Rust从不保存回调或context,因而没有后台注销阶段。回调禁止同句柄重入/销毁、C++异常、longjmp或跨边界展开;若要异步回调必须另设计停止、等待、注销协议。label的UTF-8验证不验证地址可读性,合法指针和长度由C保证;不能把悬垂指针当负例执行。

参数/编码/溢出错误可修正输入重试;PANIC=4时不保证回滚,停止使用并销毁句柄。catch_unwind仅捕获unwind,不捕获OOM abort、越界原始指针UB或进程终止;panic hook仍可能输出。本工程不提供故意panic入口,PANIC分支仅静态审阅。没有持久数据;清理是销毁句柄后再卸载库,按需删除本工程build和target产物。本轮没有产生这些产物。

综合练习 ​

在不修改ABI的情况下,给C宿主新增合法的内嵌零标签和无回调更新,列出它们对总和及回调次数的影响,再解释如何捕获保留标签。

参考解答

使用const uint8_t label[] = {'a', 0, 'b'}及sizeof label,零是有效UTF-8,不会截断到a;其增量会提交且有回调。另一次callback=NULL的成功更新会改变总和但不增加次数。若C想在函数返回后查看标签,必须在回调期间分配并复制len字节,另存长度并自行释放;不能只保存Rust传来的地址。预期值随新增delta计算,不能沿用原宿主最后值5。

继续阅读