441 lines
14 KiB
Rust
441 lines
14 KiB
Rust
//! 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<String> {
|
||
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<String> {
|
||
vec![]
|
||
}
|
||
|
||
/// 运行自测,返回每项功能的测试结果
|
||
/// 默认实现:所有声明的 capability 均标记为通过
|
||
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
||
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<dyn std::any::Any + Send>) -> String {
|
||
let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
|
||
(*s).to_string()
|
||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||
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,
|
||
};
|
||
};
|
||
}
|