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,
GetCliFlag = 16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownRuntimeActionId(pub u32);
impl core::fmt::Display for UnknownRuntimeActionId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "unknown runtime action id {}", self.0)
}
}
impl std::error::Error for UnknownRuntimeActionId {}
impl TryFrom<u32> for RuntimeActionId {
type Error = UnknownRuntimeActionId;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::SendMessage),
1 => Ok(Self::SendUserMessage),
2 => Ok(Self::AppendEntry),
3 => Ok(Self::SetSessionName),
4 => Ok(Self::GetActiveTools),
5 => Ok(Self::SetActiveTools),
6 => Ok(Self::SetModel),
7 => Ok(Self::GetThinkingLevel),
8 => Ok(Self::SetThinkingLevel),
9 => Ok(Self::Compact),
10 => Ok(Self::GetSystemPrompt),
11 => Ok(Self::NewSession),
12 => Ok(Self::Fork),
13 => Ok(Self::NavigateTree),
14 => Ok(Self::SwitchSession),
15 => Ok(Self::Reload),
16 => Ok(Self::GetCliFlag),
other => Err(UnknownRuntimeActionId(other)),
}
}
}
impl From<RuntimeActionId> for u32 {
fn from(value: RuntimeActionId) -> Self {
value as u32
}
}
pub type RuntimeActionFn = extern "C" fn(
action_id: u32,
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;
pub type LegacyRuntimeActionFn = extern "C" fn(
action_id: u32,
args_json: StbStringRef,
out: *mut StbString,
user_data: *mut c_void,
) -> i32;
#[repr(C)]
pub struct LegacyPluginApiV1 {
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: LegacyRuntimeActionFn,
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 LegacyPluginApiV1 {}
unsafe impl Sync for LegacyPluginApiV1 {}
#[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 = 2;
pub const LEGACY_PLUGIN_ABI_VERSION: u32 = 1;
pub const REGISTER_SYMBOL_V2: &[u8] = b"rpi_plugin_register_v2\0";
pub const REGISTER_SYMBOL: &[u8] = REGISTER_SYMBOL_V2;
pub const LEGACY_REGISTER_SYMBOL: &[u8] = b"rpi_plugin_register\0";
pub type RpiPluginRegister = extern "C" fn(api: *const PluginApiVt, abi_version: u32) -> i32;
pub type LegacyRpiPluginRegister =
extern "C" fn(api: *const LegacyPluginApiV1, abi_version: u32) -> i32;
#[macro_export]
macro_rules! export_plugin_v2 {
($body:expr) => {
#[no_mangle]
pub extern "C" fn rpi_plugin_register_v2(
api: *const $crate::PluginApiVt,
abi_version: u32,
) -> i32 {
$crate::register_entrypoint(api, abi_version, $body)
}
};
}
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::*;
std::thread_local! {
static FREED: std::cell::Cell<usize> = std::cell::Cell::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.with(|freed| freed.set(freed.get() + 1));
}
fn reset_freed() -> usize {
FREED.with(|freed| freed.replace(0))
}
fn freed_count() -> usize {
FREED.with(std::cell::Cell::get)
}
extern "C" fn noop_runtime_action(
_action_id: u32,
_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_count(), 1);
}
#[test]
fn empty_stbstring_free_is_noop() {
let _ = reset_freed();
StbString::empty().free_with(Some(test_free));
assert_eq!(freed_count(), 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 legacy_v1_layout_is_frozen_and_matches_v2_shared_slots() {
use core::mem::{align_of, offset_of, size_of};
let pointer_size = size_of::<*const ()>();
assert_eq!(size_of::<LegacyPluginApiV1>(), 14 * pointer_size);
assert_eq!(align_of::<LegacyPluginApiV1>(), align_of::<*const ()>());
assert_eq!(size_of::<PluginApiVt>(), size_of::<LegacyPluginApiV1>());
assert_eq!(align_of::<PluginApiVt>(), align_of::<LegacyPluginApiV1>());
macro_rules! assert_same_offset {
($field:ident, $index:expr) => {
assert_eq!(
offset_of!(LegacyPluginApiV1, $field),
$index * pointer_size,
concat!("unexpected ABI v1 offset for ", stringify!($field))
);
assert_eq!(
offset_of!(PluginApiVt, $field),
offset_of!(LegacyPluginApiV1, $field),
concat!("v1/v2 shared field moved: ", stringify!($field))
);
};
}
assert_same_offset!(free_string, 0);
assert_same_offset!(register_tool, 1);
assert_same_offset!(register_command, 2);
assert_same_offset!(register_shortcut, 3);
assert_same_offset!(register_flag, 4);
assert_same_offset!(register_provider, 5);
assert_same_offset!(register_message_renderer, 6);
assert_same_offset!(register_markdown_transformer, 7);
assert_same_offset!(register_entry_renderer, 8);
assert_same_offset!(register_event_handler, 9);
assert_same_offset!(register_resources_discover, 10);
assert_same_offset!(runtime_action, 11);
assert_same_offset!(dispatch_event, 12);
assert_same_offset!(user_data, 13);
assert!(!core::mem::needs_drop::<LegacyPluginApiV1>());
}
#[test]
fn runtime_action_ids_are_explicitly_validated() {
let ids = [
RuntimeActionId::SendMessage,
RuntimeActionId::SendUserMessage,
RuntimeActionId::AppendEntry,
RuntimeActionId::SetSessionName,
RuntimeActionId::GetActiveTools,
RuntimeActionId::SetActiveTools,
RuntimeActionId::SetModel,
RuntimeActionId::GetThinkingLevel,
RuntimeActionId::SetThinkingLevel,
RuntimeActionId::Compact,
RuntimeActionId::GetSystemPrompt,
RuntimeActionId::NewSession,
RuntimeActionId::Fork,
RuntimeActionId::NavigateTree,
RuntimeActionId::SwitchSession,
RuntimeActionId::Reload,
RuntimeActionId::GetCliFlag,
];
for (raw, expected) in ids.into_iter().enumerate() {
assert_eq!(RuntimeActionId::try_from(raw as u32), Ok(expected));
assert_eq!(u32::from(expected), raw as u32);
}
assert_eq!(
RuntimeActionId::try_from(17),
Err(UnknownRuntimeActionId(17))
);
assert_eq!(
RuntimeActionId::try_from(u32::MAX),
Err(UnknownRuntimeActionId(u32::MAX))
);
}
#[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(),
};
assert_eq!(RPI_PLUGIN_ABI_VERSION, 2);
assert_eq!(LEGACY_PLUGIN_ABI_VERSION, 1);
assert_eq!(REGISTER_SYMBOL, REGISTER_SYMBOL_V2);
assert_ne!(REGISTER_SYMBOL_V2, LEGACY_REGISTER_SYMBOL);
let rc = register_entrypoint(core::ptr::null(), 1, |_| {
panic!("body must not run on version mismatch");
});
assert_eq!(rc, 1);
let rc = register_entrypoint(&vt, RPI_PLUGIN_ABI_VERSION + 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();
}
}