Skip to main content

nautilus_common/ffi/
clock.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
16use std::{
17    ffi::c_char,
18    ops::{Deref, DerefMut},
19};
20
21#[cfg(feature = "python")]
22use nautilus_core::correctness::FAILED;
23use nautilus_core::{
24    UnixNanos,
25    ffi::{
26        cvec::CVec,
27        parsing::u8_as_bool,
28        string::{cstr_as_str, str_to_cstr},
29    },
30};
31#[cfg(feature = "python")]
32use pyo3::{ffi, prelude::*};
33
34use super::timer::TimeEventHandler_API;
35#[cfg(feature = "python")]
36use crate::timer::TimeEventCallback;
37use crate::{
38    clock::{Clock, TestClock},
39    live::clock::LiveClock,
40    timer::TimeEvent,
41};
42
43/// C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`].
44///
45/// This struct wraps `TestClock` in a way that makes it compatible with C function
46/// calls, enabling interaction with `TestClock` in a C environment.
47///
48/// It implements the `Deref` trait, allowing instances of `TestClock_API` to be
49/// dereferenced to `TestClock`, providing access to `TestClock`'s methods without
50/// having to manually access the underlying `TestClock` instance.
51#[repr(C)]
52#[derive(Debug)]
53#[allow(non_camel_case_types)]
54pub struct TestClock_API(Box<TestClock>);
55
56impl Deref for TestClock_API {
57    type Target = TestClock;
58
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64impl DerefMut for TestClock_API {
65    fn deref_mut(&mut self) -> &mut Self::Target {
66        &mut self.0
67    }
68}
69
70#[unsafe(no_mangle)]
71pub extern "C" fn test_clock_new() -> TestClock_API {
72    TestClock_API(Box::default())
73}
74
75#[unsafe(no_mangle)]
76pub extern "C" fn test_clock_drop(clock: TestClock_API) {
77    drop(clock); // Memory freed here
78}
79
80/// Registers the default callback handler for `TestClock`.
81///
82/// # Safety
83///
84/// Assumes `callback_ptr` is a valid `PyCallable` pointer.
85///
86/// # Panics
87///
88/// Panics if the `callback_ptr` is null or represents the Python `None` object.
89#[cfg(feature = "python")]
90#[unsafe(no_mangle)]
91pub unsafe extern "C" fn test_clock_register_default_handler(
92    clock: &mut TestClock_API,
93    callback_ptr: *mut ffi::PyObject,
94) {
95    assert!(!callback_ptr.is_null());
96    assert!(unsafe { ffi::Py_None() } != callback_ptr);
97
98    let callback = Python::attach(|py| unsafe {
99        Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
100    });
101    let callback = TimeEventCallback::from_python_legacy_capsule(callback);
102
103    clock.register_default_handler(callback);
104}
105
106/// Cancels the default callback handler for `TestClock` (releases the held callback).
107#[unsafe(no_mangle)]
108pub extern "C" fn test_clock_cancel_default_handler(clock: &mut TestClock_API) {
109    clock.cancel_default_handler();
110}
111
112/// Cancels all registered named callbacks for `TestClock` (releases held callbacks).
113#[unsafe(no_mangle)]
114pub extern "C" fn test_clock_cancel_callbacks(clock: &mut TestClock_API) {
115    clock.cancel_callbacks();
116}
117
118#[unsafe(no_mangle)]
119pub extern "C" fn test_clock_set_time(clock: &TestClock_API, to_time_ns: u64) {
120    clock.set_time(to_time_ns.into());
121}
122
123#[unsafe(no_mangle)]
124pub extern "C" fn test_clock_timestamp(clock: &TestClock_API) -> f64 {
125    clock.get_time()
126}
127
128#[unsafe(no_mangle)]
129pub extern "C" fn test_clock_timestamp_ms(clock: &TestClock_API) -> u64 {
130    clock.get_time_ms()
131}
132
133#[unsafe(no_mangle)]
134pub extern "C" fn test_clock_timestamp_us(clock: &TestClock_API) -> u64 {
135    clock.get_time_us()
136}
137
138#[unsafe(no_mangle)]
139pub extern "C" fn test_clock_timestamp_ns(clock: &TestClock_API) -> u64 {
140    clock.get_time_ns().as_u64()
141}
142
143#[unsafe(no_mangle)]
144pub extern "C" fn test_clock_timer_names(clock: &TestClock_API) -> *const c_char {
145    // For simplicity we join a string with a reasonably unique delimiter.
146    // This is a temporary solution pending the removal of Cython.
147    str_to_cstr(&clock.timer_names().join("<,>"))
148}
149
150#[unsafe(no_mangle)]
151pub extern "C" fn test_clock_timer_count(clock: &mut TestClock_API) -> usize {
152    clock.timer_count()
153}
154
155/// # Safety
156///
157/// This function assumes:
158/// - `name_ptr` is a valid C string pointer.
159/// - `callback_ptr` is a valid `PyCallable` pointer.
160///
161/// # Panics
162///
163/// Panics if `callback_ptr` is null or if setting the timer fails.
164#[cfg(feature = "python")]
165#[unsafe(no_mangle)]
166pub unsafe extern "C" fn test_clock_set_time_alert(
167    clock: &mut TestClock_API,
168    name_ptr: *const c_char,
169    alert_time_ns: UnixNanos,
170    callback_ptr: *mut ffi::PyObject,
171    allow_past: u8,
172) {
173    assert!(!callback_ptr.is_null());
174
175    let name = unsafe { cstr_as_str(name_ptr) };
176    let callback = if callback_ptr == unsafe { ffi::Py_None() } {
177        None
178    } else {
179        let callback = Python::attach(|py| unsafe {
180            Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
181        });
182        Some(TimeEventCallback::from_python_legacy_capsule(callback))
183    };
184
185    clock
186        .set_time_alert_ns(name, alert_time_ns, callback, Some(allow_past != 0))
187        .expect(FAILED);
188}
189
190/// # Safety
191///
192/// This function assumes:
193/// - `name_ptr` is a valid C string pointer.
194/// - `callback_ptr` is a valid `PyCallable` pointer.
195///
196/// # Parameters
197///
198/// - `start_time_ns`: UNIX timestamp in nanoseconds. Use `0` to indicate "use current time".
199/// - `stop_time_ns`: UNIX timestamp in nanoseconds. Use `0` to indicate "no stop time".
200///
201/// # Panics
202///
203/// Panics if `callback_ptr` is null or represents the Python `None` object.
204#[cfg(feature = "python")]
205#[unsafe(no_mangle)]
206pub unsafe extern "C" fn test_clock_set_timer(
207    clock: &mut TestClock_API,
208    name_ptr: *const c_char,
209    interval_ns: u64,
210    start_time_ns: UnixNanos,
211    stop_time_ns: UnixNanos,
212    callback_ptr: *mut ffi::PyObject,
213    allow_past: u8,
214    fire_immediately: u8,
215) {
216    assert!(!callback_ptr.is_null());
217
218    let name = unsafe { cstr_as_str(name_ptr) };
219    // C API convention: 0 means None (use defaults)
220    let start_time_ns = (start_time_ns != 0).then_some(start_time_ns);
221    let stop_time_ns = (stop_time_ns != 0).then_some(stop_time_ns);
222    let callback = if callback_ptr == unsafe { ffi::Py_None() } {
223        None
224    } else {
225        let callback = Python::attach(|py| unsafe {
226            Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
227        });
228        Some(TimeEventCallback::from_python_legacy_capsule(callback))
229    };
230
231    clock
232        .set_timer_ns(
233            name,
234            interval_ns,
235            start_time_ns,
236            stop_time_ns,
237            callback,
238            Some(allow_past != 0),
239            Some(fire_immediately != 0),
240        )
241        .expect(FAILED);
242}
243
244/// # Safety
245///
246/// Assumes `set_time` is a correct `uint8_t` of either 0 or 1.
247#[unsafe(no_mangle)]
248pub unsafe extern "C" fn test_clock_advance_time(
249    clock: &mut TestClock_API,
250    to_time_ns: u64,
251    set_time: u8,
252) -> CVec {
253    let events: Vec<TimeEvent> = clock.advance_time(to_time_ns.into(), u8_as_bool(set_time));
254    let t: Vec<TimeEventHandler_API> = clock
255        .match_handlers(events)
256        .into_iter()
257        .map(Into::into)
258        .collect();
259    t.into()
260}
261
262// TODO: This drop helper may leak Python callbacks when handlers own Python objects.
263//       We need to mirror the `ffi::timer` registry so reference counts are decremented properly.
264/// Drops a `CVec` of `TimeEventHandler_API` values.
265///
266/// # Panics
267///
268/// Panics if `CVec` invariants are violated (corrupted metadata).
269#[unsafe(no_mangle)]
270pub extern "C" fn vec_time_event_handlers_drop(v: CVec) {
271    let CVec { ptr, len, cap } = v;
272
273    assert!(
274        len <= cap,
275        "vec_time_event_handlers_drop: len ({len}) > cap ({cap}) - memory corruption or wrong drop helper"
276    );
277    assert!(
278        len == 0 || !ptr.is_null(),
279        "vec_time_event_handlers_drop: null ptr with non-zero len ({len}) - memory corruption or wrong drop helper"
280    );
281
282    let data: Vec<TimeEventHandler_API> =
283        unsafe { Vec::from_raw_parts(ptr.cast::<TimeEventHandler_API>(), len, cap) };
284    drop(data); // Memory freed here
285}
286
287/// # Safety
288///
289/// Assumes `name_ptr` is a valid C string pointer.
290#[unsafe(no_mangle)]
291pub unsafe extern "C" fn test_clock_next_time(
292    clock: &mut TestClock_API,
293    name_ptr: *const c_char,
294) -> UnixNanos {
295    let name = unsafe { cstr_as_str(name_ptr) };
296    clock.next_time_ns(name).unwrap_or_default()
297}
298
299/// # Safety
300///
301/// Assumes `name_ptr` is a valid C string pointer.
302#[unsafe(no_mangle)]
303pub unsafe extern "C" fn test_clock_cancel_timer(
304    clock: &mut TestClock_API,
305    name_ptr: *const c_char,
306) {
307    let name = unsafe { cstr_as_str(name_ptr) };
308    clock.cancel_timer(name);
309}
310
311#[unsafe(no_mangle)]
312pub extern "C" fn test_clock_cancel_timers(clock: &mut TestClock_API) {
313    clock.cancel_timers();
314}
315
316/// C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`].
317///
318/// This struct wraps `LiveClock` in a way that makes it compatible with C function
319/// calls, enabling interaction with `LiveClock` in a C environment.
320///
321/// It implements the `Deref` and `DerefMut` traits, allowing instances of `LiveClock_API` to be
322/// dereferenced to `LiveClock`, providing access to `LiveClock`'s methods without
323/// having to manually access the underlying `LiveClock` instance. This includes
324/// both mutable and immutable access.
325#[repr(C)]
326#[derive(Debug)]
327#[allow(non_camel_case_types)]
328pub struct LiveClock_API(Box<LiveClock>);
329
330impl Deref for LiveClock_API {
331    type Target = LiveClock;
332
333    fn deref(&self) -> &Self::Target {
334        &self.0
335    }
336}
337
338impl DerefMut for LiveClock_API {
339    fn deref_mut(&mut self) -> &mut Self::Target {
340        &mut self.0
341    }
342}
343
344#[unsafe(no_mangle)]
345pub extern "C" fn live_clock_new() -> LiveClock_API {
346    // Initialize a live clock without a time event sender
347    LiveClock_API(Box::new(LiveClock::new(None)))
348}
349
350#[unsafe(no_mangle)]
351pub extern "C" fn live_clock_drop(clock: LiveClock_API) {
352    drop(clock); // Memory freed here
353}
354
355/// # Safety
356///
357/// Assumes `callback_ptr` is a valid `PyCallable` pointer.
358///
359/// # Panics
360///
361/// Panics if `callback_ptr` is null or represents the Python `None` object.
362#[cfg(feature = "python")]
363#[unsafe(no_mangle)]
364pub unsafe extern "C" fn live_clock_register_default_handler(
365    clock: &mut LiveClock_API,
366    callback_ptr: *mut ffi::PyObject,
367) {
368    assert!(!callback_ptr.is_null());
369    assert!(unsafe { ffi::Py_None() } != callback_ptr);
370
371    let callback = Python::attach(|py| unsafe {
372        Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
373    });
374    let callback = TimeEventCallback::from_python_legacy_capsule(callback);
375
376    clock.register_default_handler(callback);
377}
378
379/// Cancels the default callback handler for `LiveClock` (releases the held callback).
380#[unsafe(no_mangle)]
381pub extern "C" fn live_clock_cancel_default_handler(clock: &mut LiveClock_API) {
382    clock.cancel_default_handler();
383}
384
385/// Cancels all registered named callbacks for `LiveClock` (releases held callbacks).
386#[unsafe(no_mangle)]
387pub extern "C" fn live_clock_cancel_callbacks(clock: &mut LiveClock_API) {
388    clock.cancel_callbacks();
389}
390
391#[unsafe(no_mangle)]
392pub extern "C" fn live_clock_timestamp(clock: &mut LiveClock_API) -> f64 {
393    clock.get_time()
394}
395
396#[unsafe(no_mangle)]
397pub extern "C" fn live_clock_timestamp_ms(clock: &mut LiveClock_API) -> u64 {
398    clock.get_time_ms()
399}
400
401#[unsafe(no_mangle)]
402pub extern "C" fn live_clock_timestamp_us(clock: &mut LiveClock_API) -> u64 {
403    clock.get_time_us()
404}
405
406#[unsafe(no_mangle)]
407pub extern "C" fn live_clock_timestamp_ns(clock: &mut LiveClock_API) -> u64 {
408    clock.get_time_ns().as_u64()
409}
410
411#[unsafe(no_mangle)]
412pub extern "C" fn live_clock_timer_names(clock: &LiveClock_API) -> *const c_char {
413    // For simplicity we join a string with a reasonably unique delimiter.
414    // This is a temporary solution pending the removal of Cython.
415    str_to_cstr(&clock.timer_names().join("<,>"))
416}
417
418#[unsafe(no_mangle)]
419pub extern "C" fn live_clock_timer_count(clock: &mut LiveClock_API) -> usize {
420    clock.timer_count()
421}
422
423/// # Safety
424///
425/// This function assumes:
426/// - `name_ptr` is a valid C string pointer.
427/// - `callback_ptr` is a valid `PyCallable` pointer.
428///
429/// # Panics
430///
431/// This function panics if:
432/// - `name` is not a valid string.
433/// - `callback_ptr` is NULL and no default callback has been assigned on the clock.
434#[cfg(feature = "python")]
435#[unsafe(no_mangle)]
436pub unsafe extern "C" fn live_clock_set_time_alert(
437    clock: &mut LiveClock_API,
438    name_ptr: *const c_char,
439    alert_time_ns: UnixNanos,
440    callback_ptr: *mut ffi::PyObject,
441    allow_past: u8,
442) {
443    assert!(!callback_ptr.is_null());
444
445    let name = unsafe { cstr_as_str(name_ptr) };
446    let callback = if callback_ptr == unsafe { ffi::Py_None() } {
447        None
448    } else {
449        let callback = Python::attach(|py| unsafe {
450            Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
451        });
452        Some(TimeEventCallback::from_python_legacy_capsule(callback))
453    };
454
455    clock
456        .set_time_alert_ns(name, alert_time_ns, callback, Some(allow_past != 0))
457        .expect(FAILED);
458}
459
460/// # Safety
461///
462/// This function assumes:
463/// - `name_ptr` is a valid C string pointer.
464/// - `callback_ptr` is a valid `PyCallable` pointer.
465///
466/// # Parameters
467///
468/// - `start_time_ns`: UNIX timestamp in nanoseconds. Use `0` to indicate "use current time".
469/// - `stop_time_ns`: UNIX timestamp in nanoseconds. Use `0` to indicate "no stop time".
470///
471/// # Panics
472///
473/// This function panics if:
474/// - `name` is not a valid string.
475/// - `callback_ptr` is NULL and no default callback has been assigned on the clock.
476#[cfg(feature = "python")]
477#[unsafe(no_mangle)]
478pub unsafe extern "C" fn live_clock_set_timer(
479    clock: &mut LiveClock_API,
480    name_ptr: *const c_char,
481    interval_ns: u64,
482    start_time_ns: UnixNanos,
483    stop_time_ns: UnixNanos,
484    callback_ptr: *mut ffi::PyObject,
485    allow_past: u8,
486    fire_immediately: u8,
487) {
488    assert!(!callback_ptr.is_null());
489
490    let name = unsafe { cstr_as_str(name_ptr) };
491    // C API convention: 0 means None (use defaults)
492    let start_time_ns = (start_time_ns != 0).then_some(start_time_ns);
493    let stop_time_ns = (stop_time_ns != 0).then_some(stop_time_ns);
494    let callback = if callback_ptr == unsafe { ffi::Py_None() } {
495        None
496    } else {
497        let callback = Python::attach(|py| unsafe {
498            Bound::<PyAny>::from_borrowed_ptr(py, callback_ptr).unbind()
499        });
500        Some(TimeEventCallback::from_python_legacy_capsule(callback))
501    };
502
503    clock
504        .set_timer_ns(
505            name,
506            interval_ns,
507            start_time_ns,
508            stop_time_ns,
509            callback,
510            Some(allow_past != 0),
511            Some(fire_immediately != 0),
512        )
513        .expect(FAILED);
514}
515
516/// # Safety
517///
518/// Assumes `name_ptr` is a valid C string pointer.
519#[unsafe(no_mangle)]
520pub unsafe extern "C" fn live_clock_next_time(
521    clock: &mut LiveClock_API,
522    name_ptr: *const c_char,
523) -> UnixNanos {
524    let name = unsafe { cstr_as_str(name_ptr) };
525    clock.next_time_ns(name).unwrap_or_default()
526}
527
528/// # Safety
529///
530/// Assumes `name_ptr` is a valid C string pointer.
531#[unsafe(no_mangle)]
532pub unsafe extern "C" fn live_clock_cancel_timer(
533    clock: &mut LiveClock_API,
534    name_ptr: *const c_char,
535) {
536    let name = unsafe { cstr_as_str(name_ptr) };
537    clock.cancel_timer(name);
538}
539
540#[unsafe(no_mangle)]
541pub extern "C" fn live_clock_cancel_timers(clock: &mut LiveClock_API) {
542    clock.cancel_timers();
543}
544
545#[cfg(all(test, feature = "python"))]
546mod tests {
547    use std::ffi::CString;
548
549    use nautilus_core::UUID4;
550    use pyo3::{
551        Bound, Py, PyAny, PyResult, Python,
552        types::{
553            PyAnyMethods, PyCFunction, PyDict, PyList, PyListMethods, PyTuple, PyTupleMethods,
554            PyTypeMethods,
555        },
556    };
557    use rstest::rstest;
558    use ustr::Ustr;
559
560    use super::*;
561
562    #[rstest]
563    fn test_clock_ffi_python_callbacks_use_legacy_capsules() {
564        Python::initialize();
565
566        Python::attach(|py| {
567            let seen = PyList::empty(py);
568            let callback = record_arg_type_callback(py, &seen);
569            let callback_ptr = callback.as_ptr();
570
571            let mut test_clock = test_clock_new();
572            unsafe { test_clock_register_default_handler(&mut test_clock, callback_ptr) };
573            test_clock.get_handler(time_event("test-default")).run();
574
575            let test_alert_name = CString::new("test-alert").unwrap();
576            unsafe {
577                test_clock_set_time_alert(
578                    &mut test_clock,
579                    test_alert_name.as_ptr(),
580                    UnixNanos::from(1_000),
581                    callback_ptr,
582                    1,
583                );
584            }
585            test_clock.get_handler(time_event("test-alert")).run();
586
587            let test_timer_name = CString::new("test-timer").unwrap();
588            unsafe {
589                test_clock_set_timer(
590                    &mut test_clock,
591                    test_timer_name.as_ptr(),
592                    1_000,
593                    UnixNanos::from(1_000),
594                    UnixNanos::from(0),
595                    callback_ptr,
596                    1,
597                    0,
598                );
599            }
600            test_clock.get_handler(time_event("test-timer")).run();
601
602            let mut live_clock = live_clock_new();
603            unsafe { live_clock_register_default_handler(&mut live_clock, callback_ptr) };
604            live_clock.get_handler(time_event("live-default")).run();
605
606            let live_now = live_clock_timestamp_ns(&mut live_clock);
607            let live_alert_name = CString::new("live-alert").unwrap();
608            unsafe {
609                live_clock_set_time_alert(
610                    &mut live_clock,
611                    live_alert_name.as_ptr(),
612                    UnixNanos::from(live_now + 10_000_000_000),
613                    callback_ptr,
614                    0,
615                );
616            }
617            live_clock.get_handler(time_event("live-alert")).run();
618
619            let live_timer_name = CString::new("live-timer").unwrap();
620            unsafe {
621                live_clock_set_timer(
622                    &mut live_clock,
623                    live_timer_name.as_ptr(),
624                    10_000_000_000,
625                    UnixNanos::from(0),
626                    UnixNanos::from(0),
627                    callback_ptr,
628                    1,
629                    0,
630                );
631            }
632            live_clock.get_handler(time_event("live-timer")).run();
633            live_clock.cancel_timers();
634
635            let seen_types = seen
636                .iter()
637                .map(|item| item.extract::<String>().unwrap())
638                .collect::<Vec<_>>();
639            assert_eq!(seen_types, vec!["PyCapsule".to_string(); 6]);
640        });
641    }
642
643    fn record_arg_type_callback(py: Python<'_>, seen: &Bound<'_, PyList>) -> Py<PyAny> {
644        let seen_obj = seen.clone().unbind().into_any();
645
646        new_sync_py_callback(
647            py,
648            move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<()> {
649                let arg = args.get_item(0)?;
650                let type_name = arg.get_type().name()?.to_string();
651                seen_obj.call_method1(args.py(), "append", (type_name,))?;
652                Ok(())
653            },
654        )
655        .expect("callback should create")
656        .into_any()
657        .unbind()
658    }
659
660    fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
661    where
662        F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()>
663            + Send
664            + Sync
665            + 'static,
666    {
667        PyCFunction::new_closure(py, None, None, closure)
668    }
669
670    fn time_event(name: &str) -> TimeEvent {
671        TimeEvent::new(
672            Ustr::from(name),
673            UUID4::from("00000000-0000-4000-8000-000000000011"),
674            UnixNanos::from(100),
675            UnixNanos::from(99),
676        )
677    }
678}