Skip to main content

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}