//! 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 CapabilityTestResult { pub capability: String, pub passed: bool, pub message: 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, }, ConfigReloaded(serde_json::Value), 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, } } /// 复制为 Rust String(不释放底层内存) /// /// # Safety /// ptr 必须指向有效的 null-terminated C 字符串 pub unsafe fn to_string(&self) -> Option { if self.ptr.is_null() { return None; } unsafe { std::ffi::CStr::from_ptr(self.ptr) } .to_str() .ok() .map(str::to_owned) } } #[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(ctx: *mut c_void, 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_ctx: *mut c_void, 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 free_string: unsafe extern "C" fn(s: FfiString), pub destroy: unsafe extern "C" fn(handle: PluginHandle), pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString, pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString, } // ── 高级接口:插件作者实现此 trait ── /// 消息发送器 — 封装 SendCallback,提供安全的 Rust API pub struct MessageSender { ctx: *mut c_void, cb: SendCallback, } impl MessageSender { pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self { Self { ctx, 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)(self.ctx, 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; /// 声明插件支持的功能列表(默认空) fn capabilities(&self) -> Vec { vec![] } /// 运行自测,返回每项功能的测试结果 /// 默认实现:所有声明的 capability 均标记为通过 fn self_test(&mut self) -> Vec { self.capabilities() .into_iter() .map(|c| CapabilityTestResult { capability: c, passed: true, message: "no test defined".into(), }) .collect() } /// 初始化,收到配置 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) => { fn __showen_panic_error(payload: Box) -> String { let msg = if let Some(s) = payload.downcast_ref::<&'static str>() { (*s).to_string() } else if let Some(s) = payload.downcast_ref::() { s.clone() } else { "unknown panic".to_string() }; format!("plugin panicked: {}", msg) } unsafe extern "C" fn __showen_create() -> $crate::PluginHandle { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let plugin: Box<$plugin_type> = Box::new($constructor); Box::into_raw(plugin) as $crate::PluginHandle })) { Ok(handle) => handle, Err(_) => std::ptr::null_mut(), } } unsafe extern "C" fn __showen_get_info(handle: $crate::PluginHandle) -> $crate::FfiString { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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(), } })) { Ok(info) => info, Err(_) => $crate::FfiString::null(), } } unsafe extern "C" fn __showen_init( handle: $crate::PluginHandle, config_json: $crate::FfiStr, send_ctx: *mut std::ffi::c_void, send_cb: $crate::SendCallback, ) -> $crate::FfiResult { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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_ctx, send_cb); match <$plugin_type as $crate::ShowenPlugin>::init(plugin, config, sender) { Ok(()) => $crate::FfiResult::ok(), Err(e) => $crate::FfiResult::err(e), } })) { Ok(result) => result, Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)), } } unsafe extern "C" fn __showen_start(handle: $crate::PluginHandle) -> $crate::FfiResult { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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), } })) { Ok(result) => result, Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)), } } unsafe extern "C" fn __showen_handle_message( handle: $crate::PluginHandle, message_json: $crate::FfiStr, ) -> $crate::FfiResult { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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), } })) { Ok(result) => result, Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)), } } unsafe extern "C" fn __showen_stop(handle: $crate::PluginHandle) -> $crate::FfiResult { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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), } })) { Ok(result) => result, Err(payload) => $crate::FfiResult::err(__showen_panic_error(payload)), } } unsafe extern "C" fn __showen_destroy(handle: $crate::PluginHandle) { if let Ok(()) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if !handle.is_null() { drop(unsafe { Box::from_raw(handle as *mut $plugin_type) }); } })) { let _ = (); } } unsafe extern "C" fn __showen_free_string(s: $crate::FfiString) { if !s.ptr.is_null() { drop(unsafe { std::ffi::CString::from_raw(s.ptr) }); } } unsafe extern "C" fn __showen_get_capabilities( handle: $crate::PluginHandle, ) -> $crate::FfiString { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let plugin = unsafe { &*(handle as *const $plugin_type) }; let caps = <$plugin_type as $crate::ShowenPlugin>::capabilities(plugin); match serde_json::to_string(&caps) { Ok(json) => $crate::FfiString::from_string(json), Err(_) => $crate::FfiString::from_string("[]".to_string()), } })) { Ok(caps) => caps, Err(_) => $crate::FfiString::null(), } } unsafe extern "C" fn __showen_self_test(handle: $crate::PluginHandle) -> $crate::FfiString { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let plugin = unsafe { &mut *(handle as *mut $plugin_type) }; let results = <$plugin_type as $crate::ShowenPlugin>::self_test(plugin); match serde_json::to_string(&results) { Ok(json) => $crate::FfiString::from_string(json), Err(_) => $crate::FfiString::from_string("[]".to_string()), } })) { Ok(results) => results, Err(_) => $crate::FfiString::null(), } } #[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, free_string: __showen_free_string, destroy: __showen_destroy, get_capabilities: __showen_get_capabilities, self_test: __showen_self_test, }; }; }