nautilus_common/ffi/
timer.rs

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