use crate::{
NativeCommandContext, NativeLifecycleContext, NativeServiceContext, PluginEvent, PluginService,
ServiceEnvelopeKind, ServiceResponse, TypedServiceRegistry, decode_service_envelope,
encode_service_envelope,
};
use std::cell::RefCell;
use std::ffi::{CString, c_char};
use std::future::Future;
use std::ptr;
use std::sync::{OnceLock, RwLock};
use std::time::{Duration, Instant};
const PLUGIN_LOCK_LATENCY_BUDGET: Duration = Duration::from_millis(10);
pub const EXIT_OK: i32 = 0;
pub const EXIT_ERROR: i32 = 1;
pub const EXIT_USAGE: i32 = 64;
pub const EXIT_UNAVAILABLE: i32 = 70;
#[derive(Debug, Clone)]
pub struct PluginCommandError {
pub code: i32,
pub message: String,
}
impl PluginCommandError {
#[must_use]
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
#[must_use]
pub fn failed(message: impl Into<String>) -> Self {
Self::new(EXIT_ERROR, message)
}
#[must_use]
pub fn unknown_command(name: &str) -> Self {
Self::new(EXIT_USAGE, format!("unknown command '{name}'"))
}
#[must_use]
pub fn invalid_arguments(message: impl Into<String>) -> Self {
Self::new(EXIT_USAGE, message)
}
#[must_use]
pub fn unavailable(message: impl Into<String>) -> Self {
Self::new(EXIT_UNAVAILABLE, message)
}
}
impl std::fmt::Display for PluginCommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PluginCommandError {}
impl From<String> for PluginCommandError {
fn from(message: String) -> Self {
Self::failed(message)
}
}
impl From<&str> for PluginCommandError {
fn from(message: &str) -> Self {
Self::failed(message)
}
}
impl From<std::io::Error> for PluginCommandError {
fn from(error: std::io::Error) -> Self {
Self::failed(error.to_string())
}
}
impl From<serde_json::Error> for PluginCommandError {
fn from(error: serde_json::Error) -> Self {
Self::failed(error.to_string())
}
}
impl From<toml::de::Error> for PluginCommandError {
fn from(error: toml::de::Error) -> Self {
Self::failed(error.to_string())
}
}
impl From<Box<dyn std::error::Error>> for PluginCommandError {
fn from(error: Box<dyn std::error::Error>) -> Self {
Self::failed(error.to_string())
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for PluginCommandError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
Self::failed(error.to_string())
}
}
fn result_to_exit_code(result: Result<i32, PluginCommandError>) -> i32 {
match result {
Ok(code) => code,
Err(error) => error.code,
}
}
thread_local! {
static LAST_COMMAND_ERROR: RefCell<Option<PluginCommandError>> = const { RefCell::new(None) };
}
#[must_use]
pub fn take_last_command_error() -> Option<PluginCommandError> {
LAST_COMMAND_ERROR.with(|slot| slot.borrow_mut().take())
}
fn store_last_command_error(error: PluginCommandError) {
LAST_COMMAND_ERROR.with(|slot| {
*slot.borrow_mut() = Some(error);
});
}
const SERVICE_STATUS_OK: i32 = 0;
const SERVICE_STATUS_INVALID_ARGUMENT: i32 = 2;
const SERVICE_STATUS_DECODE_FAILED: i32 = 3;
const SERVICE_STATUS_BUFFER_TOO_SMALL: i32 = 4;
const SERVICE_STATUS_ENCODE_FAILED: i32 = 5;
const SERVICE_STATUS_PLUGIN_UNAVAILABLE: i32 = 70;
pub trait RustPlugin: Default + Send + 'static {
type Contract: crate::PluginContract;
fn run_command(&mut self, _context: NativeCommandContext) -> Result<i32, PluginCommandError> {
Err(PluginCommandError::unknown_command(""))
}
fn activate(&mut self, _context: NativeLifecycleContext) -> Result<i32, PluginCommandError> {
Ok(EXIT_OK)
}
fn activate_with_async(
&mut self,
context: NativeLifecycleContext,
_async_handle: HostAsyncHandle,
) -> Result<i32, PluginCommandError> {
self.activate(context)
}
fn deactivate(&mut self, _context: NativeLifecycleContext) -> Result<i32, PluginCommandError> {
Ok(EXIT_OK)
}
fn handle_event(&mut self, _event: PluginEvent) -> Result<i32, PluginCommandError> {
Ok(EXIT_OK)
}
fn invoke_service(&self, context: NativeServiceContext) -> ServiceResponse {
ServiceResponse::error(
"unsupported_service",
format!(
"plugin '{}' does not implement service '{}:{}'",
context.plugin_id, context.request.service.interface_id, context.request.operation,
),
)
}
fn register_typed_services(
&self,
_context: TypedServiceRegistrationContext<'_>,
_registry: &mut TypedServiceRegistry,
) {
}
fn declared_services() -> crate::Result<Vec<PluginService>>
where
Self: Sized,
{
<Self::Contract as crate::PluginContract>::service_declarations()
}
}
#[derive(Debug, Clone)]
pub struct TypedServiceRegistrationContext<'a> {
pub plugin_id: &'a str,
pub host_kernel_bridge: Option<&'a crate::HostKernelBridge>,
pub required_capabilities: &'a [String],
pub provided_capabilities: &'a [String],
pub services: &'a [crate::RegisteredService],
pub available_capabilities: &'a [String],
pub enabled_plugins: &'a [String],
pub plugin_search_roots: &'a [String],
pub host: &'a crate::HostMetadata,
pub connection: &'a crate::HostConnectionInfo,
pub plugin_settings_map: &'a std::collections::BTreeMap<String, toml::Value>,
}
#[derive(Debug, Clone)]
pub struct HostAsyncHandle {
inner: switchy::unsync::runtime::Handle,
}
impl HostAsyncHandle {
#[must_use]
pub const fn new(inner: switchy::unsync::runtime::Handle) -> Self {
Self { inner }
}
pub fn try_current() -> std::result::Result<Self, String> {
switchy::unsync::runtime::Handle::try_current()
.map(Self::new)
.map_err(|error| error.to_string())
}
pub fn spawn<T: Send + 'static>(
&self,
future: impl Future<Output = T> + Send + 'static,
) -> switchy::unsync::task::JoinHandle<T> {
self.inner.spawn(future)
}
pub fn spawn_with_name<T: Send + 'static>(
&self,
name: &str,
future: impl Future<Output = T> + Send + 'static,
) -> switchy::unsync::task::JoinHandle<T> {
self.inner.spawn_with_name(name, future)
}
pub fn spawn_blocking<T: Send + 'static>(
&self,
f: impl FnOnce() -> T + Send + 'static,
) -> switchy::unsync::task::JoinHandle<T> {
self.inner.spawn_blocking(f)
}
}
#[doc(hidden)]
pub fn plugin_instance<P: RustPlugin>(
instance: &'static OnceLock<RwLock<P>>,
) -> &'static RwLock<P> {
instance.get_or_init(|| RwLock::new(P::default()))
}
#[doc(hidden)]
pub fn register_typed_services_bundled<P: RustPlugin>(
instance: &'static RwLock<P>,
context: TypedServiceRegistrationContext<'_>,
) -> TypedServiceRegistry {
let mut registry = TypedServiceRegistry::new();
if let Ok(plugin) = instance.read() {
plugin.register_typed_services(context, &mut registry);
}
registry
}
#[doc(hidden)]
pub fn activate_with_async_bundled<P: RustPlugin>(
instance: &'static RwLock<P>,
context: NativeLifecycleContext,
async_handle: HostAsyncHandle,
) -> i32 {
instance.write().map_or(EXIT_UNAVAILABLE, |mut plugin| {
result_to_exit_code(plugin.activate_with_async(context, async_handle))
})
}
#[doc(hidden)]
pub fn declared_services_bundled<P: RustPlugin>() -> crate::Result<Vec<PluginService>> {
P::declared_services()
}
#[doc(hidden)]
pub fn manifest_toml_ptr(
manifest_toml: &'static str,
cached: &'static OnceLock<Option<CString>>,
) -> *const c_char {
let cached = cached.get_or_init(|| CString::new(manifest_toml).ok());
cached
.as_ref()
.map_or(std::ptr::null(), |value| value.as_ptr())
}
#[doc(hidden)]
pub fn run_command_export<P: RustPlugin>(
instance: &'static RwLock<P>,
input_ptr: *const u8,
input_len: usize,
) -> i32 {
parse_binary_input::<NativeCommandContext>(input_ptr, input_len, 2, 3).map_or_else(
|code| code,
|payload| {
instance.write().map_or(EXIT_UNAVAILABLE, |mut plugin| {
let result = plugin.run_command(payload);
if let Err(error) = &result {
store_last_command_error(error.clone());
}
result_to_exit_code(result)
})
},
)
}
#[doc(hidden)]
pub fn activate_export<P: RustPlugin>(
instance: &'static RwLock<P>,
input_ptr: *const u8,
input_len: usize,
) -> i32 {
parse_binary_input::<NativeLifecycleContext>(input_ptr, input_len, 2, 3).map_or_else(
|code| code,
|payload| {
instance.write().map_or(EXIT_UNAVAILABLE, |mut plugin| {
result_to_exit_code(plugin.activate(payload))
})
},
)
}
#[doc(hidden)]
pub fn deactivate_export<P: RustPlugin>(
instance: &'static RwLock<P>,
input_ptr: *const u8,
input_len: usize,
) -> i32 {
parse_binary_input::<NativeLifecycleContext>(input_ptr, input_len, 2, 3).map_or_else(
|code| code,
|payload| {
instance.write().map_or(EXIT_UNAVAILABLE, |mut plugin| {
result_to_exit_code(plugin.deactivate(payload))
})
},
)
}
#[doc(hidden)]
pub fn handle_event_export<P: RustPlugin>(
instance: &'static RwLock<P>,
input_ptr: *const u8,
input_len: usize,
) -> i32 {
parse_binary_input::<PluginEvent>(input_ptr, input_len, 2, 3).map_or_else(
|code| code,
|payload| {
instance.write().map_or(EXIT_UNAVAILABLE, |mut plugin| {
result_to_exit_code(plugin.handle_event(payload))
})
},
)
}
#[doc(hidden)]
pub fn invoke_service_export<P: RustPlugin>(
instance: &'static RwLock<P>,
input_ptr: *const u8,
input_len: usize,
output_ptr: *mut u8,
output_capacity: usize,
output_len: *mut usize,
) -> i32 {
if input_ptr.is_null() || output_len.is_null() {
return SERVICE_STATUS_INVALID_ARGUMENT;
}
let input = unsafe { std::slice::from_raw_parts(input_ptr, input_len) };
let Ok((request_id, context)) =
decode_service_envelope::<NativeServiceContext>(input, ServiceEnvelopeKind::Request)
else {
return SERVICE_STATUS_DECODE_FAILED;
};
let plugin_id = context.plugin_id.clone();
let interface_id = context.request.service.interface_id.clone();
let operation = context.request.operation.clone();
let lock_started = Instant::now();
let response = {
let Ok(plugin) = instance.read() else {
return SERVICE_STATUS_PLUGIN_UNAVAILABLE;
};
let lock_wait = lock_started.elapsed();
if lock_wait > PLUGIN_LOCK_LATENCY_BUDGET {
tracing::warn!(
request_id,
plugin_id = plugin_id.as_str(),
interface_id = interface_id.as_str(),
operation = operation.as_str(),
wait_us = lock_wait.as_micros(),
"plugin service read lock wait exceeded latency budget"
);
}
let call_started = Instant::now();
let response = plugin.invoke_service(context);
let lock_hold = call_started.elapsed();
if lock_hold > PLUGIN_LOCK_LATENCY_BUDGET {
tracing::warn!(
request_id,
plugin_id = plugin_id.as_str(),
interface_id = interface_id.as_str(),
operation = operation.as_str(),
hold_us = lock_hold.as_micros(),
"plugin service read lock hold exceeded latency budget"
);
}
response
};
let Ok(encoded) = encode_service_envelope(request_id, ServiceEnvelopeKind::Response, &response)
else {
return SERVICE_STATUS_ENCODE_FAILED;
};
unsafe {
*output_len = encoded.len();
}
if output_ptr.is_null() || encoded.len() > output_capacity {
return SERVICE_STATUS_BUFFER_TOO_SMALL;
}
unsafe {
ptr::copy_nonoverlapping(encoded.as_ptr(), output_ptr, encoded.len());
}
SERVICE_STATUS_OK
}
fn parse_binary_input<T>(
input_ptr: *const u8,
input_len: usize,
null_code: i32,
parse_code: i32,
) -> Result<T, i32>
where
T: serde::de::DeserializeOwned,
{
if input_ptr.is_null() {
return Err(null_code);
}
let payload = unsafe { std::slice::from_raw_parts(input_ptr, input_len) };
bmux_codec::from_bytes(payload).map_err(|_| parse_code)
}