repr(C):解决FFI的内存布局差异

这是在开发网关插件的时候遇到的一个问题,场景如下:

  • 插件使用Rust编写,编译为.so动态库
  • 网关加载这个动态库,执行插件逻辑
  • 当新版本的网关加载旧版本的插件后执行插件时,插件内部获取到错误的值,或者程序直接崩溃的情况。

其中插件定义如下:

rust 复制代码
#[async_trait]
pub trait Plugin: Send + Sync {
    /// 插件名称
    fn name(&self) -> &str;
    /// 插件信息
    fn info(&self) -> PluginInfo;
    /// 执行插件
    async fn execute(&self, context: &HttpContext, config: &Value) -> Result<Value, PluginError>;
}

后来在检查插件参数时,发现网关处传入的参数和旧版本插件的接收参数顺序不一致或字段个数不一致,会导致插件内部拿到不正确的值。当传入参数定义和接收参数不一致时,会导致崩溃。

其根本原因是:由于传递了一个引用,使得网关和插件访问的是同一个内存地址,然而由于插件编译为cdylib,其内存布局和Rust的不一致,导致在访问变量时,获取到了错误的值,或者访问了非法内存导致程序崩溃(在字段类型不一致时发生)。

Rust在编译结构体时,可能会对字段进行重新排序以及对齐等优化,导致实际的内存布局可能不兼容C风格的布局。因此和外部库交互(也就是FFI)时可能会发生不可预测的行为。

对于网关插件这个场景,要避免这个问题,有以下两种做法:

  • 修改插件参数为可序列化的,如JSON、Protobuf等。
  • 强制结构体使用C兼容的内存布局。

由于插件可能调用多次,需要保证高性能的执行,序列化/反序列化对性能影响较大,且部分字段可能无法序列化,因此只能使用第二种方式。

使用#[repr(C)]便可以兼容C布局。它的主要作用如下:

  • 保证 C 兼容的内存布局:确保 Rust 结构体按照 C 语言的标准进行内存排列。
  • 防止编译器优化重排:阻止 Rust 编译器对字段顺序进行优化调整。

按照Rust官方的说法,#[repr(C)]就是做C语言能做的事。字段的顺序、大小和对齐方式完全符合C或C++的预期。任何通过FFI的类型都应具有#[repr(C)]

除了repr(C)外,常见的还有这几个:

  • repr(transparent):让包装类型与被包装类型的内存布局完全相同,要求包装结构体必须有且仅有一个非零大小的字段。
  • repr(u*) / repr(i*):主要用在枚举上,指定枚举的类型和范围,并保持和C一致的内存布局。Rust默认的枚举类型是isize,指定类型后可少占用点空间。这个只能用于无字段类型的枚举。
相关推荐
mit6.8247 小时前
rust等于C++的最佳实践
rust
初恋叫萱萱15 小时前
基于 Rust 与 DeepSeek 构建高性能 Text-to-SQL 数据库代理服务
数据库·sql·rust
鸿乃江边鸟16 小时前
Spark Datafusion Comet 向量化Rust Native--执行Datafusion计划
大数据·rust·spark·native
鸿乃江边鸟2 天前
Spark Datafusion Comet 向量化Rust Native--创建Datafusion计划
rust·spark·native
咸甜适中2 天前
rust的docx-rs库,自定义docx模版批量分页生成一个docx文档(方便打印)(逐行注释)
rust·办公自动化·docx-rs
Vallelonga2 天前
Rust Option.as_ref() 方法
开发语言·rust
大卫小东(Sheldon)2 天前
GIM 2.0 发布:真正让 AI 提交消息可定制、可控、可项目级优化
git·rust·gim
roamingcode3 天前
我是如何 Vibe Coding,将 AI CLI 工具从 Node.js 迁移到 Rust 并成功发布的
人工智能·rust·node.js·github·claude·github copilot
初恋叫萱萱3 天前
构建高性能生成式AI应用:基于Rust Axum与蓝耘DeepSeek-V3.2大模型服务的全栈开发实战
开发语言·人工智能·rust
superman超哥4 天前
Serde 性能优化的终极武器
开发语言·rust·编程语言·rust serde·serde性能优化·rust开发工具