local_clock/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! An instance-local, reprogrammable, real time clock.
5//!
6//! This crate's primary export is the [`LocalClock`] trait, which defines an
7//! interface for interacting with instances of reprogrammable real time clocks,
8//! decoupled from any particular platform's global real time clock.
9//!
10//! Also included in `local_time` is [`SystemTimeClock`], which is a basic
11//! in-tree implementation of [`LocalClock`] backed by
12//! [`std::time::SystemTime`]. Unless you're worried about handling instances
13//! where `SystemTime` jumps backwards, this is perfectly reasonable
14//! implementation to use.
15//!
16//! This crate deliberately takes a minimalist approach to dependencies, and
17//! avoids pulling in any "heavyweight" date/time crates by default (e.g:
18//! `time`, `chrono`, etc...). That said, native integration with some of these
19//! crates is provided via optional features.
20//!
21//! # Features
22//!
23//! - `inspect` - Derives the [`Inspect`] trait on various types.
24//! - `time_exts` - Integration with the [`time`] crate
25//!     - Provides `From`/`Into` implementations to interop with duration and
26//!       date/time types from `time`.
27//!     - Changes the `Debug` implementation of [`LocalClockTime`] to print a
28//!       human readable date (instead of a raw duration since the Unix Epoch).
29//!
30//! [`Inspect`]: inspect::Inspect
31
32mod clock_impls;
33
34pub use clock_impls::MockLocalClock;
35pub use clock_impls::MockLocalClockAccessor;
36pub use clock_impls::SystemTimeClock;
37#[cfg(feature = "inspect")]
38pub use inspect_ext::InspectableLocalClock;
39
40use std::fmt::Debug;
41
42/// A local real-time clock, hanging-off the platform's global real-time clock.
43///
44/// One way to think about [`LocalClock`] is that it matches the semantics of
45/// the POSIX methods `clock_gettime` and `clock_settime` when backed by
46/// `CLOCK_REALTIME`, except setting the time on a [`LocalClock`] will only
47/// affect that particular instance (as opposed to changing the global system
48/// time).
49///
50/// NOTE: These methods may be invoked fairly often, and as such, implementors
51/// should ensure these methods do not block!
52pub trait LocalClock: Send {
53    /// Return the current clock time.
54    ///
55    /// # First call
56    ///
57    /// If `set_time` has yet to be called, the `LocalClock` trait makes _no
58    /// guarantees_ as to what time `get_time` will return!
59    ///
60    /// Conceptually, this would be akin to pulling the real-time-block battery
61    /// from a physical machine, thereby resetting the clock to its "default"
62    /// state - whatever that might be.
63    ///
64    /// A simple implementation would be to just return a hard-coded, fixed
65    /// value, corresponding to some arbitrary date.
66    ///
67    /// ...that being said, a far more useful implementation would be to simply
68    /// report the platform's current real time. That way, even if `set_time`
69    /// never gets invoked, (e.g: as a result of communicating with a NTP
70    /// server, a synthetic real-time assist virtual device, manual user input,
71    /// etc...), the reported real time will still be reasonably close to the
72    /// current date.
73    ///
74    /// # Subsequent calls
75    ///
76    /// On subsequent calls to this function, this method MUST return the sum of
77    /// the previously set time set via `set_time`, _plus_ the wall-clock time
78    /// that has elapsed since.
79    ///
80    /// NOTE: implementations SHOULD ensure that real time continues to tick
81    /// even when the device / platform itself has shutdown / been paused.
82    fn get_time(&mut self) -> LocalClockTime;
83
84    /// Set the current clock time.
85    fn set_time(&mut self, new_time: LocalClockTime);
86}
87
88/// A delta between two [`LocalClockTime`] instances.
89///
90/// Unlike [`std::time::Duration`], a `LocalClockDelta` may be negative, as
91/// unlike [`std::time::Instant`] or [`std::time::SystemTime`], it's perfectly
92/// reasonable (and expected!) that [`LocalClock`] returns a [`LocalClockTime`]
93/// that is _earlier_ than a previously returned `LocalClockTime` (as would be
94/// the case if a `LocalClock` is re-programmed to an earlier time).
95///
96/// This type doesn't expose a particularly "rich" API for working the the
97/// contained time delta. Rather, consumers of this type are expected to us it
98/// alongside an external time/date library (such as `time` or `chrono`) in
99/// order to more easily manipulate the time delta.
100#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
101#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
102#[cfg_attr(feature = "inspect", inspect(debug))]
103pub struct LocalClockDelta {
104    // DEVNOTE: see DEVNOTE in `LocalClockTime` for rationale around storing
105    // duration in units of milliseconds.
106    millis: i64,
107}
108
109impl Debug for LocalClockDelta {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(
112            f,
113            "{}{:?}",
114            if self.millis.is_negative() { "-" } else { "" },
115            std::time::Duration::from_millis(self.millis.unsigned_abs())
116        )
117    }
118}
119
120impl LocalClockDelta {
121    /// A delta of zero milliseconds.
122    pub const ZERO: Self = Self { millis: 0 };
123
124    /// Return the duration in milliseconds.
125    pub const fn as_millis(self) -> i64 {
126        self.millis
127    }
128
129    /// Create a duration from milliseconds.
130    pub const fn from_millis(millis: i64) -> Self {
131        Self { millis }
132    }
133}
134
135/// An opaque type, representing an instant in time.
136///
137/// This type doesn't expose a particularly "rich" API for working the the
138/// contained time. Rather, consumers of this type are expected to us it
139/// alongside an external time/date library (such as `time` or `chrono`) in
140/// order to more easily manipulate time/date values.
141#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
142#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
143#[cfg_attr(feature = "inspect", inspect(debug))]
144pub struct LocalClockTime {
145    // A couple DEVNOTES:
146    //
147    // - The decision to store time using the Unix Epoch as an "anchor" was
148    //   primarily chosen out of convenience, and could just as well be swapped
149    //   out for something else if that turns out to be more expedient.
150    //
151    // - The decision to store time in units of milliseconds was a purely
152    //   pragmatic one: RTC devices typically don't have resolutions greater
153    //   than 1ms. That said, if this assumption turns out to be untrue, the
154    //   fact that `LocalClockTime` is an opaque type means that we could
155    //   non-intrusively bump the contained resolution without affecting
156    //   existing consumers.
157    //
158    // - i64::MAX / i64::MIN correspond to ~300000000 years on either end of the
159    //   unix epoch, in case you're worried about not being able to represent a
160    //   particular date.
161    millis_since_epoch: i64,
162}
163
164impl Debug for LocalClockTime {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        #[cfg(not(feature = "time_exts"))]
167        {
168            write!(
169                f,
170                "{}{:?}",
171                if self.millis_since_epoch.is_negative() {
172                    "-"
173                } else {
174                    ""
175                },
176                std::time::Duration::from_millis(self.millis_since_epoch.unsigned_abs())
177            )
178        }
179
180        #[cfg(feature = "time_exts")]
181        {
182            let date_time: Result<time::OffsetDateTime, _> = (*self).try_into();
183            match date_time {
184                Ok(date_time) => write!(f, "{}", date_time),
185                Err(e) => write!(f, "{:?}", e),
186            }
187        }
188    }
189}
190
191impl LocalClockTime {
192    /// Return the number of millis since the Unix Epoch (in UTC).
193    ///
194    /// Also see: [`std::time::UNIX_EPOCH`]
195    pub fn as_millis_since_unix_epoch(&self) -> i64 {
196        self.millis_since_epoch
197    }
198
199    /// Create a new [`LocalClockTime`] containing the time some number of
200    /// millis offset from the Unix Epoch (in UTC).
201    ///
202    /// Also see: [`std::time::UNIX_EPOCH`]
203    pub fn from_millis_since_unix_epoch(millis: i64) -> Self {
204        Self {
205            millis_since_epoch: millis,
206        }
207    }
208}
209
210impl std::ops::Sub for LocalClockDelta {
211    type Output = LocalClockDelta;
212
213    fn sub(self, rhs: Self) -> Self::Output {
214        LocalClockDelta {
215            millis: self.millis - rhs.millis,
216        }
217    }
218}
219
220impl std::ops::Add for LocalClockDelta {
221    type Output = LocalClockDelta;
222
223    fn add(self, rhs: Self) -> Self::Output {
224        LocalClockDelta {
225            millis: self.millis + rhs.millis,
226        }
227    }
228}
229
230impl std::ops::Sub for LocalClockTime {
231    type Output = LocalClockDelta;
232
233    fn sub(self, rhs: Self) -> Self::Output {
234        LocalClockDelta::from_millis(self.millis_since_epoch - rhs.millis_since_epoch)
235    }
236}
237
238impl std::ops::Sub<LocalClockDelta> for LocalClockTime {
239    type Output = LocalClockTime;
240
241    fn sub(self, rhs: LocalClockDelta) -> Self::Output {
242        LocalClockTime {
243            millis_since_epoch: self.millis_since_epoch - rhs.as_millis(),
244        }
245    }
246}
247
248impl std::ops::Add<LocalClockDelta> for LocalClockTime {
249    type Output = LocalClockTime;
250
251    fn add(self, rhs: LocalClockDelta) -> Self::Output {
252        LocalClockTime {
253            millis_since_epoch: self.millis_since_epoch + rhs.as_millis(),
254        }
255    }
256}
257
258// only allow one-way conversion, as std::time::Duration doesn't support
259// negative time
260impl From<std::time::Duration> for LocalClockDelta {
261    fn from(d: std::time::Duration) -> Self {
262        LocalClockDelta::from_millis(d.as_millis().try_into().unwrap())
263    }
264}
265
266impl From<std::time::SystemTime> for LocalClockTime {
267    fn from(sys_time: std::time::SystemTime) -> Self {
268        let millis_since_epoch = if std::time::UNIX_EPOCH < sys_time {
269            let since_epoch = sys_time.duration_since(std::time::UNIX_EPOCH).unwrap();
270            let millis: i64 = since_epoch.as_millis().try_into().unwrap();
271            millis
272        } else {
273            let since_epoch = std::time::UNIX_EPOCH.duration_since(sys_time).unwrap();
274            let millis: i64 = since_epoch.as_millis().try_into().unwrap();
275            -millis
276        };
277
278        LocalClockTime::from_millis_since_unix_epoch(millis_since_epoch)
279    }
280}
281
282impl From<LocalClockTime> for std::time::SystemTime {
283    fn from(clock_time: LocalClockTime) -> Self {
284        let millis_since_epoch = clock_time.as_millis_since_unix_epoch();
285        if millis_since_epoch.is_negative() {
286            let before_epoch = std::time::Duration::from_millis((-millis_since_epoch) as u64);
287            std::time::UNIX_EPOCH.checked_sub(before_epoch).unwrap()
288        } else {
289            let after_epoch = std::time::Duration::from_millis(millis_since_epoch as u64);
290            std::time::UNIX_EPOCH.checked_add(after_epoch).unwrap()
291        }
292    }
293}
294
295/// Indicates that an overflow error occurred during conversion.
296#[derive(Debug)]
297pub struct OverflowError;
298
299#[cfg(feature = "time_exts")]
300mod time_ext {
301    use super::LocalClockDelta;
302    use super::LocalClockTime;
303
304    impl From<time::OffsetDateTime> for LocalClockTime {
305        fn from(date_time: time::OffsetDateTime) -> LocalClockTime {
306            let since_epoch = date_time - time::OffsetDateTime::UNIX_EPOCH;
307            LocalClockTime::from_millis_since_unix_epoch(
308                since_epoch.whole_milliseconds().try_into().unwrap(),
309            )
310        }
311    }
312
313    impl From<time::Duration> for LocalClockDelta {
314        fn from(time_duration: time::Duration) -> LocalClockDelta {
315            LocalClockDelta::from_millis(time_duration.whole_milliseconds().try_into().unwrap())
316        }
317    }
318
319    impl From<LocalClockDelta> for time::Duration {
320        fn from(clock_duration: LocalClockDelta) -> time::Duration {
321            time::Duration::milliseconds(clock_duration.as_millis())
322        }
323    }
324
325    impl TryFrom<LocalClockTime> for time::OffsetDateTime {
326        type Error = super::OverflowError;
327
328        fn try_from(clock_time: LocalClockTime) -> Result<time::OffsetDateTime, Self::Error> {
329            let duration = time::Duration::milliseconds(clock_time.as_millis_since_unix_epoch());
330            time::OffsetDateTime::UNIX_EPOCH
331                .checked_add(duration)
332                .ok_or(super::OverflowError)
333        }
334    }
335}
336
337/// Defines a trait that combines LocalClock and Inspect
338#[cfg(feature = "inspect")]
339mod inspect_ext {
340    use super::*;
341    use inspect::Inspect;
342
343    /// Extends [`LocalClock`] with a bound on [`Inspect`]
344    pub trait InspectableLocalClock: LocalClock + Inspect {}
345    impl<T: LocalClock + Inspect> InspectableLocalClock for T {}
346}