nautilus_plugin/bridge/registry.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//! Per-instance opaque context attached to every plug-in strategy or actor.
17//!
18//! The host hands the plug-in a `*const HostContext` at create time; the
19//! plug-in passes the same pointer back through `HostVTable::submit_order`
20//! and friends. The host side interprets the pointer as
21//! [`HostContextInner`] to recover the calling adapter's `ActorId`, then
22//! looks the adapter up via the thread-local actor registry.
23//!
24//! The host owns the allocation: adapters allocate one [`HostContextInner`]
25//! per instance via [`leak_host_context`] when they create the plug-in handle,
26//! and release it via [`drop_host_context`] when they drop the handle.
27
28#![allow(unsafe_code)]
29
30#[cfg(test)]
31use std::sync::{
32 Mutex, MutexGuard,
33 atomic::{AtomicUsize, Ordering},
34};
35
36use nautilus_model::identifiers::ActorId;
37
38use crate::host::{ControllerHostContext, HostContext};
39
40#[cfg(test)]
41static HOST_CONTEXT_LIVE: AtomicUsize = AtomicUsize::new(0);
42
43#[cfg(test)]
44static CONTROLLER_HOST_CONTEXT_LIVE: AtomicUsize = AtomicUsize::new(0);
45
46#[cfg(test)]
47static HOST_CONTEXT_TEST_LOCK: Mutex<()> = Mutex::new(());
48
49/// Serializes leak-counter assertions across parallel tests. Acquire this
50/// guard at the top of any test that reads [`host_context_live_count`].
51/// Test-only.
52#[cfg(test)]
53pub fn host_context_test_lock() -> MutexGuard<'static, ()> {
54 // Poisoning can occur if a panic interrupts a holder; clear it so
55 // subsequent tests still serialize cleanly.
56 HOST_CONTEXT_TEST_LOCK
57 .lock()
58 .unwrap_or_else(|poisoned| poisoned.into_inner())
59}
60
61/// Returns the number of [`HostContextInner`] allocations currently alive.
62/// Test-only: used to verify the adapter's leak/free pairing.
63#[cfg(test)]
64#[must_use]
65pub fn host_context_live_count() -> usize {
66 HOST_CONTEXT_LIVE.load(Ordering::SeqCst)
67}
68
69/// Serializes controller-context leak-counter assertions across parallel tests.
70/// Test-only.
71#[cfg(test)]
72pub fn controller_host_context_test_lock() -> MutexGuard<'static, ()> {
73 // Poisoning can occur if a panic interrupts a holder; clear it so
74 // subsequent tests still serialize cleanly.
75 HOST_CONTEXT_TEST_LOCK
76 .lock()
77 .unwrap_or_else(|poisoned| poisoned.into_inner())
78}
79
80/// Returns the number of controller host-context allocations currently alive.
81/// Test-only.
82#[cfg(test)]
83#[must_use]
84pub fn controller_host_context_live_count() -> usize {
85 CONTROLLER_HOST_CONTEXT_LIVE.load(Ordering::SeqCst)
86}
87
88/// Inner payload behind the opaque `*const HostContext` the host hands every
89/// plug-in instance.
90#[repr(C)]
91#[derive(Debug)]
92pub struct HostContextInner {
93 /// Canonical identifier of the host-side adapter that owns the plug-in
94 /// instance. Looked up in the thread-local actor registry by the host
95 /// vtable's order-command thunks.
96 pub actor_id: ActorId,
97
98 /// Whether the registered adapter is a strategy. The host's order-command
99 /// thunks reject calls coming from actor contexts because actors must not
100 /// submit orders.
101 pub is_strategy: bool,
102}
103
104/// Inner payload behind the opaque `*const ControllerHostContext`.
105#[repr(C)]
106#[derive(Debug)]
107pub struct ControllerHostContextInner {
108 /// Plug-in name from the manifest.
109 pub plugin_name: String,
110
111 /// Canonical controller type name.
112 pub type_name: String,
113}
114
115/// Boxes `inner` on the heap, leaks it, and returns the resulting pointer as
116/// a `*const HostContext` to hand to a plug-in.
117///
118/// Pair every leak with a matching [`drop_host_context`] when the plug-in
119/// instance is dropped to avoid leaking the allocation.
120#[must_use]
121pub fn leak_host_context(inner: HostContextInner) -> *const HostContext {
122 #[cfg(test)]
123 HOST_CONTEXT_LIVE.fetch_add(1, Ordering::SeqCst);
124 Box::into_raw(Box::new(inner)).cast::<HostContext>()
125}
126
127/// Boxes `inner` on the heap and returns it as a `*const ControllerHostContext`.
128#[must_use]
129pub fn leak_controller_host_context(
130 inner: ControllerHostContextInner,
131) -> *const ControllerHostContext {
132 #[cfg(test)]
133 CONTROLLER_HOST_CONTEXT_LIVE.fetch_add(1, Ordering::SeqCst);
134 Box::into_raw(Box::new(inner)).cast::<ControllerHostContext>()
135}
136
137/// Reclaims a previously [`leak_host_context`]-leaked allocation.
138///
139/// # Safety
140///
141/// `ctx` must originate from [`leak_host_context`] and must not be aliased.
142pub unsafe fn drop_host_context(ctx: *const HostContext) {
143 if ctx.is_null() {
144 return;
145 }
146 #[cfg(test)]
147 HOST_CONTEXT_LIVE.fetch_sub(1, Ordering::SeqCst);
148 // SAFETY: caller upholds the origin and aliasing contract.
149 unsafe {
150 drop(Box::from_raw(ctx.cast_mut().cast::<HostContextInner>()));
151 }
152}
153
154/// Reclaims a previously [`leak_controller_host_context`]-leaked allocation.
155///
156/// # Safety
157///
158/// `ctx` must originate from [`leak_controller_host_context`] and must not be
159/// aliased.
160pub unsafe fn drop_controller_host_context(ctx: *const ControllerHostContext) {
161 if ctx.is_null() {
162 return;
163 }
164 #[cfg(test)]
165 CONTROLLER_HOST_CONTEXT_LIVE.fetch_sub(1, Ordering::SeqCst);
166 // SAFETY: caller upholds the origin and aliasing contract.
167 unsafe {
168 drop(Box::from_raw(
169 ctx.cast_mut().cast::<ControllerHostContextInner>(),
170 ));
171 }
172}
173
174/// Interprets `ctx` as a `*const HostContextInner` and returns a reference.
175///
176/// Returns `None` if `ctx` is null.
177///
178/// # Safety
179///
180/// `ctx` must originate from [`leak_host_context`] and must still be live.
181#[must_use]
182pub unsafe fn host_context_inner<'a>(ctx: *const HostContext) -> Option<&'a HostContextInner> {
183 if ctx.is_null() {
184 return None;
185 }
186 // SAFETY: caller upholds the origin and liveness contract.
187 Some(unsafe { &*ctx.cast::<HostContextInner>() })
188}
189
190/// Interprets `ctx` as a `*const ControllerHostContextInner`.
191///
192/// Returns `None` if `ctx` is null.
193///
194/// # Safety
195///
196/// `ctx` must originate from [`leak_controller_host_context`] and must still
197/// be live.
198#[must_use]
199pub unsafe fn controller_host_context_inner<'a>(
200 ctx: *const ControllerHostContext,
201) -> Option<&'a ControllerHostContextInner> {
202 if ctx.is_null() {
203 return None;
204 }
205 // SAFETY: caller upholds the origin and liveness contract.
206 Some(unsafe { &*ctx.cast::<ControllerHostContextInner>() })
207}
208
209#[cfg(test)]
210mod tests {
211 use rstest::rstest;
212
213 use super::*;
214
215 #[rstest]
216 fn leak_round_trip() {
217 let _guard = host_context_test_lock();
218 let before = host_context_live_count();
219 let id = ActorId::from("TEST-001");
220 let ctx = leak_host_context(HostContextInner {
221 actor_id: id,
222 is_strategy: true,
223 });
224 assert_eq!(host_context_live_count(), before + 1);
225
226 // SAFETY: ctx came from leak_host_context, still live.
227 let inner = unsafe { host_context_inner(ctx) }.unwrap();
228 assert_eq!(inner.actor_id, id);
229 assert!(inner.is_strategy);
230
231 // SAFETY: ctx came from leak_host_context.
232 unsafe { drop_host_context(ctx) };
233 assert_eq!(host_context_live_count(), before);
234 }
235
236 #[rstest]
237 fn host_context_inner_null_returns_none() {
238 // SAFETY: documented behaviour for null input.
239 assert!(unsafe { host_context_inner(std::ptr::null()) }.is_none());
240 }
241
242 #[rstest]
243 fn drop_host_context_null_is_noop() {
244 // SAFETY: documented behaviour for null input.
245 unsafe { drop_host_context(std::ptr::null()) };
246 }
247}