use std::io;
#[cfg(target_os = "linux")]
const CLOCK_MONOTONIC: i32 = 1;
#[cfg(any(target_os = "macos", target_os = "ios"))]
const CLOCK_MONOTONIC: i32 = 6;
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
))]
const CLOCK_MONOTONIC: i32 = 4;
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
)))]
const CLOCK_MONOTONIC: i32 = 1;
#[cfg(target_os = "linux")]
const CLOCK_BOOTTIME: i32 = 7;
#[cfg(any(target_os = "macos", target_os = "ios"))]
const CLOCK_MONOTONIC_RAW: i32 = 4;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ClockSource {
#[default]
Monotonic,
Boottime,
MonotonicRaw,
}
impl std::fmt::Display for ClockSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClockSource::Monotonic => f.write_str("monotonic"),
ClockSource::Boottime => f.write_str("boottime"),
ClockSource::MonotonicRaw => f.write_str("monotonic-raw"),
}
}
}
impl std::str::FromStr for ClockSource {
type Err = ClockSourceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"monotonic" => Ok(ClockSource::Monotonic),
"boottime" => Ok(ClockSource::Boottime),
"monotonic-raw" | "monotonic_raw" => Ok(ClockSource::MonotonicRaw),
other => Err(ClockSourceParseError {
raw: other.to_string(),
}),
}
}
}
#[derive(Debug)]
pub struct ClockSourceParseError {
pub raw: String,
}
impl std::fmt::Display for ClockSourceParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"unknown clock source {:?}: expected one of `monotonic`, `boottime`, `monotonic-raw`",
self.raw
)
}
}
impl std::error::Error for ClockSourceParseError {}
impl ClockSource {
pub fn as_u8(self) -> u8 {
match self {
ClockSource::Monotonic => 0,
ClockSource::Boottime => 1,
ClockSource::MonotonicRaw => 2,
}
}
pub fn from_u8(byte: u8) -> Self {
match byte {
1 => ClockSource::Boottime,
2 => ClockSource::MonotonicRaw,
_ => ClockSource::Monotonic,
}
}
pub fn clk_id(self) -> Option<i32> {
match self {
ClockSource::Monotonic => Some(CLOCK_MONOTONIC),
#[cfg(target_os = "linux")]
ClockSource::Boottime => Some(CLOCK_BOOTTIME),
#[cfg(not(target_os = "linux"))]
ClockSource::Boottime => None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
ClockSource::MonotonicRaw => Some(CLOCK_MONOTONIC_RAW),
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
ClockSource::MonotonicRaw => None,
}
}
}
#[derive(Debug)]
pub enum ClockError {
Unsupported {
source: ClockSource,
platform: &'static str,
},
Os(io::Error),
}
impl std::fmt::Display for ClockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClockError::Unsupported { source, platform } => {
let hint = match source {
ClockSource::Boottime => {
" (Linux only; on macOS use `monotonic-raw` for advance-through-sleep semantics)"
}
ClockSource::MonotonicRaw => {
" (macOS / iOS only; on Linux use `boottime` for advance-through-sleep semantics)"
}
ClockSource::Monotonic => "",
};
write!(
f,
"clock source `{source}` is not supported on `{platform}`{hint}"
)
}
ClockError::Os(e) => write!(f, "clock_gettime: {e}"),
}
}
}
impl std::error::Error for ClockError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ClockError::Unsupported { .. } => None,
ClockError::Os(e) => Some(e),
}
}
}
impl From<ClockError> for io::Error {
fn from(e: ClockError) -> Self {
match e {
ClockError::Os(inner) => inner,
ClockError::Unsupported { .. } => {
io::Error::new(io::ErrorKind::Unsupported, e.to_string())
}
}
}
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[repr(C)]
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
))]
#[repr(C)]
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
)))]
#[repr(C)]
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
extern "C" {
fn clock_gettime(clk_id: i32, tp: *mut Timespec) -> i32;
}
pub fn clock_gettime_raw(clk_id: i32) -> io::Result<u64> {
let mut tp = Timespec {
tv_sec: 0,
tv_nsec: 0,
};
let rc = unsafe { clock_gettime(clk_id, &mut tp as *mut Timespec) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
let sec = if tp.tv_sec < 0 {
0u64
} else {
tp.tv_sec as u64
};
let nsec = if tp.tv_nsec < 0 {
0u64
} else {
tp.tv_nsec as u64
};
let total = sec
.checked_mul(1_000_000_000)
.and_then(|s| s.checked_add(nsec))
.unwrap_or(u64::MAX);
Ok(total)
}
pub struct Clock {
source: ClockSource,
start_ns: u64,
}
impl Clock {
pub fn new(source: ClockSource) -> Result<Self, ClockError> {
let clk_id = source.clk_id().ok_or(ClockError::Unsupported {
source,
platform: std::env::consts::OS,
})?;
let start_ns = clock_gettime_raw(clk_id).map_err(ClockError::Os)?;
Ok(Self { source, start_ns })
}
pub fn probe(source: ClockSource) -> Result<(), ClockError> {
Self::new(source).map(|_| ())
}
pub fn now_ns(&self) -> u64 {
let clk_id = match self.source.clk_id() {
Some(id) => id,
None => return 0,
};
let raw = clock_gettime_raw(clk_id).unwrap_or(self.start_ns);
raw.saturating_sub(self.start_ns)
}
pub fn source(&self) -> ClockSource {
self.source
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn parse_all_clock_source_variants() {
assert_eq!(
ClockSource::from_str("monotonic").unwrap(),
ClockSource::Monotonic
);
assert_eq!(
ClockSource::from_str("boottime").unwrap(),
ClockSource::Boottime
);
assert_eq!(
ClockSource::from_str("monotonic-raw").unwrap(),
ClockSource::MonotonicRaw
);
assert_eq!(
ClockSource::from_str("monotonic_raw").unwrap(),
ClockSource::MonotonicRaw
);
}
#[test]
fn parse_unknown_value_errors() {
let e = ClockSource::from_str("wallclock").unwrap_err();
assert_eq!(e.raw, "wallclock");
}
#[test]
fn display_round_trip() {
for src in [
ClockSource::Monotonic,
ClockSource::Boottime,
ClockSource::MonotonicRaw,
] {
let s = format!("{src}");
assert_eq!(ClockSource::from_str(&s).unwrap(), src);
}
}
#[test]
fn as_u8_from_u8_round_trip() {
for src in [
ClockSource::Monotonic,
ClockSource::Boottime,
ClockSource::MonotonicRaw,
] {
assert_eq!(ClockSource::from_u8(src.as_u8()), src);
}
}
#[test]
fn monotonic_forward_only() {
let clk = Clock::new(ClockSource::Monotonic).expect("CLOCK_MONOTONIC must be supported");
let a = clk.now_ns();
let b = clk.now_ns();
assert!(b >= a, "monotonic clock regressed: {a} -> {b}");
}
#[cfg(target_os = "linux")]
#[test]
fn boottime_forward_only_on_linux() {
let clk = Clock::new(ClockSource::Boottime).expect("CLOCK_BOOTTIME must work on Linux");
let a = clk.now_ns();
let b = clk.now_ns();
assert!(b >= a, "boottime clock regressed: {a} -> {b}");
}
#[cfg(not(target_os = "linux"))]
#[test]
fn boottime_rejected_on_unsupported_platform() {
match Clock::new(ClockSource::Boottime) {
Err(ClockError::Unsupported { source, .. }) => {
assert_eq!(source, ClockSource::Boottime);
}
Err(other) => panic!("expected Unsupported, got {other:?}"),
Ok(_) => panic!("expected Boottime to be rejected on non-Linux"),
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn monotonic_raw_forward_only_on_macos() {
let clk =
Clock::new(ClockSource::MonotonicRaw).expect("CLOCK_MONOTONIC_RAW must work on macOS");
let a = clk.now_ns();
let b = clk.now_ns();
assert!(b >= a, "monotonic-raw clock regressed: {a} -> {b}");
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
#[test]
fn monotonic_raw_rejected_on_non_macos() {
match Clock::new(ClockSource::MonotonicRaw) {
Err(ClockError::Unsupported { source, .. }) => {
assert_eq!(source, ClockSource::MonotonicRaw);
}
Err(other) => panic!("expected Unsupported, got {other:?}"),
Ok(_) => panic!("expected MonotonicRaw to be rejected outside macOS / iOS"),
}
}
#[test]
fn now_ns_baseline_starts_near_zero() {
let clk = Clock::new(ClockSource::Monotonic).unwrap();
let first = clk.now_ns();
assert!(
first < 1_000_000_000,
"first now_ns reading too large: {first}"
);
}
}