1. 背景与痛点
在构建 “Rust Core + 移动端 UI"的架构时,我们通常会将项目分为三层:
- Domain/Feature Layer: 纯 Rust 实现的业务逻辑,不依赖任何 FFI 库,保持纯洁性。
- FFI Layer: 负责将 Rust 接口暴露给 Kotlin,通常使用 uniffi。
- Client Layert:移动端平台原生代码。
痛点:双重模型定义(The Dual Model Problem)
为了让 Uniffi 能生成胶水代码,我们需要在结构体上标记(例如#[derive(uniffi::Record)])。但 Domain Layer 不想也不应该引入 uniffi 依赖。于是,我们不得不写两套代码:
- Domain Layer
// domain/src/user.rs
pub struct User { pub id: i64, pub name: String }
- FFI Model (Wrapper)
// ffi/src/adapter.rs
#[derive(uniffi::Record)]
struct FfiUser { pub id: i64, pub name: String }
impl From<User> for FfiUser { ... } // 繁琐的数据转换
这导致了代码冗余、维护成本翻倍以及运行时内存拷贝开销。
2. 解决方案: 条件编译
我们的目标是:One Model, Two Behaviors。同一个结构体,在纯 Rust 环境下是普通的 Struct ,在 FFI 编译环境下自动获得跨语言能力。
我们利用Rust的#[cfg_attr]属性宏来实现这一点。
2.1 架构设计
我们将所有共享的数据模型提取到一个独立的 Crate(例如 shared-model),并为其定义一个可选的 ffi 特性。
目录结构:
workspace/
├── domain-core/ # 业务逻辑 (Pure Rust)
├── android-ffi/ # 适配层
└── shared-model/ # 共享数据模型
├── Cargo.toml
└── src/
├── lib.rs
├── auth.rs # 业务模型
└── primitive.rs # 基础类型
2.2 实现细节
Step 1: 定义 Feature 开关
在shared-model/Cargo.toml中:
[features]
default = []
# 只有当开启 ffi feature 时,才引入 uniffi 依赖
ffi = ["dev:uniffi"]
[dependencies]
serde = { version = "1.0", features = {"derive"} }
uniffi = { version = "0.30", optional = true }
Step 2: 编写双模结构体
在shared-model/src/auth.rs中:
use serde::{Deserialize, Serialize};
// ✨ 核心技巧:
// #[cfg_attr(condition, attributes)]
// 当 feature = "ffi" 激活时,展开为 #[derive(uniffi::Record)]
// 否则,它只是个普通的 Rust 宏,不会产生任何 FFI 依赖。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct UserInfo {
pub id: i64,
pub username: String,
pub avatar_url: Option<String>,
}
// 枚举同理
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "ffi", derive(uniffi::Enum))]
pub enum UserStatus {
Active,
Banned,
}
Step 3: FFI 层的零成本引用
在 android-ffi Crate 中,我们不需要再重新定义 FfiUser,而是直接引用:
# android-ffi/Cargo.toml
[dependencies]
# 显式开启 shared-model 的 ffi 能力
shared-model = { path = "../shared-model", features = ["ffi"] }
// android-ffi/src/lib.rs
use shared_model::auth::UserInfo;
#[uniffi::export]
pub fn get_current_user() -> UserInfo {
// 直接返回业务层对象,无需 into()/from() 转换
// 因为 UserInfo 在此时已经具备了 Record 特性
domain_core::auth::get_cached_user()
}
3. 方案收益
通过这种架构重构,我们获得了显著的提升:
- 单一真理源:模型定义存在于一个文件。修改字段只需修改一次, Android/iOS 侧同步更新。
- 零开销抽象: 消除了 FFI 边界的所有数据转换代码。业务对象直接“流入” FFI 管道,没有内存拷贝,没有 CPU 消耗。
- 架构清晰度:
Domain Layer保持了 100% 的平台无关性(不知道 FFI 存在)。FFI Layer退化为极薄的 API 暴露层, 符合 “Humble Object” 模式。
不仅代码量减少了 50%,更重要的是,我们利用 Rust 编译时特性,完美平衡了架构的整洁性与实用性。