Files
ShowenV2/plugin-sdk/src/lib.rs

441 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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,
};
};
}