Skip to main content

nautilus_common/ffi/
timer.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//! FFI types and functions for time event handling.
17//!
18//! The [`TimeEventHandler_API`] handed to Cython stores only a borrowed `Py<PyAny>*`
19//! (`callback_ptr`). To make sure the pointed-to Python object stays alive while
20//! *any* handler referencing it exists, we keep a single `Arc<Py<PyAny>>` per raw
21//! pointer in an internal registry together with a manual reference counter.
22//!
23//! Why a registry instead of extra fields:
24//! - The C ABI must remain `struct { TimeEvent, char * }` - adding bytes to the
25//!   struct would break all generated headers.
26//! - `Arc<Py<..>>` guarantees GIL-safe INC/DEC but cannot be represented in C.
27//!   Storing it externally preserves layout while retaining safety.
28//!
29//! Drop strategy:
30//! 1. Cloning a handler increments the per-pointer counter.
31//! 2. Dropping a handler decrements it; if the count hits zero we remove the
32//!    entry *then* release the `Arc` under `Python::attach`. The drop happens
33//!    *outside* the mutex guard to avoid dead-locking when Python finalisers
34//!    re-enter the registry.
35//!
36//! This design removes all manual INCREF/DECREF on `callback_ptr`, eliminates
37//! leaks, and is safe on any thread.
38
39use std::ffi::c_char;
40#[cfg(feature = "python")]
41use std::sync::{Mutex, OnceLock};
42
43#[cfg(feature = "python")]
44use ahash::AHashMap;
45#[cfg(feature = "python")]
46use nautilus_core::MUTEX_POISONED;
47#[cfg(feature = "python")]
48use nautilus_core::python::clone_py_object;
49use nautilus_core::{
50    UUID4,
51    ffi::string::{cstr_to_ustr, str_to_cstr},
52};
53#[cfg(feature = "python")]
54use pyo3::prelude::*;
55use ustr::ustr;
56
57use crate::timer::{TimeEvent, TimeEventCallback, TimeEventHandler};
58
59#[repr(C)]
60#[derive(Debug)]
61#[allow(non_camel_case_types)]
62/// FFI time event handler for Cython interoperability.
63///
64/// Associates a `TimeEvent` with a callback function that is triggered
65/// when the event's timestamp is reached.
66pub struct TimeEventHandler_API {
67    /// The time event.
68    pub event: TimeEvent,
69    /// The callable raw pointer.
70    pub callback_ptr: *mut c_char,
71}
72
73#[cfg(feature = "python")]
74type CallbackEntry = (Py<PyAny>, usize); // (object, ref_count)
75
76#[cfg(feature = "python")]
77fn registry() -> &'static Mutex<AHashMap<usize, CallbackEntry>> {
78    static REG: OnceLock<Mutex<AHashMap<usize, CallbackEntry>>> = OnceLock::new();
79    REG.get_or_init(|| Mutex::new(AHashMap::new()))
80}
81
82// Helper to obtain the registry lock, tolerant to poisoning so Drop cannot panic
83#[cfg(feature = "python")]
84fn registry_lock() -> std::sync::MutexGuard<'static, AHashMap<usize, CallbackEntry>> {
85    match registry().lock() {
86        Ok(g) => g,
87        Err(poisoned) => poisoned.into_inner(),
88    }
89}
90
91#[cfg(feature = "python")]
92pub fn registry_size() -> usize {
93    registry_lock().len()
94}
95
96#[cfg(feature = "python")]
97pub fn cleanup_callback_registry() {
98    // Drain entries while locked, then drop callbacks with the GIL outside
99    let callbacks: Vec<Py<PyAny>> = {
100        let mut map = registry_lock();
101        map.drain().map(|(_, (obj, _))| obj).collect()
102    };
103
104    Python::attach(|_| {
105        for cb in callbacks {
106            drop(cb);
107        }
108    });
109}
110
111// Conversion from TimeEventHandler to TimeEventHandler_API for FFI
112// Only supports Python callbacks; available when `python` feature is enabled
113#[cfg(feature = "python")]
114impl From<TimeEventHandler> for TimeEventHandler_API {
115    /// # Panics
116    ///
117    /// Panics if the provided `TimeEventHandler` contains a Rust callback,
118    /// since only Python callbacks are supported by `TimeEventHandler_API`.
119    fn from(value: TimeEventHandler) -> Self {
120        match value.callback {
121            TimeEventCallback::Python(callback) => {
122                let raw_ptr = callback.callback().as_ptr().cast::<c_char>();
123
124                // Keep an explicit ref-count per raw pointer in the registry.
125                let key = raw_ptr as usize;
126                let mut map = registry_lock();
127                match map.entry(key) {
128                    std::collections::hash_map::Entry::Occupied(mut e) => {
129                        e.get_mut().1 += 1;
130                    }
131                    std::collections::hash_map::Entry::Vacant(e) => {
132                        e.insert((clone_py_object(callback.callback()), 1));
133                    }
134                }
135
136                Self {
137                    event: value.event,
138                    callback_ptr: raw_ptr,
139                }
140            }
141            TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => {
142                panic!("Legacy time event handler is not supported for Rust callbacks")
143            }
144        }
145    }
146}
147
148// Remove the callback from the registry when the last handler using the raw
149// pointer is about to disappear.  We only drop the Arc if its strong count is
150// 1 (i.e. this handler owns the final reference).  Dropping happens while
151// holding the GIL so it is always safe.
152
153#[cfg(feature = "python")]
154impl Drop for TimeEventHandler_API {
155    fn drop(&mut self) {
156        if self.callback_ptr.is_null() {
157            return;
158        }
159
160        let key = self.callback_ptr as usize;
161        let mut map = registry().lock().expect(MUTEX_POISONED);
162        if let Some(entry) = map.get_mut(&key) {
163            if entry.1 > 1 {
164                entry.1 -= 1;
165                return;
166            }
167            // This was the final handler – remove entry and drop Arc under GIL
168            let (arc, _) = map.remove(&key).unwrap();
169            Python::attach(|_| drop(arc));
170        }
171    }
172}
173
174impl Clone for TimeEventHandler_API {
175    fn clone(&self) -> Self {
176        #[cfg(feature = "python")]
177        {
178            if !self.callback_ptr.is_null() {
179                let key = self.callback_ptr as usize;
180                let mut map = registry_lock();
181                if let Some(entry) = map.get_mut(&key) {
182                    entry.1 += 1;
183                }
184            }
185        }
186
187        Self {
188            event: self.event.clone(),
189            callback_ptr: self.callback_ptr,
190        }
191    }
192}
193
194impl TimeEventHandler_API {
195    /// Creates a null (sentinel) `TimeEventHandler_API`.
196    ///
197    /// Used to indicate "no event" when returning from pop operations.
198    #[must_use]
199    pub fn null() -> Self {
200        Self {
201            event: TimeEvent::new(ustr(""), UUID4::default(), 0.into(), 0.into()),
202            callback_ptr: std::ptr::null_mut(),
203        }
204    }
205}
206
207/// Drops a `TimeEventHandler_API`, releasing any Python callback reference.
208///
209/// The handler must be valid and not previously dropped.
210#[unsafe(no_mangle)]
211pub extern "C" fn time_event_handler_drop(handler: TimeEventHandler_API) {
212    drop(handler);
213}
214
215#[cfg(all(test, feature = "python"))]
216mod tests {
217    use nautilus_core::UUID4;
218    use pyo3::{Py, Python, types::PyList};
219    use rstest::rstest;
220    use ustr::Ustr;
221
222    use super::*;
223    use crate::timer::{TimeEvent, TimeEventCallback};
224
225    #[rstest]
226    fn registry_clears_after_handler_drop() {
227        Python::initialize();
228        Python::attach(|py| {
229            let py_list = PyList::empty(py);
230            let callback = TimeEventCallback::from(Py::from(py_list.getattr("append").unwrap()));
231
232            let handler = TimeEventHandler::new(
233                TimeEvent::new(Ustr::from("TEST"), UUID4::new(), 1.into(), 1.into()),
234                callback,
235            );
236
237            // Wrap in block so handler drops before we assert size
238            {
239                let _api: TimeEventHandler_API = handler.into();
240                assert_eq!(registry_size(), 1);
241            }
242
243            // After drop registry should be empty
244            assert_eq!(registry_size(), 0);
245        });
246    }
247}
248
249// Fallback conversion for non-Python callbacks: Rust callbacks only
250#[cfg(not(feature = "python"))]
251impl From<TimeEventHandler> for TimeEventHandler_API {
252    fn from(value: TimeEventHandler) -> Self {
253        // Only Rust callbacks are supported in non-python builds
254        match value.callback {
255            TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => Self {
256                event: value.event,
257                callback_ptr: std::ptr::null_mut(),
258            },
259            #[cfg(feature = "python")]
260            TimeEventCallback::Python(_) => {
261                unreachable!("Python callback not supported without python feature")
262            }
263        }
264    }
265}
266
267/// # Safety
268///
269/// Assumes `name_ptr` is borrowed from a valid Python UTF-8 `str`.
270#[unsafe(no_mangle)]
271pub unsafe extern "C" fn time_event_new(
272    name_ptr: *const c_char,
273    event_id: UUID4,
274    ts_event: u64,
275    ts_init: u64,
276) -> TimeEvent {
277    // SAFETY: `name_ptr` is guaranteed to be a valid C string by the FFI caller contract.
278    TimeEvent::new(
279        unsafe { cstr_to_ustr(name_ptr) },
280        event_id,
281        ts_event.into(),
282        ts_init.into(),
283    )
284}
285
286/// Returns a [`TimeEvent`] as a C string pointer.
287#[unsafe(no_mangle)]
288pub extern "C" fn time_event_to_cstr(event: &TimeEvent) -> *const c_char {
289    str_to_cstr(&event.to_string())
290}
291
292// This function only exists so that `TimeEventHandler_API` is included in the definitions
293#[unsafe(no_mangle)]
294pub const extern "C" fn dummy(v: TimeEventHandler_API) -> TimeEventHandler_API {
295    v
296}