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:
10
plugin-sdk/Cargo.toml
Normal file
10
plugin-sdk/Cargo.toml
Normal 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
45
plugin-sdk/README.md
Normal 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
292
plugin-sdk/src/lib.rs
Normal 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,
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user