发布 CLI、crate 和版本管理
C 项目交付给别人用时,问题通常不在“源码能不能编译”,而在这些细节:
工具怎么安装到 PATH
库文件和头文件放在哪里
用户该链接哪个版本
升级会不会破坏已有代码
公开 API 改了算不算兼容
二进制 ABI 能不能长期稳定
Rust 也要处理这些问题。只是发布对象分成了两类:
阅读更多C 项目交付给别人用时,问题通常不在“源码能不能编译”,而在这些细节:
工具怎么安装到 PATH
库文件和头文件放在哪里
用户该链接哪个版本
升级会不会破坏已有代码
公开 API 改了算不算兼容
二进制 ABI 能不能长期稳定
Rust 也要处理这些问题。只是发布对象分成了两类:
阅读更多C 项目交付前,通常不会只看“能编译”。
还要看:
是不是 release 优化
目标架构和 ABI 对不对
链接的是目标系统的库还是宿主机库
二进制有没有带多余符号
动态库依赖目标系统有没有
体积是否符合设备限制
崩溃后还能不能符号化
Rust 交付 CLI、系统库或 C/Rust 边界产物时,也要回答这些问题。Cargo 简化了构建命令,但没有取消交付检查。
阅读更多C 项目长大以后,通常会出现这些东西:
多个静态库和命令行工具
公共头文件和内部头文件
Makefile 子目录
CONFIG_XXX / #ifdef 开关
Debug/Release 编译选项
目标平台差异
Rust 项目也会遇到同样的问题,只是工具名字不同:
workspace -> 管多个 crate
crate -> 一个库或一个可执行目标的边界
feature -> 可选能力和可选依赖
profile -> debug/release 等构建参数
cfg -> 平台和条件编译
这一篇不讲 Cargo 全部语法,只讲写库、CLI 和 C/Rust 边界时最常用的工程组织方式。
阅读更多写 FFI 时,声明 C 函数只是第一步:
unsafe extern "C" {
fn sensor_read(out: *mut u8, len: usize) -> i32;
}
这只告诉 Rust:有一个 C ABI 函数叫 sensor_read。
但链接器还要知道:
这个函数在哪个库里?
库文件在哪个目录?
还需要链接哪些依赖库?
交叉编译时用的是目标系统的库,还是宿主机的库?
C 项目里这些通常由 Makefile、CMake、pkg-config、sysroot 和交叉工具链处理。Rust 项目里,对应工具是 #[link]、build.rs 和 build dependency。
前面几篇讲的是函数级 FFI:extern "C"、字符串、内存归属、回调和 void *ctx。
真正交付给 C 项目时,还要回答另一个问题:
C 工程到底链接什么文件?
头文件声明哪些符号?
这些符号的 ABI 能不能长期稳定?
升级 Rust 代码后,C 侧要不要改?
C 开发者熟悉 .a、.so、.h 和 ABI。Rust 交付给 C 使用时,也要回到这些概念,而不是把 Cargo 内部模型暴露给 C。
C 里的回调接口通常长这样:
typedef void (*event_cb_t)(void *ctx, int event);
int device_register_callback(struct device *dev, event_cb_t cb, void *ctx);
void device_unregister_callback(struct device *dev);
这个设计很常见:函数指针负责执行,ctx 负责保存调用方环境。
Rust 内部可以用 closure 表达“代码 + 环境”。但跨 C ABI 时,捕获环境的 closure 不能直接变成 C 函数指针。C 只认识固定 ABI 的函数地址和一个 void *ctx。
写 FFI 时,extern "C" 只是入口。
真正容易出事故的地方通常是三类:
C 字符串到底是不是有效的 NUL 结尾字符串
返回的指针由谁分配、由谁释放、用哪个函数释放
错误是整数码、输出参数,还是错误文本
C 代码里这些靠接口文档和团队约定维护。Rust 可以把一部分规则包进类型,但跨语言边界本身仍然要设计清楚。
阅读更多C 里写线程并发,基本工具很直接:
pthread_create(&tid, NULL, worker, ctx);
pthread_mutex_lock(&lock);
/* modify shared state */
pthread_mutex_unlock(&lock);
这些工具能写出可靠系统,但规则主要靠人维护:
ctx 指向的数据能不能跨线程使用Rust 没有让并发自动变简单。它做的是把一部分线程安全要求放进类型系统:
阅读更多C 里写回调,最常见的形状是函数指针加上下文指针:
typedef void (*event_cb_t)(void *ctx, int event);
void register_event(event_cb_t cb, void *ctx);
cb 是要调用的函数,ctx 是调用时带回来的环境。这个模式很成熟,但也有老问题:
ctx 的真实类型靠约定ctx 活多久靠约定Rust 的 closure 解决的是同一个问题:把“要执行的代码”和“它需要的环境”放在一起。
阅读更多C 程序员写遍历,第一反应通常是索引或指针:
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
或者:
for (uint8_t *p = buf; p < buf + len; p++) {
*p ^= key;
}
Rust 当然也能写循环,但更常用的模型是 iterator。它不是“高级语法糖”,而是把遍历过程变成一个可以传递、组合、消费的值。
阅读更多