epics-base-rs 0.24.3

Pure Rust EPICS IOC core — record system, database, iocsh, calc engine
Documentation
use std::time::{Duration, Instant, SystemTime};

use crate::types::WallTime;

/// Current wall-clock time as a [`WallTime`].
///
/// Returns [`WallTime`] rather than [`SystemTime`] so a snapshot built from
/// "now" shares one timestamp type with snapshots built from exact wire
/// integers. The OS clock is still read via [`SystemTime::now`]; on Windows
/// that clock is itself 100 ns-granular, which `WallTime` does not change.
pub fn now_wall() -> WallTime {
    SystemTime::now().into()
}

pub fn now_mono() -> Instant {
    Instant::now()
}

pub fn deadline_from_now(d: Duration) -> Instant {
    Instant::now() + d
}

/// The OS clock-tick period, in seconds — C `epicsThreadSleepQuantum()`.
///
/// posix (`libcom/src/osi/os/posix/osdThread.c:1108-1116`):
///
/// ```c
/// double epicsThreadSleepQuantum(void)
/// {
///     double hz = sysconf(_SC_CLK_TCK);
///     if (hz <= 0) return 0.0;
///     return 1.0 / hz;
/// }
/// ```
///
/// Records use it to round a delay field to a whole number of ticks — e.g.
/// `sseqRecord.c:197-200` quantizes every `DLYn` at init. `_SC_CLK_TCK` is 100
/// on Linux and macOS, hence the 0.01 s fallback on the targets where `libc`
/// is not linked (it is a Linux-only dependency of this crate). Returns 0.0
/// when the tick rate is unavailable, exactly as C does; callers must treat
/// that as "no quantization" rather than dividing by it.
pub fn thread_sleep_quantum() -> f64 {
    #[cfg(target_os = "linux")]
    {
        // SAFETY: `sysconf` is a pure query with no preconditions.
        let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
        if hz <= 0.0 { 0.0 } else { 1.0 / hz }
    }
    #[cfg(not(target_os = "linux"))]
    {
        0.01
    }
}

/// Round `seconds` to the nearest whole [`thread_sleep_quantum`] tick, the way
/// C records do it:
///
/// ```c
/// #define NINT(f) (long)((f)>0 ? (f)+0.5 : (f)-0.5)
/// plinkGroup->dly = epicsThreadSleepQuantum() *
///                   NINT(plinkGroup->dly / epicsThreadSleepQuantum());
/// ```
///
/// (`sseqRecord.c:67`, `:197-199`.) The `NINT` cast is to a C `long` (i64),
/// NOT an f64 round, and the served DLY must reproduce that cast byte-for-byte
/// — see [`c_long_cast`]. Two boundaries C's cast owns that an `f64::trunc`
/// port gets wrong:
///
///   * **Overflow.** A `dly` large enough that `ticks` rounds past 2^63
///     overflows the `(long)` cast. On x86-64 (the target the oracle runs on)
///     `cvttsd2si` maps every out-of-range value — and NaN/±inf — to
///     i64::MIN = `0x8000_0000_0000_0000`, so the field becomes `quantum *
///     i64::MIN` ≈ -9.22e16, exactly what C serves for a huge `caput`. An
///     `f64::trunc` port instead keeps the huge value (or `inf`).
///   * **Negative zero.** A `dly` that rounds to zero yields the *integer* 0,
///     and `quantum * 0` is `+0.0`. An `f64::trunc` port produces `-0.0` for
///     the `0.0` default (`(-0.0 - 0.5).trunc()` = `-0.0`) and for any tiny or
///     negative input that rounds to zero, which renders as `-0` where C
///     renders `0`.
///
/// With a zero quantum (C's `hz <= 0` path) the value is returned unchanged
/// rather than dividing by zero.
pub fn quantize_to_sleep_quantum(seconds: f64) -> f64 {
    let quantum = thread_sleep_quantum();
    if quantum <= 0.0 {
        return seconds;
    }
    let ticks = seconds / quantum;
    // C `NINT(f) = (long)((f) > 0 ? (f) + 0.5 : (f) - 0.5)`.
    let rounded = if ticks > 0.0 {
        ticks + 0.5
    } else {
        ticks - 0.5
    };
    quantum * c_long_cast(rounded) as f64
}

/// Reproduce C's `(long)` cast of a `double` with x86-64 `cvttsd2si`
/// semantics: an in-range finite value truncates toward zero (as Rust's
/// `as i64` already does); every out-of-range value and NaN/±inf yields
/// i64::MIN, the "integer indefinite" the instruction returns.
///
/// Rust's own `as i64` *saturates* out-of-range inputs instead (2^63 →
/// i64::MAX, -inf → i64::MIN, NaN → 0), so the explicit range check is what
/// makes the port match C on the overflow boundary.
fn c_long_cast(f: f64) -> i64 {
    // i64::MIN is exactly -2^63 and representable as f64; i64::MAX rounds up to
    // 2^63 as f64 (out of range), so the upper bound is a strict `< 2^63`.
    const MIN: f64 = -9_223_372_036_854_775_808.0; // -2^63
    const LIMIT: f64 = 9_223_372_036_854_775_808.0; //  2^63
    if f.is_nan() || f < MIN || f >= LIMIT {
        i64::MIN
    } else {
        f as i64
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_now_wall() {
        let t = now_wall();
        assert!(t.since_unix_epoch().as_secs() > 0);
    }

    #[test]
    fn test_now_mono() {
        let t1 = now_mono();
        let t2 = now_mono();
        assert!(t2 >= t1);
    }

    #[test]
    fn test_deadline_from_now() {
        let before = Instant::now();
        let deadline = deadline_from_now(Duration::from_secs(10));
        assert!(deadline > before);
        assert!(deadline <= before + Duration::from_secs(11));
    }

    /// C's `(long)` cast, boundary by boundary: in-range truncation toward
    /// zero, and i64::MIN for everything x86-64 `cvttsd2si` cannot represent.
    #[test]
    fn c_long_cast_matches_cvttsd2si() {
        // In range: plain truncation toward zero, both signs.
        assert_eq!(c_long_cast(0.0), 0);
        assert_eq!(c_long_cast(-0.0), 0);
        assert_eq!(c_long_cast(0.5), 0);
        assert_eq!(c_long_cast(-0.5), 0); // toward zero, NOT -1
        assert_eq!(c_long_cast(1.9), 1);
        assert_eq!(c_long_cast(-1.9), -1);
        // The representable extremes.
        assert_eq!(c_long_cast(-9_223_372_036_854_775_808.0), i64::MIN);
        // Out of range and non-finite all collapse to the "integer indefinite".
        assert_eq!(c_long_cast(9_223_372_036_854_775_808.0), i64::MIN); // +2^63
        assert_eq!(c_long_cast(1e300), i64::MIN);
        assert_eq!(c_long_cast(-1e300), i64::MIN);
        assert_eq!(c_long_cast(f64::INFINITY), i64::MIN);
        assert_eq!(c_long_cast(f64::NEG_INFINITY), i64::MIN);
        assert_eq!(c_long_cast(f64::NAN), i64::MIN);
    }

    /// The DLY quantization the served value must match, by invariant boundary.
    #[test]
    fn quantize_dly_boundaries_match_c() {
        let q = thread_sleep_quantum();
        assert!(q > 0.0, "test assumes a positive clock quantum, got {q}");

        // The default DLY is 0.0; it must serve as +0.0, never -0.0.
        let zero = quantize_to_sleep_quantum(0.0);
        assert_eq!(zero, 0.0);
        assert!(
            zero.is_sign_positive(),
            "DLY=0.0 must round to +0.0 (renders \"0\"), got a negative zero"
        );

        // A -0.0 input must also normalize to +0.0 (C's `NINT` yields integer 0).
        let neg_zero = quantize_to_sleep_quantum(-0.0);
        assert_eq!(neg_zero, 0.0);
        assert!(
            neg_zero.is_sign_positive(),
            "DLY=-0.0 must round to +0.0, got a negative zero"
        );

        // A tiny positive below half a tick rounds to +0.0.
        let tiny = quantize_to_sleep_quantum(q / 4.0);
        assert_eq!(tiny, 0.0);
        assert!(tiny.is_sign_positive(), "tiny +dly rounds to +0.0");

        // A small negative that rounds to zero: C truncates `-0.x` to 0 → +0.0.
        let tiny_neg = quantize_to_sleep_quantum(-q / 4.0);
        assert_eq!(tiny_neg, 0.0);
        assert!(
            tiny_neg.is_sign_positive(),
            "a -dly rounding to zero must serve +0.0, not -0.0"
        );

        // An exact quantum multiple is preserved exactly.
        assert_eq!(quantize_to_sleep_quantum(3.0 * q), 3.0 * q);
        assert_eq!(quantize_to_sleep_quantum(-3.0 * q), -3.0 * q);

        // Round-half-away-from-zero at the tick boundary.
        assert_eq!(quantize_to_sleep_quantum(1.5 * q), 2.0 * q);
        assert_eq!(quantize_to_sleep_quantum(-1.5 * q), -2.0 * q);

        // A huge finite `dly` overflows the `(long)` cast to i64::MIN, so the
        // served value is `quantum * i64::MIN` — negative, and NOT `inf`.
        let huge = quantize_to_sleep_quantum(1e300);
        assert_eq!(huge, q * (i64::MIN as f64));
        assert!(
            huge.is_finite() && huge < 0.0,
            "a huge +dly must serve C's ~-9.22e16, not inf, got {huge}"
        );
        // A huge negative `dly` overflows the same way.
        let huge_neg = quantize_to_sleep_quantum(-1e300);
        assert_eq!(huge_neg, q * (i64::MIN as f64));

        // ±inf also collapses to i64::MIN (an `f64::trunc` port kept inf).
        assert_eq!(
            quantize_to_sleep_quantum(f64::INFINITY),
            q * (i64::MIN as f64)
        );
        assert_eq!(
            quantize_to_sleep_quantum(f64::NEG_INFINITY),
            q * (i64::MIN as f64)
        );
    }
}