Skip to content

Latest commit

 

History

History
378 lines (294 loc) · 16.6 KB

File metadata and controls

378 lines (294 loc) · 16.6 KB

windows-time: idiomatic TimeSpan and DateTime for Rust

Status: Shipped as windows-time v0.1.0. This document records the design decisions made during development and serves as a reference for the crate's rationale and architecture.

Background

Windows.Foundation.TimeSpan and Windows.Foundation.DateTime are two of the most pervasive value types in the Windows runtime. They appear directly in hundreds of WinRT APIs and indirectly in many more through generics such as IReference<TimeSpan>, IVector<DateTime>, IAsyncOperation<DateTime>, IMapView<HSTRING, DateTime>, and the IPropertyValue::GetTimeSpan / GetDateTime accessors.

Today they are projected as bare ABI structs in the umbrella windows crate:

#[repr(C)] pub struct TimeSpan { pub Duration:      i64 } // 100-ns ticks
#[repr(C)] pub struct DateTime { pub UniversalTime: i64 } // 100-ns ticks since 1601-01-01 UTC

There is one extension file, crates/libs/windows/src/extensions/Foundation/TimeSpan.rs, providing From<core::time::Duration> for TimeSpan and the reverse. It does not cover negative TimeSpan values, overflows on the reverse path, has no companion for DateTime, and provides none of the arithmetic, ordering, or formatting one expects of a duration / instant type in Rust.

Why not C++/WinRT's substitution trick

C++/WinRT replaces TimeSpan with std::chrono::duration<int64_t, std::ratio<1, 10'000'000>> and DateTime with the matching time_point. That works because the chrono types are guaranteed to be a single int64_t and therefore bit-for-bit ABI-compatible with the WinRT struct.

There is no equivalent in Rust:

  • core::time::Duration is { u64 seconds, u32 nanos } (96 bits, unsigned). It cannot represent negative TimeSpan values, and it is not ABI-compatible with i64.
  • std::time::SystemTime is opaque and platform-defined; on some targets it cannot represent dates before the Unix epoch at all.

So a "swap the type at the projection layer" approach is not on the table for Rust. The underlying ABI struct (a single i64) must stay.

Why a windows-time crate is the right shape

windows-numerics is the closest existing precedent and the right model to copy:

  • Vector2, Vector3, Vector4, Matrix3x2, Matrix4x4 are POD #[repr(C)] value types from the Windows.Foundation.Numerics namespace.
  • The crate's src/bindings.rs is generated by windows-bindgen and contains exactly those structs.
  • The crate then layers inherent methods (new, zero, dot, length, …) and operator trait impls (Add, Sub, Mul, Neg, Display, …) directly on the generated types.
  • lib.rs is just pub use bindings::*; plus the extension modules (mod vector2; …).
  • The umbrella windows crate depends on windows-numerics and bindgen (crates/libs/bindgen/src/lib.rs) is configured so any reference to Windows.Foundation.Numerics.Vector2 in another generated module is routed to windows_numerics::Vector2 instead of being redeclared.

TimeSpan and DateTime have the same shape — POD #[repr(C)] value types with a single integer field — and the same need: a small, hand-written layer of idiomatic methods and conversions sitting on top of the generated struct, shared by every module that references them.

What windows-numerics is for graphics math, windows-time is for clock and duration math. The wrapper is thin precisely because the type is already correct at the ABI; the value is the ergonomics layer on top.

This avoids the downsides of the two alternatives we considered:

  • Bindgen signature substitution (replace TimeSpanDuration, DateTimeSystemTime in projected method signatures): lossy for negative TimeSpan, platform-dependent for SystemTime, doesn't help inside generics like IReference<TimeSpan> or IVector<DateTime>, and is a breaking API change.
  • Per-file extension impls only (the current extensions/Foundation/TimeSpan.rs pattern): keeps the type bare, scatters the API, and has no natural home for Win32::Foundation::FILETIME / SYSTEMTIME interop. The extensions file would still grow into "the windows-time crate, but in the wrong place".

Design

Crate layout

A new crate crates/libs/time/, mirroring crates/libs/numerics/:

crates/libs/time/
    Cargo.toml          # name = "windows-time", version = "0.1.0"
    readme.md
    src/
        bindings.rs     # generated by bindgen, contains TimeSpan and DateTime
        lib.rs          # pub use bindings::*; mod timespan; mod datetime;
        timespan.rs     # inherent methods + traits + std interop for TimeSpan
        datetime.rs     # inherent methods + traits + std interop for DateTime

Cargo.toml mirrors windows-numerics:

  • windows-core = { workspace = true } (default-features = false)
  • Optional windows-link = { workspace = true } — only needed if DateTime::now is implemented by calling GetSystemTimePreciseAsFileTime. If we instead build now on std::time::SystemTime::now(), no windows-link dependency is required, the crate stays forbid(unsafe_code) like windows-numerics, and now is gated on feature = "std".
  • default = ["std"], std = ["windows-core/std"].
  • [lints] workspace = true and [package.metadata.docs.rs] targets = [].

Bindgen wiring

  1. Add crates/tools/bindings/src/time.txt:

    --out crates/libs/time/src/bindings.rs
    --flat
    
    --filter
        Windows.Foundation.DateTime
        Windows.Foundation.TimeSpan
    
  2. Add bindgen(["--etc", "crates/tools/bindings/src/time.txt"]).unwrap(); to crates/tools/bindings/src/main.rs in the same block as the other small crates.

  3. In crates/libs/bindgen/src/lib.rs, extend the prepend_default_refs table that already handles windows_collections / windows_reference / windows_numerics with:

    ("Windows.Foundation", "windows_time",
     &["Windows.Foundation.DateTime",
       "Windows.Foundation.TimeSpan"][..]),
    

    Once that entry is in place, every regenerated module that references Windows.Foundation.TimeSpan or Windows.Foundation.DateTime will route to windows_time::TimeSpan / windows_time::DateTime instead of emitting its own copy. The structs are removed from crates/libs/windows/src/Windows/Foundation/mod.rs (they are still surfaced to users through pub use windows_time::{TimeSpan, DateTime}; in the umbrella crate, just like the numerics types are surfaced from windows-numerics).

  4. Add windows-time = { workspace = true } to crates/libs/windows/Cargo.toml and extend the workspace Cargo.toml alongside windows-numerics. Add it to the std feature in the same way numerics is wired (std = [..., "windows-time/std"]).

  5. Update windows-reference (crates/libs/reference/src/bindings.rs) so its IPropertyValue / IReference bindings also route TimeSpan and DateTime through windows-time. Today crates/tools/bindings/src/reference.txt lists Windows.Foundation.TimeSpan and Windows.Foundation.DateTime in its filter, which causes the reference crate to redeclare them. Remove those two filter lines so that they come in as cross-crate references to windows_time instead. The StockReference impl in crates/libs/reference/src/reference.rs already uses bindings::TimeSpan / bindings::DateTime; with the routing change these become windows_time::TimeSpan / windows_time::DateTime transparently.

  6. Delete crates/libs/windows/src/extensions/Foundation/TimeSpan.rs and the pub mod TimeSpan; line in crates/libs/windows/src/extensions/Foundation.rs. The replacement lives in windows-time::timespan.

TimeSpan API (in crates/libs/time/src/timespan.rs)

TimeSpan's unit is 100-nanosecond ticks (same as .NET TimeSpan.Ticks), signed.

Associated constants:

  • TimeSpan::ZERO, TimeSpan::MIN, TimeSpan::MAX.
  • TimeSpan::TICKS_PER_MICROSECOND = 10, _PER_MILLISECOND = 10_000, _PER_SECOND = 10_000_000, _PER_MINUTE, _PER_HOUR, _PER_DAY.

Constructors (all const where possible):

  • from_ticks(i64) -> TimeSpan
  • from_nanos(i64), from_micros(i64), from_millis(i64), from_seconds(i64), from_minutes(i64), from_hours(i64), from_days(i64) — saturating on overflow, documented as such. Plus try_from_* variants returning Option<TimeSpan> (or an error type — see Error handling below) for callers that need to detect overflow.

Accessors:

  • ticks() -> i64
  • whole_nanos() -> i128, whole_micros() -> i64, whole_millis() -> i64, whole_seconds() -> i64, whole_minutes() -> i64, whole_hours() -> i64, whole_days() -> i64.
  • subsec_nanos() -> i32 (for Display).
  • is_zero(), is_negative(), is_positive().

Arithmetic:

  • abs(), signum().
  • checked_add(TimeSpan) -> Option<TimeSpan>, checked_sub, checked_neg, checked_mul(i64), checked_div(i64).
  • saturating_add, saturating_sub, saturating_neg.
  • Operator traits: Add, Sub, Neg, AddAssign, SubAssign, Mul<i64>, Div<i64> (all panic on overflow in debug, wrap in release — matching standard Rust integer behaviour, with checked_* available).

Comparison and hashing:

  • Manually derive PartialOrd, Ord, Hash (the generated TimeSpan already derives Clone, Copy, PartialEq; the additional derives go on the generated #[repr(C)] struct because bindgen will allow it via the existing derive mechanism, or as impl blocks in timespan.rs if not).

Formatting:

  • impl Display for TimeSpan producing PnDTnHnMn.fffffffS (ISO-8601 duration).
  • The generated Debug is TimeSpan { Duration: <ticks> }; we keep it unchanged so debug output remains unambiguous about the raw storage.

std interop (always available — core::time::Duration is in core):

  • impl TryFrom<core::time::Duration> for TimeSpan — fails on overflow.
  • impl TryFrom<TimeSpan> for core::time::Duration — fails when negative.
  • Keep the existing impl From<core::time::Duration> for TimeSpan as a From-style convenience only if it can be made infallible; otherwise drop it in favour of TryFrom. The current From impl truncates with as i64 and the reverse cast (value.Duration * 100) as u64 is a bug for negative values — moving to TryFrom fixes both.

Win32 interop (gated #[cfg(windows)], free of any link-time dependency):

  • impl From<windows::Win32::Foundation::FILETIME> is not appropriate for TimeSpan (FILETIME is an instant, not a duration). No Win32 conversions are defined for TimeSpan itself.

DateTime API (in crates/libs/time/src/datetime.rs)

DateTime's unit is 100-nanosecond ticks since 1601-01-01 00:00:00 UTC, the same epoch as Win32 FILETIME. The offset to the Unix epoch (1970-01-01 00:00:00 UTC) is EPOCH_DIFFERENCE_SECS = 11_644_473_600.

Associated constants:

  • DateTime::UNIX_EPOCH (the DateTime value corresponding to 1970-01-01 UTC), DateTime::MIN, DateTime::MAX.
  • DateTime::TICKS_PER_SECOND = 10_000_000 (re-exported / aligned with TimeSpan).

Constructors:

  • from_ticks(i64) — 100-ns since 1601 UTC.
  • from_unix_secs(i64), from_unix_millis(i64), from_unix_nanos(i128) — saturating on overflow, with try_from_* variants.

Accessors:

  • ticks() -> i64
  • unix_secs() -> i64, unix_millis() -> i64, unix_nanos() -> i128.

Arithmetic:

  • Add<TimeSpan> for DateTime -> DateTime
  • Sub<TimeSpan> for DateTime -> DateTime
  • Sub<DateTime> for DateTime -> TimeSpan
  • AddAssign<TimeSpan>, SubAssign<TimeSpan>
  • checked_add(TimeSpan), checked_sub(TimeSpan), checked_duration_since(DateTime) -> Option<TimeSpan>, saturating_* variants.

Comparison: PartialOrd, Ord, Hash.

Formatting:

  • impl Display for DateTime producing YYYY-MM-DDTHH:MM:SS.fffffffZ (ISO-8601 UTC). The Gregorian conversion is small, well-known (Howard Hinnant's days_from_civil), and adds no dependency.
  • Debug is left as the generated DateTime { UniversalTime: <ticks> }.

std interop (gated #[cfg(feature = "std")]):

  • impl TryFrom<std::time::SystemTime> for DateTime — fails when out of range.
  • impl TryFrom<DateTime> for std::time::SystemTime — fails when the value is outside the range representable by SystemTime on the current platform.
  • DateTime::now() -> DateTime — implemented via std::time::SystemTime::now(). Gated on feature = "std" to keep no_std builds (and forbid(unsafe_code)) intact.

Win32 interop (gated #[cfg(windows)], in the umbrella windows crate, not in windows-time itself, to avoid a circular dependency on windows-sys):

  • impl From<Win32::Foundation::FILETIME> for DateTime and back. FILETIME is { u32 dwLowDateTime, u32 dwHighDateTime } representing exactly the same 1601-based 100-ns clock, so the conversion is a lossless i64 round-trip and is bit-equivalent (the one place where the C++/WinRT "bit-for-bit" framing actually applies). These impls live in crates/libs/windows/src/extensions/Foundation/DateTime.rs because that is where the FILETIME type is visible.

What we deliberately do not do

  • No chrono / time crate dependency. Users wanting those representations can convert through the ticks / unix_secs / unix_nanos accessors.
  • No new bindgen sugar to rewrite method signatures from TimeSpanDuration or DateTimeSystemTime. The wrapper crate gives callers idiomatic ergonomics without breaking any signature or changing behaviour inside generics.
  • No removal or renaming of the public TimeSpan / DateTime names from the windows umbrella crate — they continue to live at windows::Foundation::TimeSpan / DateTime (re-exported from windows-time).

Error handling

For fallible conversions, use the existing windows_core::Error / windows_core::Result only if these crates already depend on windows-core (they do — see windows-numerics's Cargo.toml). Specifically:

  • TryFrom impls return their own associated Error = TimeError (or a units-named variant like TimeRangeError) — a tiny #[non_exhaustive] unit/error struct in windows-time, with impl core::error::Error and a Display like "value out of range for TimeSpan". This keeps windows-time independent of windows-result and avoids dragging an HRESULT-shaped error into infallible arithmetic conversions.

Versioning and rollout

  • windows-time shipped at 0.1.0 alongside the coordinated release.
  • It is sourced from metadata and re-exported by the umbrella windows crate, so for users of the umbrella crate this was a non-breaking change: windows::Foundation::TimeSpan and DateTime keep the same path, same layout, same field names (the generated struct is unchanged), and gained new inherent methods and trait impls.
  • The only breaking surface was for callers that depended directly on the buggy From<core::time::Duration> for TimeSpan / From<TimeSpan> for core::time::Duration impls in windows::extensions::Foundation::TimeSpan, which were replaced by TryFrom. Release notes called this out.
  • The windows-reference crate keeps working: with the bindgen routing change above, its IPropertyValue / IReference bindings reference windows_time::{TimeSpan, DateTime} instead of locally-emitted copies.

Test plan

In crates/libs/time/tests/:

  • Round-trip via ticks for representative values including MIN, MAX, ZERO, negative.
  • TimeSpan ↔ Duration round-trip for representable values; TryFrom failure for negative TimeSpan and for Duration larger than TimeSpan::MAX.
  • DateTime ↔ SystemTime round-trip for UNIX_EPOCH, now(), and a pre-Unix value (test gated on platforms where SystemTime supports it).
  • Arithmetic identities: a + (b - a) == b, (a - a).is_zero(), (-x) + x == ZERO, ordering consistent with ticks ordering.
  • Display snapshot tests for a handful of canonical values (ZERO, 1s, -1s500ms, 1d2h3m4.5s, UNIX_EPOCH, 1601-01-01T00:00:00Z, 9999-12-31T23:59:59.9999999Z).
  • checked_* overflow tests at MIN / MAX.

For the umbrella crate, add a test in crates/tests/winrt/ (or wherever TimeSpan-using APIs are exercised) confirming that an IReference<TimeSpan> round-trips via windows_time::TimeSpan end-to-end, to verify the bindgen routing change works through pinterfaces.

Validation commands

Standard windows-rs flow:

  • cargo fmt --all
  • cargo clippy -p windows-time --all-targets
  • cargo test -p windows-time
  • Regenerate bindings: cargo run -p bindings
  • cargo test -p test_bindgen --test fixtures and cargo check --all --target x86_64-pc-windows-gnu --tests to confirm the routing change doesn't regress generated code anywhere in the workspace.