feat: 实现动态插件系统 (6阶段完成)

- 阶段1: 消息类型序列化 (Serialize/Deserialize, &'static str → String)
- 阶段2: FFI 边界类型 + Plugin SDK (plugin_abi, showen-plugin-sdk crate)
- 阶段3: PluginLoader + DynamicPlugin (libloading 动态加载 .so)
- 阶段4: 版本管理 + 错误策略 (VersionManager, PluginState, 自动回退)
- 阶段5: 远程仓库客户端 (HTTP 下载 + tar.gz 安装)
- 阶段6: 示例插件 + HTTP 管理 API + 全目录 README 文档

54/54 测试通过,0 warnings。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
showen
2026-03-13 03:38:08 +08:00
parent 5dcc1ad98e
commit 7135f28545
62 changed files with 3501 additions and 299 deletions

10
plugin-sdk/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "showen-plugin-sdk"
version = "0.2.0"
authors = ["showen"]
edition = "2018"
description = "SDK for building ShowenV2 dynamic plugins"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

45
plugin-sdk/README.md Normal file
View File

@@ -0,0 +1,45 @@
# ShowenV2 Plugin SDK
插件开发者使用此 SDK 编写动态插件(`.so` 文件),运行时由主程序动态加载。
## 核心接口
- **`ShowenPlugin` trait**: 插件作者实现的高级接口init / start / handle_message / stop
- **`MessageSender`**: 封装 FFI SendCallback插件通过它向主程序发送消息
- **`export_plugin!` 宏**: 自动生成 `extern "C"` 胶水代码,导出 `PluginVTable`
## 类型
SDK 独立定义了与主程序 JSON 兼容的消息类型:
- `PluginInfo`, `Envelope`, `Destination`, `Message`
## 用法
```rust
use showen_plugin_sdk::{export_plugin, ShowenPlugin, MessageSender, PluginInfo, Message};
struct MyPlugin { sender: Option<MessageSender> }
impl ShowenPlugin for MyPlugin {
fn info(&self) -> PluginInfo { /* ... */ }
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> { Ok(()) }
fn start(&mut self) -> Result<(), String> { Ok(()) }
fn handle_message(&mut self, msg_json: &str) -> Result<(), String> { Ok(()) }
fn stop(&mut self) -> Result<(), String> { Ok(()) }
}
export_plugin!(MyPlugin, MyPlugin::new);
```
## 编译
```bash
cd plugin-sdk
cargo build
```
插件项目在 `Cargo.toml` 中依赖此 SDK
```toml
[dependencies]
showen-plugin-sdk = { path = "../plugin-sdk" }
```

292
plugin-sdk/src/lib.rs Normal file
View File

@@ -0,0 +1,292 @@
//! ShowenV2 Plugin SDK
//!
//! 插件开发者使用此 SDK 编写动态插件。
//! 实现 `ShowenPlugin` trait然后用 `export_plugin!` 宏导出。
use serde::{Deserialize, Serialize};
use std::ffi::{c_char, c_int, c_void, CString};
use std::ptr;
// ── 重新导出消息类型(与主程序共享 JSON 契约) ──
/// 插件信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: String,
pub platform: String,
}
/// 消息信封
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub from: String,
pub to: Destination,
pub message: Message,
}
/// 消息目的地
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Destination {
Plugin(String),
Broadcast,
Manager,
}
/// 消息类型 — 与主程序 Message 枚举保持 JSON 兼容
/// 动态插件只需处理自己关心的消息变体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Message {
PlayerCommand(serde_json::Value),
PlayerStatus(serde_json::Value),
Trigger { name: String, value: String },
StateChanged { old_state: String, new_state: String },
ScreenLockRequest(bool),
CursorVisibility(bool),
WifiCommand(serde_json::Value),
WifiResult(String),
WifiProvisioned { ssid: String, ip: String },
ConfigReloadRequest,
Shutdown,
PluginReady(String),
Custom { kind: String, payload: String },
}
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
pub type PluginHandle = *mut c_void;
pub type FfiStr = *const c_char;
#[repr(C)]
pub struct FfiString {
pub ptr: *mut c_char,
pub len: usize,
}
impl FfiString {
pub fn from_string(s: String) -> Self {
match CString::new(s) {
Ok(cstr) => {
let len = cstr.as_bytes().len();
Self {
ptr: cstr.into_raw(),
len,
}
}
Err(_) => Self::null(),
}
}
pub fn null() -> Self {
Self {
ptr: ptr::null_mut(),
len: 0,
}
}
}
#[repr(C)]
pub struct FfiResult {
pub code: c_int,
pub error: FfiString,
}
impl FfiResult {
pub fn ok() -> Self {
Self {
code: 0,
error: FfiString::null(),
}
}
pub fn err(msg: String) -> Self {
Self {
code: -1,
error: FfiString::from_string(msg),
}
}
}
pub type SendCallback = unsafe extern "C" fn(envelope_json: FfiStr);
#[repr(C)]
pub struct PluginVTable {
pub create: unsafe extern "C" fn() -> PluginHandle,
pub get_info: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
pub init: unsafe extern "C" fn(handle: PluginHandle, config_json: FfiStr, send_cb: SendCallback) -> FfiResult,
pub start: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub handle_message: unsafe extern "C" fn(handle: PluginHandle, message_json: FfiStr) -> FfiResult,
pub stop: unsafe extern "C" fn(handle: PluginHandle) -> FfiResult,
pub destroy: unsafe extern "C" fn(handle: PluginHandle),
}
// ── 高级接口:插件作者实现此 trait ──
/// 消息发送器 — 封装 SendCallback提供安全的 Rust API
pub struct MessageSender {
cb: SendCallback,
}
impl MessageSender {
pub fn new(cb: SendCallback) -> Self {
Self { cb }
}
/// 发送消息信封到主程序
pub fn send(&self, envelope: &Envelope) {
if let Ok(json) = serde_json::to_string(envelope) {
if let Ok(cstr) = CString::new(json) {
unsafe { (self.cb)(cstr.as_ptr()) };
}
}
}
/// 便捷方法:发送消息给指定插件
pub fn send_to(&self, from: &str, to_plugin: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
to: Destination::Plugin(to_plugin.to_string()),
message,
});
}
/// 便捷方法:广播消息
pub fn broadcast(&self, from: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
to: Destination::Broadcast,
message,
});
}
/// 便捷方法:发送消息给管理层
pub fn send_to_manager(&self, from: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
to: Destination::Manager,
message,
});
}
}
// SendCallback 是 extern "C" fn 指针,可跨线程安全传递
unsafe impl Send for MessageSender {}
unsafe impl Sync for MessageSender {}
/// 动态插件 trait — 插件作者实现此接口
pub trait ShowenPlugin: Send {
/// 插件信息
fn info(&self) -> PluginInfo;
/// 初始化,收到配置 JSON 和消息发送器
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
/// 启动
fn start(&mut self) -> Result<(), String>;
/// 处理消息 JSON已反序列化为 Message
fn handle_message(&mut self, message: Message) -> Result<(), String>;
/// 停止
fn stop(&mut self) -> Result<(), String>;
}
// ── 导出宏:自动生成 extern "C" 胶水代码 ──
/// 将 ShowenPlugin 实现导出为 C FFI 接口
///
/// # 用法
/// ```ignore
/// struct MyPlugin { ... }
/// impl ShowenPlugin for MyPlugin { ... }
///
/// export_plugin!(MyPlugin, MyPlugin::new);
/// ```
#[macro_export]
macro_rules! export_plugin {
($plugin_type:ty, $constructor:expr) => {
unsafe extern "C" fn __showen_create() -> $crate::PluginHandle {
let plugin: Box<$plugin_type> = Box::new($constructor);
Box::into_raw(plugin) as $crate::PluginHandle
}
unsafe extern "C" fn __showen_get_info(handle: $crate::PluginHandle) -> $crate::FfiString {
let plugin = unsafe { &*(handle as *const $plugin_type) };
let info = <$plugin_type as $crate::ShowenPlugin>::info(plugin);
match serde_json::to_string(&info) {
Ok(json) => $crate::FfiString::from_string(json),
Err(_) => $crate::FfiString::null(),
}
}
unsafe extern "C" fn __showen_init(
handle: $crate::PluginHandle,
config_json: $crate::FfiStr,
send_cb: $crate::SendCallback,
) -> $crate::FfiResult {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
let config = match unsafe { std::ffi::CStr::from_ptr(config_json) }.to_str() {
Ok(s) => s,
Err(e) => return $crate::FfiResult::err(format!("invalid config UTF-8: {e}")),
};
let sender = $crate::MessageSender::new(send_cb);
match <$plugin_type as $crate::ShowenPlugin>::init(plugin, config, sender) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
}
unsafe extern "C" fn __showen_start(handle: $crate::PluginHandle) -> $crate::FfiResult {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
match <$plugin_type as $crate::ShowenPlugin>::start(plugin) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
}
unsafe extern "C" fn __showen_handle_message(
handle: $crate::PluginHandle,
message_json: $crate::FfiStr,
) -> $crate::FfiResult {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
let json_str = match unsafe { std::ffi::CStr::from_ptr(message_json) }.to_str() {
Ok(s) => s,
Err(e) => return $crate::FfiResult::err(format!("invalid message UTF-8: {e}")),
};
let message: $crate::Message = match serde_json::from_str(json_str) {
Ok(m) => m,
Err(e) => return $crate::FfiResult::err(format!("invalid message JSON: {e}")),
};
match <$plugin_type as $crate::ShowenPlugin>::handle_message(plugin, message) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
}
unsafe extern "C" fn __showen_stop(handle: $crate::PluginHandle) -> $crate::FfiResult {
let plugin = unsafe { &mut *(handle as *mut $plugin_type) };
match <$plugin_type as $crate::ShowenPlugin>::stop(plugin) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => $crate::FfiResult::err(e),
}
}
unsafe extern "C" fn __showen_destroy(handle: $crate::PluginHandle) {
if !handle.is_null() {
drop(unsafe { Box::from_raw(handle as *mut $plugin_type) });
}
}
#[no_mangle]
pub static showen_plugin_vtable: $crate::PluginVTable = $crate::PluginVTable {
create: __showen_create,
get_info: __showen_get_info,
init: __showen_init,
start: __showen_start,
handle_message: __showen_handle_message,
stop: __showen_stop,
destroy: __showen_destroy,
};
};
}