12 KiB
WASM 插件系统设计 — 可行性分析与原型验证计划
基于
docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md的全面审查
1. 总体评估
结论:技术可行,但需要对设计做重要调整。
设计方案的核心思路(WASM 沙箱 + 宿主代理 API + 配置驱动 UI)是正确的架构方向。Wasmtime v43.x + Component Model + WASI 0.2/0.3 已达到生产就绪状态。但设计中有 7 个关键问题 和 5 个改进点 需要在实施前解决。
可行性评分:
| 维度 | 评分 | 说明 |
|---|---|---|
| 技术可行性 | 8/10 | Wasmtime 成熟,核心 API 可用;动态表和事务支持需额外封装 |
| 架构兼容性 | 6/10 | 与现有 FromRef 状态模式、静态路由模式存在结构性冲突 |
| 安全性 | 9/10 | WASM 沙箱 + 权限模型 + 租户隔离设计扎实 |
| 前端可行性 | 7/10 | PluginCRUDPage 概念正确但实现细节不足 |
| 实施复杂度 | 高 | 估计 3 个 Phase、6-8 周工作量 |
2. 与现有代码的兼容性分析
2.1 现有架构快照
| 组件 | 现状 | 设计目标 | 差距 |
|---|---|---|---|
| ErpModule trait | 7 个方法(name/version/dependencies/register_event_handlers/on_tenant_created/on_tenant_deleted/as_any) | 15+ 方法(新增 id/module_type/on_startup/on_shutdown/health_check/public_routes/protected_routes/migrations/config_schema) | 重大差距 |
| ModuleRegistry | Arc<Vec<Arc<dyn ErpModule>>>,Builder 模式注册,无路由收集 |
需要 build_routes()、topological_sort()、load_wasm_plugins()、health_check_all() | 重大差距 |
| EventBus | tokio::broadcast,仅有 subscribe()(全量订阅) |
需要 subscribe_filtered() + unsubscribe() | 中等差距 |
| 路由 | main.rs 手动 merge 静态方法 | registry.build_routes() 自动收集 | 中等差距 |
| 状态注入 | FromRef 模式(编译时桥接 AppState → 各模块 State) | WASM 插件需要运行时状态注入 | 结构性冲突 |
| 前端菜单 | MainLayout.tsx 硬编码 3 组菜单 | 从 PluginStore 动态生成 | 中等差距 |
| 前端路由 | App.tsx 静态定义,React.lazy 懒加载 | DynamicRouter 根据插件配置动态生成 | 中等差距 |
2.2 关键差距详解
差距 1:ErpModule trait 路由方法缺失
当前路由不是 trait 的一部分,而是每个模块的关联函数:
// 当前(静态关联函数,非 trait 方法)
impl AuthModule {
pub fn public_routes<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
pub fn protected_routes<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
}
设计将路由提升为 trait 方法,但这引入了泛型参数问题:Router<S> 中的 S 是 AppState 类型,而 trait 不能有泛型方法(会导致 trait 不是 object-safe)。需要设计新的路由注入机制。
差距 2:FromRef 状态模式与 WASM 插件的冲突
当前每个模块有自己的 State 类型(AuthState、ConfigState 等),通过 FromRef 从 AppState 桥接。WASM 插件无法定义编译时的 FromRef 实现,需要运行时状态传递机制。
差距 3:EventBus 缺少类型化订阅
subscribe() 返回 broadcast::Receiver<DomainEvent>,订阅者需要自行过滤。这会导致每个插件都收到所有事件,增加不必要的开销。设计中的 subscribe_filtered() 是必要的扩展。
3. 关键问题(Critical Issues)
C1: 路由注入机制设计不完整
问题: 设计中的 fn public_routes(&self) -> Option<Router> 和 fn protected_routes(&self) -> Option<Router> 缺少泛型参数 S(AppState)。Axum 的 Router 依赖状态类型,而 trait object 不能携带泛型。
影响: 这是路由自动收集的基础。如果无法解决,整个 registry.build_routes() 的设计就不能实现。
建议方案:
// 方案:使用 Router<()>, 由 ModuleRegistry 在 build_routes() 时添加 .with_state()
pub trait ErpModule: Send + Sync {
fn protected_routes(&self) -> Option<Router<()>> { None }
fn public_routes(&self) -> Option<Router<()>> { None }
}
impl ModuleRegistry {
pub fn build_routes(&self, state: AppState) -> (Router, Router) {
let public = self.modules.iter()
.filter_map(|m| m.public_routes())
.fold(Router::new(), |acc, r| acc.merge(r))
.with_state(state.clone());
let protected = self.modules.iter()
.filter_map(|m| m.protected_routes())
.fold(Router::new(), |acc, r| acc.merge(r))
.with_state(state);
(public, protected)
}
}
C2: 事务支持缺失
问题: Host API 每次 db 调用都是独立事务。但 ERP 业务经常需要多步骤原子操作。
建议: 增加声明式事务,插件提交一组操作,宿主在单事务中执行。
C3: 插件间依赖未解决
建议: Phase 7 暂不实现,在 plugin.toml 中预留 plugins = [...] 字段。
C4: 版本号严重过时
建议: 锁定 wasmtime = "43" + wit-bindgen = "0.55"。
C5: 动态表 Schema 迁移策略缺失
建议: 添加 schema_version + 迁移 SQL 机制。
C6: PluginCRUDPage 实现细节不足
建议: 扩展关联数据、主从表、文件上传支持。
C7: 错误传播机制不完整
建议: 定义结构化插件错误协议。
4. 替代方案比较
结论:WASM 方案优于 Lua 脚本和进程外 gRPC。 dylib 因安全性排除。
5. Wasmtime 原型验证计划
5.1 验证目标
验证 Wasmtime Component Model 与 ERP 插件系统核心需求的集成可行性:
| # | 验证项 | 关键问题 |
|---|---|---|
| V1 | WIT 接口定义 + bindgen! 宏 | C4: 版本兼容性 |
| V2 | Host 调用插件导出函数 | init / handle_event 能否正常工作 |
| V3 | 插件调用 Host 导入函数 | db_insert / log_write 能否正常回调 |
| V4 | async 支持 | Host async 函数(数据库操作)能否正确桥接 |
| V5 | Fuel + Epoch 资源限制 | 是否能限制插件 CPU 时间和内存 |
| V6 | 从二进制动态加载 | 从数据库/文件加载 WASM 并实例化 |
5.2 原型项目结构
在 workspace 中创建独立的原型 crate(不影响现有代码):
crates/
erp-plugin-prototype/ ← 新增原型 crate
Cargo.toml
wit/
plugin.wit ← WIT 接口定义
src/
lib.rs ← Host 端:运行时 + Host API 实现
main.rs ← 测试入口:加载插件并调用
tests/
test_plugin_integration.rs ← 集成测试
erp-plugin-test-sample/ ← 新增测试插件 crate
Cargo.toml
src/
lib.rs ← 插件端:实现 Guest trait
5.3 WIT 接口(验证用最小子集)
package erp:plugin;
interface host {
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
log-write: func(level: string, message: string);
}
interface plugin {
init: func() -> result<_, string>;
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
}
world plugin-world {
import host;
export plugin;
}
5.4 Host 端实现(验证要点)
// crates/erp-plugin-prototype/src/lib.rs
use wasmtime::component::*;
use wasmtime::{Config, Engine, Store};
use wasmtime::StoreLimitsBuilder;
// bindgen! 生成类型化绑定
bindgen!({
path: "./wit/plugin.wit",
world: "plugin-world",
async: true, // ← 验证 V4: async 支持
imports: { default: async | trappable },
exports: { default: async },
});
struct HostState {
fuel_consumed: u64,
logs: Vec<(String, String)>,
db_ops: Vec<(String, Vec<u8>)>,
}
// 实现 bindgen 生成的 Host trait
impl Host for HostState {
async fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, String> {
// 模拟数据库操作
self.db_ops.push((entity, data.clone()));
Ok(br#"{"id":"test-uuid","tenant_id":"tenant-1"}"#.to_vec())
}
async fn log_write(&mut self, level: String, message: String) {
self.logs.push((level, message));
}
}
5.5 测试插件
// crates/erp-plugin-test-sample/src/lib.rs
wit_bindgen::generate!({
path: "../../erp-plugin-prototype/wit/plugin.wit",
world: "plugin-world",
});
struct TestPlugin;
impl Guest for TestPlugin {
fn init() -> Result<(), String> {
host::log_write("info", "测试插件初始化成功");
Ok(())
}
fn handle_event(event_type: String, payload: Vec<u8>) -> Result<(), String> {
// 调用 Host API 验证双向通信
let result = host::db_insert("test_entity", br#"{"name":"test"}"#.to_vec())
.map_err(|e| format!("db_insert 失败: {}", e))?;
host::log_write("info", &format!("处理事件 {} 成功", event_type));
Ok(())
}
}
export!(TestPlugin);
5.6 验证测试用例
// crates/erp-plugin-prototype/tests/test_plugin_integration.rs
#[tokio::test]
async fn test_plugin_lifecycle() {
// V1: WIT 接口 + bindgen 编译通过(隐式验证)
// V6: 从文件加载 WASM 二进制
let wasm_bytes = std::fs::read("../erp-plugin-test-sample/target/.../test_plugin.wasm")
.expect("请先编译测试插件");
let engine = setup_engine(); // V5: 启用 fuel + epoch
let module = Module::from_binary(&engine, &wasm_bytes).unwrap();
let mut store = setup_store(&engine); // V5: 设置资源限制
let instance = instantiate(&mut store, &module).await.unwrap();
// V2: Host 调用插件 init()
instance.plugin().call_init(&mut store).await.unwrap();
// V3: Host 调用插件 handle_event(),插件回调 Host API
instance.plugin().call_handle_event(
&mut store,
"test.event".to_string(),
vec![],
).await.unwrap();
// 验证 Host 端收到了插件的操作
let state = store.data();
assert!(state.logs.iter().any(|(l, m)| m.contains("测试插件初始化成功")));
assert_eq!(state.db_ops.len(), 1);
}
#[tokio::test]
async fn test_fuel_limit() {
// V5: 验证 fuel 耗尽时正确 trap
let mut store = setup_store_with_fuel(100); // 极低 fuel
let result = instance.plugin().call_init(&mut store).await;
assert!(result.is_err()); // 应该因 fuel 耗尽而失败
}
5.7 验证步骤
步骤 1: 添加 crate 和 Cargo.toml 依赖
- crates/erp-plugin-prototype/Cargo.toml
wasmtime = "43", wasmtime-wasi = "43", tokio, anyhow
- crates/erp-plugin-test-sample/Cargo.toml
wit-bindgen = "0.55", serde, serde_json, crate-type = ["cdylib"]
步骤 2: 编写 WIT 接口文件
- crates/erp-plugin-prototype/wit/plugin.wit
步骤 3: 实现 Host 端(bindgen + Host trait)
- crates/erp-plugin-prototype/src/lib.rs
步骤 4: 实现测试插件
- crates/erp-plugin-test-sample/src/lib.rs
步骤 5: 编译测试插件为 WASM
- cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release
步骤 6: 运行集成测试
- cargo test -p erp-plugin-prototype
步骤 7: 验证资源限制
- fuel 耗尽 trap
- 内存限制
- epoch 中断
5.8 验证成功标准
| 标准 | 衡量方式 |
|---|---|
| V1 编译通过 | Host 和插件 crate 均能 cargo check 通过 |
| V2 Host→插件调用 | init() 返回 Ok,Host 端日志记录初始化成功 |
| V3 插件→Host回调 | handle_event() 中调用 host::db_insert() 成功返回数据 |
| V4 async 正确 | Host 的 async db_insert 在 tokio runtime 中正确执行 |
| V5 资源限制 | 低 fuel 时 init() 返回错误而非无限循环 |
| V6 动态加载 | 从 .wasm 文件加载并实例化成功 |
| 编译大小 | 测试插件 WASM < 2MB |
| 启动耗时 | 单个插件实例化 < 100ms |
5.9 关键文件清单
| 文件 | 用途 |
|---|---|
crates/erp-plugin-prototype/Cargo.toml |
新建 - Host 端 crate 配置 |
crates/erp-plugin-prototype/wit/plugin.wit |
新建 - WIT 接口定义 |
crates/erp-plugin-prototype/src/lib.rs |
新建 - Host 运行时 + API 实现 |
crates/erp-plugin-prototype/src/main.rs |
新建 - 手动测试入口 |
crates/erp-plugin-prototype/tests/test_plugin_integration.rs |
新建 - 集成测试 |
crates/erp-plugin-test-sample/Cargo.toml |
新建 - 插件 crate 配置 |
crates/erp-plugin-test-sample/src/lib.rs |
新建 - 测试插件实现 |
Cargo.toml |
修改 - 添加两个新 workspace member |