use core::ffi::c_char;
use core::ffi::c_void;
use core::ptr;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbString {
pub ptr: *mut c_char,
pub len: usize,
}
unsafe impl Send for StbString {}
unsafe impl Sync for StbString {}
impl StbString {
pub const fn empty() -> Self {
Self { ptr: ptr::null_mut(), len: 0 }
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
#[cfg(any(feature = "json", test))]
impl StbString {
pub fn from_owned(buf: Box<[u8]>) -> Self {
let len = buf.len();
let ptr = Box::into_raw(buf) as *mut [u8] as *mut u8 as *mut c_char;
let _ = len;
Self { ptr, len }
}
pub fn from_string(s: String) -> Self {
Self::from_vec(s.into_bytes())
}
pub fn from_vec(v: Vec<u8>) -> Self {
Self::from_owned(v.into_boxed_slice())
}
pub fn from_boxed_str(s: Box<str>) -> Self {
let string: String = s.into();
Self::from_vec(string.into_bytes())
}
pub fn to_string_lossy(&self) -> String {
if self.len == 0 || self.ptr.is_null() {
return String::new();
}
let slice = unsafe { core::slice::from_raw_parts(self.ptr as *const u8, self.len) };
String::from_utf8_lossy(slice).into_owned()
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbStringRef {
pub ptr: *const c_char,
pub len: usize,
}
unsafe impl Send for StbStringRef {}
unsafe impl Sync for StbStringRef {}
impl StbStringRef {
pub const fn empty() -> Self {
Self { ptr: ptr::null(), len: 0 }
}
pub fn from_str(s: &str) -> Self {
Self { ptr: s.as_ptr() as *const c_char, len: s.len() }
}
pub unsafe fn as_str<'a>(&self) -> &'a str {
if self.len == 0 || self.ptr.is_null() {
return "";
}
let slice = unsafe { core::slice::from_raw_parts(self.ptr as *const u8, self.len) };
unsafe { core::str::from_utf8_unchecked(slice) }
}
}
pub type FreeStringFn = extern "C" fn(s: StbString);
impl StbString {
pub fn free_with(self, free_fn: Option<FreeStringFn>) {
if let Some(free_fn) = free_fn {
if !self.is_empty() {
free_fn(self);
}
}
}
}
#[cfg(any(feature = "json", doc))]
pub mod json {
use super::StbString;
use serde_json::Value;
pub fn to_stable(value: &Value, free_fn: Option<super::FreeStringFn>) -> StbString {
let s = serde_json::to_string(value).unwrap_or_else(|_| "null".to_string());
let stb = StbString::from_string(s);
let _ = free_fn;
stb
}
pub fn from_stable(s: &StbString) -> Value {
let text = s.to_string_lossy();
if text.is_empty() {
return Value::Null;
}
serde_json::from_str(&text).unwrap_or(Value::Null)
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StableToolSchema {
pub name: StbString,
pub description: StbString,
pub parameters: StbString,
}
unsafe impl Send for StableToolSchema {}
unsafe impl Sync for StableToolSchema {}
pub type StepHandle = *mut c_void;
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepResultTag {
Pending = 0,
Done = 1,
Err = 2,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbPending {
pub progress: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbDone {
pub result: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbErr {
pub message: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union StepResultPayload {
pub pending: StbPending,
pub done: StbDone,
pub err: StbErr,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StepResult {
pub tag: StepResultTag,
pub payload: StepResultPayload,
}
impl StepResult {
pub fn pending(progress: StbString) -> Self {
Self { tag: StepResultTag::Pending, payload: StepResultPayload { pending: StbPending { progress } } }
}
pub fn done(result: StbString) -> Self {
Self { tag: StepResultTag::Done, payload: StepResultPayload { done: StbDone { result } } }
}
pub fn err(message: StbString) -> Self {
Self { tag: StepResultTag::Err, payload: StepResultPayload { err: StbErr { message } } }
}
pub unsafe fn pending_payload(&self) -> &StbPending {
unsafe { &self.payload.pending }
}
pub unsafe fn done_payload(&self) -> &StbDone {
unsafe { &self.payload.done }
}
pub unsafe fn err_payload(&self) -> &StbErr {
unsafe { &self.payload.err }
}
}
pub type ToolPartialCb = extern "C" fn(partial: StbString, user_data: *mut c_void);
pub type ToolExecuteFn = extern "C" fn(
tool_call_id: StbStringRef,
params: StbString,
free_params: Option<FreeStringFn>,
) -> StepHandle;
pub type ToolPollFn =
extern "C" fn(handle: StepHandle, partial_cb: Option<ToolPartialCb>, user_data: *mut c_void) -> StepResult;
pub type ToolCancelFn = extern "C" fn(handle: StepHandle);
pub type ToolDestroyFn = extern "C" fn(handle: StepHandle);
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventTag {
ProjectTrust = 0,
ResourcesDiscover = 1,
SessionStart = 2,
SessionInfoChanged = 3,
SessionBeforeSwitch = 4,
SessionBeforeFork = 5,
SessionBeforeCompact = 6,
SessionCompact = 7,
SessionShutdown = 8,
SessionBeforeTree = 9,
SessionTree = 10,
Context = 11,
BeforeProviderRequest = 12,
BeforeProviderHeaders = 13,
AfterProviderResponse = 14,
BeforeAgentStart = 15,
AgentStart = 16,
AgentEnd = 17,
AgentSettled = 18,
TurnStart = 19,
TurnEnd = 20,
MessageStart = 21,
MessageUpdate = 22,
MessageEnd = 23,
ToolExecutionStart = 24,
ToolExecutionUpdate = 25,
ToolExecutionEnd = 26,
ModelSelect = 27,
ThinkingLevelSelect = 28,
ToolCall = 29,
ToolResult = 30,
UserBash = 31,
Input = 32,
}
pub const EVENT_TAG_COUNT: usize = 33;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventEmpty {
_opaque: u8,
}
impl EventEmpty {
pub const INSTANCE: EventEmpty = EventEmpty { _opaque: 0 };
}
impl Default for EventEmpty {
fn default() -> Self {
Self::INSTANCE
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventMessage {
pub message: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventToolCall {
pub tool_call_id: StbString,
pub tool_name: StbString,
pub params: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventToolResult {
pub tool_call_id: StbString,
pub tool_name: StbString,
pub result: StbString,
pub is_error: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventError {
pub message: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct EventData {
pub data: StbString,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union EventPayload {
pub empty: EventEmpty,
pub message: EventMessage,
pub tool_call: EventToolCall,
pub tool_result: EventToolResult,
pub error: EventError,
pub data: EventData,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StablePluginEvent {
pub tag: EventTag,
pub payload: EventPayload,
}
impl StablePluginEvent {
pub fn empty(tag: EventTag) -> Self {
Self { tag, payload: EventPayload { empty: EventEmpty::INSTANCE } }
}
pub fn message(tag: EventTag, message: StbString) -> Self {
debug_assert!(matches!(
tag,
EventTag::MessageStart | EventTag::MessageUpdate | EventTag::MessageEnd
));
Self { tag, payload: EventPayload { message: EventMessage { message } } }
}
pub fn tool_call(tag: EventTag, tool_call_id: StbString, tool_name: StbString, params: StbString) -> Self {
debug_assert!(matches!(
tag,
EventTag::ToolCall | EventTag::ToolExecutionStart | EventTag::ToolExecutionUpdate
));
Self { tag, payload: EventPayload { tool_call: EventToolCall { tool_call_id, tool_name, params } } }
}
pub fn tool_result(
tag: EventTag,
tool_call_id: StbString,
tool_name: StbString,
result: StbString,
is_error: bool,
) -> Self {
debug_assert!(matches!(tag, EventTag::ToolResult | EventTag::ToolExecutionEnd));
Self {
tag,
payload: EventPayload {
tool_result: EventToolResult { tool_call_id, tool_name, result, is_error: is_error as u8 },
},
}
}
pub fn error(tag: EventTag, message: StbString) -> Self {
Self { tag, payload: EventPayload { error: EventError { message } } }
}
pub fn data(tag: EventTag, data: StbString) -> Self {
Self { tag, payload: EventPayload { data: EventData { data } } }
}
}
pub type EventHandlerFn = extern "C" fn(event: StablePluginEvent, user_data: *mut c_void) -> i32;
pub type ResourcesDiscoverFn = extern "C" fn(
cwd: StbStringRef,
reason: StbStringRef,
out: *mut StbString,
user_data: *mut c_void,
) -> i32;
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeActionId {
SendMessage = 0,
SendUserMessage = 1,
AppendEntry = 2,
SetSessionName = 3,
GetActiveTools = 4,
SetActiveTools = 5,
SetModel = 6,
GetThinkingLevel = 7,
SetThinkingLevel = 8,
Compact = 9,
GetSystemPrompt = 10,
NewSession = 11,
Fork = 12,
NavigateTree = 13,
SwitchSession = 14,
Reload = 15,
}
pub type RuntimeActionFn = extern "C" fn(
action: RuntimeActionId,
args_json: StbStringRef,
out: *mut StbString,
user_data: *mut c_void,
) -> i32;
pub type CommandHandlerFn =
extern "C" fn(args_json: StbStringRef, out: *mut StbString, user_data: *mut c_void) -> i32;
pub type RenderFn = extern "C" fn(input_json: StbStringRef, out: *mut StbString, user_data: *mut c_void) -> i32;
pub type ProviderRequestFn =
extern "C" fn(req_json: StbStringRef, out: *mut StbString, user_data: *mut c_void) -> i32;
#[repr(C)]
pub struct PluginApiVt {
pub free_string: FreeStringFn,
pub register_tool: Option<
extern "C" fn(
schema: *const StableToolSchema,
execute_fn: ToolExecuteFn,
poll_fn: ToolPollFn,
cancel_fn: ToolCancelFn,
destroy_fn: ToolDestroyFn,
plugin_free_string: FreeStringFn,
) -> i32,
>,
pub register_command: Option<extern "C" fn(name: StbStringRef, description: StbStringRef, handler: CommandHandlerFn) -> i32>,
pub register_shortcut: Option<extern "C" fn(key: StbStringRef, description: StbStringRef) -> i32>,
pub register_flag: Option<extern "C" fn(name: StbStringRef, description: StbStringRef) -> i32>,
pub register_provider: Option<
extern "C" fn(
provider_id: StbStringRef,
base_url: StbStringRef,
api_style: StbStringRef,
request_fn: ProviderRequestFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32,
>,
pub register_message_renderer: Option<
extern "C" fn(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32,
>,
pub register_markdown_transformer: Option<
extern "C" fn(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32,
>,
pub register_entry_renderer: Option<
extern "C" fn(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32,
>,
pub register_event_handler: Option<extern "C" fn(tag: EventTag, handler: EventHandlerFn, user_data: *mut c_void) -> i32>,
pub register_resources_discover: Option<
extern "C" fn(handler: ResourcesDiscoverFn, plugin_free_string: FreeStringFn, user_data: *mut c_void) -> i32,
>,
pub runtime_action: RuntimeActionFn,
pub dispatch_event: Option<extern "C" fn(event: StablePluginEvent, user_data: *mut c_void) -> i32>,
pub user_data: *mut c_void,
}
unsafe impl Send for PluginApiVt {}
unsafe impl Sync for PluginApiVt {}
pub const RPI_PLUGIN_ABI_VERSION: u32 = 1;
pub const REGISTER_SYMBOL: &[u8] = b"rpi_plugin_register\0";
pub type RpiPluginRegister = extern "C" fn(api: *const PluginApiVt, abi_version: u32) -> i32;
pub fn register_entrypoint(
api: *const PluginApiVt,
abi_version: u32,
body: impl FnOnce(&PluginApiVt) -> i32,
) -> i32 {
if abi_version != RPI_PLUGIN_ABI_VERSION {
return 1;
}
if api.is_null() {
return 2;
}
let api = unsafe { &*api };
body(api)
}
#[cfg(test)]
mod tests {
use super::*;
static FREED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
extern "C" fn test_free(s: StbString) {
if s.is_empty() || s.ptr.is_null() {
return;
}
unsafe {
let slice = core::slice::from_raw_parts(s.ptr as *const u8, s.len);
let _ = Box::from_raw(slice as *const [u8] as *mut [u8]);
}
FREED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
fn reset_freed() -> usize {
FREED.swap(0, std::sync::atomic::Ordering::SeqCst)
}
extern "C" fn noop_runtime_action(
_action: RuntimeActionId,
_args: StbStringRef,
_out: *mut StbString,
_user_data: *mut c_void,
) -> i32 {
0
}
#[test]
fn stbstring_round_trip_and_free_once() {
let prev = reset_freed();
let _ = prev;
let s = StbString::from_string("hello, pi".to_string());
assert_eq!(s.len, 9);
assert_eq!(s.to_string_lossy(), "hello, pi");
s.free_with(Some(test_free));
assert_eq!(FREED.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test]
fn empty_stbstring_free_is_noop() {
let _ = reset_freed();
StbString::empty().free_with(Some(test_free));
assert_eq!(FREED.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[test]
fn json_round_trip_preserves_structure() {
let val = serde_json::json!({ "name": "echo", "args": [1, 2, 3], "ok": true });
let stb = json::to_stable(&val, None);
let back = json::from_stable(&stb);
assert_eq!(val, back);
stb.free_with(Some(test_free));
let _ = reset_freed();
}
#[test]
fn step_result_done_round_trip() {
let result_json = StbString::from_string(r#"{"content":[{"text":"hi"}]}"#.to_string());
let sr = StepResult::done(result_json);
assert_eq!(sr.tag, StepResultTag::Done);
let done = unsafe { sr.done_payload() };
assert_eq!(done.result.to_string_lossy(), r#"{"content":[{"text":"hi"}]}"#);
done.result.free_with(Some(test_free));
let _ = reset_freed();
}
#[test]
fn step_result_pending_and_err() {
let prog = StbString::from_string("...".to_string());
let srp = StepResult::pending(prog);
assert_eq!(srp.tag, StepResultTag::Pending);
unsafe {
assert_eq!(srp.pending_payload().progress.to_string_lossy(), "...");
}
unsafe { srp.pending_payload().progress.free_with(Some(test_free)) };
let msg = StbString::from_string("boom".to_string());
let sre = StepResult::err(msg);
assert_eq!(sre.tag, StepResultTag::Err);
unsafe {
assert_eq!(sre.err_payload().message.to_string_lossy(), "boom");
sre.err_payload().message.free_with(Some(test_free));
}
let _ = reset_freed();
}
#[test]
fn event_tag_count_is_33() {
let tags = [
EventTag::ProjectTrust,
EventTag::ResourcesDiscover,
EventTag::SessionStart,
EventTag::SessionInfoChanged,
EventTag::SessionBeforeSwitch,
EventTag::SessionBeforeFork,
EventTag::SessionBeforeCompact,
EventTag::SessionCompact,
EventTag::SessionShutdown,
EventTag::SessionBeforeTree,
EventTag::SessionTree,
EventTag::Context,
EventTag::BeforeProviderRequest,
EventTag::BeforeProviderHeaders,
EventTag::AfterProviderResponse,
EventTag::BeforeAgentStart,
EventTag::AgentStart,
EventTag::AgentEnd,
EventTag::AgentSettled,
EventTag::TurnStart,
EventTag::TurnEnd,
EventTag::MessageStart,
EventTag::MessageUpdate,
EventTag::MessageEnd,
EventTag::ToolExecutionStart,
EventTag::ToolExecutionUpdate,
EventTag::ToolExecutionEnd,
EventTag::ModelSelect,
EventTag::ThinkingLevelSelect,
EventTag::ToolCall,
EventTag::ToolResult,
EventTag::UserBash,
EventTag::Input,
];
assert_eq!(tags.len(), EVENT_TAG_COUNT);
assert_eq!(EVENT_TAG_COUNT, 33);
let mut discs: Vec<u32> = tags.iter().map(|t| *t as u32).collect();
discs.sort();
assert_eq!(discs, (0..33).collect::<Vec<u32>>());
}
#[test]
fn event_payloads_construct_and_free() {
let m = StbString::from_string("msg".to_string());
let ev = StablePluginEvent::message(EventTag::MessageEnd, m);
assert_eq!(ev.tag, EventTag::MessageEnd);
unsafe {
assert_eq!(ev.payload.message.message.to_string_lossy(), "msg");
ev.payload.message.message.free_with(Some(test_free));
}
let tc = StablePluginEvent::tool_call(
EventTag::ToolCall,
StbString::from_string("call_1".to_string()),
StbString::from_string("echo".to_string()),
StbString::from_string("{}".to_string()),
);
unsafe {
assert_eq!(tc.payload.tool_call.tool_name.to_string_lossy(), "echo");
tc.payload.tool_call.tool_call_id.free_with(Some(test_free));
tc.payload.tool_call.tool_name.free_with(Some(test_free));
tc.payload.tool_call.params.free_with(Some(test_free));
}
let _ = reset_freed();
}
#[test]
fn plugin_api_vt_is_pod_and_sized() {
let vt = PluginApiVt {
free_string: test_free,
register_tool: None,
register_command: None,
register_shortcut: None,
register_flag: None,
register_provider: None,
register_message_renderer: None,
register_markdown_transformer: None,
register_entry_renderer: None,
register_event_handler: None,
register_resources_discover: None,
runtime_action: noop_runtime_action,
dispatch_event: None,
user_data: core::ptr::null_mut(),
};
assert!(vt.register_tool.is_none());
assert!(vt.register_event_handler.is_none());
assert!(vt.register_resources_discover.is_none());
let _copy = vt;
assert!(!core::mem::needs_drop::<PluginApiVt>());
assert!(!core::mem::needs_drop::<StbString>());
assert!(!core::mem::needs_drop::<StepResult>());
assert!(!core::mem::needs_drop::<StablePluginEvent>());
assert!(!core::mem::needs_drop::<StableToolSchema>());
}
#[test]
fn register_entrypoint_version_mismatch_refuses() {
let vt = PluginApiVt {
free_string: test_free,
register_tool: None,
register_command: None,
register_shortcut: None,
register_flag: None,
register_provider: None,
register_message_renderer: None,
register_markdown_transformer: None,
register_entry_renderer: None,
register_event_handler: None,
register_resources_discover: None,
runtime_action: noop_runtime_action,
dispatch_event: None,
user_data: core::ptr::null_mut(),
};
let rc = register_entrypoint(&vt, RPI_PLUGIN_ABI_VERSION.wrapping_add(1), |_| {
panic!("body must not run on version mismatch");
});
assert_ne!(rc, 0);
let rc = register_entrypoint(&vt, RPI_PLUGIN_ABI_VERSION, |_| 0);
assert_eq!(rc, 0);
let rc = register_entrypoint(&vt, RPI_PLUGIN_ABI_VERSION, |_| 42);
assert_eq!(rc, 42);
let rc = register_entrypoint(core::ptr::null(), RPI_PLUGIN_ABI_VERSION, |_| 0);
assert_ne!(rc, 0);
let _ = reset_freed();
}
}