1#![allow(unsafe_code)]
19#![allow(
20 clippy::multiple_unsafe_ops_per_block,
21 reason = "vtable deref and FFI call form a single boundary callback; \
22 SAFETY comments cover both ops together"
23)]
24
25use std::{
26 fmt::Debug,
27 panic::{AssertUnwindSafe, catch_unwind},
28};
29
30use nautilus_common::timer::TimeEvent;
31
32use crate::{
33 boundary::{BorrowedStr, OwnedBytes, PluginResult},
34 bridge::registry::{
35 ControllerHostContextInner, drop_controller_host_context, leak_controller_host_context,
36 },
37 host::{ControllerHostContext, ControllerHostVTable},
38 manifest::ValidatedControllerVTable,
39 surfaces::controller::PluginControllerHandle,
40};
41
42pub struct PluginControllerAdapter {
45 plugin_name: String,
46 type_name: String,
47 vtable: ValidatedControllerVTable,
48 handle: *mut PluginControllerHandle,
49 ctx: *const ControllerHostContext,
50}
51
52unsafe impl Send for PluginControllerAdapter {}
57
58impl Debug for PluginControllerAdapter {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct(stringify!(PluginControllerAdapter))
61 .field("plugin_name", &self.plugin_name)
62 .field("type_name", &self.type_name)
63 .finish()
64 }
65}
66
67impl PluginControllerAdapter {
68 pub unsafe fn new(
83 plugin_name: impl Into<String>,
84 type_name: impl Into<String>,
85 vtable: ValidatedControllerVTable,
86 host: *const ControllerHostVTable,
87 config_json: &str,
88 ) -> anyhow::Result<Self> {
89 let plugin_name = plugin_name.into();
90 let type_name = type_name.into();
91 let create = unsafe { validated_slot!(ControllerVTable, vtable.as_ptr(), create) };
93 let ctx = leak_controller_host_context(ControllerHostContextInner {
94 plugin_name: plugin_name.clone(),
95 type_name: type_name.clone(),
96 });
97
98 let cfg = BorrowedStr::from_str(config_json);
99 let handle = guard_call(&plugin_name, &type_name, "create", || unsafe {
102 create(host, ctx, cfg)
103 })
104 .ok_or_else(|| {
105 unsafe { drop_controller_host_context(ctx) };
107 anyhow::anyhow!("plug-in controller '{type_name}' panicked in create")
108 })?;
109
110 if handle.is_null() {
111 unsafe { drop_controller_host_context(ctx) };
113 anyhow::bail!("plug-in controller '{type_name}' returned a null handle from create");
114 }
115
116 Ok(Self {
117 plugin_name,
118 type_name,
119 vtable,
120 handle,
121 ctx,
122 })
123 }
124
125 pub fn prepare(&self, request_json: &str) -> anyhow::Result<OwnedBytes> {
131 let request = BorrowedStr::from_str(request_json);
132 let result = guard_call(&self.plugin_name, &self.type_name, "prepare", || unsafe {
133 validated_slot!(ControllerVTable, self.vtable.as_ptr(), prepare)(request)
134 });
135 finish_bytes(result, &self.plugin_name, &self.type_name, "prepare")
136 }
137
138 #[must_use]
140 pub fn type_name(&self) -> &str {
141 &self.type_name
142 }
143
144 #[must_use]
146 pub fn plugin_name(&self) -> &str {
147 &self.plugin_name
148 }
149
150 pub fn on_start(&mut self) -> anyhow::Result<()> {
156 invoke_lifecycle(self, "on_start", |adapter| unsafe {
157 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_start)(adapter.handle)
158 })
159 }
160
161 pub fn on_stop(&mut self) -> anyhow::Result<()> {
167 invoke_lifecycle(self, "on_stop", |adapter| unsafe {
168 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_stop)(adapter.handle)
169 })
170 }
171
172 pub fn on_resume(&mut self) -> anyhow::Result<()> {
178 invoke_lifecycle(self, "on_resume", |adapter| unsafe {
179 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_resume)(adapter.handle)
180 })
181 }
182
183 pub fn on_reset(&mut self) -> anyhow::Result<()> {
189 invoke_lifecycle(self, "on_reset", |adapter| unsafe {
190 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_reset)(adapter.handle)
191 })
192 }
193
194 pub fn on_dispose(&mut self) -> anyhow::Result<()> {
200 invoke_lifecycle(self, "on_dispose", |adapter| unsafe {
201 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_dispose)(adapter.handle)
202 })
203 }
204
205 pub fn on_degrade(&mut self) -> anyhow::Result<()> {
211 invoke_lifecycle(self, "on_degrade", |adapter| unsafe {
212 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_degrade)(adapter.handle)
213 })
214 }
215
216 pub fn on_fault(&mut self) -> anyhow::Result<()> {
222 invoke_lifecycle(self, "on_fault", |adapter| unsafe {
223 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_fault)(adapter.handle)
224 })
225 }
226
227 pub fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
233 invoke_event(self, "on_time_event", event, |adapter, p| unsafe {
234 validated_slot!(ControllerVTable, adapter.vtable.as_ptr(), on_time_event)(
235 adapter.handle,
236 p,
237 )
238 })
239 }
240}
241
242impl Drop for PluginControllerAdapter {
243 fn drop(&mut self) {
244 if !self.handle.is_null() {
245 let _ = catch_unwind(AssertUnwindSafe(|| {
246 unsafe {
248 validated_slot!(ControllerVTable, self.vtable.as_ptr(), drop_handle)(
249 self.handle,
250 );
251 };
252 }));
253 self.handle = std::ptr::null_mut();
254 }
255 unsafe { drop_controller_host_context(self.ctx) };
257 self.ctx = std::ptr::null();
258 }
259}
260
261fn guard_call<R>(plugin: &str, type_name: &str, method: &str, f: impl FnOnce() -> R) -> Option<R> {
262 match catch_unwind(AssertUnwindSafe(f)) {
263 Ok(r) => Some(r),
264 Err(_payload) => {
265 log::error!(
266 target: "nautilus_plugin",
267 "plug-in '{plugin}' ({type_name}) panicked in {method}",
268 );
269 None
270 }
271 }
272}
273
274fn invoke_lifecycle(
275 adapter: &PluginControllerAdapter,
276 method: &str,
277 f: impl FnOnce(&PluginControllerAdapter) -> PluginResult<()>,
278) -> anyhow::Result<()> {
279 let plugin_name = adapter.plugin_name.clone();
280 let type_name = adapter.type_name.clone();
281 let result = guard_call(&plugin_name, &type_name, method, || f(adapter));
282 finish(result, &plugin_name, &type_name, method)
283}
284
285fn invoke_event<T>(
286 adapter: &PluginControllerAdapter,
287 method: &str,
288 payload: &T,
289 f: impl FnOnce(&PluginControllerAdapter, *const T) -> PluginResult<()>,
290) -> anyhow::Result<()> {
291 let plugin_name = adapter.plugin_name.clone();
292 let type_name = adapter.type_name.clone();
293 let ptr: *const T = payload;
294 let result = guard_call(&plugin_name, &type_name, method, || f(adapter, ptr));
295 finish(result, &plugin_name, &type_name, method)
296}
297
298fn finish(
299 result: Option<PluginResult<()>>,
300 plugin_name: &str,
301 type_name: &str,
302 method: &str,
303) -> anyhow::Result<()> {
304 match result {
305 Some(r) => r.into_result().map_err(|e| {
306 anyhow::anyhow!(
307 "plug-in '{plugin_name}' ({type_name}) {method} returned error: {}",
308 e.message_string()
309 )
310 }),
311 None => anyhow::bail!("plug-in '{plugin_name}' ({type_name}) panicked in {method}"),
312 }
313}
314
315fn finish_bytes(
316 result: Option<PluginResult<OwnedBytes>>,
317 plugin_name: &str,
318 type_name: &str,
319 method: &str,
320) -> anyhow::Result<OwnedBytes> {
321 match result {
322 Some(r) => r.into_result().map_err(|e| {
323 anyhow::anyhow!(
324 "plug-in '{plugin_name}' ({type_name}) {method} returned error: {}",
325 e.message_string()
326 )
327 }),
328 None => anyhow::bail!("plug-in '{plugin_name}' ({type_name}) panicked in {method}"),
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use std::sync::atomic::{AtomicU64, Ordering};
335
336 use rstest::rstest;
337
338 use super::*;
339 use crate::{
340 bridge::{
341 host::controller_host_vtable,
342 registry::{controller_host_context_live_count, controller_host_context_test_lock},
343 },
344 host::{ControllerHostContext, ControllerHostVTable},
345 surfaces::controller::{ControllerVTable, PluginController, controller_vtable},
346 };
347
348 static STARTS: AtomicU64 = AtomicU64::new(0);
349
350 struct DropTestController;
351
352 impl PluginController for DropTestController {
353 const TYPE_NAME: &'static str = "DropTestController";
354
355 fn new(
356 _host: *const ControllerHostVTable,
357 _ctx: *const ControllerHostContext,
358 _config_json: &str,
359 ) -> Self {
360 Self
361 }
362
363 fn on_start(&mut self) -> anyhow::Result<()> {
364 STARTS.fetch_add(1, Ordering::SeqCst);
365 Ok(())
366 }
367 }
368
369 fn drop_test_controller_vtable() -> ValidatedControllerVTable {
370 unsafe {
373 ValidatedControllerVTable::from_raw_unchecked(controller_vtable::<DropTestController>())
374 }
375 }
376
377 static NULL_CREATE_VTABLE: ControllerVTable = ControllerVTable {
378 prepare: Some(null_create_prepare),
379 create: Some(null_create),
380 drop_handle: Some(null_create_drop_handle),
381 type_name: Some(null_create_type_name),
382 on_start: Some(null_create_lifecycle),
383 on_stop: Some(null_create_lifecycle),
384 on_resume: Some(null_create_lifecycle),
385 on_reset: Some(null_create_lifecycle),
386 on_dispose: Some(null_create_lifecycle),
387 on_degrade: Some(null_create_lifecycle),
388 on_fault: Some(null_create_lifecycle),
389 on_time_event: Some(null_create_time_event),
390 };
391
392 unsafe extern "C" fn null_create_prepare(
393 _request_json: BorrowedStr<'_>,
394 ) -> PluginResult<OwnedBytes> {
395 PluginResult::Ok(OwnedBytes::empty())
396 }
397
398 unsafe extern "C" fn null_create(
399 _host: *const ControllerHostVTable,
400 _ctx: *const ControllerHostContext,
401 _config_json: BorrowedStr<'_>,
402 ) -> *mut PluginControllerHandle {
403 std::ptr::null_mut()
404 }
405
406 unsafe extern "C" fn null_create_drop_handle(_handle: *mut PluginControllerHandle) {}
407
408 unsafe extern "C" fn null_create_type_name() -> BorrowedStr<'static> {
409 BorrowedStr::from_str("NullCreateController")
410 }
411
412 unsafe extern "C" fn null_create_lifecycle(
413 _handle: *mut PluginControllerHandle,
414 ) -> PluginResult<()> {
415 PluginResult::Ok(())
416 }
417
418 unsafe extern "C" fn null_create_time_event(
419 _handle: *mut PluginControllerHandle,
420 _event: *const TimeEvent,
421 ) -> PluginResult<()> {
422 PluginResult::Ok(())
423 }
424
425 fn null_create_vtable() -> ValidatedControllerVTable {
426 unsafe { ValidatedControllerVTable::from_raw_unchecked(&raw const NULL_CREATE_VTABLE) }
429 }
430
431 #[rstest]
432 fn drop_frees_controller_host_context() {
433 let _guard = controller_host_context_test_lock();
434 let before = controller_host_context_live_count();
435 let adapter = unsafe {
437 PluginControllerAdapter::new(
438 "test-plugin",
439 DropTestController::TYPE_NAME,
440 drop_test_controller_vtable(),
441 controller_host_vtable(),
442 "{}",
443 )
444 }
445 .expect("controller adapter construction succeeds");
446 assert_eq!(controller_host_context_live_count(), before + 1);
447
448 drop(adapter);
449
450 assert_eq!(controller_host_context_live_count(), before);
451 }
452
453 #[rstest]
454 fn null_create_frees_controller_host_context() {
455 let _guard = controller_host_context_test_lock();
456 let before = controller_host_context_live_count();
457
458 let error = unsafe {
460 PluginControllerAdapter::new(
461 "test-plugin",
462 "NullCreateController",
463 null_create_vtable(),
464 controller_host_vtable(),
465 "{}",
466 )
467 }
468 .expect_err("null controller handle is rejected");
469
470 assert!(
471 error
472 .to_string()
473 .contains("returned a null handle from create")
474 );
475 assert_eq!(controller_host_context_live_count(), before);
476 }
477
478 #[rstest]
479 fn lifecycle_dispatches_to_controller() {
480 let _guard = controller_host_context_test_lock();
481 STARTS.store(0, Ordering::SeqCst);
482 let mut adapter = unsafe {
484 PluginControllerAdapter::new(
485 "test-plugin",
486 DropTestController::TYPE_NAME,
487 drop_test_controller_vtable(),
488 controller_host_vtable(),
489 "{}",
490 )
491 }
492 .expect("controller adapter construction succeeds");
493
494 adapter.on_start().expect("on_start dispatches");
495
496 assert_eq!(STARTS.load(Ordering::SeqCst), 1);
497 }
498}