Status: Shipped as
windows-timev0.1.0. This document records the design decisions made during development and serves as a reference for the crate's rationale and architecture.
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 UTCThere 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.
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::Durationis{ u64 seconds, u32 nanos }(96 bits, unsigned). It cannot represent negativeTimeSpanvalues, and it is not ABI-compatible withi64.std::time::SystemTimeis 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.
windows-numerics is the closest existing precedent and the right model to
copy:
Vector2,Vector3,Vector4,Matrix3x2,Matrix4x4are POD#[repr(C)]value types from theWindows.Foundation.Numericsnamespace.- The crate's
src/bindings.rsis generated bywindows-bindgenand 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.rsis justpub use bindings::*;plus the extension modules (mod vector2;…).- The umbrella
windowscrate depends onwindows-numericsand bindgen (crates/libs/bindgen/src/lib.rs) is configured so any reference toWindows.Foundation.Numerics.Vector2in another generated module is routed towindows_numerics::Vector2instead 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
TimeSpan→Duration,DateTime→SystemTimein projected method signatures): lossy for negativeTimeSpan, platform-dependent forSystemTime, doesn't help inside generics likeIReference<TimeSpan>orIVector<DateTime>, and is a breaking API change. - Per-file extension impls only (the current
extensions/Foundation/TimeSpan.rspattern): keeps the type bare, scatters the API, and has no natural home forWin32::Foundation::FILETIME/SYSTEMTIMEinterop. The extensions file would still grow into "the windows-time crate, but in the wrong place".
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 ifDateTime::nowis implemented by callingGetSystemTimePreciseAsFileTime. If we instead buildnowonstd::time::SystemTime::now(), nowindows-linkdependency is required, the crate staysforbid(unsafe_code)likewindows-numerics, andnowis gated onfeature = "std". default = ["std"],std = ["windows-core/std"].[lints] workspace = trueand[package.metadata.docs.rs] targets = [].
-
Add
crates/tools/bindings/src/time.txt:--out crates/libs/time/src/bindings.rs --flat --filter Windows.Foundation.DateTime Windows.Foundation.TimeSpan -
Add
bindgen(["--etc", "crates/tools/bindings/src/time.txt"]).unwrap();tocrates/tools/bindings/src/main.rsin the same block as the other small crates. -
In
crates/libs/bindgen/src/lib.rs, extend theprepend_default_refstable that already handleswindows_collections/windows_reference/windows_numericswith:("Windows.Foundation", "windows_time", &["Windows.Foundation.DateTime", "Windows.Foundation.TimeSpan"][..]),Once that entry is in place, every regenerated module that references
Windows.Foundation.TimeSpanorWindows.Foundation.DateTimewill route towindows_time::TimeSpan/windows_time::DateTimeinstead of emitting its own copy. The structs are removed fromcrates/libs/windows/src/Windows/Foundation/mod.rs(they are still surfaced to users throughpub use windows_time::{TimeSpan, DateTime};in the umbrella crate, just like the numerics types are surfaced fromwindows-numerics). -
Add
windows-time = { workspace = true }tocrates/libs/windows/Cargo.tomland extend the workspaceCargo.tomlalongsidewindows-numerics. Add it to thestdfeature in the same way numerics is wired (std = [..., "windows-time/std"]). -
Update
windows-reference(crates/libs/reference/src/bindings.rs) so itsIPropertyValue/IReferencebindings also routeTimeSpanandDateTimethroughwindows-time. Todaycrates/tools/bindings/src/reference.txtlistsWindows.Foundation.TimeSpanandWindows.Foundation.DateTimein its filter, which causes the reference crate to redeclare them. Remove those two filter lines so that they come in as cross-crate references towindows_timeinstead. TheStockReferenceimpl incrates/libs/reference/src/reference.rsalready usesbindings::TimeSpan/bindings::DateTime; with the routing change these becomewindows_time::TimeSpan/windows_time::DateTimetransparently. -
Delete
crates/libs/windows/src/extensions/Foundation/TimeSpan.rsand thepub mod TimeSpan;line incrates/libs/windows/src/extensions/Foundation.rs. The replacement lives inwindows-time::timespan.
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) -> TimeSpanfrom_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. Plustry_from_*variants returningOption<TimeSpan>(or an error type — see Error handling below) for callers that need to detect overflow.
Accessors:
ticks() -> i64whole_nanos() -> i128,whole_micros() -> i64,whole_millis() -> i64,whole_seconds() -> i64,whole_minutes() -> i64,whole_hours() -> i64,whole_days() -> i64.subsec_nanos() -> i32(forDisplay).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, withchecked_*available).
Comparison and hashing:
- Manually derive
PartialOrd,Ord,Hash(the generatedTimeSpanalready derivesClone,Copy,PartialEq; the additional derives go on the generated#[repr(C)]struct because bindgen will allow it via the existing derive mechanism, or asimplblocks intimespan.rsif not).
Formatting:
impl Display for TimeSpanproducingPnDTnHnMn.fffffffS(ISO-8601 duration).- The generated
DebugisTimeSpan { 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 TimeSpanas aFrom-style convenience only if it can be made infallible; otherwise drop it in favour ofTryFrom. The currentFromimpl truncates withas i64and the reverse cast(value.Duration * 100) as u64is a bug for negative values — moving toTryFromfixes both.
Win32 interop (gated #[cfg(windows)], free of any link-time dependency):
impl From<windows::Win32::Foundation::FILETIME>is not appropriate forTimeSpan(FILETIME is an instant, not a duration). No Win32 conversions are defined forTimeSpanitself.
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(theDateTimevalue corresponding to 1970-01-01 UTC),DateTime::MIN,DateTime::MAX.DateTime::TICKS_PER_SECOND= 10_000_000 (re-exported / aligned withTimeSpan).
Constructors:
from_ticks(i64)— 100-ns since 1601 UTC.from_unix_secs(i64),from_unix_millis(i64),from_unix_nanos(i128)— saturating on overflow, withtry_from_*variants.
Accessors:
ticks() -> i64unix_secs() -> i64,unix_millis() -> i64,unix_nanos() -> i128.
Arithmetic:
Add<TimeSpan> for DateTime -> DateTimeSub<TimeSpan> for DateTime -> DateTimeSub<DateTime> for DateTime -> TimeSpanAddAssign<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 DateTimeproducingYYYY-MM-DDTHH:MM:SS.fffffffZ(ISO-8601 UTC). The Gregorian conversion is small, well-known (Howard Hinnant'sdays_from_civil), and adds no dependency.Debugis left as the generatedDateTime { 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 bySystemTimeon the current platform.DateTime::now() -> DateTime— implemented viastd::time::SystemTime::now(). Gated onfeature = "std"to keepno_stdbuilds (andforbid(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 DateTimeand back.FILETIMEis{ u32 dwLowDateTime, u32 dwHighDateTime }representing exactly the same 1601-based 100-ns clock, so the conversion is a losslessi64round-trip and is bit-equivalent (the one place where the C++/WinRT "bit-for-bit" framing actually applies). These impls live incrates/libs/windows/src/extensions/Foundation/DateTime.rsbecause that is where theFILETIMEtype is visible.
- No
chrono/timecrate dependency. Users wanting those representations can convert through theticks/unix_secs/unix_nanosaccessors. - No new bindgen sugar to rewrite method signatures from
TimeSpan→DurationorDateTime→SystemTime. The wrapper crate gives callers idiomatic ergonomics without breaking any signature or changing behaviour inside generics. - No removal or renaming of the public
TimeSpan/DateTimenames from thewindowsumbrella crate — they continue to live atwindows::Foundation::TimeSpan/DateTime(re-exported fromwindows-time).
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:
TryFromimpls return their own associatedError = TimeError(or a units-named variant likeTimeRangeError) — a tiny#[non_exhaustive]unit/error struct inwindows-time, withimpl core::error::Errorand aDisplaylike"value out of range for TimeSpan". This keepswindows-timeindependent ofwindows-resultand avoids dragging anHRESULT-shaped error into infallible arithmetic conversions.
windows-timeshipped at0.1.0alongside the coordinated release.- It is sourced from metadata and re-exported by the umbrella
windowscrate, so for users of the umbrella crate this was a non-breaking change:windows::Foundation::TimeSpanandDateTimekeep 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::Durationimpls inwindows::extensions::Foundation::TimeSpan, which were replaced byTryFrom. Release notes called this out. - The
windows-referencecrate keeps working: with the bindgen routing change above, itsIPropertyValue/IReferencebindings referencewindows_time::{TimeSpan, DateTime}instead of locally-emitted copies.
In crates/libs/time/tests/:
- Round-trip via
ticksfor representative values includingMIN,MAX,ZERO, negative. TimeSpan ↔ Durationround-trip for representable values;TryFromfailure for negativeTimeSpanand forDurationlarger thanTimeSpan::MAX.DateTime ↔ SystemTimeround-trip forUNIX_EPOCH,now(), and a pre-Unix value (test gated on platforms whereSystemTimesupports it).- Arithmetic identities:
a + (b - a) == b,(a - a).is_zero(),(-x) + x == ZERO, ordering consistent with ticks ordering. Displaysnapshot 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 atMIN/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.
Standard windows-rs flow:
cargo fmt --allcargo clippy -p windows-time --all-targetscargo test -p windows-time- Regenerate bindings:
cargo run -p bindings cargo test -p test_bindgen --test fixturesandcargo check --all --target x86_64-pc-windows-gnu --teststo confirm the routing change doesn't regress generated code anywhere in the workspace.