azul_core/task.rs
1//! Timer and thread management for asynchronous operations.
2//!
3//! This module provides:
4//! - `TimerId` / `ThreadId`: Unique identifiers for timers and background threads
5//! - `Instant` / `Duration`: Cross-platform time types (works on no_std with tick counters)
6//! - `ThreadReceiver`: Channel for receiving messages from the main thread
7//! - Callback types for thread communication and system time queries
8
9#[cfg(not(feature = "std"))]
10use alloc::string::{String, ToString};
11use alloc::{
12 boxed::Box,
13 collections::btree_map::BTreeMap,
14 sync::{Arc, Weak},
15 vec::Vec,
16};
17use core::{
18 ffi::c_void,
19 fmt,
20 mem::ManuallyDrop,
21 sync::atomic::{AtomicUsize, Ordering},
22};
23#[cfg(feature = "std")]
24use std::sync::mpsc::{Receiver, Sender};
25#[cfg(feature = "std")]
26use std::sync::Mutex;
27#[cfg(feature = "std")]
28use std::thread::{self, JoinHandle};
29#[cfg(feature = "std")]
30use std::time::Duration as StdDuration;
31#[cfg(feature = "std")]
32use std::time::Instant as StdInstant;
33
34use azul_css::{props::property::CssProperty, AzString};
35use rust_fontconfig::FcFontCache;
36
37use crate::{
38 callbacks::{FocusTarget, TimerCallbackReturn, Update},
39 dom::{DomId, DomNodeId, OptionDomNodeId},
40 geom::{LogicalPosition, OptionLogicalPosition},
41 gl::OptionGlContextPtr,
42 hit_test::ScrollPosition,
43 id::NodeId,
44 refany::{OptionRefAny, RefAny},
45 resources::{ImageCache, ImageMask, ImageRef},
46 styled_dom::NodeHierarchyItemId,
47 window::RawWindowHandle,
48 FastBTreeSet, OrderedMap,
49};
50
51/// Should a timer terminate or not - used to remove active timers
52#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub enum TerminateTimer {
55 /// Remove the timer from the list of active timers
56 Terminate,
57 /// Do nothing and let the timers continue to run
58 Continue,
59}
60
61// ============================================================================
62// Reserved System Timer IDs (0x0000 - 0x00FF)
63// ============================================================================
64// User timers start at 0x0100 to avoid conflicts with system timers.
65// These constants define well-known timer IDs for internal framework use.
66
67/// Timer ID for cursor blinking in contenteditable elements (~530ms interval)
68pub const CURSOR_BLINK_TIMER_ID: TimerId = TimerId { id: 0x0001 };
69/// Timer ID for scroll momentum/inertia animation
70pub const SCROLL_MOMENTUM_TIMER_ID: TimerId = TimerId { id: 0x0002 };
71/// Timer ID for auto-scroll during drag operations near edges
72pub const DRAG_AUTOSCROLL_TIMER_ID: TimerId = TimerId { id: 0x0003 };
73/// Timer ID for tooltip show delay.
74///
75/// Started by the platform event loop when the hover target changes to a node
76/// that advertises a tooltip source (`aria-label` / `alt` / `title`); fires
77/// once after `SystemStyle::input_metrics.hover_time_ms` (`SPI_GETMOUSEHOVERTIME`
78/// on Windows, default 400ms) and emits a `ShowTooltip` `CallbackChange`. The
79/// timer is torn down on hover loss, which also emits `HideTooltip`.
80///
81/// Double-click detection used to live on a neighbouring reserved ID but is
82/// now handled entirely by `GestureManager::detect_double_click`, so no
83/// equivalent `DOUBLE_CLICK_TIMER_ID` exists.
84pub const TOOLTIP_DELAY_TIMER_ID: TimerId = TimerId { id: 0x0004 };
85/// Timer ID for the single-threaded capability pump (MWA-A1).
86///
87/// Armed by `sync_capability_pump_timer` whenever a capability source needs
88/// polling or draining while the app is otherwise idle (gamepad listeners,
89/// sensor listeners, an active geolocation subscription). Each tick wakes the
90/// blocked platform loop; `invoke_expired_timers` then runs an event pass,
91/// whose top-of-pass pump drains the async capability channels. There is NO
92/// pump thread by design — a recurring shell timer is the only wake
93/// mechanism, so the identical code path works on WASM (no threads).
94pub const CAPABILITY_PUMP_TIMER_ID: TimerId = TimerId { id: 0x0005 };
95/// Timer ID for the one-shot long-press wake-up (MWA-B12).
96///
97/// Armed on every `MouseDown` for the long-press threshold: a motionless
98/// press generates no further events, so no pass would ever evaluate
99/// `detect_long_press` — this timer wakes the loop exactly once at the
100/// threshold, `invoke_expired_timers` runs an event pass, and the
101/// detection fires (or doesn't — moved/released holds are no-ops).
102pub const LONG_PRESS_TIMER_ID: TimerId = TimerId { id: 0x0006 };
103
104/// Reserved timer ID for the caret / selection tween driver (~16ms).
105///
106/// Armed by the shared event dispatcher whenever a text tween is in flight; the
107/// callback terminates itself the tick after the tween state goes idle.
108pub const CARET_TWEEN_TIMER_ID: TimerId = TimerId { id: 0x0007 };
109
110/// First available ID for user-defined timers
111pub const USER_TIMER_ID_START: usize = 0x0100;
112
113// User timers start at 0x0100 to avoid conflicts with reserved system timer IDs
114static MAX_TIMER_ID: AtomicUsize = AtomicUsize::new(USER_TIMER_ID_START);
115
116/// ID for uniquely identifying a timer
117#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
118#[repr(C)]
119pub struct TimerId {
120 pub id: usize,
121}
122
123impl TimerId {
124 /// Generates a new, unique `TimerId`.
125 #[must_use]
126 pub fn unique() -> Self {
127 Self {
128 id: MAX_TIMER_ID.fetch_add(1, Ordering::SeqCst),
129 }
130 }
131}
132
133impl_option!(
134 TimerId,
135 OptionTimerId,
136 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
137);
138
139impl_vec!(
140 TimerId,
141 TimerIdVec,
142 TimerIdVecDestructor,
143 TimerIdVecDestructorType,
144 TimerIdVecSlice,
145 OptionTimerId
146);
147impl_vec_debug!(TimerId, TimerIdVec);
148impl_vec_clone!(TimerId, TimerIdVec, TimerIdVecDestructor);
149impl_vec_partialeq!(TimerId, TimerIdVec);
150impl_vec_partialord!(TimerId, TimerIdVec);
151
152// Thread IDs 0-4 are reserved for internal framework use.
153// User threads start at RESERVED_THREAD_ID_COUNT.
154const RESERVED_THREAD_ID_COUNT: usize = 5;
155static MAX_THREAD_ID: AtomicUsize = AtomicUsize::new(RESERVED_THREAD_ID_COUNT);
156
157/// ID for uniquely identifying a background thread
158#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
159#[repr(C)]
160pub struct ThreadId {
161 id: usize,
162}
163
164impl_option!(
165 ThreadId,
166 OptionThreadId,
167 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
168);
169
170impl_vec!(
171 ThreadId,
172 ThreadIdVec,
173 ThreadIdVecDestructor,
174 ThreadIdVecDestructorType,
175 ThreadIdVecSlice,
176 OptionThreadId
177);
178impl_vec_debug!(ThreadId, ThreadIdVec);
179impl_vec_clone!(ThreadId, ThreadIdVec, ThreadIdVecDestructor);
180impl_vec_partialeq!(ThreadId, ThreadIdVec);
181impl_vec_partialord!(ThreadId, ThreadIdVec);
182
183impl ThreadId {
184 /// Generates a new, unique `ThreadId`.
185 #[must_use]
186 pub fn unique() -> Self {
187 Self {
188 id: MAX_THREAD_ID.fetch_add(1, Ordering::SeqCst),
189 }
190 }
191}
192
193/// A point in time, either from the system clock or a tick counter.
194///
195/// Use `Instant::System` on platforms with std, `Instant::Tick` on `embedded/no_std`.
196#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
197#[repr(C, u8)]
198pub enum Instant {
199 /// System time from `std::time::Instant` (requires "std" feature)
200 System(InstantPtr),
201 /// Tick-based time for embedded systems without a real-time clock
202 Tick(SystemTick),
203}
204
205#[cfg(feature = "std")]
206impl From<StdInstant> for Instant {
207 fn from(s: StdInstant) -> Self {
208 Self::System(s.into())
209 }
210}
211
212#[cfg(feature = "std")]
213std::thread_local! {
214 /// Injectable test-clock offset, in milliseconds, added to every
215 /// `Instant::now()` **on this thread**.
216 ///
217 /// Driven by the E2E `tick_ms` op. Everything time-driven in the engine —
218 /// scroll momentum, scrollbar fade, cursor blink, animations, timers —
219 /// reads the clock through `Instant::now()` / `get_system_time_libstd()`,
220 /// so advancing this offset moves all of them forward by exactly N ms
221 /// WITHOUT sleeping. That is what makes "drive the animation to completion
222 /// and assert it converges" deterministic instead of a `wait { ms }` race.
223 ///
224 /// Zero in production; only the debug-server `tick_ms` op ever writes it.
225 ///
226 /// # Why this is a thread-local and not a `static AtomicU64`
227 ///
228 /// It used to be process-global, which made the clock a shared mutable
229 /// resource: every scenario that ticked had to run SERIALLY, or scenario
230 /// A's `tick_ms` would shift scenario B's animations mid-frame. Since the
231 /// corpus is dominated by idle/animation scenarios, that serialised
232 /// essentially the whole suite.
233 ///
234 /// The read path is [`GetSystemTimeCallbackType`] — a bare
235 /// `extern "C" fn() -> Instant` in the public C API — plus ~140 direct
236 /// `Instant::now()` calls. Neither can carry a window, an app or a clock
237 /// handle without either breaking the C ABI for every language binding or
238 /// threading a time source through every call site including `no_std`
239 /// ones. A thread-local is the narrowest scope a context-free C callback
240 /// can read: it turns "the whole process" into "the thread that owns this
241 /// scenario", which is exactly the ownership boundary the parallel E2E
242 /// runner already establishes (one scenario runs start-to-finish on one
243 /// worker thread). [`reset_test_clock`] makes that boundary explicit.
244 static TEST_CLOCK_OFFSET_MS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
245}
246
247/// Advance the injectable test clock by `ms` (E2E `tick_ms`), returning the new
248/// offset. Affects only the CURRENT thread — see [`TEST_CLOCK_OFFSET_MS`].
249#[cfg(feature = "std")]
250#[must_use]
251pub fn advance_test_clock_ms(ms: u64) -> u64 {
252 TEST_CLOCK_OFFSET_MS.with(|c| {
253 let next = c.get().saturating_add(ms);
254 c.set(next);
255 next
256 })
257}
258
259/// The current test-clock offset in ms (0 unless `tick_ms` was used on this
260/// thread).
261#[cfg(feature = "std")]
262#[must_use]
263pub fn test_clock_offset_ms() -> u64 {
264 TEST_CLOCK_OFFSET_MS.with(core::cell::Cell::get)
265}
266
267#[cfg(feature = "std")]
268#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
269std::thread_local! {
270 /// When set, this thread's clock is FROZEN at this instant: `Instant::now()`
271 /// answers `base + TEST_CLOCK_OFFSET_MS` and real time does not flow into it
272 /// at all. See [`freeze_test_clock`].
273 static TEST_CLOCK_BASE: core::cell::Cell<Option<StdInstant>> =
274 const { core::cell::Cell::new(None) };
275}
276
277/// Freeze this thread's clock, so engine time advances ONLY when a scenario says
278/// it does (`tick_ms` / `wait`) and never because wall time passed.
279///
280/// Offsetting alone is not enough. `Instant::now()` was
281/// `StdInstant::now() + offset`, so the REAL component still flowed and every
282/// time-driven behaviour rode on however long the machine happened to take:
283/// elapsed = (exact virtual) + (whatever this build, under this load, spent
284/// computing). The E2E suite runs 8 scenarios per core, so that second term is
285/// both large and variable, and an assertion on a blinking caret's phase would
286/// flip between runs on a loaded runner while passing every time in isolation.
287///
288/// Frozen, engine time becomes a pure function of the ops a scenario executed —
289/// identical on a debug build, a release build and a saturated CI box. That is
290/// also what makes an off-by-one in animation timing *observable*: advance
291/// exactly one interval and the frame either flipped or it did not, with no
292/// jitter to hide behind.
293///
294/// This deliberately does NOT touch [`Instant::Tick`]. Interval constants are
295/// built as `Duration::System` (e.g. the cursor blink in `text_edit`), and
296/// `Duration::greater_than` compares only matching variants — handing the engine
297/// `Tick` elapsed values against `System` intervals would mismatch and silently
298/// answer "not yet" forever. Freezing keeps every existing comparison intact.
299///
300/// Idempotent: re-freezing an already-frozen clock keeps the original base, so
301/// the offset stays the single source of elapsed time.
302#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
303pub fn freeze_test_clock() {
304 TEST_CLOCK_BASE.with(|c| {
305 if c.get().is_none() {
306 c.set(Some(StdInstant::now()));
307 }
308 });
309}
310
311/// Whether this thread's clock is frozen (see [`freeze_test_clock`]).
312#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
313#[must_use]
314pub fn test_clock_is_frozen() -> bool {
315 TEST_CLOCK_BASE.with(core::cell::Cell::get).is_some()
316}
317
318/// Put this thread's test clock back on real time.
319///
320/// Worker threads are REUSED across scenarios, so without this the next
321/// scenario scheduled onto this thread would inherit the previous one's
322/// accumulated offset — the same cross-contamination the process-global
323/// offset had, just at thread granularity. The E2E runner calls this at the
324/// start of every scenario. Clears the freeze as well, so a scenario cannot
325/// leave the next one's clock stopped.
326#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
327pub fn reset_test_clock() {
328 TEST_CLOCK_OFFSET_MS.with(|c| c.set(0));
329 TEST_CLOCK_BASE.with(|c| c.set(None));
330}
331
332/// Monotonic frame counter, and the ONLY clock a wasm build has.
333///
334/// `std::time::Instant::now()` PANICS on wasm32-unknown-unknown, and
335/// `#[cfg(feature = "std")]` does not exclude wasm here: azul-core is built with
336/// `default = ["std"]` for the web target, so every `std` path is compiled in.
337///
338/// Answering `Tick(0)` forever would stop the panic and freeze every animation
339/// instead — a silent stall, which is worse than a loud crash. So the web build
340/// gets a real monotonic source: the browser drives redraw, each produced DOM
341/// patch is one frame, and one frame is exactly what a `t` (tick) duration
342/// counts. `AzStartup_buildPatch` calls [`advance_system_tick`] once per patch.
343#[cfg(feature = "std")]
344static SYSTEM_TICK: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
345
346/// Advance the frame counter by one. Called once per produced frame by backends
347/// that have no wall clock. Cheap enough to call unconditionally.
348#[cfg(feature = "std")]
349pub fn advance_system_tick() {
350 SYSTEM_TICK.fetch_add(1, Ordering::Relaxed);
351}
352
353/// The current frame counter.
354#[cfg(feature = "std")]
355#[must_use]
356pub fn system_tick_now() -> u64 {
357 SYSTEM_TICK.load(Ordering::Relaxed)
358}
359
360/// `std::time::Instant::now()` shifted by the injectable test-clock offset, or —
361/// when the clock is frozen — built from the frozen base so real time cannot
362/// leak in.
363///
364/// NOT COMPILED on wasm32, where `std::time::Instant::now()` panics.
365///
366/// `web_lift` is deliberately NOT included here. That backend compiles natively
367/// and is lifted to wasm afterwards, so `target_arch` reads `x86_64` — but the
368/// lift walks the LLVM graph and auto-inserts calls out to JS for things like
369/// time, so it supplies its own clock and does not want this arm disabled.
370#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
371fn std_now_with_test_offset() -> StdInstant {
372 let offset = test_clock_offset_ms();
373 if let Some(base) = TEST_CLOCK_BASE.with(core::cell::Cell::get) {
374 return base + core::time::Duration::from_millis(offset);
375 }
376 if offset == 0 {
377 StdInstant::now()
378 } else {
379 StdInstant::now() + core::time::Duration::from_millis(offset)
380 }
381}
382
383impl Instant {
384 /// Returns the current system time.
385 ///
386 /// On systems with std, this uses `std::time::Instant::now()`.
387 /// On `no_std` systems, this returns a zero tick.
388 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
389 #[must_use]
390 pub fn now() -> Self {
391 std_now_with_test_offset().into()
392 }
393
394 /// Returns the current time on wasm32, which has no clock to read.
395 ///
396 /// `std::time::Instant::now()` panics on wasm32-unknown-unknown, and
397 /// `#[cfg(feature = "std")]` does not exclude wasm here — azul-core is built
398 /// with `default = ["std"]` for the web target, so the std path is compiled
399 /// in and would trap on the first frame.
400 ///
401 /// This deliberately does NOT answer a constant `Tick(0)`. That stops the
402 /// panic and freezes every animation instead, which is a silent stall — the
403 /// worse failure of the two. The browser drives redraw and each produced DOM
404 /// patch is one frame, so the frame counter IS the clock, and a frame is
405 /// exactly what a `t` (tick) duration counts. Elapsed values come out as
406 /// `Tick` and convert against `System` intervals through
407 /// `Duration::as_nanos`, so `60t` compares equal to one second.
408 #[cfg(all(feature = "std", target_arch = "wasm32"))]
409 #[must_use]
410 pub fn now() -> Self {
411 Instant::Tick(SystemTick::new(system_tick_now()))
412 }
413
414 /// Returns the current system time (no_std fallback).
415 #[cfg(not(feature = "std"))]
416 pub fn now() -> Self {
417 Instant::Tick(SystemTick::new(0))
418 }
419
420 /// Returns a number from 0.0 to 1.0 indicating the current
421 /// linear interpolation value between (start, end)
422 #[must_use]
423 pub fn linear_interpolate(&self, mut start: Self, mut end: Self) -> f32 {
424 use core::mem;
425
426 if end < start {
427 mem::swap(&mut start, &mut end);
428 }
429
430 if *self < start {
431 return 0.0;
432 }
433 if *self > end {
434 return 1.0;
435 }
436
437 // Zero-length interval: `duration_current / duration_total` would be
438 // `0/0 = NaN`. Treat a collapsed interval as fully elapsed (1.0) rather
439 // than propagating NaN into animation progress.
440 if start == end {
441 return 1.0;
442 }
443
444 let duration_total = end.duration_since(&start);
445 let duration_current = self.duration_since(&start);
446
447 let ratio = duration_current.div(&duration_total);
448 if ratio.is_nan() {
449 return 1.0;
450 }
451 ratio.clamp(0.0, 1.0)
452 }
453
454 /// Adds a duration to the instant.
455 ///
456 /// The duration's UNIT need not match the instant's: a `Tick` duration added
457 /// to a `System` instant is converted at [`TICKS_PER_SECOND`], and a `System`
458 /// duration added to a `Tick` instant is converted to whole ticks.
459 ///
460 /// # Why the mismatch is converted rather than dropped
461 ///
462 /// This used to return `self` unchanged for a unit mismatch, which turned a
463 /// `Duration::Tick` interval on a wall-clock timer into a schedule point of
464 /// `last_run + 0` — `Timer::instant_of_next_run` is literally
465 /// `last_run + delay + interval`, so the timer reported itself permanently
466 /// overdue and `LayoutWindow::time_until_next_timer_ms` answered `Some(0)`
467 /// for it, i.e. "block for zero milliseconds" to any loop that consults it.
468 ///
469 /// `System + System` still overflow-panics on an absurd duration (that is
470 /// `StdInstant`'s own behaviour, characterised in the tests); the tick arms
471 /// saturate.
472 #[must_use]
473 pub fn add_optional_duration(&self, duration: Option<&Duration>) -> Self {
474 duration.map_or_else(
475 || self.clone(),
476 |d| match (self, d) {
477 (Self::System(i), Duration::System(d)) => {
478 #[cfg(feature = "std")]
479 {
480 let s: StdInstant = i.clone().into();
481 let d: StdDuration = (*d).into();
482 let new: InstantPtr = (s + d).into();
483 Self::System(new)
484 }
485 #[cfg(not(feature = "std"))]
486 {
487 // A `System` instant cannot be constructed on no_std, so
488 // this arm is unreachable in practice; return self rather
489 // than aborting.
490 let _ = (i, d);
491 self.clone()
492 }
493 }
494 (Self::Tick(s), Duration::Tick(d)) => Self::Tick(SystemTick {
495 // Saturate so a runaway tick delta cannot overflow-panic.
496 tick_counter: s.tick_counter.saturating_add(d.tick_diff),
497 }),
498 // System instant + Tick duration: convert the frame count to wall
499 // time. Routed through the same `System + System` arm so the
500 // overflow behaviour is identical for both units.
501 (Self::System(_), Duration::Tick(_)) => self.add_optional_duration(Some(
502 &Duration::System(SystemTimeDiff::from_nanos_u128(d.as_nanos())),
503 )),
504 // Tick instant + System duration: convert to WHOLE ticks. A
505 // sub-frame duration therefore advances nothing, which is the
506 // truthful answer on a clock whose resolution is one frame.
507 (Self::Tick(s), Duration::System(_)) => Self::Tick(SystemTick {
508 tick_counter: s.tick_counter.saturating_add(d.as_ticks()),
509 }),
510 },
511 )
512 }
513
514 /// Converts to `std::time::Instant` (panics if Tick variant).
515 #[cfg(feature = "std")]
516 #[must_use]
517 pub fn into_std_instant(self) -> StdInstant {
518 match self {
519 Self::System(s) => s.into(),
520 Self::Tick(_) => unreachable!(),
521 }
522 }
523
524 /// Calculates the duration since an earlier point in time.
525 ///
526 /// Saturates to a zero duration in the degenerate cases (earlier is actually
527 /// *later* than `self`, or the two instants are of mismatched kinds) instead
528 /// of panicking — this runs on the hot event-loop path and must not crash.
529 #[must_use]
530 pub fn duration_since(&self, earlier: &Self) -> Duration {
531 match (earlier, self) {
532 (Self::System(prev), Self::System(now)) => {
533 #[cfg(feature = "std")]
534 {
535 let prev_instant: StdInstant = prev.clone().into();
536 let now_instant: StdInstant = now.clone().into();
537 // `saturating_duration_since` yields 0 if `prev` is later
538 // than `now` (monotonic-clock skew / reordered instants).
539 Duration::System(now_instant.saturating_duration_since(prev_instant).into())
540 }
541 #[cfg(not(feature = "std"))]
542 {
543 // Unreachable on no_std (no System instants); saturate to 0.
544 let _ = (prev, now);
545 Duration::Tick(SystemTickDiff { tick_diff: 0 })
546 }
547 }
548 (
549 Self::Tick(SystemTick { tick_counter: prev }),
550 Self::Tick(SystemTick { tick_counter: now }),
551 ) => Duration::Tick(SystemTickDiff {
552 // Saturate: a "negative" span (prev > now) clamps to 0.
553 tick_diff: now.saturating_sub(*prev),
554 }),
555 // Mismatched kinds: no meaningful span -> saturate to 0.
556 _ => Duration::Tick(SystemTickDiff { tick_diff: 0 }),
557 }
558 }
559}
560
561/// Tick-based timestamp for systems without a real-time clock.
562///
563/// Used on embedded systems where time is measured in frame ticks or cycles.
564#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
565#[repr(C)]
566pub struct SystemTick {
567 pub tick_counter: u64,
568}
569
570impl SystemTick {
571 /// Creates a new tick timestamp from a counter value.
572 #[must_use]
573 pub const fn new(tick_counter: u64) -> Self {
574 Self { tick_counter }
575 }
576}
577
578/// FFI-safe wrapper around `std::time::Instant` with custom clone/drop callbacks.
579///
580/// Allows crossing FFI boundaries while maintaining proper memory management.
581#[repr(C)]
582pub struct InstantPtr {
583 /// `ManuallyDrop` so the owned `Box` is freed ONLY when `run_destructor` is
584 /// still set (see `Drop`). The codegen FFI wrappers (`AzTimerCallbackInfo`
585 /// etc.) embed this by value AND have their own `Drop` that `drop_in_place`s
586 /// the real type first; Rust's drop glue would then drop this `ptr` field a
587 /// SECOND time on the same bytes. Gating the `Box` free on `run_destructor`
588 /// (cleared by the first drop) makes that second drop a safe no-op. Layout is
589 /// unchanged: `ManuallyDrop<Box<T>>` is one pointer, like the old `Box<T>`.
590 #[cfg(feature = "std")]
591 pub ptr: ManuallyDrop<Box<StdInstant>>,
592 #[cfg(not(feature = "std"))]
593 pub ptr: *const c_void,
594 pub clone_fn: InstantPtrCloneCallback,
595 pub destructor: InstantPtrDestructorCallback,
596 pub run_destructor: bool,
597}
598
599pub type InstantPtrCloneCallbackType = extern "C" fn(*const InstantPtr) -> InstantPtr;
600#[repr(C)]
601pub struct InstantPtrCloneCallback {
602 pub cb: InstantPtrCloneCallbackType,
603}
604impl_callback_simple!(InstantPtrCloneCallback);
605
606pub type InstantPtrDestructorCallbackType = extern "C" fn(*mut InstantPtr);
607#[repr(C)]
608pub struct InstantPtrDestructorCallback {
609 pub cb: InstantPtrDestructorCallbackType,
610}
611impl_callback_simple!(InstantPtrDestructorCallback);
612
613// ---- LIBSTD implementation for InstantPtr BEGIN
614#[cfg(feature = "std")]
615impl fmt::Debug for InstantPtr {
616 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
617 write!(f, "{:?}", self.get())
618 }
619}
620
621#[cfg(not(feature = "std"))]
622impl core::fmt::Debug for InstantPtr {
623 fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
624 write!(f, "{:?}", self.ptr as usize)
625 }
626}
627
628#[cfg(feature = "std")]
629impl core::hash::Hash for InstantPtr {
630 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
631 self.get().hash(state);
632 }
633}
634
635#[cfg(not(feature = "std"))]
636impl core::hash::Hash for InstantPtr {
637 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
638 (self.ptr as usize).hash(state);
639 }
640}
641
642#[cfg(feature = "std")]
643impl PartialEq for InstantPtr {
644 fn eq(&self, other: &Self) -> bool {
645 self.get() == other.get()
646 }
647}
648
649#[cfg(not(feature = "std"))]
650impl PartialEq for InstantPtr {
651 fn eq(&self, other: &InstantPtr) -> bool {
652 (self.ptr as usize).eq(&(other.ptr as usize))
653 }
654}
655
656impl Eq for InstantPtr {}
657
658#[cfg(feature = "std")]
659impl PartialOrd for InstantPtr {
660 fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
661 Some((self.get()).cmp(&(other.get())))
662 }
663}
664
665#[cfg(not(feature = "std"))]
666impl PartialOrd for InstantPtr {
667 fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
668 Some((self.ptr as usize).cmp(&(other.ptr as usize)))
669 }
670}
671
672#[cfg(feature = "std")]
673impl Ord for InstantPtr {
674 fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
675 (self.get()).cmp(&(other.get()))
676 }
677}
678
679#[cfg(not(feature = "std"))]
680impl Ord for InstantPtr {
681 fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
682 (self.ptr as usize).cmp(&(other.ptr as usize))
683 }
684}
685
686#[cfg(feature = "std")]
687impl InstantPtr {
688 fn get(&self) -> StdInstant {
689 (**self.ptr)
690 }
691}
692
693impl Clone for InstantPtr {
694 fn clone(&self) -> Self {
695 (self.clone_fn.cb)(self)
696 }
697}
698
699#[cfg(feature = "std")]
700extern "C" fn std_instant_clone(ptr: *const InstantPtr) -> InstantPtr {
701 let az_instant_ptr = unsafe { &*ptr };
702 InstantPtr {
703 ptr: ManuallyDrop::new((*az_instant_ptr.ptr).clone()),
704 clone_fn: az_instant_ptr.clone_fn,
705 destructor: az_instant_ptr.destructor,
706 run_destructor: true,
707 }
708}
709
710#[cfg(feature = "std")]
711impl From<StdInstant> for InstantPtr {
712 fn from(s: StdInstant) -> Self {
713 Self {
714 ptr: ManuallyDrop::new(Box::new(s)),
715 clone_fn: InstantPtrCloneCallback {
716 cb: std_instant_clone,
717 },
718 destructor: InstantPtrDestructorCallback {
719 cb: std_instant_drop,
720 },
721 run_destructor: true,
722 }
723 }
724}
725
726#[cfg(feature = "std")]
727impl From<InstantPtr> for StdInstant {
728 fn from(s: InstantPtr) -> Self {
729 s.get()
730 }
731}
732
733impl Drop for InstantPtr {
734 fn drop(&mut self) {
735 if self.run_destructor {
736 self.run_destructor = false;
737 (self.destructor.cb)(self);
738 // Free the owned Box exactly once, here under the run_destructor guard.
739 // A second drop on the same bytes (the codegen wrapper's field-drop after
740 // its own `_delete` already ran the real drop) sees run_destructor=false
741 // and skips this -> no double-free. (non-std `ptr` is a raw POD pointer
742 // freed by the destructor callback above, so nothing to drop here.)
743 // SAFETY: `run_destructor` is set false above, so this arm runs at
744 // most once per InstantPtr value; the `Box` inside was never moved
745 // out, so it is live and owned here and safe to drop exactly once.
746 #[cfg(feature = "std")]
747 unsafe {
748 ManuallyDrop::drop(&mut self.ptr);
749 }
750 }
751 }
752}
753
754#[cfg(feature = "std")]
755const extern "C" fn std_instant_drop(_: *mut InstantPtr) {}
756
757// ---- LIBSTD implementation for InstantPtr END
758
759/// A span of time, either from the system clock or as tick difference.
760///
761/// Mirrors `Instant` variants - System durations work with System instants,
762/// Tick durations work with Tick instants.
763#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
764#[repr(C, u8)]
765pub enum Duration {
766 /// System duration from `std::time::Duration` (requires "std" feature)
767 System(SystemTimeDiff),
768 /// Tick-based duration for embedded systems
769 Tick(SystemTickDiff),
770}
771
772impl fmt::Display for Duration {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match self {
775 #[cfg(feature = "std")]
776 Self::System(s) => {
777 let s: StdDuration = (*s).into();
778 write!(f, "{s:?}")
779 }
780 #[cfg(not(feature = "std"))]
781 Duration::System(s) => write!(f, "({}s, {}ns)", s.secs, s.nanos),
782 Self::Tick(tick) => write!(f, "{} ticks", tick.tick_diff),
783 }
784 }
785}
786
787#[cfg(feature = "std")]
788impl From<StdDuration> for Duration {
789 fn from(s: StdDuration) -> Self {
790 Self::System(s.into())
791 }
792}
793
794/// Nominal engine tick (frame) rate — the single exchange rate between
795/// [`Duration::Tick`] (frames) and [`Duration::System`] (wall time).
796///
797/// Re-exported from `azul-css` so the CSS `t` unit and the engine's `Duration`
798/// arithmetic cannot drift apart. See [`azul_css::props::basic::time::TICKS_PER_SECOND`].
799pub use azul_css::props::basic::time::TICKS_PER_SECOND;
800
801impl Duration {
802 /// This duration on ONE canonical scale, in nanoseconds — the common ground
803 /// on which a `Tick` span and a `System` span can be compared.
804 ///
805 /// `u128` because a `System` duration holds up to `u64::MAX` *seconds*
806 /// (~1.8e28 ns), which does not fit `u64`. The tick conversion multiplies
807 /// before it divides so whole seconds stay exact: `60t` is `1_000_000_000`ns,
808 /// not `60 * 16_666_666 = 999_999_960`ns.
809 ///
810 /// Note this also normalises a DENORMALISED `SystemTimeDiff` (`nanos` past
811 /// `1e9`) the same way `std::time::Duration::new` would — except it cannot
812 /// panic on overflow while doing it.
813 // `as u128` rather than `u128::from`: this is a `const fn` and `From` is not
814 // const. Every one of these widenings is lossless.
815 #[allow(clippy::cast_lossless)]
816 #[must_use]
817 pub const fn as_nanos(&self) -> u128 {
818 match self {
819 Self::System(s) => (s.secs as u128) * (NANOS_PER_SEC as u128) + (s.nanos as u128),
820 Self::Tick(t) => {
821 (t.tick_diff as u128) * (NANOS_PER_SEC as u128) / (TICKS_PER_SECOND as u128)
822 }
823 }
824 }
825
826 /// A wall-clock duration of `ms` whole milliseconds.
827 #[must_use]
828 pub const fn from_millis(ms: u64) -> Self {
829 Self::System(SystemTimeDiff::from_millis(ms))
830 }
831
832 /// A duration of `ticks` engine frames — the clockless unit, and what the
833 /// CSS `t` unit becomes.
834 #[must_use]
835 pub const fn from_ticks(ticks: u64) -> Self {
836 Self::Tick(SystemTickDiff { tick_diff: ticks })
837 }
838
839 /// This duration in whole ticks (frames), truncating toward zero.
840 ///
841 /// A sub-frame span is **zero** ticks, not one: "how many whole frames fit",
842 /// never "round up so that something happens".
843 // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The widenings
844 // are lossless and the u128 -> u64 narrowing is range-checked immediately
845 // above it.
846 #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
847 #[must_use]
848 pub const fn as_ticks(&self) -> u64 {
849 match self {
850 Self::Tick(t) => t.tick_diff,
851 Self::System(_) => {
852 let ticks = self.as_nanos() * (TICKS_PER_SECOND as u128) / (NANOS_PER_SEC as u128);
853 if ticks > u64::MAX as u128 {
854 u64::MAX
855 } else {
856 ticks as u64
857 }
858 }
859 }
860 }
861
862 /// This duration in whole milliseconds, truncating toward zero and
863 /// saturating at `u64::MAX` rather than wrapping.
864 // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The u128 ->
865 // u64 narrowing is range-checked immediately above it.
866 #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
867 #[must_use]
868 pub const fn as_millis_u64(&self) -> u64 {
869 let ms = self.as_nanos() / (NANOS_PER_MILLI as u128);
870 if ms > u64::MAX as u128 {
871 u64::MAX
872 } else {
873 ms as u64
874 }
875 }
876
877 /// Returns the maximum possible duration.
878 #[must_use]
879 pub fn max() -> Self {
880 #[cfg(feature = "std")]
881 {
882 Self::System(StdDuration::new(core::u64::MAX, NANOS_PER_SEC - 1).into())
883 }
884 #[cfg(not(feature = "std"))]
885 {
886 Duration::Tick(SystemTickDiff {
887 tick_diff: u64::MAX,
888 })
889 }
890 }
891
892 /// Divides this duration by another, returning the ratio as f32.
893 ///
894 /// Same-unit division goes through the unit's own `div` so its exact
895 /// floating-point result is unchanged. Cross-unit division falls back to the
896 /// canonical nanosecond scale rather than returning `0.0` — a `0.0` ratio
897 /// here means "animation is at 0% progress", which is a frozen animation, not
898 /// an error anyone would notice.
899 // the f64 ratio is intentionally narrowed to the f32 return type; the value
900 // is a duration ratio, far inside f32's range.
901 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
902 #[must_use]
903 pub fn div(&self, other: &Self) -> f32 {
904 use self::Duration::{System, Tick};
905 match (self, other) {
906 (System(s), System(s2)) => s.div(s2) as f32,
907 (Tick(t), Tick(t2)) => t.div(t2) as f32,
908 // u128 -> f64 loses precision only past 2^53 ns (~104 days), and the
909 // result is a ratio that is then narrowed to f32 anyway.
910 _ => (self.as_nanos() as f64 / other.as_nanos() as f64) as f32,
911 }
912 }
913
914 /// Returns the smaller of two durations.
915 #[must_use]
916 pub const fn min(self, other: Self) -> Self {
917 if self.smaller_than(&other) {
918 self
919 } else {
920 other
921 }
922 }
923
924 /// Returns true if self > other.
925 ///
926 /// Compares on the canonical nanosecond scale ([`Self::as_nanos`]), so a
927 /// `Tick` span and a `System` span compare TRUTHFULLY against each other.
928 ///
929 /// # Why this is not "mismatched kinds saturate to false"
930 ///
931 /// It used to be. That made a unit mismatch invisible and permanent instead
932 /// of loud: the engine's interval constants are `Duration::System` (the
933 /// cursor blink, the scrollbar fade, the tooltip delay), so the moment a
934 /// clock produced `Tick` elapsed values every one of those comparisons
935 /// answered "not yet" — forever. Nothing panicked, nothing logged, the UI
936 /// simply stopped animating. That is precisely the failure a clockless unit
937 /// is supposed to make *catchable*, so the comparison has to be total.
938 ///
939 /// Three behaviours changed, all in the safe direction:
940 ///
941 /// 1. Cross-unit comparisons now answer, instead of always `false`.
942 /// 2. On `no_std` the `System`/`System` arm used to be hardcoded `false`
943 /// (there was no `StdDuration` to defer to); it now compares properly.
944 /// 3. A denormalised `SystemTimeDiff` whose `secs + nanos/1e9` overflows
945 /// `u64` used to panic inside `StdDuration::new`; `u128` nanoseconds
946 /// cannot overflow.
947 #[must_use]
948 pub const fn greater_than(&self, other: &Self) -> bool {
949 self.as_nanos() > other.as_nanos()
950 }
951
952 /// Returns true if self < other.
953 ///
954 /// Canonical-scale comparison; see [`Self::greater_than`] for why this is
955 /// unit-aware rather than saturating to `false` on a unit mismatch.
956 #[must_use]
957 pub const fn smaller_than(&self, other: &Self) -> bool {
958 self.as_nanos() < other.as_nanos()
959 }
960}
961
962/// Represents a difference in ticks for systems that
963/// don't support timing
964#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
965#[repr(C)]
966pub struct SystemTickDiff {
967 pub tick_diff: u64,
968}
969
970impl SystemTickDiff {
971 /// Divide duration A by duration B.
972 /// Returns `Inf` or `NaN` if `other` is zero.
973 // tick counts -> f64 for the ratio; precision only degrades past 2^53 ticks.
974 #[allow(clippy::cast_precision_loss)]
975 #[must_use]
976 pub fn div(&self, other: &Self) -> f64 {
977 self.tick_diff as f64 / other.tick_diff as f64
978 }
979}
980
981/// Duration represented as seconds + nanoseconds (mirrors `std::time::Duration`).
982#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
983#[repr(C)]
984pub struct SystemTimeDiff {
985 pub secs: u64,
986 pub nanos: u32,
987}
988
989impl SystemTimeDiff {
990 /// Divide duration A by duration B.
991 /// Returns `Inf` or `NaN` if `other` is zero.
992 #[must_use]
993 pub fn div(&self, other: &Self) -> f64 {
994 self.as_secs_f64() / other.as_secs_f64()
995 }
996 // secs (u64) -> f64 loses precision only past 2^53 seconds (~285M years).
997 #[allow(clippy::cast_precision_loss)]
998 fn as_secs_f64(&self) -> f64 {
999 (self.secs as f64) + (f64::from(self.nanos) / f64::from(NANOS_PER_SEC))
1000 }
1001}
1002
1003#[cfg(feature = "std")]
1004impl From<StdDuration> for SystemTimeDiff {
1005 fn from(d: StdDuration) -> Self {
1006 Self {
1007 secs: d.as_secs(),
1008 nanos: d.subsec_nanos(),
1009 }
1010 }
1011}
1012
1013#[cfg(feature = "std")]
1014impl From<SystemTimeDiff> for StdDuration {
1015 fn from(d: SystemTimeDiff) -> Self {
1016 Self::new(d.secs, d.nanos)
1017 }
1018}
1019
1020const MILLIS_PER_SEC: u64 = 1_000;
1021const NANOS_PER_MILLI: u32 = 1_000_000;
1022const NANOS_PER_SEC: u32 = 1_000_000_000;
1023
1024impl SystemTimeDiff {
1025 /// Creates a duration from whole seconds.
1026 #[must_use]
1027 pub const fn from_secs(secs: u64) -> Self {
1028 Self { secs, nanos: 0 }
1029 }
1030 /// Creates a duration from milliseconds.
1031 #[must_use]
1032 pub const fn from_millis(millis: u64) -> Self {
1033 Self {
1034 secs: millis / MILLIS_PER_SEC,
1035 nanos: ((millis % MILLIS_PER_SEC) as u32) * NANOS_PER_MILLI,
1036 }
1037 }
1038 /// Creates a duration from nanoseconds.
1039 // const fn (no const TryFrom); `nanos % NANOS_PER_SEC` is always < 10^9, which
1040 // fits u32, so the narrowing cast cannot truncate.
1041 #[allow(clippy::cast_possible_truncation)]
1042 #[must_use]
1043 pub const fn from_nanos(nanos: u64) -> Self {
1044 Self {
1045 secs: nanos / (NANOS_PER_SEC as u64),
1046 nanos: (nanos % (NANOS_PER_SEC as u64)) as u32,
1047 }
1048 }
1049
1050 /// Creates a duration from a `u128` nanosecond count, saturating at the
1051 /// largest representable duration instead of wrapping.
1052 ///
1053 /// Needed because [`Duration::as_nanos`] is `u128`: a tick count near
1054 /// `u64::MAX` converts to ~3e26 ns, far past what `u64` nanoseconds hold.
1055 // `nanos % NANOS_PER_SEC` is always < 10^9 and `secs` is range-checked above,
1056 // so neither narrowing cast can truncate. `as` widenings rather than `From`
1057 // because this is a `const fn`.
1058 #[allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
1059 #[must_use]
1060 pub const fn from_nanos_u128(nanos: u128) -> Self {
1061 let secs = nanos / (NANOS_PER_SEC as u128);
1062 if secs > u64::MAX as u128 {
1063 Self {
1064 secs: u64::MAX,
1065 nanos: NANOS_PER_SEC - 1,
1066 }
1067 } else {
1068 Self {
1069 secs: secs as u64,
1070 nanos: (nanos % (NANOS_PER_SEC as u128)) as u32,
1071 }
1072 }
1073 }
1074 /// Adds two durations, returning None on overflow.
1075 #[must_use]
1076 pub const fn checked_add(self, rhs: Self) -> Option<Self> {
1077 if let Some(mut secs) = self.secs.checked_add(rhs.secs) {
1078 let mut nanos = self.nanos + rhs.nanos;
1079 if nanos >= NANOS_PER_SEC {
1080 nanos -= NANOS_PER_SEC;
1081 if let Some(new_secs) = secs.checked_add(1) {
1082 secs = new_secs;
1083 } else {
1084 return None;
1085 }
1086 }
1087 Some(Self { secs, nanos })
1088 } else {
1089 None
1090 }
1091 }
1092
1093 /// Returns the total duration in milliseconds.
1094 ///
1095 /// Saturates at `u64::MAX` instead of overflow-panicking for enormous
1096 /// `secs` values (`secs * 1000` overflows around ~1.8e16 seconds).
1097 #[must_use]
1098 pub const fn millis(&self) -> u64 {
1099 self.secs
1100 .saturating_mul(MILLIS_PER_SEC)
1101 .saturating_add((self.nanos / NANOS_PER_MILLI) as u64)
1102 }
1103
1104 /// Converts to `std::time::Duration`.
1105 #[cfg(feature = "std")]
1106 #[must_use]
1107 pub fn get(&self) -> StdDuration {
1108 (*self).into()
1109 }
1110}
1111
1112/// Bridge from the CSS-level duration to the engine-level one, preserving the
1113/// unit.
1114///
1115/// This is the join that makes a CSS `5t` mean five FRAMES all the way down to
1116/// the timer: `ms`/`s` become `Duration::System`, `t` becomes `Duration::Tick`.
1117/// Collapsing ticks to milliseconds here would put the wall clock back in the
1118/// path and make "advance exactly 5 ticks, assert the 5th frame flipped"
1119/// untestable again.
1120impl From<azul_css::props::basic::time::CssDuration> for Duration {
1121 fn from(d: azul_css::props::basic::time::CssDuration) -> Self {
1122 use azul_css::props::basic::time::CssDurationUnit;
1123 match d.unit {
1124 CssDurationUnit::Milliseconds => Self::from_millis(u64::from(d.inner)),
1125 CssDurationUnit::Ticks => Self::from_ticks(u64::from(d.inner)),
1126 }
1127 }
1128}
1129
1130impl_option!(
1131 Instant,
1132 OptionInstant,
1133 copy = false,
1134 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1135);
1136impl_option!(
1137 Duration,
1138 OptionDuration,
1139 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1140);
1141#[allow(variant_size_differences)]
1142// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1143/// Message that can be sent from the main thread to the Thread using the `ThreadId`.
1144///
1145/// The thread can ignore the event.
1146#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1147#[repr(C, u8)]
1148pub enum ThreadSendMsg {
1149 /// The thread should terminate at the nearest
1150 TerminateThread,
1151 /// Next frame tick
1152 Tick,
1153 /// Custom data
1154 Custom(RefAny),
1155}
1156
1157impl_option!(
1158 ThreadSendMsg,
1159 OptionThreadSendMsg,
1160 copy = false,
1161 [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1162);
1163
1164/// Channel endpoint for receiving messages from the main thread in a background thread.
1165///
1166/// Thread-safe wrapper around the receiver end of a message channel.
1167#[derive(Debug)]
1168#[repr(C)]
1169pub struct ThreadReceiver {
1170 #[cfg(feature = "std")]
1171 pub ptr: Box<Arc<Mutex<ThreadReceiverInner>>>,
1172 #[cfg(not(feature = "std"))]
1173 pub ptr: *const c_void,
1174 pub run_destructor: bool,
1175 /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1176 pub ctx: OptionRefAny,
1177}
1178
1179impl Clone for ThreadReceiver {
1180 fn clone(&self) -> Self {
1181 Self {
1182 ptr: self.ptr.clone(),
1183 run_destructor: true,
1184 ctx: self.ctx.clone(),
1185 }
1186 }
1187}
1188
1189impl Drop for ThreadReceiver {
1190 fn drop(&mut self) {
1191 self.run_destructor = false;
1192 }
1193}
1194
1195impl ThreadReceiver {
1196 /// Creates a new receiver (no-op on no_std).
1197 #[cfg(not(feature = "std"))]
1198 pub fn new(_t: ThreadReceiverInner) -> Self {
1199 Self {
1200 ptr: core::ptr::null(),
1201 run_destructor: false,
1202 ctx: OptionRefAny::None,
1203 }
1204 }
1205
1206 /// Creates a new receiver wrapping the inner channel.
1207 #[cfg(feature = "std")]
1208 #[must_use]
1209 pub fn new(t: ThreadReceiverInner) -> Self {
1210 Self {
1211 ptr: Box::new(Arc::new(Mutex::new(t))),
1212 run_destructor: true,
1213 ctx: OptionRefAny::None,
1214 }
1215 }
1216
1217 /// Get the FFI context (e.g., Python callable)
1218 #[must_use]
1219 pub fn get_ctx(&self) -> OptionRefAny {
1220 self.ctx.clone()
1221 }
1222
1223 /// Receives a message (returns None on no_std).
1224 #[cfg(not(feature = "std"))]
1225 pub fn recv(&mut self) -> OptionThreadSendMsg {
1226 None.into()
1227 }
1228
1229 /// Receives a message from the main thread, if available.
1230 #[cfg(feature = "std")]
1231 pub fn recv(&mut self) -> OptionThreadSendMsg {
1232 let Some(ts) = self.ptr.lock().ok() else {
1233 return None.into();
1234 };
1235 (ts.recv_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()) as *const c_void)
1236 }
1237}
1238
1239/// Inner receiver state containing the actual channel and callbacks.
1240#[derive(Debug)]
1241#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
1242#[repr(C)]
1243pub struct ThreadReceiverInner {
1244 #[cfg(feature = "std")]
1245 pub ptr: Box<Receiver<ThreadSendMsg>>,
1246 #[cfg(not(feature = "std"))]
1247 pub ptr: *const c_void,
1248 pub recv_fn: ThreadRecvCallback,
1249 pub destructor: ThreadReceiverDestructorCallback,
1250}
1251
1252#[cfg(not(feature = "std"))]
1253unsafe impl Send for ThreadReceiverInner {}
1254
1255#[cfg(feature = "std")]
1256impl core::hash::Hash for ThreadReceiverInner {
1257 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1258 (std::ptr::from_ref(self.ptr.as_ref()) as usize).hash(state);
1259 }
1260}
1261
1262#[cfg(feature = "std")]
1263impl PartialEq for ThreadReceiverInner {
1264 fn eq(&self, other: &Self) -> bool {
1265 std::ptr::eq(self.ptr.as_ref(), other.ptr.as_ref())
1266 }
1267}
1268
1269#[cfg(feature = "std")]
1270impl Eq for ThreadReceiverInner {}
1271
1272#[cfg(feature = "std")]
1273impl PartialOrd for ThreadReceiverInner {
1274 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1275 Some(
1276 (std::ptr::from_ref(self.ptr.as_ref()) as usize)
1277 .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize)),
1278 )
1279 }
1280}
1281
1282#[cfg(feature = "std")]
1283impl Ord for ThreadReceiverInner {
1284 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1285 (std::ptr::from_ref(self.ptr.as_ref()) as usize)
1286 .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize))
1287 }
1288}
1289
1290impl Drop for ThreadReceiverInner {
1291 fn drop(&mut self) {
1292 (self.destructor.cb)(self);
1293 }
1294}
1295
1296/// Get the current system type, equivalent to `std::time::Instant::now()`, except it
1297/// also works on systems that don't have a clock (such as embedded timers)
1298pub type GetSystemTimeCallbackType = extern "C" fn() -> Instant;
1299#[repr(C)]
1300pub struct GetSystemTimeCallback {
1301 pub cb: GetSystemTimeCallbackType,
1302}
1303impl_callback_simple!(GetSystemTimeCallback);
1304
1305/// Default implementation that gets the current system time.
1306///
1307/// On WASM targets `std::time::Instant::now()` panics, so we fall back to
1308/// a zero-tick instant instead.
1309#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1310#[must_use]
1311pub extern "C" fn get_system_time_libstd() -> Instant {
1312 // Honours the injectable E2E test clock (see TEST_CLOCK_OFFSET_MS).
1313 std_now_with_test_offset().into()
1314}
1315
1316/// Fallback for WASM (where `Instant::now()` panics) and no-std targets.
1317#[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
1318pub extern "C" fn get_system_time_libstd() -> Instant {
1319 Instant::Tick(SystemTick::new(0))
1320}
1321
1322/// Callback to check if a thread has finished execution.
1323pub type CheckThreadFinishedCallbackType =
1324 extern "C" fn(/* dropcheck */ *const c_void) -> bool;
1325/// Wrapper for thread completion check callback.
1326#[repr(C)]
1327pub struct CheckThreadFinishedCallback {
1328 pub cb: CheckThreadFinishedCallbackType,
1329}
1330impl_callback_simple!(CheckThreadFinishedCallback);
1331
1332/// Callback to send a message to a background thread.
1333pub type LibrarySendThreadMsgCallbackType =
1334 extern "C" fn(/* Sender<ThreadSendMsg> */ *const c_void, ThreadSendMsg) -> bool;
1335/// Wrapper for thread message send callback.
1336#[repr(C)]
1337pub struct LibrarySendThreadMsgCallback {
1338 pub cb: LibrarySendThreadMsgCallbackType,
1339}
1340impl_callback_simple!(LibrarySendThreadMsgCallback);
1341
1342/// Callback for a running thread to receive messages from the main thread.
1343pub type ThreadRecvCallbackType =
1344 extern "C" fn(/* receiver.ptr */ *const c_void) -> OptionThreadSendMsg;
1345/// Wrapper for thread message receive callback.
1346#[repr(C)]
1347pub struct ThreadRecvCallback {
1348 pub cb: ThreadRecvCallbackType,
1349}
1350impl_callback_simple!(ThreadRecvCallback);
1351
1352/// Callback to destroy a `ThreadReceiver`.
1353pub type ThreadReceiverDestructorCallbackType = extern "C" fn(*mut ThreadReceiverInner);
1354/// Wrapper for thread receiver destructor callback.
1355#[repr(C)]
1356pub struct ThreadReceiverDestructorCallback {
1357 pub cb: ThreadReceiverDestructorCallbackType,
1358}
1359impl_callback_simple!(ThreadReceiverDestructorCallback);
1360
1361#[cfg(test)]
1362#[path = "task_test.rs"]
1363mod task_test;