Skip to main content

nautilus_plugin/surfaces/
controller.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Controller plug point.
17//!
18//! Controllers can prepare runtime strategy definitions and issue lifecycle
19//! commands through a controller-specific host service table. The surface uses
20//! JSON request and response envelopes for controller-host calls because those
21//! commands are orchestration actions rather than per-event market data paths.
22
23#![allow(unsafe_code)]
24
25use std::marker::PhantomData;
26
27use nautilus_common::timer::TimeEvent;
28
29use crate::{
30    boundary::{BorrowedStr, OwnedBytes, PluginError, PluginErrorCode, PluginResult},
31    host::{ControllerHostContext, ControllerHostVTable},
32    normalize::BoundaryNormalize,
33    panic::{guard, guard_infallible},
34};
35
36/// Opaque handle to a plug-in controller instance owned by the cdylib.
37#[repr(C)]
38pub struct PluginControllerHandle {
39    _opaque: [u8; 0],
40}
41
42/// Function table for a single plug-in controller type.
43///
44/// Slots are nullable at the ABI type level so the host can reject malformed
45/// manifests with null callbacks before constructing a controller. Generated
46/// vtables fill every required slot.
47#[repr(C)]
48pub struct ControllerVTable {
49    /// Prepares a controller request before the host registers runtime state.
50    pub prepare:
51        Option<unsafe extern "C" fn(request_json: BorrowedStr<'_>) -> PluginResult<OwnedBytes>>,
52
53    /// Constructs a fresh controller instance bound to the supplied host
54    /// vtable and instance context.
55    pub create: Option<
56        unsafe extern "C" fn(
57            host: *const ControllerHostVTable,
58            ctx: *const ControllerHostContext,
59            config_json: BorrowedStr<'_>,
60        ) -> *mut PluginControllerHandle,
61    >,
62
63    /// Drops the controller instance and releases all of its resources.
64    pub drop_handle: Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle)>,
65
66    /// Returns the canonical type name for this controller.
67    pub type_name: Option<unsafe extern "C" fn() -> BorrowedStr<'static>>,
68
69    pub on_start:
70        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
71    pub on_stop:
72        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
73    pub on_resume:
74        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
75    pub on_reset:
76        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
77    pub on_dispose:
78        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
79    pub on_degrade:
80        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
81    pub on_fault:
82        Option<unsafe extern "C" fn(handle: *mut PluginControllerHandle) -> PluginResult<()>>,
83
84    pub on_time_event: Option<
85        unsafe extern "C" fn(
86            handle: *mut PluginControllerHandle,
87            event: *const TimeEvent,
88        ) -> PluginResult<()>,
89    >,
90}
91
92/// Author-facing trait for a plug-in controller.
93///
94/// Controllers can define a static [`PluginController::prepare`] hook and
95/// runtime lifecycle callbacks. Every callback has a no-op default. Override
96/// only what you need.
97pub trait PluginController: 'static + Send + Sized {
98    /// Canonical type name. Must be unique across a Nautilus deployment.
99    const TYPE_NAME: &'static str;
100
101    /// Prepares a JSON controller request and returns a JSON response envelope.
102    #[allow(unused_variables)]
103    fn prepare(request_json: &str) -> anyhow::Result<Vec<u8>> {
104        Ok(Vec::new())
105    }
106
107    /// Constructs a fresh controller instance bound to the supplied host
108    /// vtable and instance context.
109    fn new(
110        host: *const ControllerHostVTable,
111        ctx: *const ControllerHostContext,
112        config_json: &str,
113    ) -> Self;
114
115    #[allow(unused_variables)]
116    fn on_start(&mut self) -> anyhow::Result<()> {
117        Ok(())
118    }
119
120    #[allow(unused_variables)]
121    fn on_stop(&mut self) -> anyhow::Result<()> {
122        Ok(())
123    }
124
125    #[allow(unused_variables)]
126    fn on_resume(&mut self) -> anyhow::Result<()> {
127        Ok(())
128    }
129
130    #[allow(unused_variables)]
131    fn on_reset(&mut self) -> anyhow::Result<()> {
132        Ok(())
133    }
134
135    #[allow(unused_variables)]
136    fn on_dispose(&mut self) -> anyhow::Result<()> {
137        Ok(())
138    }
139
140    #[allow(unused_variables)]
141    fn on_degrade(&mut self) -> anyhow::Result<()> {
142        Ok(())
143    }
144
145    #[allow(unused_variables)]
146    fn on_fault(&mut self) -> anyhow::Result<()> {
147        Ok(())
148    }
149
150    #[allow(unused_variables)]
151    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
152        Ok(())
153    }
154}
155
156/// Returns a `*const ControllerVTable` for the given [`PluginController`] type.
157#[must_use]
158pub fn controller_vtable<T>() -> *const ControllerVTable
159where
160    T: PluginController,
161{
162    &VTableTag::<T>::VTABLE
163}
164
165struct VTableTag<T>(PhantomData<T>);
166
167impl<T> VTableTag<T>
168where
169    T: PluginController,
170{
171    const VTABLE: ControllerVTable = ControllerVTable {
172        prepare: Some(prepare_thunk::<T>),
173        create: Some(create_thunk::<T>),
174        drop_handle: Some(drop_handle_thunk::<T>),
175        type_name: Some(type_name_thunk::<T>),
176        on_start: Some(on_start_thunk::<T>),
177        on_stop: Some(on_stop_thunk::<T>),
178        on_resume: Some(on_resume_thunk::<T>),
179        on_reset: Some(on_reset_thunk::<T>),
180        on_dispose: Some(on_dispose_thunk::<T>),
181        on_degrade: Some(on_degrade_thunk::<T>),
182        on_fault: Some(on_fault_thunk::<T>),
183        on_time_event: Some(on_time_event_thunk::<T>),
184    };
185}
186
187unsafe extern "C" fn prepare_thunk<T: PluginController>(
188    request_json: BorrowedStr<'_>,
189) -> PluginResult<OwnedBytes> {
190    guard(|| {
191        // SAFETY: host promises `request_json` borrows storage that is live
192        // for the duration of this call.
193        let request = unsafe { request_json.as_str() };
194        T::prepare(request)
195            .map(OwnedBytes::from_vec)
196            .map_err(|e| PluginError::new(PluginErrorCode::Generic, e.to_string()))
197    })
198}
199
200unsafe extern "C" fn create_thunk<T: PluginController>(
201    host: *const ControllerHostVTable,
202    ctx: *const ControllerHostContext,
203    config_json: BorrowedStr<'_>,
204) -> *mut PluginControllerHandle {
205    guard_infallible("controller::create", || {
206        // SAFETY: host promises `config_json` borrows storage that is live
207        // for the duration of this call.
208        let cfg = unsafe { config_json.as_str() };
209        Box::into_raw(Box::new(T::new(host, ctx, cfg))).cast::<PluginControllerHandle>()
210    })
211}
212
213unsafe extern "C" fn drop_handle_thunk<T: PluginController>(handle: *mut PluginControllerHandle) {
214    if handle.is_null() {
215        return;
216    }
217    guard_infallible("controller::drop", || {
218        // SAFETY: handle was allocated via `Box::into_raw(Box::new(T))`.
219        unsafe {
220            drop(Box::from_raw(handle.cast::<T>()));
221        }
222    });
223}
224
225unsafe extern "C" fn type_name_thunk<T: PluginController>() -> BorrowedStr<'static> {
226    BorrowedStr::from_str(T::TYPE_NAME)
227}
228
229fn handle_as_mut<'a, T: PluginController>(handle: *mut PluginControllerHandle) -> &'a mut T {
230    // SAFETY: handle is non-null and originates from a `Box::into_raw` of a
231    // `T`. The host promises exclusive access while a callback is running.
232    unsafe { &mut *handle.cast::<T>() }
233}
234
235fn ok_or_err<E: ::core::fmt::Display>(r: Result<(), E>) -> Result<(), PluginError> {
236    r.map_err(|e| PluginError::new(PluginErrorCode::Generic, e.to_string()))
237}
238
239macro_rules! lifecycle_thunk {
240    ($name:ident, $method:ident) => {
241        unsafe extern "C" fn $name<T: PluginController>(
242            handle: *mut PluginControllerHandle,
243        ) -> PluginResult<()> {
244            guard(|| {
245                let controller = handle_as_mut::<T>(handle);
246                ok_or_err(controller.$method())
247            })
248        }
249    };
250}
251
252lifecycle_thunk!(on_start_thunk, on_start);
253lifecycle_thunk!(on_stop_thunk, on_stop);
254lifecycle_thunk!(on_resume_thunk, on_resume);
255lifecycle_thunk!(on_reset_thunk, on_reset);
256lifecycle_thunk!(on_dispose_thunk, on_dispose);
257lifecycle_thunk!(on_degrade_thunk, on_degrade);
258lifecycle_thunk!(on_fault_thunk, on_fault);
259
260unsafe extern "C" fn on_time_event_thunk<T: PluginController>(
261    handle: *mut PluginControllerHandle,
262    event: *const TimeEvent,
263) -> PluginResult<()> {
264    guard(|| {
265        // SAFETY: host keeps `event` live for the duration of the call; the
266        // plug-in only borrows it for the trait-method invocation.
267        let event = unsafe { &*event }.boundary_normalized();
268        let controller = handle_as_mut::<T>(handle);
269        ok_or_err(controller.on_time_event(&event))
270    })
271}