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