use std::ffi::c_void;
use std::sync::{Arc, Mutex};
use rpi_plugin_sdk::{
EventHandlerFn, EventTag, FreeStringFn, PluginApiVt, ProviderRequestFn, RenderFn,
ResourcesDiscoverFn, RuntimeActionFn, StablePluginEvent, StableToolSchema, StbString,
StbStringRef, ToolCancelFn, ToolDestroyFn, ToolExecuteFn, ToolPollFn,
};
use thiserror::Error;
pub use actions::{
reload_callback_from_mailbox, trampoline_runtime_action, ActionBridge, ReloadMailbox,
RuntimeActionHost,
};
pub use loader::{
load_dir, load_one, load_session, load_session_mixed, merge_registries, ExtensionSession,
LoadedPlugin, PluginKeepalive, PluginLoadError,
};
pub use provider::PluggableProvider;
pub use provider_hooks::ExtensionProviderHooks;
pub use registry::{
assert_active, ExtensionRegistry, ExtensionTool, RegisteredHandler, RegisteredProvider,
RegisteredRenderer, RegisteredRendererKind, RegistryEntry, RegistrySnapshot,
ResourcesDiscoverHandler,
};
pub use resources::{emit_resources_discover, DiscoveredResources};
pub use tool::{PluginToolAdapter, PluginToolHandle};
pub use translate::{ExtensionEmitter, TeeEmitter};
mod actions;
mod loader;
mod provider;
mod provider_hooks;
mod registry;
mod resources;
mod tool;
mod translate;
pub trait PluginDiagnostics: Send + Sync {
fn warn(&self, message: &str);
fn unsupported(&self, message: &str);
}
#[derive(Default)]
pub struct NullDiagnostics;
impl PluginDiagnostics for NullDiagnostics {
fn warn(&self, _message: &str) {}
fn unsupported(&self, _message: &str) {}
}
#[derive(Debug, Error)]
pub enum PluginToolError {
#[error("plugin execute returned a null handle (allocation failure)")]
NullHandle,
#[error("plugin error: {0}")]
Plugin(String),
#[error("runtime unavailable: {0}")]
NoRuntime(String),
}
pub extern "C" fn host_free_string(s: StbString) {
if s.is_empty() || s.ptr.is_null() {
return;
}
unsafe {
let slice = std::slice::from_raw_parts(s.ptr as *const u8, s.len);
let _ = Box::from_raw(slice as *const [u8] as *mut [u8]);
}
}
pub struct HostApi {
registry: Mutex<Option<ExtensionRegistry>>,
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Option<Arc<ActionBridge>>,
}
impl HostApi {
pub fn new(registry: ExtensionRegistry, diagnostics: Arc<dyn PluginDiagnostics>) -> Arc<Self> {
Arc::new(Self {
registry: Mutex::new(Some(registry)),
diagnostics,
action_bridge: None,
})
}
pub fn with_action_bridge(
registry: ExtensionRegistry,
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Arc<ActionBridge>,
) -> Arc<Self> {
Arc::new(Self {
registry: Mutex::new(Some(registry)),
diagnostics,
action_bridge: Some(action_bridge),
})
}
fn with_registry<R>(&self, f: impl FnOnce(&mut ExtensionRegistry) -> R) -> Option<R> {
let mut guard = self.registry.lock().expect("host api registry lock");
guard.as_mut().map(f)
}
pub fn take_registry(&self) -> Option<ExtensionRegistry> {
self.registry.lock().expect("host api registry lock").take()
}
pub fn build_vtable(self: &Arc<Self>) -> PluginApiVt {
let (runtime_action_fn, ud) = match &self.action_bridge {
Some(bridge) => (
trampoline_runtime_action as RuntimeActionFn,
Arc::as_ptr(bridge) as *mut c_void,
),
None => (
stub_runtime_action as RuntimeActionFn,
Arc::as_ptr(self) as *mut c_void,
),
};
PluginApiVt {
free_string: host_free_string,
register_tool: Some(trampoline_register_tool),
register_command: Some(trampoline_register_command),
register_shortcut: Some(trampoline_register_shortcut),
register_flag: Some(trampoline_register_flag),
register_provider: Some(trampoline_register_provider), register_message_renderer: Some(trampoline_register_message_renderer), register_markdown_transformer: Some(trampoline_register_markdown_transformer), register_entry_renderer: Some(trampoline_register_entry_renderer), register_event_handler: Some(trampoline_register_event_handler),
register_resources_discover: Some(trampoline_register_resources_discover),
runtime_action: runtime_action_fn,
dispatch_event: Some(trampoline_dispatch_event),
user_data: ud,
}
}
}
thread_local! {
static CURRENT_HOST_API: std::cell::Cell<*const HostApi> = std::cell::Cell::new(std::ptr::null());
}
unsafe fn set_current_api(api: &Arc<HostApi>) {
CURRENT_HOST_API.with(|c| c.set(Arc::as_ptr(api) as *const HostApi));
}
fn clear_current_api() {
CURRENT_HOST_API.with(|c| c.set(std::ptr::null()));
}
fn with_current_api<R>(f: impl FnOnce(&HostApi) -> R) -> Option<R> {
let ptr = CURRENT_HOST_API.with(|c| c.get());
if ptr.is_null() {
return None;
}
let api = unsafe { &*ptr };
Some(f(api))
}
fn current_api_present() -> bool {
let ptr = CURRENT_HOST_API.with(|c| c.get());
!ptr.is_null()
}
extern "C" fn trampoline_register_tool(
schema: *const StableToolSchema,
execute_fn: ToolExecuteFn,
poll_fn: ToolPollFn,
cancel_fn: ToolCancelFn,
destroy_fn: ToolDestroyFn,
plugin_free_string: FreeStringFn,
) -> i32 {
if !current_api_present() {
return -1;
}
if schema.is_null() {
return 1;
}
let (name, description, parameters_value, schema_owned) = unsafe {
let s = &*schema;
(
s.name.to_string_lossy(),
s.description.to_string_lossy(),
serde_json::from_str::<serde_json::Value>(&s.parameters.to_string_lossy())
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new())),
*s,
)
};
plugin_free_string(schema_owned.name);
plugin_free_string(schema_owned.description);
plugin_free_string(schema_owned.parameters);
let tool = rpi_ai::types::Tool {
name,
description,
parameters: rpi_ai::types::Schema::new(parameters_value),
constrained_sampling: None,
};
let handle = PluginToolHandle {
execute_fn,
poll_fn,
cancel_fn,
destroy_fn,
plugin_free_string,
};
let ok =
with_current_api(
|api| match api.with_registry(|reg| reg.register_tool(tool, handle)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_command(
name: StbStringRef,
description: StbStringRef,
handler: rpi_plugin_sdk::CommandHandlerFn,
) -> i32 {
if !current_api_present() {
return -1;
}
let (name, description) =
unsafe { (name.as_str().to_string(), description.as_str().to_string()) };
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_command(name, description, handler, std::ptr::null_mut())
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_shortcut(_key: StbStringRef, _description: StbStringRef) -> i32 {
with_current_api(|api| api.diagnostics.unsupported("register_shortcut (TUI — B5)"));
0
}
extern "C" fn trampoline_register_flag(_name: StbStringRef, _description: StbStringRef) -> i32 {
with_current_api(|api| api.diagnostics.unsupported("register_flag (CLI args — B5)"));
0
}
extern "C" fn trampoline_register_event_handler(
tag: EventTag,
handler: EventHandlerFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| reg.register_event_handler(tag, handler, user_data)) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_provider(
provider_id: StbStringRef,
base_url: StbStringRef,
api_style: StbStringRef,
request_fn: ProviderRequestFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let record = crate::registry::RegisteredProvider {
provider_id: unsafe { provider_id.as_str().to_string() },
base_url: unsafe { base_url.as_str().to_string() },
api_style: unsafe { api_style.as_str().to_string() },
request_fn,
plugin_free_string,
user_data,
};
let ok = with_current_api(
|api| match api.with_registry(|reg| reg.register_provider(record)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
fn register_renderer_common(
name: StbStringRef,
kind: crate::registry::RegisteredRendererKind,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let record = crate::registry::RegisteredRenderer {
name: unsafe { name.as_str().to_string() },
kind,
render_fn,
plugin_free_string,
user_data,
};
let ok = with_current_api(
|api| match api.with_registry(|reg| reg.register_renderer(record)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_message_renderer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Message,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_register_markdown_transformer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Markdown,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_register_entry_renderer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Entry,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_dispatch_event(_event: StablePluginEvent, _user_data: *mut c_void) -> i32 {
0
}
extern "C" fn trampoline_register_resources_discover(
handler: ResourcesDiscoverFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_resources_discover(handler, plugin_free_string, user_data)
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn stub_runtime_action(
_action: rpi_plugin_sdk::RuntimeActionId,
_args: StbStringRef,
_out: *mut StbString,
_user_data: *mut c_void,
) -> i32 {
-1
}
#[allow(dead_code)]
type _RuntimeActionFnDoc = RuntimeActionFn;