test+docs: 新增4个测试(66总计) + SDK API文档 + 员工soul更新

This commit is contained in:
showen
2026-03-13 05:52:26 +08:00
parent f764f27d77
commit 086b4600eb
9 changed files with 1220 additions and 55 deletions

View File

@@ -9,36 +9,58 @@ use std::ptr;
// ── 重新导出消息类型(与主程序共享 JSON 契约) ──
/// 插件信息
/// 描述插件元数据。
///
/// 主程序会在加载动态库后读取此结构体,用于展示插件名称、版本、平台信息,
/// 并帮助第三方开发者确认插件是否按预期被正确识别。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
/// 插件的人类可读名称。
pub name: String,
/// 插件版本号,通常使用语义化版本格式。
pub version: String,
/// 插件用途简介,会显示给使用者或调试工具。
pub description: String,
/// 插件面向的平台标识,例如 `linux` 或 `cross-platform`。
pub platform: String,
}
/// 单项能力测试结果
/// 表示一次能力自检中的单项结果
///
/// `self_test` 返回的列表会由主程序或测试工具消费,用于判断插件声明的能力
/// 是否已经完成最基本的运行时验证。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityTestResult {
/// 被测试的能力名称,通常与 `ShowenPlugin::capabilities` 中的条目对应。
pub capability: String,
/// 该能力是否通过自检。
pub passed: bool,
/// 面向开发者的结果说明,可用于记录失败原因或补充上下文。
pub message: String,
}
/// 消息信封
/// 插件系统中的标准消息信封
///
/// 所有跨插件、插件到主程序、或插件到管理层的通信都通过此结构体完成。
/// 它定义了统一的发送者、目的地和消息载荷格式,并通过 JSON 在 ABI 边界上传输。
#[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,
}
@@ -46,46 +68,104 @@ pub enum Destination {
/// 动态插件只需处理自己关心的消息变体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Message {
/// 发送给播放器模块的命令。
///
/// 载荷内容由播放器插件或主程序定义,通常是 JSON 对象,包含播放、暂停、
/// 跳转等控制信息。
PlayerCommand(serde_json::Value),
/// 来自播放器模块的状态快照或状态变更。
///
/// 适用于同步当前曲目、播放状态、进度等信息。
PlayerStatus(serde_json::Value),
/// 触发器事件。
///
/// `name` 表示触发器名称,`value` 表示本次触发附带的值,适合按钮、传感器
/// 或自动化规则触发。
Trigger {
/// 触发器名称。
name: String,
/// 触发器携带的值。
value: String,
},
/// 状态机切换事件。
///
/// 当系统状态从一个命名状态迁移到另一个状态时使用,便于插件同步行为。
StateChanged {
/// 切换前的状态名。
old_state: String,
/// 切换后的状态名。
new_state: String,
},
/// 请求屏幕锁定或解锁。
///
/// `true` 表示请求锁定,`false` 表示请求解除锁定。
ScreenLockRequest(bool),
/// 请求显示或隐藏光标。
///
/// `true` 表示显示光标,`false` 表示隐藏光标。
CursorVisibility(bool),
/// Wi-Fi 控制命令。
///
/// 具体 JSON 字段由网络相关插件或主程序约定。
WifiCommand(serde_json::Value),
/// Wi-Fi 操作结果字符串。
///
/// 一般用于返回简短状态、错误描述或执行结果摘要。
WifiResult(String),
/// Wi-Fi 配网成功事件。
WifiProvisioned {
/// 当前接入的 SSID。
ssid: String,
/// 当前设备获取到的 IP 地址。
ip: String,
},
/// 配置重载完成通知。
///
/// JSON 载荷通常是更新后的配置片段或完整配置。
ConfigReloaded(serde_json::Value),
/// 请求主程序重新加载配置。
ConfigReloadRequest,
/// 请求插件或主程序进入关闭流程。
Shutdown,
/// 插件就绪通知。
///
/// 字符串内容通常为就绪插件的名称。
PluginReady(String),
/// 自定义业务消息。
///
/// 当标准消息不足以表达插件间协议时,可通过 `kind` 区分消息类型,
/// 并在 `payload` 中承载自定义内容。
Custom {
/// 自定义消息类型标识。
kind: String,
/// 自定义消息的文本载荷。
payload: String,
},
}
// ── FFI 类型(与主程序 plugin_abi.rs 完全对应) ──
/// 插件实例在 FFI 边界上的不透明句柄。
pub type PluginHandle = *mut c_void;
/// 指向以 null 结尾的 C 字符串。
pub type FfiStr = *const c_char;
/// ABI 安全的字符串返回类型。
///
/// 主程序从插件取回 JSON 或错误信息时使用该结构体。内存由插件分配,
/// 再通过 `PluginVTable::free_string` 释放。
#[repr(C)]
pub struct FfiString {
/// 字符串起始指针;为空时表示没有内容。
pub ptr: *mut c_char,
/// 字节长度,不包含结尾的 `\0`。
pub len: usize,
}
impl FfiString {
/// 从 Rust `String` 构造 FFI 字符串。
///
/// 如果字符串中包含内部 `NUL` 字节,会返回空字符串表示失败。
pub fn from_string(s: String) -> Self {
match CString::new(s) {
Ok(cstr) => {
@@ -99,6 +179,7 @@ impl FfiString {
}
}
/// 构造空的 FFI 字符串。
pub fn null() -> Self {
Self {
ptr: ptr::null_mut(),
@@ -121,13 +202,19 @@ impl FfiString {
}
}
/// ABI 安全的返回值结构。
///
/// `code == 0` 表示成功,非零表示失败;`error` 中包含面向开发者的错误文本。
#[repr(C)]
pub struct FfiResult {
/// 状态码,约定 `0` 为成功,`-1` 为失败。
pub code: c_int,
/// 错误消息;成功时通常为空字符串。
pub error: FfiString,
}
impl FfiResult {
/// 创建表示成功的返回值。
pub fn ok() -> Self {
Self {
code: 0,
@@ -135,6 +222,7 @@ impl FfiResult {
}
}
/// 创建表示失败的返回值,并附带错误消息。
pub fn err(msg: String) -> Self {
Self {
code: -1,
@@ -143,42 +231,91 @@ impl FfiResult {
}
}
/// 主程序提供给插件的消息发送回调。
///
/// 插件通常无需直接调用该类型,而是通过 [`MessageSender`] 使用安全封装。
pub type SendCallback = unsafe extern "C" fn(ctx: *mut c_void, envelope_json: FfiStr);
/// 插件导出给主程序的函数表。
///
/// 该结构与主程序中的 ABI 定义一一对应。普通插件作者一般不需要手动构造,
/// 使用 [`export_plugin!`] 宏即可自动生成。
#[repr(C)]
pub struct PluginVTable {
/// 创建插件实例。
pub create: unsafe extern "C" fn() -> PluginHandle,
/// 获取 [`PluginInfo`] 的 JSON 序列化结果。
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,
/// 处理一条 JSON 序列化消息。
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),
/// 获取插件能力列表的 JSON 序列化结果。
pub get_capabilities: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
/// 获取插件自检结果列表的 JSON 序列化结果。
pub self_test: unsafe extern "C" fn(handle: PluginHandle) -> FfiString,
}
// ── 高级接口:插件作者实现此 trait ──
/// 消息发送器 — 封装 SendCallback提供安全的 Rust API
/// 消息发送器,封装底层 FFI 回调并提供安全的 Rust API
///
/// 插件在 [`ShowenPlugin::init`] 中会收到一个 `MessageSender`,之后可将其保存到
/// 插件状态中,供运行期间向其他插件或主程序发送消息。
pub struct MessageSender {
ctx: *mut c_void,
cb: SendCallback,
}
impl MessageSender {
/// 基于底层发送上下文和回调创建发送器。
///
/// 普通插件作者通常不需要手动调用该方法;主程序在初始化插件时会自动构造。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{MessageSender, SendCallback};
/// use std::ffi::c_void;
///
/// unsafe extern "C" fn callback(_: *mut c_void, _: *const std::ffi::c_char) {}
///
/// let sender = MessageSender::new(std::ptr::null_mut(), callback as SendCallback);
/// # let _ = sender;
/// ```
pub fn new(ctx: *mut c_void, cb: SendCallback) -> Self {
Self { ctx, cb }
}
/// 发送消息信封到主程序
/// 发送完整的消息信封
///
/// 当你已经手动构造好 [`Envelope`],或者需要完全控制发送者、目标和消息载荷时,
/// 直接使用此方法最合适。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{Destination, Envelope, Message, MessageSender};
///
/// # let sender: MessageSender = unimplemented!();
/// sender.send(&Envelope {
/// from: "clock".to_string(),
/// to: Destination::Manager,
/// message: Message::PluginReady("clock".to_string()),
/// });
/// ```
pub fn send(&self, envelope: &Envelope) {
if let Ok(json) = serde_json::to_string(envelope) {
if let Ok(cstr) = CString::new(json) {
@@ -187,7 +324,24 @@ impl MessageSender {
}
}
/// 便捷方法:发送消息给指定插件
/// 发送消息给指定插件
///
/// 这是构造 [`Destination::Plugin`] 的便捷方法,适合点对点通信。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{Message, MessageSender};
///
/// # let sender: MessageSender = unimplemented!();
/// sender.send_to(
/// "clock",
/// "player",
/// Message::Custom {
/// kind: "sync-request".to_string(),
/// payload: "{}".to_string(),
/// },
/// );
/// ```
pub fn send_to(&self, from: &str, to_plugin: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
@@ -196,7 +350,23 @@ impl MessageSender {
});
}
/// 便捷方法:广播消息
/// 广播消息给所有接收方。
///
/// 适合发送系统事件、状态变更或多个插件都可能关心的通知。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{Message, MessageSender};
///
/// # let sender: MessageSender = unimplemented!();
/// sender.broadcast(
/// "network",
/// Message::WifiProvisioned {
/// ssid: "Office-WiFi".to_string(),
/// ip: "192.168.1.8".to_string(),
/// },
/// );
/// ```
pub fn broadcast(&self, from: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
@@ -205,7 +375,20 @@ impl MessageSender {
});
}
/// 便捷方法:发送消息给管理层
/// 发送消息给主程序管理层
///
/// 适用于请求重载配置、上报就绪状态、或提交不属于单个插件的系统级事件。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{Message, MessageSender};
///
/// # let sender: MessageSender = unimplemented!();
/// sender.send_to_manager(
/// "config-watcher",
/// Message::ConfigReloadRequest,
/// );
/// ```
pub fn send_to_manager(&self, from: &str, message: Message) {
self.send(&Envelope {
from: from.to_string(),
@@ -219,18 +402,91 @@ impl MessageSender {
unsafe impl Send for MessageSender {}
unsafe impl Sync for MessageSender {}
/// 动态插件 trait — 插件作者实现此接口
/// 动态插件的高层 Rust 接口
///
/// 第三方插件作者只需要实现此 trait再调用 [`export_plugin!`] 宏,即可导出一个
/// 可被主程序加载的动态库。主程序会按照 `info -> init -> start -> handle_message -> stop`
/// 的生命周期驱动插件。
pub trait ShowenPlugin: Send {
/// 插件信息
/// 返回插件的静态元信息
///
/// 主程序会在插件加载后尽早调用此方法,用于识别插件、展示描述并进行兼容性判断。
///
/// # Examples
/// ```ignore
/// use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
///
/// struct DemoPlugin;
///
/// impl ShowenPlugin for DemoPlugin {
/// fn info(&self) -> PluginInfo {
/// PluginInfo {
/// name: "demo".to_string(),
/// version: "0.1.0".to_string(),
/// description: "Example plugin".to_string(),
/// platform: "linux".to_string(),
/// }
/// }
///
/// fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
/// fn start(&mut self) -> Result<(), String> { Ok(()) }
/// fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// }
/// ```
fn info(&self) -> PluginInfo;
/// 声明插件支持的功能列表(默认空)
/// 声明插件支持的能力列表。
///
/// 能力字符串通常用于主程序展示、诊断以及自测分组。默认实现返回空列表。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
/// # struct DemoPlugin;
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// fn capabilities(&self) -> Vec<String> {
/// vec!["wifi.scan".to_string(), "wifi.connect".to_string()]
/// }
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// # }
/// ```
fn capabilities(&self) -> Vec<String> {
vec![]
}
/// 运行自测,返回每项能的测试结果
/// 默认实现:所有声明的 capability 均标记为通过
/// 运行插件自测,返回每项能的测试结果
///
/// 默认实现会把 `capabilities` 返回的每个能力都标记为通过,并给出
/// `"no test defined"` 提示。若插件依赖外部设备、系统命令或网络状态,建议覆写此方法。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{CapabilityTestResult, PluginInfo, ShowenPlugin};
/// # struct DemoPlugin;
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// fn self_test(&mut self) -> Vec<CapabilityTestResult> {
/// vec![CapabilityTestResult {
/// capability: "wifi.scan".to_string(),
/// passed: true,
/// message: "scan backend reachable".to_string(),
/// }]
/// }
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// # }
/// ```
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
self.capabilities()
.into_iter()
@@ -242,30 +498,193 @@ pub trait ShowenPlugin: Send {
.collect()
}
/// 初始化,收到配置 JSON 和消息发送器
/// 初始化插件。
///
/// 主程序会把插件配置的 JSON 文本和一个 [`MessageSender`] 传入。插件通常在此阶段
/// 解析配置、保存发送器、准备运行所需资源,但不应启动长期运行任务。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{MessageSender, PluginInfo, ShowenPlugin};
/// # struct DemoPlugin {
/// # sender: Option<MessageSender>,
/// # }
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> {
/// let _config: serde_json::Value =
/// serde_json::from_str(config_json).map_err(|e| e.to_string())?;
/// self.sender = Some(sender);
/// Ok(())
/// }
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// # }
/// ```
fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String>;
/// 启动
/// 启动插件。
///
/// 在 `init` 成功之后调用。适合在这里启动后台线程、注册监听器或发送
/// [`Message::PluginReady`] 等就绪通知。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{Message, MessageSender, PluginInfo, ShowenPlugin};
/// # struct DemoPlugin {
/// # sender: Option<MessageSender>,
/// # }
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// # fn init(&mut self, _: &str, sender: MessageSender) -> Result<(), String> { self.sender = Some(sender); Ok(()) }
/// fn start(&mut self) -> Result<(), String> {
/// if let Some(sender) = &self.sender {
/// sender.send_to_manager("demo", Message::PluginReady("demo".to_string()));
/// }
/// Ok(())
/// }
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// # }
/// ```
fn start(&mut self) -> Result<(), String>;
/// 处理消息 JSON已反序列化为 Message
/// 处理主程序转发给插件的消息。
///
/// 进入此方法前JSON 已经被 SDK 反序列化为 [`Message`]。插件应只匹配自己关心的
/// 消息变体,并在必要时返回错误字符串帮助定位问题。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{Message, PluginInfo, ShowenPlugin};
/// # struct DemoPlugin;
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
/// # fn start(&mut self) -> Result<(), String> { Ok(()) }
/// fn handle_message(&mut self, message: Message) -> Result<(), String> {
/// match message {
/// Message::Shutdown => Ok(()),
/// Message::Custom { kind, payload } if kind == "sync" => {
/// let _ = payload;
/// Ok(())
/// }
/// _ => Ok(()),
/// }
/// }
/// # fn stop(&mut self) -> Result<(), String> { Ok(()) }
/// # }
/// ```
fn handle_message(&mut self, message: Message) -> Result<(), String>;
/// 停止
/// 停止插件并释放运行期资源。
///
/// 该方法通常用于停止后台线程、撤销监听、关闭文件句柄或网络连接。执行完成后,
/// 主程序可能很快销毁插件实例。
///
/// # Examples
/// ```ignore
/// # use showen_plugin_sdk::{PluginInfo, ShowenPlugin};
/// # struct DemoPlugin {
/// # running: bool,
/// # }
/// # impl ShowenPlugin for DemoPlugin {
/// # fn info(&self) -> PluginInfo {
/// # PluginInfo { name: "demo".into(), version: "0.1.0".into(), description: "Example".into(), platform: "linux".into() }
/// # }
/// # fn init(&mut self, _: &str, _: showen_plugin_sdk::MessageSender) -> Result<(), String> { Ok(()) }
/// # fn start(&mut self) -> Result<(), String> { self.running = true; Ok(()) }
/// # fn handle_message(&mut self, _: showen_plugin_sdk::Message) -> Result<(), String> { Ok(()) }
/// fn stop(&mut self) -> Result<(), String> {
/// self.running = false;
/// Ok(())
/// }
/// # }
/// ```
fn stop(&mut self) -> Result<(), String>;
}
// ── 导出宏:自动生成 extern "C" 胶水代码 ──
/// 将 ShowenPlugin 实现导出为 C FFI 接口
/// 将 [`ShowenPlugin`] 实现导出为主程序可加载的 C ABI 入口。
///
/// # 用法
/// 该宏会自动生成完整的 `extern "C"` 胶水代码,包括:
///
/// - 创建与销毁插件实例
/// - 把 `PluginInfo`、capabilities、自测结果序列化为 JSON
/// - 在 ABI 边界上捕获 panic避免传播到主程序
/// - 把传入的 JSON 消息反序列化为 [`Message`] 后再调用 trait 方法
/// - 导出主程序约定名称的 `showen_plugin_vtable`
///
/// 传入参数:
///
/// - 第一个参数是插件具体类型
/// - 第二个参数是无参构造表达式,例如 `MyPlugin::new()`
///
/// 使用此宏时,插件类型必须实现 [`ShowenPlugin`],且构造表达式应返回该类型实例。
///
/// # Examples
/// ```ignore
/// struct MyPlugin { ... }
/// impl ShowenPlugin for MyPlugin { ... }
/// use showen_plugin_sdk::{
/// export_plugin, Message, MessageSender, PluginInfo, ShowenPlugin,
/// };
///
/// export_plugin!(MyPlugin, MyPlugin::new);
/// struct MyPlugin {
/// sender: Option<MessageSender>,
/// }
///
/// impl MyPlugin {
/// fn new() -> Self {
/// Self { sender: None }
/// }
/// }
///
/// impl ShowenPlugin for MyPlugin {
/// fn info(&self) -> PluginInfo {
/// PluginInfo {
/// name: "my-plugin".to_string(),
/// version: "0.1.0".to_string(),
/// description: "Example ShowenV2 plugin".to_string(),
/// platform: "linux".to_string(),
/// }
/// }
///
/// fn init(&mut self, _config_json: &str, sender: MessageSender) -> Result<(), String> {
/// self.sender = Some(sender);
/// Ok(())
/// }
///
/// fn start(&mut self) -> Result<(), String> {
/// if let Some(sender) = &self.sender {
/// sender.send_to_manager(
/// "my-plugin",
/// Message::PluginReady("my-plugin".to_string()),
/// );
/// }
/// Ok(())
/// }
///
/// fn handle_message(&mut self, _message: Message) -> Result<(), String> {
/// Ok(())
/// }
///
/// fn stop(&mut self) -> Result<(), String> {
/// Ok(())
/// }
/// }
///
/// export_plugin!(MyPlugin, MyPlugin::new());
/// ```
///
/// 生成的 `showen_plugin_vtable` 会被主程序在加载动态库后自动发现,因此插件 crate
/// 通常只需在 `lib.rs` 末尾调用一次该宏。
#[macro_export]
macro_rules! export_plugin {
($plugin_type:ty, $constructor:expr) => {