chore: apply cargo fmt across workspace and update docs
- Run cargo fmt on all Rust crates for consistent formatting - Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions - Update wiki: add WASM plugin architecture, rewrite dev environment docs - Minor frontend cleanup (unused imports)
This commit is contained in:
208
crates/erp-plugin-prototype/src/lib.rs
Normal file
208
crates/erp-plugin-prototype/src/lib.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
//! WASM 插件原型验证 — Host 端运行时
|
||||
//!
|
||||
//! 验证目标:
|
||||
//! - V1: WIT 接口定义 + bindgen! 宏编译通过
|
||||
//! - V2: Host 调用插件导出函数(init / handle_event)
|
||||
//! - V3: 插件调用 Host 导入函数(db_insert / log_write)
|
||||
//! - V4: async 支持(Host async 函数正确桥接)
|
||||
//! - V5: Fuel + Epoch 资源限制
|
||||
//! - V6: 从二进制动态加载
|
||||
|
||||
use anyhow::Result;
|
||||
use wasmtime::component::{Component, HasSelf, Linker, bindgen};
|
||||
use wasmtime::{Config, Engine, Store, StoreLimits, StoreLimitsBuilder};
|
||||
|
||||
/// Host 端状态,绑定到每个 Store 实例
|
||||
pub struct HostState {
|
||||
/// Store 级资源限制
|
||||
pub(crate) limits: StoreLimits,
|
||||
/// 模拟数据库操作记录
|
||||
pub db_ops: Vec<DbOperation>,
|
||||
/// 日志记录
|
||||
pub logs: Vec<(String, String)>,
|
||||
/// 发布的事件
|
||||
pub events: Vec<(String, Vec<u8>)>,
|
||||
/// 配置存储(模拟)
|
||||
pub config_map: std::collections::HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
/// 数据库操作记录
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DbOperation {
|
||||
pub op_type: String,
|
||||
pub entity: String,
|
||||
pub data: Option<Vec<u8>>,
|
||||
pub id: Option<String>,
|
||||
pub version: Option<i64>,
|
||||
}
|
||||
|
||||
impl Default for HostState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
limits: StoreLimitsBuilder::new().build(),
|
||||
db_ops: Vec::new(),
|
||||
logs: Vec::new(),
|
||||
events: Vec::new(),
|
||||
config_map: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bindgen! 生成类型化绑定(包含 Host trait 和 add_to_linker)
|
||||
bindgen!({
|
||||
path: "./wit/plugin.wit",
|
||||
world: "plugin-world",
|
||||
});
|
||||
|
||||
// 实现 bindgen 生成的 Host trait — 插件调用 Host API 的入口
|
||||
impl erp::plugin::host_api::Host for HostState {
|
||||
fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
let id = format!("id-{}", self.db_ops.len() + 1);
|
||||
let record = serde_json::json!({
|
||||
"id": id,
|
||||
"tenant_id": "tenant-default",
|
||||
"entity": entity,
|
||||
"data": serde_json::from_slice::<serde_json::Value>(&data).unwrap_or(serde_json::Value::Null),
|
||||
});
|
||||
let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?;
|
||||
|
||||
self.db_ops.push(DbOperation {
|
||||
op_type: "insert".into(),
|
||||
entity: entity.clone(),
|
||||
data: Some(data),
|
||||
id: Some(id),
|
||||
version: None,
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn db_query(
|
||||
&mut self,
|
||||
entity: String,
|
||||
_filter: Vec<u8>,
|
||||
_pagination: Vec<u8>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let results: Vec<serde_json::Value> = self
|
||||
.db_ops
|
||||
.iter()
|
||||
.filter(|op| op.entity == entity && op.op_type == "insert")
|
||||
.map(|op| {
|
||||
serde_json::json!({
|
||||
"id": op.id,
|
||||
"entity": op.entity,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_vec(&results).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn db_update(
|
||||
&mut self,
|
||||
entity: String,
|
||||
id: String,
|
||||
data: Vec<u8>,
|
||||
version: i64,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let record = serde_json::json!({
|
||||
"id": id,
|
||||
"tenant_id": "tenant-default",
|
||||
"entity": entity,
|
||||
"version": version + 1,
|
||||
"data": serde_json::from_slice::<serde_json::Value>(&data).unwrap_or(serde_json::Value::Null),
|
||||
});
|
||||
let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?;
|
||||
|
||||
self.db_ops.push(DbOperation {
|
||||
op_type: "update".into(),
|
||||
entity,
|
||||
data: Some(data),
|
||||
id: Some(id),
|
||||
version: Some(version),
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn db_delete(&mut self, entity: String, id: String) -> Result<(), String> {
|
||||
self.db_ops.push(DbOperation {
|
||||
op_type: "delete".into(),
|
||||
entity,
|
||||
data: None,
|
||||
id: Some(id),
|
||||
version: None,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn event_publish(&mut self, event_type: String, payload: Vec<u8>) -> Result<(), String> {
|
||||
self.events.push((event_type, payload));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_get(&mut self, key: String) -> Result<Vec<u8>, String> {
|
||||
self.config_map
|
||||
.get(&key)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("配置项 '{}' 不存在", key))
|
||||
}
|
||||
|
||||
fn log_write(&mut self, level: String, message: String) {
|
||||
self.logs.push((level, message));
|
||||
}
|
||||
|
||||
fn current_user(&mut self) -> Result<Vec<u8>, String> {
|
||||
let user = serde_json::json!({
|
||||
"id": "user-default",
|
||||
"username": "admin",
|
||||
"tenant_id": "tenant-default",
|
||||
});
|
||||
serde_json::to_vec(&user).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn check_permission(&mut self, _permission: String) -> Result<bool, String> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建 Wasmtime Engine(启用 Fuel 限制)
|
||||
pub fn create_engine() -> Result<Engine> {
|
||||
let mut config = Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.consume_fuel(true);
|
||||
Ok(Engine::new(&config)?)
|
||||
}
|
||||
|
||||
/// 创建带 Fuel 限制的 Store
|
||||
pub fn create_store(engine: &Engine, fuel: u64) -> Result<Store<HostState>> {
|
||||
let state = HostState::new();
|
||||
let mut store = Store::new(engine, state);
|
||||
store.set_fuel(fuel)?;
|
||||
store.limiter(|state| &mut state.limits);
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// 从 WASM 二进制加载并实例化插件
|
||||
pub async fn load_plugin(
|
||||
engine: &Engine,
|
||||
wasm_bytes: &[u8],
|
||||
fuel: u64,
|
||||
) -> Result<(Store<HostState>, PluginWorld)> {
|
||||
let mut store = create_store(engine, fuel)?;
|
||||
let component = Component::from_binary(engine, wasm_bytes)?;
|
||||
|
||||
let mut linker = Linker::new(engine);
|
||||
// 注册 Host API 到 Linker,使插件可以调用 host 函数
|
||||
// HasSelf<HostState> 表示 Data<'a> = &'a mut HostState
|
||||
PluginWorld::add_to_linker::<_, HasSelf<HostState>>(&mut linker, |state| state)?;
|
||||
|
||||
let instance = PluginWorld::instantiate_async(&mut store, &component, &linker).await?;
|
||||
|
||||
Ok((store, instance))
|
||||
}
|
||||
10
crates/erp-plugin-prototype/src/main.rs
Normal file
10
crates/erp-plugin-prototype/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use erp_plugin_prototype::*;
|
||||
|
||||
fn _discover_api(instance: PluginWorld, mut store: wasmtime::Store<HostState>) {
|
||||
let api = instance.erp_plugin_plugin_api();
|
||||
let _ = api.call_init(&mut store);
|
||||
let _ = api.call_on_tenant_created(&mut store, "test-tenant");
|
||||
let _ = api.call_handle_event(&mut store, "test.event", &[1u8]);
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
Reference in New Issue
Block a user