mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-24 16:02:44 +00:00
965 lines
28 KiB
Markdown
965 lines
28 KiB
Markdown
# mlua Lua 插件方案
|
||
|
||
> 在现有 WASM + JS 双引擎插件系统基础上,新增 Lua (mlua) 作为第三运行时,
|
||
> 提供最轻量的插件开发选择,原生 serde 集成,零序列化开销。
|
||
|
||
---
|
||
|
||
## 1. 背景
|
||
|
||
### 1.1 现状
|
||
|
||
当前插件系统支持两种运行时:
|
||
|
||
- **WASM (wasmtime)** — Rust 编写,编译为 WASM,安全隔离最强
|
||
- **JavaScript (rquickjs)** — JS/TS 编写,社区生态最大
|
||
|
||
### 1.2 为什么需要 Lua
|
||
|
||
| 场景 | WASM | JS | Lua |
|
||
|------|------|----|-----|
|
||
| 嵌入式/边缘部署 | 体积大(+15MB) | 中等(+2-4MB) | **最小(+0.5-1MB)** |
|
||
| 编译时间增量 | 基准 | +2-5 min | **+30-60s** |
|
||
| 数据传递 | 手动 ABI(指针+长度前缀) | JSON 字符串中转 | **原生 serde(零拷贝 table 映射)** |
|
||
| 游戏引擎/Nginx/Redis 用户 | — | — | **Lua 是通用脚本语言** |
|
||
| 配置型插件 | 过重 | 可用 | **最适合(table = config)** |
|
||
| 沙箱控制 | fuel | interrupt handler | **细粒度 stdlib 裁剪** |
|
||
|
||
### 1.3 目标
|
||
|
||
- 支持用 Lua 编写插件,体积和编译时间影响最小
|
||
- 原生 serde 集成 — Rust 结构体直接映射为 Lua table,无 JSON 中转
|
||
- 与现有 WASM/JS 插件共享 Hook 调度机制
|
||
- 安全隔离(内存限制、指令计数超时、stdlib 裁剪)
|
||
|
||
### 1.4 选型对比
|
||
|
||
| 维度 | wasmtime (WASM) | rquickjs (JS) | **mlua (Lua)** |
|
||
|------|----------------|---------------|----------------|
|
||
| 引擎体积 (release) | ~15MB | ~2-4MB | **~0.5-1MB** |
|
||
| 编译时间增量 | 基准 | +2-5 min | **+30-60s** |
|
||
| 依赖 crate 数 | ~280 | ~5 | **~3** |
|
||
| 冷启动 | ~5-50ms | ~1-5ms | **<1ms** |
|
||
| 内存/实例 | ~1-2MB | ~200-500KB | **~50-100KB** |
|
||
| 数据传递 | 手动 ABI | JSON 字符串 | **serde 原生映射** |
|
||
| 沙箱粒度 | fuel | interrupt handler | **stdlib 裁剪 + hook** |
|
||
| 语言生态 | Rust | JavaScript(全球最大) | Lua(游戏/运维/嵌入式) |
|
||
| async 支持 | N/A | AsyncRuntime | **coroutine + call_async** |
|
||
|
||
**选择 mlua 的核心理由:**
|
||
|
||
1. **最轻量** — Lua 5.4 编译后 ~300KB,对嵌入式场景友好
|
||
2. **原生 serde** — `LuaSerdeExt::to_value/from_value`,Rust struct ↔ Lua table 零开销
|
||
3. **沙箱成熟** — 按需加载 stdlib(排除 `io`/`os`/`debug`/`package`)
|
||
4. **活跃维护** — 0.11.6(2026-01),支持 Lua 5.1-5.5、LuaJIT、Luau
|
||
5. **API 稳定** — `send` feature 使其 `Send+Sync`,直接在 tokio 中使用
|
||
|
||
---
|
||
|
||
## 2. 架构设计
|
||
|
||
### 2.1 模块结构
|
||
|
||
```
|
||
src/plugins/
|
||
├── mod.rs # PluginManager(三引擎统一分派)
|
||
├── manifest.rs # 复用 manifest(runtime 字段区分引擎)
|
||
├── engine.rs # WASM 引擎(不变)
|
||
├── host.rs # WASM host functions(不变)
|
||
├── engine_js.rs # QuickJS 引擎(不变)
|
||
├── js_host.rs # JS 宿主函数(不变)
|
||
├── engine_lua.rs # Lua 引擎(新增)
|
||
└── lua_host.rs # Lua 宿主函数(新增)
|
||
```
|
||
|
||
### 2.2 类型体系
|
||
|
||
```rust
|
||
// mod.rs — 插件实例枚举
|
||
enum LoadedPluginInstance {
|
||
Wasm(Box<RwLock<WasmInstance>>),
|
||
Js(String), // plugin_id
|
||
Lua(String), // plugin_id
|
||
}
|
||
|
||
pub struct PluginManager {
|
||
#[cfg(feature = "plugin-wasm")]
|
||
engine: wasmtime::Engine,
|
||
#[cfg(feature = "plugin-js")]
|
||
js_engine: JsEngine,
|
||
#[cfg(feature = "plugin-lua")]
|
||
lua_engine: LuaEngine,
|
||
plugins: RwLock<HashMap<String, LoadedPlugin>>,
|
||
config: Arc<AppConfig>,
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 2.3 三引擎分派
|
||
|
||
```rust
|
||
impl PluginManager {
|
||
pub async fn dispatch_filter<T>(&self, hook: HookPoint, input: T) -> AppResult<T> {
|
||
let plugins = self.plugins.read().await;
|
||
let mut sorted: Vec<_> = plugins.values().collect();
|
||
sorted.sort_by_key(/* priority */);
|
||
|
||
let mut current = input;
|
||
for plugin in sorted {
|
||
let func_name = hook.wasm_func_name();
|
||
if !plugin.manifest.hooks.contains_key(func_name) { continue; }
|
||
|
||
match &plugin.instance {
|
||
LoadedPluginInstance::Wasm(wasm) => {
|
||
// 现有 WASM 分派
|
||
}
|
||
LoadedPluginInstance::Js(id) => {
|
||
// 现有 JS 分派
|
||
}
|
||
LoadedPluginInstance::Lua(id) => {
|
||
match self.lua_engine
|
||
.call_filter(id, func_name, ¤t)
|
||
.await
|
||
{
|
||
Ok(Some(result)) => current = result,
|
||
Ok(None) => {}
|
||
Err(e) => tracing::warn!("lua plugin {} hook {} failed: {e}", ...),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(current)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.4 线程模型
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ tokio runtime │
|
||
│ │
|
||
│ PluginManager (Arc) │
|
||
│ ├── wasm_engine: wasmtime::Engine │
|
||
│ ├── js_engine: JsEngine │
|
||
│ │ └── AsyncRuntime (Arc<Mutex>) │
|
||
│ ├── lua_engine: LuaEngine │
|
||
│ │ └── states: Mutex<HashMap<String,Lua>> │
|
||
│ └── plugins: RwLock<HashMap> │
|
||
│ │
|
||
│ axum handler │
|
||
│ └── dispatch_filter() │
|
||
│ ├── WASM: sync call │
|
||
│ ├── JS: ctx.with().await │
|
||
│ └── Lua: lua.lock().call_async().await │
|
||
│ │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
- `mlua::Lua` 配合 `send` feature 后为 `Send+Sync`(内部 `Arc<ReentrantMutex>`)
|
||
- 每个插件一个 `Lua` 实例(独立全局作用域)
|
||
- 异步调用通过 `call_async()` 在 tokio 中执行
|
||
- 不需要独立线程
|
||
|
||
---
|
||
|
||
## 3. 引擎实现
|
||
|
||
### 3.1 LuaEngine
|
||
|
||
```rust
|
||
// engine_lua.rs
|
||
use std::collections::HashMap;
|
||
use std::sync::Arc;
|
||
use std::time::Instant;
|
||
|
||
use mlua::{Lua, LuaOptions, StdLib, HookTriggers, Error as LuaError, VmState, LuaSerdeExt};
|
||
use serde::de::DeserializeOwned;
|
||
use serde::Serialize;
|
||
use tokio::sync::Mutex;
|
||
|
||
use crate::config::app::AppConfig;
|
||
|
||
pub struct LuaEngine {
|
||
states: Mutex<HashMap<String, Lua>>,
|
||
config: Arc<AppConfig>,
|
||
timeout_instructions: i64,
|
||
}
|
||
|
||
impl LuaEngine {
|
||
pub fn new(config: &AppConfig) -> anyhow::Result<Self> {
|
||
Ok(Self {
|
||
states: Mutex::new(HashMap::new()),
|
||
config: Arc::new(config.clone()),
|
||
// 默认 500 万条指令(约 2-5 秒)
|
||
timeout_instructions: 5_000_000,
|
||
})
|
||
}
|
||
|
||
/// 创建受限 Lua 状态
|
||
fn create_sandboxed_lua(memory_limit_bytes: usize) -> anyhow::Result<Lua> {
|
||
let lua = Lua::new_with(
|
||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
|
||
LuaOptions::default(),
|
||
)?;
|
||
lua.set_memory_limit(memory_limit_bytes)?;
|
||
Ok(lua)
|
||
}
|
||
|
||
/// 加载 Lua 插件代码
|
||
pub async fn load_plugin(&self, id: &str, code: &str) -> anyhow::Result<()> {
|
||
let memory_limit = (self.config.plugin_max_memory_mb as usize) * 1024 * 1024;
|
||
let lua = Self::create_sandboxed_lua(memory_limit)?;
|
||
let config = self.config.clone();
|
||
|
||
// 注册宿主函数
|
||
super::lua_host::register_host_functions(&lua, config)?;
|
||
|
||
// 执行插件代码
|
||
lua.load(code).exec()?;
|
||
|
||
self.states.lock().await.insert(id.to_string(), lua);
|
||
Ok(())
|
||
}
|
||
|
||
/// 卸载插件
|
||
pub async fn unload_plugin(&self, id: &str) {
|
||
self.states.lock().await.remove(id);
|
||
}
|
||
|
||
/// 调用 Filter Hook
|
||
pub async fn call_filter<T: Serialize + DeserializeOwned + Send>(
|
||
&self,
|
||
plugin_id: &str,
|
||
func_name: &str,
|
||
input: &T,
|
||
) -> anyhow::Result<Option<T>> {
|
||
let states = self.states.lock().await;
|
||
let lua = match states.get(plugin_id) {
|
||
Some(l) => l,
|
||
None => return Ok(None),
|
||
};
|
||
|
||
let max_instructions = self.timeout_instructions;
|
||
let result = self.exec_with_timeout(lua, max_instructions, || {
|
||
let globals = lua.globals();
|
||
let plugin_table: mlua::Table = match globals.get("Plugin") {
|
||
Ok(t) => t,
|
||
Err(_) => return Ok(None),
|
||
};
|
||
let func: mlua::Function = match plugin_table.get(func_name) {
|
||
Ok(f) => f,
|
||
Err(_) => return Ok(None),
|
||
};
|
||
|
||
let input_value = lua.to_value(input)?;
|
||
let result_value = func.call::<mlua::Value>(input_value)?;
|
||
let output: T = lua.from_value(result_value)?;
|
||
Ok(Some(output))
|
||
})?;
|
||
|
||
result
|
||
}
|
||
|
||
/// 调用 Action Hook
|
||
pub async fn call_action<T: Serialize>(
|
||
&self,
|
||
plugin_id: &str,
|
||
func_name: &str,
|
||
data: &T,
|
||
) -> anyhow::Result<()> {
|
||
let states = self.states.lock().await;
|
||
let lua = match states.get(plugin_id) {
|
||
Some(l) => l,
|
||
None => return Ok(()),
|
||
};
|
||
|
||
let max_instructions = self.timeout_instructions;
|
||
self.exec_with_timeout(lua, max_instructions, || {
|
||
let globals = lua.globals();
|
||
let plugin_table: mlua::Table = match globals.get("Plugin") {
|
||
Ok(t) => t,
|
||
Err(_) => return Ok(()),
|
||
};
|
||
let func: mlua::Function = match plugin_table.get(func_name) {
|
||
Ok(f) => f,
|
||
Err(_) => return Ok(()),
|
||
};
|
||
|
||
let data_value = lua.to_value(data)?;
|
||
func.call::<()>(data_value)?;
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
/// 调用 String Filter Hook
|
||
pub async fn call_string_filter(
|
||
&self,
|
||
plugin_id: &str,
|
||
func_name: &str,
|
||
input: &str,
|
||
) -> anyhow::Result<Option<String>> {
|
||
let states = self.states.lock().await;
|
||
let lua = match states.get(plugin_id) {
|
||
Some(l) => l,
|
||
None => return Ok(None),
|
||
};
|
||
|
||
let max_instructions = self.timeout_instructions;
|
||
self.exec_with_timeout(lua, max_instructions, || {
|
||
let globals = lua.globals();
|
||
let plugin_table: mlua::Table = match globals.get("Plugin") {
|
||
Ok(t) => t,
|
||
Err(_) => return Ok(None),
|
||
};
|
||
let func: mlua::Function = match plugin_table.get(func_name) {
|
||
Ok(f) => f,
|
||
Err(_) => return Ok(None),
|
||
};
|
||
|
||
let result: String = func.call(input)?;
|
||
Ok(Some(result))
|
||
})
|
||
}
|
||
|
||
/// 带超时执行 Lua 代码(指令计数 hook)
|
||
fn exec_with_timeout<F, R>(&self, lua: &Lua, max_instructions: i64, f: F) -> anyhow::Result<R>
|
||
where
|
||
F: FnOnce() -> anyhow::Result<R>,
|
||
{
|
||
use std::sync::atomic::{AtomicI64, Ordering};
|
||
|
||
let remaining = Arc::new(AtomicI64::new(max_instructions));
|
||
let remaining_clone = remaining.clone();
|
||
|
||
lua.set_hook(
|
||
HookTriggers::new().every_nth_instruction(1000),
|
||
move |_lua, _debug| {
|
||
if remaining_clone.fetch_sub(1000, Ordering::Relaxed) <= 1000 {
|
||
Err(LuaError::runtime("execution timeout"))
|
||
} else {
|
||
Ok(VmState::Continue)
|
||
}
|
||
},
|
||
)?;
|
||
|
||
let result = f();
|
||
lua.remove_hook();
|
||
result
|
||
}
|
||
|
||
/// 获取已加载 Lua 插件数量
|
||
pub async fn plugin_count(&self) -> usize {
|
||
self.states.lock().await.len()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.2 宿主函数
|
||
|
||
```rust
|
||
// lua_host.rs
|
||
use std::sync::Arc;
|
||
|
||
use mlua::{Function, Lua, Table};
|
||
|
||
use crate::config::app::AppConfig;
|
||
|
||
/// 注册宿主函数到 Lua 全局作用域
|
||
pub fn register_host_functions(lua: &Lua, config: Arc<AppConfig>) -> anyhow::Result<()> {
|
||
let globals = lua.globals();
|
||
let host = lua.create_table()?;
|
||
|
||
// Host.log(level, message)
|
||
let log_fn = lua.create_function(|_, (level, msg): (String, String)| {
|
||
match level.as_str() {
|
||
"warn" => tracing::warn!("[plugin:lua] {msg}"),
|
||
"error" => tracing::error!("[plugin:lua] {msg}"),
|
||
_ => tracing::info!("[plugin:lua] {msg}"),
|
||
}
|
||
Ok(())
|
||
})?;
|
||
host.set("log", log_fn)?;
|
||
|
||
// Host.getConfig(key) -> string | nil
|
||
let get_config_fn = lua.create_function(move |lua, key: String| {
|
||
match get_config_value(&config, &key) {
|
||
Some(val) => Ok(lua.create_string(&val)?.into()),
|
||
None => Ok(mlua::Value::Nil),
|
||
}
|
||
})?;
|
||
host.set("getConfig", get_config_fn)?;
|
||
|
||
globals.set("Host", host)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn get_config_value(config: &AppConfig, key: &str) -> Option<String> {
|
||
match key {
|
||
"app.host" => Some(config.host.clone()),
|
||
"app.port" => Some(config.port.to_string()),
|
||
"app.env" => Some(config.env.clone()),
|
||
"app.base_url" => Some(config.base_url.clone()),
|
||
"jwt.access_expires" => Some(config.jwt_access_expires.to_string()),
|
||
"jwt.refresh_expires" => Some(config.jwt_refresh_expires.to_string()),
|
||
"upload.dir" => Some(config.upload_dir.clone()),
|
||
"upload.max_size" => Some(config.max_upload_size.to_string()),
|
||
"plugin.max_memory_mb" => Some(config.plugin_max_memory_mb.to_string()),
|
||
"plugin.default_timeout_ms" => Some(config.plugin_default_timeout_ms.to_string()),
|
||
_ => None,
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 数据传递:Lua vs JS
|
||
|
||
这是 Lua 方案的核心优势 — **原生 serde 映射,无 JSON 中转**。
|
||
|
||
### 4.1 JS 插件(当前实现)
|
||
|
||
```javascript
|
||
// 必须手动 JSON.parse/stringify
|
||
on_post_creating: function(inputJson) {
|
||
var input = JSON.parse(inputJson);
|
||
input.title = input.title.toUpperCase();
|
||
return JSON.stringify(input); // 必须返回字符串
|
||
}
|
||
```
|
||
|
||
### 4.2 Lua 插件(直接 table 操作)
|
||
|
||
```lua
|
||
-- 原生 table,无需序列化
|
||
function Plugin.on_post_creating(input)
|
||
input.title = input.title:upper()
|
||
return input
|
||
end
|
||
```
|
||
|
||
### 4.3 Rust 侧映射
|
||
|
||
```rust
|
||
// mlua 自动完成 Rust struct ↔ Lua table 转换
|
||
let input_value = lua.to_value(&post_data)?; // Rust → Lua table
|
||
let result_value = func.call::<mlua::Value>(input_value)?;
|
||
let output: PostData = lua.from_value(result_value)?; // Lua table → Rust
|
||
|
||
// 对比 rquickjs:需要手动 JSON 序列化
|
||
let input_json = serde_json::to_string(&post_data)?;
|
||
let result_str: String = func.call((input_json,))?; // 字符串进出
|
||
let output: PostData = serde_json::from_str(&result_str)?;
|
||
```
|
||
|
||
### 4.4 类型映射表
|
||
|
||
| Rust 类型 | Lua 类型 | 说明 |
|
||
|-----------|----------|------|
|
||
| `String` | `string` | 直接映射 |
|
||
| `i32` / `u64` | `number` | Lua number(f64) |
|
||
| `bool` | `boolean` | 直接映射 |
|
||
| `Option<T>` | `T` 或 `nil` | None = nil |
|
||
| `Vec<T>` | `table` (array) | 索引从 1 开始(Lua 习惯) |
|
||
| `HashMap<K,V>` | `table` (dict) | 直接映射 |
|
||
| `serde_json::Value` | nested table | 自动递归映射 |
|
||
| `()` | 无返回值 | void |
|
||
|
||
---
|
||
|
||
## 5. 插件清单
|
||
|
||
### 5.1 plugin.toml 变更
|
||
|
||
```toml
|
||
# plugins/seo-optimizer-lua/plugin.toml
|
||
[plugin]
|
||
id = "com.rust-blog.seo-optimizer-lua"
|
||
name = "SEO Optimizer (Lua)"
|
||
version = "1.0.0"
|
||
description = "Lua 版 SEO 优化插件"
|
||
author = "rust-blog"
|
||
license = "MIT"
|
||
runtime = "lua" # "wasm" | "js" | "lua"
|
||
language = "lua" # 信息字段
|
||
entry = "init.lua" # Lua 入口文件名(默认 init.lua)
|
||
|
||
[permissions]
|
||
max_memory_mb = 8
|
||
timeout_ms = 2000
|
||
|
||
[hooks.on_post_creating]
|
||
priority = 10
|
||
|
||
[hooks.filter_html]
|
||
priority = 5
|
||
```
|
||
|
||
### 5.2 manifest.rs 变更
|
||
|
||
```rust
|
||
fn default_entry() -> String {
|
||
"index.js".into()
|
||
}
|
||
// runtime="lua" 时 entry 默认应为 "init.lua"
|
||
// 在 load_plugin_from_dir 中根据 runtime 选择默认值
|
||
```
|
||
|
||
### 5.3 插件目录结构
|
||
|
||
```
|
||
plugins/
|
||
├── seo-optimizer/ # WASM 插件
|
||
│ ├── plugin.toml
|
||
│ └── seo_optimizer.wasm
|
||
├── content-filter/ # WASM 插件
|
||
│ ├── plugin.toml
|
||
│ └── content_filter.wasm
|
||
├── welcome-email/ # JS 插件
|
||
│ ├── plugin.toml
|
||
│ └── index.js
|
||
└── excerpt-generator/ # Lua 插件
|
||
├── plugin.toml
|
||
└── init.lua
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Lua 插件开发规范
|
||
|
||
### 6.1 约定
|
||
|
||
Lua 插件必须设置一个全局 `Plugin` table,包含对应的 Hook 函数。
|
||
|
||
```lua
|
||
-- Plugin 全局 table
|
||
Plugin = {
|
||
-- Filter Hook — 接收 table,返回修改后的 table
|
||
on_post_creating = function(input)
|
||
if not input.excerpt or input.excerpt == "" then
|
||
input.excerpt = input.content:sub(1, 200) .. "..."
|
||
end
|
||
return input
|
||
end,
|
||
|
||
-- Action Hook — 接收 table,无返回值
|
||
on_post_created = function(data)
|
||
Host.log("info", "New post published: " .. data.title)
|
||
end,
|
||
|
||
-- String Filter Hook — 接收字符串,返回字符串
|
||
filter_html = function(html)
|
||
return html:gsub("<head>", '<head><meta property="og:type" content="article">')
|
||
end,
|
||
}
|
||
```
|
||
|
||
### 6.2 Hook 方法签名
|
||
|
||
| Hook 类型 | 方法签名 | 说明 |
|
||
|-----------|---------|------|
|
||
| Table Filter | `function(table): table` | 接收/返回 Lua table(自动 serde 映射) |
|
||
| Table Action | `function(table): nil` | 接收 Lua table,无返回值 |
|
||
| String Filter | `function(string): string` | 接收/返回字符串 |
|
||
| Route Handler | `function(table): table` | 返回 `{ status = 200, body = "..." }` |
|
||
|
||
### 6.3 宿主 API
|
||
|
||
| 函数 | 签名 | 说明 |
|
||
|------|------|------|
|
||
| `Host.log(level, msg)` | `(string, string) -> nil` | 写入宿主日志 |
|
||
| `Host.getConfig(key)` | `(string) -> string|nil` | 读取宿主配置 |
|
||
|
||
### 6.4 Lua 环境限制
|
||
|
||
插件运行在受限 Lua 环境中,只加载以下标准库:
|
||
|
||
| 可用 | 不可用 |
|
||
|------|--------|
|
||
| `table`(table.insert 等) | `io`(文件读写) |
|
||
| `string`(string.match 等) | `os`(系统调用) |
|
||
| `math`(math.floor 等) | `debug`(调试器) |
|
||
| `utf8`(utf8.len 等) | `package`(模块加载) |
|
||
| `coroutine`(协程) | `dofile` / `loadfile` |
|
||
|
||
---
|
||
|
||
## 7. 示例插件
|
||
|
||
### 7.1 Excerpt Generator(Lua)
|
||
|
||
```toml
|
||
# plugins/excerpt-generator/plugin.toml
|
||
[plugin]
|
||
id = "com.rust-blog.excerpt-generator"
|
||
name = "Excerpt Generator"
|
||
version = "1.0.0"
|
||
description = "自动从内容生成文章摘要"
|
||
runtime = "lua"
|
||
language = "lua"
|
||
entry = "init.lua"
|
||
|
||
[permissions]
|
||
max_memory_mb = 4
|
||
timeout_ms = 1000
|
||
|
||
[hooks.on_post_creating]
|
||
priority = 10
|
||
```
|
||
|
||
```lua
|
||
-- plugins/excerpt-generator/init.lua
|
||
Plugin = {
|
||
on_post_creating = function(input)
|
||
if not input.excerpt or input.excerpt == "" then
|
||
local plain = input.content
|
||
:gsub("```[\s\S]*?```", "")
|
||
:gsub("[#*_`]", "")
|
||
:gsub("%s+", " ")
|
||
:match("^%s*(.-)%s*$")
|
||
|
||
if #plain > 200 then
|
||
input.excerpt = plain:sub(1, 200) .. "..."
|
||
else
|
||
input.excerpt = plain
|
||
end
|
||
end
|
||
|
||
-- 读取宿主配置
|
||
local env = Host.getConfig("app.env")
|
||
if env == "development" then
|
||
Host.log("info", "excerpt generated in dev mode")
|
||
end
|
||
|
||
return input
|
||
end,
|
||
}
|
||
```
|
||
|
||
### 7.2 OG Tag Injector(Lua)
|
||
|
||
```toml
|
||
# plugins/og-tags/plugin.toml
|
||
[plugin]
|
||
id = "com.rust-blog.og-tags"
|
||
name = "OG Tag Injector"
|
||
version = "1.0.0"
|
||
description = "注入 Open Graph 和 Twitter Card 标签"
|
||
runtime = "lua"
|
||
|
||
[hooks.filter_html]
|
||
priority = 5
|
||
```
|
||
|
||
```lua
|
||
-- plugins/og-tags/init.lua
|
||
Plugin = {
|
||
filter_html = function(html)
|
||
local base_url = Host.getConfig("app.base_url") or ""
|
||
local og = string.format(
|
||
'<meta property="og:type" content="article">'
|
||
.. '<meta property="og:url" content="%s">',
|
||
base_url
|
||
)
|
||
return html:gsub("<head>", "<head>" .. og)
|
||
end,
|
||
}
|
||
```
|
||
|
||
### 7.3 Content Filter(Lua 版,对比 WASM 和 JS)
|
||
|
||
```toml
|
||
# plugins/word-filter-lua/plugin.toml
|
||
[plugin]
|
||
id = "com.rust-blog.word-filter-lua"
|
||
name = "Word Filter (Lua)"
|
||
version = "1.0.0"
|
||
description = "过滤评论中的敏感词(Lua 版)"
|
||
runtime = "lua"
|
||
|
||
[hooks.on_comment_creating]
|
||
priority = 5
|
||
```
|
||
|
||
```lua
|
||
-- plugins/word-filter-lua/init.lua
|
||
local bad_words = { "badword1", "badword2", "spam" }
|
||
|
||
Plugin = {
|
||
on_comment_creating = function(input)
|
||
local content = input.content
|
||
for _, word in ipairs(bad_words) do
|
||
content = content:gsub(word, string.rep("*", #word))
|
||
end
|
||
input.content = content
|
||
return input
|
||
end,
|
||
}
|
||
```
|
||
|
||
### 7.4 三引擎同功能对比
|
||
|
||
同一功能(过滤敏感词)的三种实现:
|
||
|
||
**WASM (Rust → WASM):**
|
||
|
||
```rust
|
||
// 需要 plugins-sdk、长度前缀 ABI、alloc/dealloc、wasm32 编译
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn on_comment_creating(ptr: i32, len: i32) -> i32 {
|
||
let input = read_input(ptr, len);
|
||
let filtered = input.replace("badword", "********");
|
||
write_output(&filtered)
|
||
}
|
||
```
|
||
|
||
**JS (QuickJS):**
|
||
|
||
```javascript
|
||
var badWords = ["badword1", "badword2"];
|
||
Plugin = {
|
||
on_comment_creating: function(inputJson) {
|
||
var input = JSON.parse(inputJson);
|
||
badWords.forEach(function(w) {
|
||
input.content = input.content.replace(new RegExp(w, "g"), "***");
|
||
});
|
||
return JSON.stringify(input);
|
||
}
|
||
};
|
||
```
|
||
|
||
**Lua (mlua):**
|
||
|
||
```lua
|
||
local bad_words = { "badword1", "badword2" }
|
||
Plugin = {
|
||
on_comment_creating = function(input)
|
||
for _, w in ipairs(bad_words) do
|
||
input.content = input.content:gsub(w, string.rep("*", #w))
|
||
end
|
||
return input
|
||
end
|
||
}
|
||
```
|
||
|
||
| 维度 | WASM | JS | Lua |
|
||
|------|------|----|-----|
|
||
| 代码行数 | ~20(+ ABI 层) | ~10 | ~7 |
|
||
| 序列化 | 手动指针+长度 | JSON.parse/stringify | **无(原生 table)** |
|
||
| 工具链依赖 | Rust + wasm32 target | Node.js/esbuild(TS时) | **无** |
|
||
| 性能 | 最高 | 中等 | 轻量场景最快(无序列化开销) |
|
||
|
||
---
|
||
|
||
## 8. Feature Flag 与编译
|
||
|
||
### 8.1 Cargo.toml
|
||
|
||
```toml
|
||
[features]
|
||
default = ["db-sqlite", "plugin-all"]
|
||
|
||
# Database backend
|
||
db-sqlite = ["sqlx/sqlite"]
|
||
db-postgres = ["sqlx/postgres"]
|
||
db-mysql = ["sqlx/mysql"]
|
||
|
||
# Plugin runtimes
|
||
plugin-wasm = ["wasmtime", "wasmtime-wasi"]
|
||
plugin-js = ["rquickjs"]
|
||
plugin-lua = ["mlua"]
|
||
plugin-all = ["plugin-wasm", "plugin-js", "plugin-lua"]
|
||
|
||
[dependencies]
|
||
# ... 现有依赖 ...
|
||
|
||
# Plugin: Lua (可选)
|
||
mlua = { version = "0.11", features = ["lua54", "vendored", "send", "serde", "async", "macros"], optional = true }
|
||
```
|
||
|
||
**Feature flag 说明:**
|
||
|
||
| Feature | 作用 |
|
||
|---------|------|
|
||
| `lua54` | 使用 Lua 5.4(推荐,最新稳定) |
|
||
| `vendored` | 从源码编译 Lua,无需系统安装 |
|
||
| `send` | 使 `Lua`/`Table`/`Function` 等为 `Send+Sync` |
|
||
| `serde` | `LuaSerdeExt` — Rust struct ↔ Lua table 自动映射 |
|
||
| `async` | `create_async_function` + `call_async` 支持 |
|
||
| `macros` | `#[mlua::lua_module]` 等便捷宏 |
|
||
|
||
### 8.2 编译示例
|
||
|
||
```bash
|
||
# 仅 WASM 插件
|
||
cargo build --features "db-sqlite,plugin-wasm"
|
||
|
||
# 仅 JS 插件
|
||
cargo build --features "db-sqlite,plugin-js"
|
||
|
||
# 仅 Lua 插件
|
||
cargo build --features "db-sqlite,plugin-lua"
|
||
|
||
# 三引擎(推荐)
|
||
cargo build --features "db-sqlite,plugin-all"
|
||
|
||
# 最小部署(仅 Lua,体积最小)
|
||
cargo build --release --features "db-sqlite,plugin-lua"
|
||
```
|
||
|
||
### 8.3 条件编译
|
||
|
||
```rust
|
||
// src/plugins/mod.rs
|
||
#[cfg(feature = "plugin-wasm")]
|
||
mod engine;
|
||
#[cfg(feature = "plugin-wasm")]
|
||
mod host;
|
||
#[cfg(feature = "plugin-js")]
|
||
mod engine_js;
|
||
#[cfg(feature = "plugin-js")]
|
||
mod js_host;
|
||
#[cfg(feature = "plugin-lua")]
|
||
mod engine_lua;
|
||
#[cfg(feature = "plugin-lua")]
|
||
mod lua_host;
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 安全机制
|
||
|
||
### 9.1 stdlib 裁剪
|
||
|
||
```rust
|
||
// 只加载安全的库,排除 io/os/debug/package
|
||
let lua = Lua::new_with(
|
||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
|
||
LuaOptions::default(),
|
||
)?;
|
||
```
|
||
|
||
排除能力:
|
||
|
||
| 被排除的库 | 危险操作 |
|
||
|------------|----------|
|
||
| `io` | 文件读写、流操作 |
|
||
| `os` | 系统命令、环境变量、文件重命名/删除 |
|
||
| `debug` | 访问内部状态、设置 metamethod |
|
||
| `package` | 加载 C 模块、执行任意代码 |
|
||
|
||
### 9.2 内存限制
|
||
|
||
```rust
|
||
lua.set_memory_limit(8 * 1024 * 1024)?; // 8MB
|
||
|
||
// 查询当前使用量
|
||
let used: usize = lua.used_memory();
|
||
```
|
||
|
||
自定义分配器跟踪每一次 alloc/dealloc,超限时返回 `Error::MemoryError`。
|
||
|
||
### 9.3 执行超时(指令计数 hook)
|
||
|
||
```rust
|
||
lua.set_hook(
|
||
HookTriggers::new().every_nth_instruction(1000),
|
||
move |_lua, _debug| {
|
||
if remaining.fetch_sub(1000, Ordering::Relaxed) <= 1000 {
|
||
Err(LuaError::runtime("execution timeout"))
|
||
} else {
|
||
Ok(VmState::Continue)
|
||
}
|
||
},
|
||
)?;
|
||
```
|
||
|
||
- 每 1000 条 VM 指令检查一次
|
||
- 超时抛出不可捕获的 `Error::RuntimeError`
|
||
- 开销 < 1%(每 1000 条指令一次原子操作)
|
||
|
||
### 9.4 三引擎安全对比
|
||
|
||
| 安全维度 | WASM | JS | Lua |
|
||
|----------|------|----|----|
|
||
| 内存隔离 | 独立线性内存 | GC 管理(同进程) | GC 管理(同进程) |
|
||
| 计算限制 | fuel(精确) | interrupt handler(时间) | **指令计数 hook** |
|
||
| 文件系统 | 无 | 无 | **stdlib 裁剪** |
|
||
| 网络访问 | 无 | 无 | **无 io 库** |
|
||
| 代码加载 | 无 | 无 | **无 package 库** |
|
||
| 逃逸风险 | 极低 | 低 | **低(stdlib 裁剪 + hook)** |
|
||
| 逃逸缓解 | fuel 耗尽 | memory limit | **set_hook + memory limit** |
|
||
|
||
---
|
||
|
||
## 10. justfile 新增命令
|
||
|
||
```justfile
|
||
# 复制 Lua 插件到 plugins/ 目录
|
||
plugins-lua-build:
|
||
@echo "Copying Lua plugins..."
|
||
@mkdir -p plugins/excerpt-generator
|
||
@cp plugins-examples-lua/excerpt-generator/plugin.toml plugins/excerpt-generator/
|
||
@cp plugins-examples-lua/excerpt-generator/init.lua plugins/excerpt-generator/
|
||
@echo "Done. Lua plugins ready in plugins/"
|
||
|
||
# 编译/复制所有插件(WASM + JS + Lua)
|
||
plugins-all: plugins-build plugins-js-build plugins-lua-build
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 实施计划
|
||
|
||
### Phase 1:最小集成(1 天)
|
||
|
||
- [ ] `Cargo.toml` 添加 `mlua` 可选依赖 + `plugin-lua` feature flag
|
||
- [ ] 创建 `engine_lua.rs`(LuaEngine 基本结构)
|
||
- [ ] 创建 `lua_host.rs`(Host.log + Host.getConfig)
|
||
- [ ] 修改 `manifest.rs`(entry 默认值根据 runtime 区分)
|
||
- [ ] 修改 `mod.rs`(三引擎分派 + `LoadedPluginInstance::Lua`)
|
||
- [ ] 创建示例 Lua 插件
|
||
- [ ] 条件编译验证
|
||
|
||
### Phase 2:功能完善(1 天)
|
||
|
||
- [ ] `dispatch_filter` / `dispatch_action` / `dispatch_render_override` Lua 分派
|
||
- [ ] 超时机制(指令计数 hook)
|
||
- [ ] 热重载支持(复用 channel 架构,监听 `.lua` 文件)
|
||
- [ ] justfile 命令
|
||
|
||
### Phase 3:测试(1 天)
|
||
|
||
- [ ] LuaEngine 单元测试(创建、加载、调用、超时、内存限制)
|
||
- [ ] 集成测试(Lua 插件 Hook 调度)
|
||
- [ ] 三引擎共存测试(WASM + JS + Lua 混合 filter chain)
|
||
- [ ] 安全机制测试(内存超限、执行超时、stdlib 限制)
|
||
|
||
---
|
||
|
||
## 12. 风险与缓解
|
||
|
||
| 风险 | 严重性 | 缓解措施 |
|
||
|------|--------|---------|
|
||
| Lua 开发者远少于 JS | 中 | 面向高级用户/内部工具,JS 面向社区 |
|
||
| Lua number 为 f64,整数大时精度丢失 | 低 | ID 用 string 传递(UUID v7 无此问题) |
|
||
| Lua table 索引从 1 开始 | 低 | mlua serde 自动处理 Vec→1-indexed |
|
||
| `send` feature 增加 Arc/Mutex 开销 | 低 | 开销极小,ReentrantMutex 为 parkin_lot 实现 |
|
||
| Lua 5.4 vs LuaJIT vs Luau 选择 | 低 | 固定 `lua54` + `vendored`,避免碎片化 |
|
||
| 指令计数 hook 有性能开销 | 低 | 每 1000 条指令检查一次,<1% 开销 |
|
||
| `#![deny(unsafe_code)]` 冲突 | 低 | mlua 内部封装 unsafe,不侵入主 crate |
|
||
|
||
---
|
||
|
||
## 13. 总结
|
||
|
||
Lua 作为第三插件运行时的定位:
|
||
|
||
| 运行时 | 定位 | 适用场景 |
|
||
|--------|------|----------|
|
||
| **WASM** | 安全最高 | 高安全要求、性能敏感、Rust 开发者 |
|
||
| **JS** | 生态最大 | 社区贡献、前端开发者、快速原型 |
|
||
| **Lua** | **最轻量** | 嵌入式部署、配置型插件、运维脚本、最小依赖 |
|
||
|
||
三引擎共存,按场景选择,共享同一套 Hook 调度和安全管理机制。
|