#![allow(unsafe_code)]
#![allow(
clippy::multiple_unsafe_ops_per_block,
reason = "vtable deref and FFI call form a single boundary callback; \
SAFETY comments cover both ops together"
)]
use std::{
fmt::Debug,
panic::{AssertUnwindSafe, catch_unwind},
};
use nautilus_common::timer::TimeEvent;
use crate::{
boundary::{BorrowedStr, OwnedBytes, PluginResult},
bridge::registry::{
ControllerHostContextInner, drop_controller_host_context, leak_controller_host_context,
},
host::{ControllerHostContext, ControllerHostVTable},
manifest::ValidatedControllerVTable,
surfaces::controller::PluginControllerHandle,
};
pub struct PluginControllerAdapter {
plugin_name: String,
type_name: String,
vtable: ValidatedControllerVTable,
handle: *mut PluginControllerHandle,
ctx: *const ControllerHostContext,
}
unsafe impl Send for PluginControllerAdapter {}
impl Debug for PluginControllerAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(PluginControllerAdapter))
.field("plugin_name", &self.plugin_name)
.field("type_name", &self.type_name)
.finish()
}
}
impl PluginControllerAdapter {
pub unsafe fn new(
plugin_name: impl Into<String>,
type_name: impl Into<String>,
vtable: ValidatedControllerVTable,
host: *const ControllerHostVTable,
config_json: &str,
) -> anyhow::Result<Self> {
let plugin_name = plugin_name.into();
let type_name = type_name.into();
let create = unsafe { validated_slot!(ControllerVTable, vtable.as_ptr(), create) };
let ctx = leak_controller_host_context(ControllerHostContextInner {
plugin_name: plugin_name.clone(),
type_name: type_name.clone(),
});
let cfg = BorrowedStr::from_str(config_json);
let handle = guard_call(&plugin_name, &type_name, "create", || unsafe {
create(host, ctx, cfg)
})
.ok_or_else(|| {
unsafe { drop_controller_host_context(ctx) };
anyhow::anyhow!("plug-in controller '{type_name}' panicked in create")
})?;
if handle.is_null() {
unsafe { drop_controller_host_context(ctx) };
anyhow::bail!("plug-in controller '{type_name}' returned a null handle from create");
}
Ok(Self {
plugin_name,
type_name,
vtable,
handle,
ctx,
})
}
pub fn prepare(&self, request_json: &str) -> anyhow::Result<OwnedBytes> {
let request = BorrowedStr::from_str(request_json);
let result = guard_call(&self.plugin_name, &self.type_name, "prepare", || unsafe {
validated_slot!(ControllerVTable, self.vtable.as_ptr(), prepare)(request)
});
finish_bytes(result, &self.plugin_name, &self.type_name, "prepare")
}
#[must_use]
pub fn type_name(&self) -> &str {
&self.type_name
}
#[must_use]
pub fn plugin_name(&self) -> &str {
&self.plugin_name
}
pub fn on_start(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_start", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_start)(adapter.handle)
})
}
pub fn on_stop(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_stop", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_stop)(adapter.handle)
})
}
pub fn on_resume(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_resume", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_resume)(adapter.handle)
})
}
pub fn on_reset(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_reset", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_reset)(adapter.handle)
})
}
pub fn on_dispose(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_dispose", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_dispose)(adapter.handle)
})
}
pub fn on_degrade(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_degrade", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_degrade)(adapter.handle)
})
}
pub fn on_fault(&mut self) -> anyhow::Result<()> {
invoke_lifecycle(self, "on_fault", |adapter| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_fault)(adapter.handle)
})
}
pub fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
invoke_event(self, "on_time_event", event, |adapter, p| unsafe {
validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_time_event)(
adapter.handle,
p,
)
})
}
}
impl Drop for PluginControllerAdapter {
fn drop(&mut self) {
if !self.handle.is_null() {
let _ = catch_unwind(AssertUnwindSafe(|| {
unsafe {
validated_slot!(ControllerVTable, self.vtable.as_ptr(), drop_handle)(
self.handle,
);
};
}));
self.handle = std::ptr::null_mut();
}
unsafe { drop_controller_host_context(self.ctx) };
self.ctx = std::ptr::null();
}
}
fn guard_call<R>(plugin: &str, type_name: &str, method: &str, f: impl FnOnce() -> R) -> Option<R> {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(r) => Some(r),
Err(_payload) => {
log::error!(
target: "nautilus_plugin",
"plug-in '{plugin}' ({type_name}) panicked in {method}",
);
None
}
}
}
fn invoke_lifecycle(
adapter: &PluginControllerAdapter,
method: &str,
f: impl FnOnce(&PluginControllerAdapter) -> PluginResult<()>,
) -> anyhow::Result<()> {
let plugin_name = adapter.plugin_name.clone();
let type_name = adapter.type_name.clone();
let result = guard_call(&plugin_name, &type_name, method, || f(adapter));
finish(result, &plugin_name, &type_name, method)
}
fn invoke_event<T>(
adapter: &PluginControllerAdapter,
method: &str,
payload: &T,
f: impl FnOnce(&PluginControllerAdapter, *const T) -> PluginResult<()>,
) -> anyhow::Result<()> {
let plugin_name = adapter.plugin_name.clone();
let type_name = adapter.type_name.clone();
let ptr: *const T = payload;
let result = guard_call(&plugin_name, &type_name, method, || f(adapter, ptr));
finish(result, &plugin_name, &type_name, method)
}
fn finish(
result: Option<PluginResult<()>>,
plugin_name: &str,
type_name: &str,
method: &str,
) -> anyhow::Result<()> {
match result {
Some(r) => r.into_result().map_err(|e| {
anyhow::anyhow!(
"plug-in '{plugin_name}' ({type_name}) {method} returned error: {}",
e.message_string()
)
}),
None => anyhow::bail!("plug-in '{plugin_name}' ({type_name}) panicked in {method}"),
}
}
fn finish_bytes(
result: Option<PluginResult<OwnedBytes>>,
plugin_name: &str,
type_name: &str,
method: &str,
) -> anyhow::Result<OwnedBytes> {
match result {
Some(r) => r.into_result().map_err(|e| {
anyhow::anyhow!(
"plug-in '{plugin_name}' ({type_name}) {method} returned error: {}",
e.message_string()
)
}),
None => anyhow::bail!("plug-in '{plugin_name}' ({type_name}) panicked in {method}"),
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
use rstest::rstest;
use super::*;
use crate::{
bridge::{
host::controller_host_vtable,
registry::{controller_host_context_live_count, controller_host_context_test_lock},
},
host::{ControllerHostContext, ControllerHostVTable},
surfaces::controller::{ControllerVTable, PluginController, controller_vtable},
};
static STARTS: AtomicU64 = AtomicU64::new(0);
struct DropTestController;
impl PluginController for DropTestController {
const TYPE_NAME: &'static str = "DropTestController";
fn new(
_host: *const ControllerHostVTable,
_ctx: *const ControllerHostContext,
_config_json: &str,
) -> Self {
Self
}
fn on_start(&mut self) -> anyhow::Result<()> {
STARTS.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
fn drop_test_controller_vtable() -> ValidatedControllerVTable {
unsafe {
ValidatedControllerVTable::from_raw_unchecked(controller_vtable::<DropTestController>())
}
}
static NULL_CREATE_VTABLE: ControllerVTable = ControllerVTable {
prepare: Some(null_create_prepare),
create: Some(null_create),
drop_handle: Some(null_create_drop_handle),
type_name: Some(null_create_type_name),
on_start: Some(null_create_lifecycle),
on_stop: Some(null_create_lifecycle),
on_resume: Some(null_create_lifecycle),
on_reset: Some(null_create_lifecycle),
on_dispose: Some(null_create_lifecycle),
on_degrade: Some(null_create_lifecycle),
on_fault: Some(null_create_lifecycle),
on_time_event: Some(null_create_time_event),
};
unsafe extern "C" fn null_create_prepare(
_request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes> {
PluginResult::Ok(OwnedBytes::empty())
}
unsafe extern "C" fn null_create(
_host: *const ControllerHostVTable,
_ctx: *const ControllerHostContext,
_config_json: BorrowedStr<'_>,
) -> *mut PluginControllerHandle {
std::ptr::null_mut()
}
unsafe extern "C" fn null_create_drop_handle(_handle: *mut PluginControllerHandle) {}
unsafe extern "C" fn null_create_type_name() -> BorrowedStr<'static> {
BorrowedStr::from_str("NullCreateController")
}
unsafe extern "C" fn null_create_lifecycle(
_handle: *mut PluginControllerHandle,
) -> PluginResult<()> {
PluginResult::Ok(())
}
unsafe extern "C" fn null_create_time_event(
_handle: *mut PluginControllerHandle,
_event: *const TimeEvent,
) -> PluginResult<()> {
PluginResult::Ok(())
}
fn null_create_vtable() -> ValidatedControllerVTable {
unsafe { ValidatedControllerVTable::from_raw_unchecked(&raw const NULL_CREATE_VTABLE) }
}
#[rstest]
fn drop_frees_controller_host_context() {
let _guard = controller_host_context_test_lock();
let before = controller_host_context_live_count();
let adapter = unsafe {
PluginControllerAdapter::new(
"test-plugin",
DropTestController::TYPE_NAME,
drop_test_controller_vtable(),
controller_host_vtable(),
"{}",
)
}
.expect("controller adapter construction succeeds");
assert_eq!(controller_host_context_live_count(), before + 1);
drop(adapter);
assert_eq!(controller_host_context_live_count(), before);
}
#[rstest]
fn null_create_frees_controller_host_context() {
let _guard = controller_host_context_test_lock();
let before = controller_host_context_live_count();
let error = unsafe {
PluginControllerAdapter::new(
"test-plugin",
"NullCreateController",
null_create_vtable(),
controller_host_vtable(),
"{}",
)
}
.expect_err("null controller handle is rejected");
assert!(
error
.to_string()
.contains("returned a null handle from create")
);
assert_eq!(controller_host_context_live_count(), before);
}
#[rstest]
fn lifecycle_dispatches_to_controller() {
let _guard = controller_host_context_test_lock();
STARTS.store(0, Ordering::SeqCst);
let mut adapter = unsafe {
PluginControllerAdapter::new(
"test-plugin",
DropTestController::TYPE_NAME,
drop_test_controller_vtable(),
controller_host_vtable(),
"{}",
)
}
.expect("controller adapter construction succeeds");
adapter.on_start().expect("on_start dispatches");
assert_eq!(STARTS.load(Ordering::SeqCst), 1);
}
}