Skip to main content

azul_layout/
thread.rs

1//! Thread callback information and utilities for azul-layout
2//!
3//! This module provides thread-related callback structures for background tasks
4//! that need to interact with the UI thread and query layout information.
5
6#[cfg(feature = "std")]
7use alloc::sync::Arc;
8#[cfg(feature = "std")]
9use std::sync::{
10    mpsc::{channel, Receiver, Sender},
11    Mutex,
12};
13#[cfg(feature = "std")]
14use std::thread::{self, JoinHandle};
15
16use azul_core::{
17    callbacks::Update,
18    refany::{OptionRefAny, RefAny},
19    task::{
20        CheckThreadFinishedCallback, CheckThreadFinishedCallbackType, LibrarySendThreadMsgCallback,
21        LibrarySendThreadMsgCallbackType, OptionThreadSendMsg, ThreadId, ThreadReceiver,
22        ThreadReceiverDestructorCallback, ThreadReceiverInner, ThreadRecvCallback, ThreadSendMsg,
23    },
24};
25
26use crate::callbacks::CallbackInfo;
27
28macro_rules! impl_callback_traits {
29    ($name:ident) => {
30        impl core::fmt::Debug for $name {
31            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32                write!(f, concat!(stringify!($name), " {{ cb: {:p} }}"), self.cb as *const ())
33            }
34        }
35        // generated for both Copy and non-Copy callback structs; the explicit field
36        // copy works uniformly (a derive can't be emitted for an externally-defined struct).
37        #[allow(clippy::expl_impl_clone_on_copy, clippy::non_canonical_clone_impl)]
38        impl Clone for $name {
39            fn clone(&self) -> Self { Self { cb: self.cb } }
40        }
41        impl PartialEq for $name {
42            fn eq(&self, other: &Self) -> bool {
43                self.cb as *const () as usize == other.cb as *const () as usize
44            }
45        }
46        impl Eq for $name {}
47        impl PartialOrd for $name {
48            fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
49                Some(self.cmp(other))
50            }
51        }
52        impl Ord for $name {
53            fn cmp(&self, other: &Self) -> core::cmp::Ordering {
54                (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
55            }
56        }
57        impl core::hash::Hash for $name {
58            fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
59                (self.cb as *const () as usize).hash(state);
60            }
61        }
62    };
63}
64
65// Types that need to be defined locally (not in azul-core)
66#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
67/// Message that is sent back from the running thread to the main thread
68#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
69#[repr(C, u8)]
70pub enum ThreadReceiveMsg {
71    WriteBack(ThreadWriteBackMsg),
72    Update(Update),
73}
74#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
75#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
76#[repr(C, u8)]
77pub enum OptionThreadReceiveMsg {
78    None,
79    Some(ThreadReceiveMsg),
80}
81
82impl From<Option<ThreadReceiveMsg>> for OptionThreadReceiveMsg {
83    fn from(inner: Option<ThreadReceiveMsg>) -> Self {
84        inner.map_or_else(|| Self::None, Self::Some)
85    }
86}
87
88impl OptionThreadReceiveMsg {
89    #[must_use] pub fn into_option(self) -> Option<ThreadReceiveMsg> {
90        match self {
91            Self::None => None,
92            Self::Some(v) => Some(v),
93        }
94    }
95
96    #[must_use] pub const fn as_ref(&self) -> Option<&ThreadReceiveMsg> {
97        match self {
98            Self::None => None,
99            Self::Some(v) => Some(v),
100        }
101    }
102}
103
104/// Message containing writeback data and callback
105#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
106#[repr(C)]
107pub struct ThreadWriteBackMsg {
108    pub refany: RefAny,
109    pub callback: WriteBackCallback,
110}
111
112impl ThreadWriteBackMsg {
113    pub fn new<C: Into<WriteBackCallback>>(callback: C, data: RefAny) -> Self {
114        Self {
115            refany: data,
116            callback: callback.into(),
117        }
118    }
119}
120
121/// `ThreadSender` allows sending messages from the background thread to the main thread
122#[derive(Debug)]
123#[repr(C)]
124pub struct ThreadSender {
125    #[cfg(feature = "std")]
126    pub ptr: Box<Arc<Mutex<ThreadSenderInner>>>,
127    #[cfg(not(feature = "std"))]
128    pub ptr: *const core::ffi::c_void,
129    pub run_destructor: bool,
130    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
131    pub ctx: OptionRefAny,
132}
133
134impl Clone for ThreadSender {
135    fn clone(&self) -> Self {
136        Self {
137            ptr: self.ptr.clone(),
138            run_destructor: true,
139            ctx: self.ctx.clone(),
140        }
141    }
142}
143
144impl Drop for ThreadSender {
145    fn drop(&mut self) {
146        self.run_destructor = false;
147    }
148}
149
150impl ThreadSender {
151    #[cfg(not(feature = "std"))]
152    pub fn new(_t: ThreadSenderInner) -> Self {
153        Self {
154            ptr: core::ptr::null(),
155            run_destructor: false,
156            ctx: OptionRefAny::None,
157        }
158    }
159
160    #[cfg(feature = "std")]
161    #[must_use] pub fn new(t: ThreadSenderInner) -> Self {
162        Self {
163            ptr: Box::new(Arc::new(Mutex::new(t))),
164            run_destructor: true,
165            ctx: OptionRefAny::None,
166        }
167    }
168
169    /// Get the FFI context (e.g., Python callable)
170    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
171        self.ctx.clone()
172    }
173
174    #[cfg(not(feature = "std"))]
175    pub fn send(&mut self, _msg: ThreadReceiveMsg) -> bool {
176        false
177    }
178
179    #[cfg(feature = "std")]
180    pub fn send(&mut self, msg: ThreadReceiveMsg) -> bool {
181        let Some(ts) = self.ptr.lock().ok() else {
182            return false;
183        };
184        (ts.send_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()).cast::<core::ffi::c_void>(), msg)
185    }
186}
187
188/// Inner state of a `ThreadSender`, holding the channel sender and associated callbacks
189#[derive(Debug)]
190#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
191#[repr(C)]
192pub struct ThreadSenderInner {
193    #[cfg(feature = "std")]
194    pub ptr: Box<Sender<ThreadReceiveMsg>>,
195    #[cfg(not(feature = "std"))]
196    pub ptr: *const core::ffi::c_void,
197    pub send_fn: ThreadSendCallback,
198    pub destructor: ThreadSenderDestructorCallback,
199}
200
201#[cfg(not(feature = "std"))]
202unsafe impl Send for ThreadSenderInner {}
203
204#[cfg(feature = "std")]
205impl core::hash::Hash for ThreadSenderInner {
206    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
207        (std::ptr::from_ref(self.ptr.as_ref()) as usize).hash(state);
208    }
209}
210
211#[cfg(feature = "std")]
212impl PartialEq for ThreadSenderInner {
213    fn eq(&self, other: &Self) -> bool {
214        std::ptr::eq(self.ptr.as_ref(), other.ptr.as_ref())
215    }
216}
217
218#[cfg(feature = "std")]
219impl Eq for ThreadSenderInner {}
220
221#[cfg(feature = "std")]
222impl PartialOrd for ThreadSenderInner {
223    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
224        Some(
225            (std::ptr::from_ref(self.ptr.as_ref()) as usize)
226                .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize)),
227        )
228    }
229}
230
231#[cfg(feature = "std")]
232impl Ord for ThreadSenderInner {
233    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
234        (std::ptr::from_ref(self.ptr.as_ref()) as usize).cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize))
235    }
236}
237
238impl Drop for ThreadSenderInner {
239    fn drop(&mut self) {
240        (self.destructor.cb)(self);
241    }
242}
243
244/// Callback for sending messages from thread to main thread
245pub type ThreadSendCallbackType = extern "C" fn(*const core::ffi::c_void, ThreadReceiveMsg) -> bool;
246
247#[allow(missing_copy_implementations)] // C-ABI fn-ptr wrapper; Clone is macro-generated (impl_callback_traits!), so Copy would trip expl_impl_clone_on_copy
248#[repr(C)]
249pub struct ThreadSendCallback {
250    pub cb: ThreadSendCallbackType,
251}
252
253impl_callback_traits!(ThreadSendCallback);
254
255/// Destructor callback for `ThreadSender`
256pub type ThreadSenderDestructorCallbackType = extern "C" fn(*mut ThreadSenderInner);
257
258#[allow(missing_copy_implementations)] // C-ABI fn-ptr wrapper; Clone is macro-generated (impl_callback_traits!), so Copy would trip expl_impl_clone_on_copy
259#[repr(C)]
260pub struct ThreadSenderDestructorCallback {
261    pub cb: ThreadSenderDestructorCallbackType,
262}
263
264impl_callback_traits!(ThreadSenderDestructorCallback);
265
266/// Callback that runs when a thread receives a `WriteBack` message
267///
268/// This callback runs on the main UI thread and has access to:
269/// - The thread's original data
270/// - Data sent back from the background thread
271/// - Full `CallbackInfo` for DOM queries and UI updates
272pub type WriteBackCallbackType = extern "C" fn(
273    /* original thread data */ RefAny,
274    /* data to write back */ RefAny,
275    /* callback info */ CallbackInfo,
276) -> Update;
277
278/// Callback that can run when a thread receives a `WriteBack` message
279#[repr(C)]
280pub struct WriteBackCallback {
281    pub cb: WriteBackCallbackType,
282    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
283    /// Native Rust code sets this to None
284    pub ctx: OptionRefAny,
285}
286
287impl WriteBackCallback {
288    /// Create a new `WriteBackCallback`
289    pub fn new(cb: WriteBackCallbackType) -> Self {
290        Self {
291            cb,
292            ctx: OptionRefAny::None,
293        }
294    }
295
296    /// Invoke the callback
297    #[must_use] pub fn invoke(
298        &self,
299        thread_data: RefAny,
300        writeback_data: RefAny,
301        callback_info: CallbackInfo,
302    ) -> Update {
303        (self.cb)(thread_data, writeback_data, callback_info)
304    }
305}
306
307impl core::fmt::Debug for WriteBackCallback {
308    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
309        write!(f, "WriteBackCallback {{ cb: {:p} }}", self.cb as *const ())
310    }
311}
312
313impl Clone for WriteBackCallback {
314    fn clone(&self) -> Self {
315        Self {
316            cb: self.cb,
317            ctx: self.ctx.clone(),
318        }
319    }
320}
321
322impl From<WriteBackCallbackType> for WriteBackCallback {
323    fn from(cb: WriteBackCallbackType) -> Self {
324        Self {
325            cb,
326            ctx: OptionRefAny::None,
327        }
328    }
329}
330
331impl PartialEq for WriteBackCallback {
332    fn eq(&self, other: &Self) -> bool {
333        std::ptr::eq(self.cb as *const (), other.cb as *const ())
334    }
335}
336
337impl Eq for WriteBackCallback {}
338
339impl PartialOrd for WriteBackCallback {
340    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
341        Some(self.cmp(other))
342    }
343}
344
345impl Ord for WriteBackCallback {
346    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
347        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
348    }
349}
350
351impl core::hash::Hash for WriteBackCallback {
352    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
353        (self.cb as *const () as usize).hash(state);
354    }
355}
356
357/// Callback type for the function that runs in the background thread
358pub type ThreadCallbackType = extern "C" fn(RefAny, ThreadSender, ThreadReceiver);
359
360#[repr(C)]
361pub struct ThreadCallback {
362    pub cb: ThreadCallbackType,
363    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
364    /// Native Rust code sets this to None
365    pub ctx: OptionRefAny,
366}
367
368impl ThreadCallback {
369    /// Create a new `ThreadCallback`
370    pub fn new(cb: ThreadCallbackType) -> Self {
371        Self {
372            cb,
373            ctx: OptionRefAny::None,
374        }
375    }
376}
377
378impl core::fmt::Debug for ThreadCallback {
379    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
380        write!(f, "ThreadCallback {{ cb: {:p} }}", self.cb as *const ())
381    }
382}
383
384impl Clone for ThreadCallback {
385    fn clone(&self) -> Self {
386        Self {
387            cb: self.cb,
388            ctx: self.ctx.clone(),
389        }
390    }
391}
392
393impl From<ThreadCallbackType> for ThreadCallback {
394    fn from(cb: ThreadCallbackType) -> Self {
395        Self {
396            cb,
397            ctx: OptionRefAny::None,
398        }
399    }
400}
401
402impl PartialEq for ThreadCallback {
403    fn eq(&self, other: &Self) -> bool {
404        std::ptr::eq(self.cb as *const (), other.cb as *const ())
405    }
406}
407
408impl Eq for ThreadCallback {}
409
410impl PartialOrd for ThreadCallback {
411    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
412        Some(self.cmp(other))
413    }
414}
415
416impl Ord for ThreadCallback {
417    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
418        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
419    }
420}
421
422impl core::hash::Hash for ThreadCallback {
423    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
424        (self.cb as *const () as usize).hash(state);
425    }
426}
427
428// Host-invoker plumbing for ThreadCallback. NOTE: this callback fires
429// on a worker thread (spawned by `Thread::create`), not the main
430// `App.run` thread. The per-language host-invoker thunk MUST acquire
431// the host VM lock before dispatching to user code:
432//   * CPython: PyGILState_Ensure / _Release
433//   * MRI Ruby: rb_thread_call_with_gvl
434//   * OpenJDK: AttachCurrentThread / DetachCurrentThread
435//   * CLR / .NET: nothing ([UnmanagedCallersOnly] auto-trampolines)
436//   * OCaml: caml_acquire_runtime_system / _release
437//   * Lua / Perl / PHP / Pharo: cannot be called from worker thread
438//     (single-threaded interpreter) — fall back to writeback-only
439//     pattern (Rust extern "C" cb on worker, host fn on main via
440//     WriteBackCallback).
441// See `scripts/BINDING_STRATEGY_PER_LANGUAGE.md` for the lock-acquire
442// table per VM.
443azul_core::impl_managed_callback! {
444    wrapper:        ThreadCallback,
445    info_ty:        ThreadSender,
446    return_ty:      (),
447    // unit default-return; written via Default::default() so clippy's unused_unit
448    // doesn't fire on a bare `()` in this macro-argument position.
449    default_ret:    Default::default(),
450    invoker_static: THREAD_CALLBACK_INVOKER,
451    invoker_ty:     AzThreadCallbackInvoker,
452    thunk_fn:       az_thread_callback_thunk,
453    setter_fn:      AzApp_setThreadCallbackInvoker,
454    from_handle_fn: AzThreadCallback_createFromHostHandle,
455    extra_args:     [receiver: ThreadReceiver],
456}
457
458/// Callback type for receiving messages from a background thread
459pub type LibraryReceiveThreadMsgCallbackType =
460    extern "C" fn(*const core::ffi::c_void) -> OptionThreadReceiveMsg;
461
462#[allow(missing_copy_implementations)] // C-ABI fn-ptr wrapper; Clone is macro-generated (impl_callback_traits!), so Copy would trip expl_impl_clone_on_copy
463#[repr(C)]
464pub struct LibraryReceiveThreadMsgCallback {
465    pub cb: LibraryReceiveThreadMsgCallbackType,
466}
467
468impl_callback_traits!(LibraryReceiveThreadMsgCallback);
469
470/// Callback type for the destructor that cleans up a `ThreadInner`
471pub type ThreadDestructorCallbackType = extern "C" fn(*mut ThreadInner);
472
473#[allow(missing_copy_implementations)] // C-ABI fn-ptr wrapper; Clone is macro-generated (impl_callback_traits!), so Copy would trip expl_impl_clone_on_copy
474#[repr(C)]
475pub struct ThreadDestructorCallback {
476    pub cb: ThreadDestructorCallbackType,
477}
478
479impl_callback_traits!(ThreadDestructorCallback);
480
481/// Wrapper around Thread because Thread needs to be clone-able
482#[derive(Debug)]
483#[repr(C)]
484pub struct Thread {
485    #[cfg(feature = "std")]
486    pub ptr: Box<Arc<Mutex<ThreadInner>>>,
487    #[cfg(not(feature = "std"))]
488    pub ptr: *const core::ffi::c_void,
489    pub run_destructor: bool,
490}
491
492impl Clone for Thread {
493    fn clone(&self) -> Self {
494        Self {
495            ptr: self.ptr.clone(),
496            run_destructor: true,
497        }
498    }
499}
500
501impl Drop for Thread {
502    fn drop(&mut self) {
503        self.run_destructor = false;
504    }
505}
506
507impl Thread {
508    #[cfg(feature = "std")]
509    #[must_use] pub fn new(ti: ThreadInner) -> Self {
510        Self {
511            ptr: Box::new(Arc::new(Mutex::new(ti))),
512            run_destructor: true,
513        }
514    }
515
516    #[cfg(not(feature = "std"))]
517    pub fn new(_ti: ThreadInner) -> Self {
518        Self {
519            ptr: core::ptr::null(),
520            run_destructor: false,
521        }
522    }
523
524    /// Creates a new thread that will execute the given callback function.
525    ///
526    /// # Arguments
527    /// * `thread_initialize_data` - Data passed to the callback when the thread starts
528    /// * `writeback_data` - Data that will be passed back when writeback messages are received
529    /// * `callback` - The callback to execute in the background thread
530    ///
531    /// # Returns
532    /// A new Thread handle that can be added to the event loop with `CallbackInfo::add_thread`
533    pub fn create<C: Into<ThreadCallback>>(
534        thread_initialize_data: RefAny,
535        writeback_data: RefAny,
536        callback: C,
537    ) -> Self {
538        create_thread_libstd(thread_initialize_data, writeback_data, callback.into())
539    }
540
541    /// Send a control message to the running worker. Interior-mutable (the worker
542    /// state is behind a `Mutex`), so this takes `&self` and is callable from a
543    /// callback that only holds `&Thread` via `CallbackInfo::get_thread`. Used to push
544    /// resize / seek / source-change messages to a persistent worker. Returns false
545    /// if the channel is closed (or always, on `no_std`).
546    #[cfg(feature = "std")]
547    #[must_use] pub fn send_message(&self, msg: ThreadSendMsg) -> bool {
548        self.ptr.lock().is_ok_and(|inner| inner.sender.send(msg).is_ok())
549    }
550    #[cfg(not(feature = "std"))]
551    pub fn send_message(&self, _msg: ThreadSendMsg) -> bool {
552        false
553    }
554
555    /// Clone the main→worker `Sender` so a holder without a `CallbackInfo` (e.g. a
556    /// dataset-merge callback) can message the running worker later — used for the
557    /// scrub/seek path, where the merge callback compares the old/new `VideoConfig`
558    /// and pushes a seek to the worker. `None` on `no_std`.
559    #[cfg(feature = "std")]
560    #[must_use] pub fn clone_sender(&self) -> Option<Sender<ThreadSendMsg>> {
561        self.ptr.lock().ok().map(|inner| (*inner.sender).clone())
562    }
563    #[cfg(not(feature = "std"))]
564    pub fn clone_sender(&self) -> Option<Sender<ThreadSendMsg>> {
565        None
566    }
567}
568
569/// A `Thread` is a separate thread that is owned by the framework.
570///
571/// In difference to a regular thread, you don't have to `await()` the result,
572/// you can just hand the Thread to the framework and it will automatically
573/// update the UI when the Thread is finished.
574#[derive(Debug)]
575#[repr(C)]
576pub struct ThreadInner {
577    #[cfg(feature = "std")]
578    pub thread_handle: Box<Option<JoinHandle<()>>>,
579    #[cfg(not(feature = "std"))]
580    pub thread_handle: *const core::ffi::c_void,
581
582    #[cfg(feature = "std")]
583    pub sender: Box<Sender<ThreadSendMsg>>,
584    #[cfg(not(feature = "std"))]
585    pub sender: *const core::ffi::c_void,
586
587    #[cfg(feature = "std")]
588    pub receiver: Box<Receiver<ThreadReceiveMsg>>,
589    #[cfg(not(feature = "std"))]
590    pub receiver: *const core::ffi::c_void,
591
592    #[cfg(feature = "std")]
593    pub dropcheck: Box<alloc::sync::Weak<()>>,
594    #[cfg(not(feature = "std"))]
595    pub dropcheck: *const core::ffi::c_void,
596
597    pub writeback_data: RefAny,
598    pub check_thread_finished_fn: CheckThreadFinishedCallback,
599    pub send_thread_msg_fn: LibrarySendThreadMsgCallback,
600    pub receive_thread_msg_fn: LibraryReceiveThreadMsgCallback,
601    pub thread_destructor_fn: ThreadDestructorCallback,
602}
603
604#[cfg(feature = "std")]
605impl ThreadInner {
606    /// Returns true if the Thread has been finished, false otherwise
607    #[must_use] pub fn is_finished(&self) -> bool {
608        (self.check_thread_finished_fn.cb)(
609            std::ptr::from_ref(self.dropcheck.as_ref()).cast::<core::ffi::c_void>()
610        )
611    }
612
613    /// Send a message to the thread
614    pub fn sender_send(&mut self, msg: ThreadSendMsg) -> bool {
615        (self.send_thread_msg_fn.cb)(
616            std::ptr::from_ref(self.sender.as_ref()).cast::<core::ffi::c_void>(),
617            msg,
618        )
619    }
620
621    /// Try to receive a message from the thread (non-blocking)
622    pub fn receiver_try_recv(&mut self) -> OptionThreadReceiveMsg {
623        (self.receive_thread_msg_fn.cb)(
624            std::ptr::from_ref(self.receiver.as_ref()).cast::<core::ffi::c_void>()
625        )
626    }
627}
628
629#[cfg(not(feature = "std"))]
630impl ThreadInner {
631    /// Returns true if the Thread has been finished, false otherwise
632    pub fn is_finished(&self) -> bool {
633        true
634    }
635
636    /// Send a message to the thread (no-op in no_std)
637    pub fn sender_send(&mut self, _msg: ThreadSendMsg) -> bool {
638        false
639    }
640
641    /// Try to receive a message from the thread (always returns None in no_std)
642    pub fn receiver_try_recv(&mut self) -> OptionThreadReceiveMsg {
643        None.into()
644    }
645}
646
647impl Drop for ThreadInner {
648    fn drop(&mut self) {
649        (self.thread_destructor_fn.cb)(self);
650    }
651}
652
653// Default callback implementations for std
654#[cfg(feature = "std")]
655extern "C" fn default_thread_destructor_fn(thread: *mut ThreadInner) {
656    let thread = unsafe { &mut *thread };
657
658    if let Some(thread_handle) = thread.thread_handle.take() {
659        drop(thread.sender.send(ThreadSendMsg::TerminateThread));
660        drop(thread_handle.join()); // ignore the result, don't panic
661    }
662}
663
664#[cfg(not(feature = "std"))]
665extern "C" fn default_thread_destructor_fn(_thread: *mut ThreadInner) {}
666
667#[cfg(feature = "std")]
668extern "C" fn library_send_thread_msg_fn(
669    sender: *const core::ffi::c_void,
670    msg: ThreadSendMsg,
671) -> bool {
672    unsafe { &*sender.cast::<Sender<ThreadSendMsg>>() }
673        .send(msg)
674        .is_ok()
675}
676
677#[cfg(not(feature = "std"))]
678extern "C" fn library_send_thread_msg_fn(
679    _sender: *const core::ffi::c_void,
680    _msg: ThreadSendMsg,
681) -> bool {
682    false
683}
684
685#[cfg(feature = "std")]
686extern "C" fn library_receive_thread_msg_fn(
687    receiver: *const core::ffi::c_void,
688) -> OptionThreadReceiveMsg {
689    unsafe { &*receiver.cast::<Receiver<ThreadReceiveMsg>>() }
690        .try_recv()
691        .ok()
692        .into()
693}
694
695#[cfg(not(feature = "std"))]
696extern "C" fn library_receive_thread_msg_fn(
697    _receiver: *const core::ffi::c_void,
698) -> OptionThreadReceiveMsg {
699    None.into()
700}
701
702#[cfg(feature = "std")]
703extern "C" fn default_send_thread_msg_fn(
704    sender: *const core::ffi::c_void,
705    msg: ThreadReceiveMsg,
706) -> bool {
707    unsafe { &*sender.cast::<Sender<ThreadReceiveMsg>>() }
708        .send(msg)
709        .is_ok()
710}
711
712#[cfg(not(feature = "std"))]
713extern "C" fn default_send_thread_msg_fn(
714    _sender: *const core::ffi::c_void,
715    _msg: ThreadReceiveMsg,
716) -> bool {
717    false
718}
719
720#[cfg(feature = "std")]
721extern "C" fn default_receive_thread_msg_fn(
722    receiver: *const core::ffi::c_void,
723) -> OptionThreadSendMsg {
724    unsafe { &*receiver.cast::<Receiver<ThreadSendMsg>>() }
725        .try_recv()
726        .ok()
727        .into()
728}
729
730#[cfg(not(feature = "std"))]
731extern "C" fn default_receive_thread_msg_fn(
732    _receiver: *const core::ffi::c_void,
733) -> OptionThreadSendMsg {
734    None.into()
735}
736
737#[cfg(feature = "std")]
738extern "C" fn default_check_thread_finished(dropcheck: *const core::ffi::c_void) -> bool {
739    let weak = unsafe { &*dropcheck.cast::<alloc::sync::Weak<()>>() };
740    weak.upgrade().is_none()
741}
742
743#[cfg(not(feature = "std"))]
744extern "C" fn default_check_thread_finished(_dropcheck: *const core::ffi::c_void) -> bool {
745    true
746}
747
748#[cfg(feature = "std")]
749const extern "C" fn thread_sender_drop(_: *mut ThreadSenderInner) {}
750
751#[cfg(not(feature = "std"))]
752extern "C" fn thread_sender_drop(_: *mut ThreadSenderInner) {}
753
754#[cfg(feature = "std")]
755const extern "C" fn thread_receiver_drop(_: *mut ThreadReceiverInner) {}
756
757#[cfg(not(feature = "std"))]
758extern "C" fn thread_receiver_drop(_: *mut ThreadReceiverInner) {}
759
760/// Function that creates a new Thread object
761pub type CreateThreadCallbackType = extern "C" fn(RefAny, RefAny, ThreadCallback) -> Thread;
762
763#[repr(C)]
764pub struct CreateThreadCallback {
765    pub cb: CreateThreadCallbackType,
766}
767
768impl_callback_traits!(CreateThreadCallback);
769impl Copy for CreateThreadCallback {}
770
771/// Create a new thread using the standard library
772#[cfg(feature = "std")]
773#[must_use] pub extern "C" fn create_thread_libstd(
774    thread_initialize_data: RefAny,
775    writeback_data: RefAny,
776    callback: ThreadCallback,
777) -> Thread {
778    let (sender_receiver, receiver_receiver) = channel::<ThreadReceiveMsg>();
779    let mut sender_receiver = ThreadSender::new(ThreadSenderInner {
780        ptr: Box::new(sender_receiver),
781        send_fn: ThreadSendCallback {
782            cb: default_send_thread_msg_fn,
783        },
784        destructor: ThreadSenderDestructorCallback {
785            cb: thread_sender_drop,
786        },
787    });
788    // Set the ctx from the callback for FFI
789    sender_receiver.ctx = callback.ctx.clone();
790
791    let (sender_sender, receiver_sender) = channel::<ThreadSendMsg>();
792    let mut receiver_sender = ThreadReceiver::new(ThreadReceiverInner {
793        ptr: Box::new(receiver_sender),
794        recv_fn: ThreadRecvCallback {
795            cb: default_receive_thread_msg_fn,
796        },
797        destructor: ThreadReceiverDestructorCallback {
798            cb: thread_receiver_drop,
799        },
800    });
801    // Set the ctx from the callback for FFI
802    receiver_sender.ctx = callback.ctx.clone();
803
804    let thread_check = Arc::new(());
805    let dropcheck = Arc::downgrade(&thread_check);
806
807    let thread_handle = Some(thread::spawn(move || {
808        // Keep thread_check alive for the entire duration of the thread
809        // by binding it to a named variable (not `_` which drops immediately)
810        let _thread_check_guard = thread_check;
811        (callback.cb)(thread_initialize_data, sender_receiver, receiver_sender);
812        // _thread_check_guard gets dropped here, signals that the thread has finished
813    }));
814
815    let thread_handle: Box<Option<JoinHandle<()>>> =
816        Box::new(thread_handle);
817    let sender: Box<Sender<ThreadSendMsg>> = Box::new(sender_sender);
818    let receiver: Box<Receiver<ThreadReceiveMsg>> =
819        Box::new(receiver_receiver);
820    let dropcheck: Box<alloc::sync::Weak<()>> = Box::new(dropcheck);
821
822    Thread::new(ThreadInner {
823        thread_handle,
824        sender,
825        receiver,
826        writeback_data,
827        dropcheck,
828        thread_destructor_fn: ThreadDestructorCallback {
829            cb: default_thread_destructor_fn,
830        },
831        check_thread_finished_fn: CheckThreadFinishedCallback {
832            cb: default_check_thread_finished,
833        },
834        send_thread_msg_fn: LibrarySendThreadMsgCallback {
835            cb: library_send_thread_msg_fn,
836        },
837        receive_thread_msg_fn: LibraryReceiveThreadMsgCallback {
838            cb: library_receive_thread_msg_fn,
839        },
840    })
841}
842
843#[cfg(not(feature = "std"))]
844pub extern "C" fn create_thread_libstd(
845    _thread_initialize_data: RefAny,
846    _writeback_data: RefAny,
847    _callback: ThreadCallback,
848) -> Thread {
849    Thread {
850        ptr: core::ptr::null(),
851        run_destructor: false,
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    extern "C" fn test_writeback_callback(
860        _thread_data: RefAny,
861        _writeback_data: RefAny,
862        _callback_info: CallbackInfo,
863    ) -> Update {
864        Update::DoNothing
865    }
866
867    #[test]
868    fn test_writeback_callback_creation() {
869        let callback = WriteBackCallback::new(test_writeback_callback);
870        assert_eq!(callback.cb as *const () as usize, test_writeback_callback as *const () as usize);
871    }
872
873    #[test]
874    fn test_writeback_callback_clone() {
875        let callback = WriteBackCallback::new(test_writeback_callback);
876        let cloned = callback.clone();
877        assert_eq!(callback, cloned);
878    }
879}
880#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
881/// Optional Thread type for API compatibility
882#[derive(Debug, Clone)]
883#[repr(C, u8)]
884pub enum OptionThread {
885    None,
886    Some(Thread),
887}
888
889impl From<Option<Thread>> for OptionThread {
890    fn from(o: Option<Thread>) -> Self {
891        o.map_or_else(|| Self::None, Self::Some)
892    }
893}
894
895impl OptionThread {
896    #[must_use] pub fn into_option(self) -> Option<Thread> {
897        match self {
898            Self::None => None,
899            Self::Some(t) => Some(t),
900        }
901    }
902}
903
904// ============================================================================
905// Sleep utilities
906// ============================================================================
907
908/// Sleeps the current thread for the specified number of milliseconds.
909///
910/// This is a cross-platform utility that can be called from C/C++/Python.
911///
912/// # Arguments
913/// * `milliseconds` - Number of milliseconds to sleep
914#[cfg(feature = "std")]
915#[must_use] pub fn thread_sleep_ms(milliseconds: u64) -> azul_css::corety::EmptyStruct {
916    thread::sleep(std::time::Duration::from_millis(milliseconds));
917    azul_css::corety::EmptyStruct::new()
918}
919
920/// Sleeps the current thread for the specified number of milliseconds (no-op on no_std).
921#[cfg(not(feature = "std"))]
922pub fn thread_sleep_ms(_milliseconds: u64) -> azul_css::corety::EmptyStruct {
923    // No-op on no_std - can't sleep without OS
924    azul_css::corety::EmptyStruct::new()
925}
926
927/// Sleeps the current thread for the specified number of microseconds.
928///
929/// # Arguments
930/// * `microseconds` - Number of microseconds to sleep
931#[cfg(feature = "std")]
932#[must_use] pub fn thread_sleep_us(microseconds: u64) -> azul_css::corety::EmptyStruct {
933    thread::sleep(std::time::Duration::from_micros(microseconds));
934    azul_css::corety::EmptyStruct::new()
935}
936
937/// Sleeps the current thread for the specified number of microseconds (no-op on no_std).
938#[cfg(not(feature = "std"))]
939pub fn thread_sleep_us(_microseconds: u64) -> azul_css::corety::EmptyStruct {
940    // No-op on no_std - can't sleep without OS
941    azul_css::corety::EmptyStruct::new()
942}
943
944/// Sleeps the current thread for the specified number of nanoseconds.
945///
946/// # Arguments
947/// * `nanoseconds` - Number of nanoseconds to sleep
948#[cfg(feature = "std")]
949#[must_use] pub fn thread_sleep_ns(nanoseconds: u64) -> azul_css::corety::EmptyStruct {
950    thread::sleep(std::time::Duration::from_nanos(nanoseconds));
951    azul_css::corety::EmptyStruct::new()
952}
953
954/// Sleeps the current thread for the specified number of nanoseconds (no-op on no_std).
955#[cfg(not(feature = "std"))]
956pub fn thread_sleep_ns(_nanoseconds: u64) -> azul_css::corety::EmptyStruct {
957    // No-op on no_std - can't sleep without OS
958    azul_css::corety::EmptyStruct::new()
959}
960
961// ============================================================================
962// Generated adversarial tests
963// ============================================================================
964
965#[cfg(all(test, feature = "std"))]
966#[allow(clippy::too_many_lines, clippy::unreadable_literal)]
967mod autotest_generated {
968    use core::{
969        hash::{Hash, Hasher},
970        sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrd},
971    };
972    use std::{
973        collections::{hash_map::DefaultHasher, BTreeMap},
974        sync::{Arc, Mutex},
975        time::Instant as StdInstant,
976    };
977
978    use azul_core::{
979        dom::{DomId, DomNodeId},
980        geom::OptionLogicalPosition,
981        gl::OptionGlContextPtr,
982        hit_test::ScrollPosition,
983        resources::RendererResources,
984        styled_dom::NodeHierarchyItemId,
985        window::{MonitorVec, RawWindowHandle},
986    };
987    use azul_css::{corety::EmptyStruct, system::SystemStyle};
988    use rust_fontconfig::FcFontCache;
989
990    use super::*;
991    #[cfg(feature = "icu")]
992    use crate::icu::IcuLocalizerHandle;
993    use crate::{
994        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
995        window::LayoutWindow,
996        window_state::FullWindowState,
997    };
998
999    // ------------------------------------------------------------------
1000    // Harness
1001    // ------------------------------------------------------------------
1002
1003    /// Upper bound on a worker's non-blocking poll loop: `ThreadReceiver::recv` is
1004    /// `try_recv` under the hood, so a worker that waits for `TerminateThread` MUST
1005    /// be bounded or a lost message would hang the whole test binary.
1006    const MAX_WORKER_POLLS: usize = 10_000;
1007
1008    fn hash_of<T: Hash>(value: &T) -> u64 {
1009        let mut hasher = DefaultHasher::new();
1010        value.hash(&mut hasher);
1011        hasher.finish()
1012    }
1013
1014    /// A live `ThreadSender` plus the receiving end of its channel.
1015    fn make_sender() -> (Receiver<ThreadReceiveMsg>, ThreadSender) {
1016        let (tx, rx) = channel::<ThreadReceiveMsg>();
1017        let sender = ThreadSender::new(ThreadSenderInner {
1018            ptr: Box::new(tx),
1019            send_fn: ThreadSendCallback {
1020                cb: default_send_thread_msg_fn,
1021            },
1022            destructor: ThreadSenderDestructorCallback {
1023                cb: thread_sender_drop,
1024            },
1025        });
1026        (rx, sender)
1027    }
1028
1029    /// Drain every message currently queued on the main->worker side.
1030    fn drain(inner: &mut ThreadInner) -> Vec<ThreadReceiveMsg> {
1031        let mut out = Vec::new();
1032        while let OptionThreadReceiveMsg::Some(msg) = inner.receiver_try_recv() {
1033            out.push(msg);
1034        }
1035        out
1036    }
1037
1038    /// Runs the thread destructor by hand (terminate + join), so every assertion
1039    /// after it observes a *finished* worker instead of racing one.
1040    fn join_worker(t: &Thread) {
1041        let mut guard = t.ptr.lock().expect("thread mutex must not be poisoned");
1042        default_thread_destructor_fn(core::ptr::from_mut::<ThreadInner>(&mut guard));
1043    }
1044
1045    /// Builds a real `CallbackInfo` (the only way to exercise `WriteBackCallback::invoke`)
1046    /// over an otherwise-empty `LayoutWindow`.
1047    fn with_callback_info<R>(f: impl FnOnce(CallbackInfo) -> R) -> R {
1048        let layout_window =
1049            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
1050        let renderer_resources = RendererResources::default();
1051        let previous_window_state: Option<FullWindowState> = None;
1052        let current_window_state = FullWindowState::default();
1053        let gl_context = OptionGlContextPtr::None;
1054        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
1055            BTreeMap::new();
1056        let window_handle = RawWindowHandle::Unsupported;
1057        let system_callbacks = ExternalSystemCallbacks::rust_internal();
1058
1059        let ref_data = CallbackInfoRefData {
1060            layout_window: &layout_window,
1061            renderer_resources: &renderer_resources,
1062            previous_window_state: &previous_window_state,
1063            current_window_state: &current_window_state,
1064            gl_context: &gl_context,
1065            current_scroll_manager: &scroll_states,
1066            current_window_handle: &window_handle,
1067            system_callbacks: &system_callbacks,
1068            system_style: Arc::new(SystemStyle::default()),
1069            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
1070            #[cfg(feature = "icu")]
1071            icu_localizer: IcuLocalizerHandle::default(),
1072            ctx: OptionRefAny::None,
1073        };
1074        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
1075
1076        let info = CallbackInfo::new(
1077            &ref_data,
1078            &changes,
1079            DomNodeId {
1080                dom: DomId::ROOT_ID,
1081                node: NodeHierarchyItemId::NONE,
1082            },
1083            OptionLogicalPosition::None,
1084            OptionLogicalPosition::None,
1085        );
1086        f(info)
1087    }
1088
1089    // ------------------------------------------------------------------
1090    // Callback fixtures
1091    // ------------------------------------------------------------------
1092
1093    static WB_THREAD_DATA: AtomicUsize = AtomicUsize::new(0);
1094    static WB_WRITEBACK_DATA: AtomicUsize = AtomicUsize::new(0);
1095
1096    extern "C" fn wb_record(
1097        mut thread_data: RefAny,
1098        mut writeback_data: RefAny,
1099        _callback_info: CallbackInfo,
1100    ) -> Update {
1101        if let Some(v) = thread_data.downcast_ref::<usize>() {
1102            WB_THREAD_DATA.store(*v, AtomicOrd::SeqCst);
1103        }
1104        if let Some(v) = writeback_data.downcast_ref::<usize>() {
1105            WB_WRITEBACK_DATA.store(*v, AtomicOrd::SeqCst);
1106        }
1107        Update::RefreshDomAllWindows
1108    }
1109
1110    extern "C" fn wb_do_nothing(
1111        _thread_data: RefAny,
1112        _writeback_data: RefAny,
1113        _callback_info: CallbackInfo,
1114    ) -> Update {
1115        Update::DoNothing
1116    }
1117
1118    /// A worker that exits immediately without ever touching its channels.
1119    extern "C" fn worker_quiet(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
1120
1121    /// A worker that pushes exactly one `Update` back to the main thread, then exits.
1122    extern "C" fn worker_send_update(_d: RefAny, mut sender: ThreadSender, _r: ThreadReceiver) {
1123        let _sent = sender.send(ThreadReceiveMsg::Update(Update::RefreshDom));
1124    }
1125
1126    /// A worker that pushes one `WriteBack` message (the RefAny-carrying variant).
1127    extern "C" fn worker_send_writeback(_d: RefAny, mut sender: ThreadSender, _r: ThreadReceiver) {
1128        let _sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
1129            wb_do_nothing as WriteBackCallbackType,
1130            RefAny::new(77_usize),
1131        )));
1132    }
1133
1134    /// Generates a worker that echoes `Tick`s back until it is told to terminate.
1135    /// Each instance gets its own statics so tests can run in parallel without racing.
1136    macro_rules! terminating_worker {
1137        ($fn_name:ident, $ticks:ident, $terminated:ident) => {
1138            static $ticks: AtomicUsize = AtomicUsize::new(0);
1139            static $terminated: AtomicBool = AtomicBool::new(false);
1140
1141            extern "C" fn $fn_name(
1142                _d: RefAny,
1143                mut sender: ThreadSender,
1144                mut receiver: ThreadReceiver,
1145            ) {
1146                for _ in 0..MAX_WORKER_POLLS {
1147                    match receiver.recv() {
1148                        OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread) => {
1149                            $terminated.store(true, AtomicOrd::SeqCst);
1150                            break;
1151                        }
1152                        OptionThreadSendMsg::Some(ThreadSendMsg::Tick) => {
1153                            $ticks.fetch_add(1, AtomicOrd::SeqCst);
1154                            let _sent = sender.send(ThreadReceiveMsg::Update(Update::RefreshDom));
1155                        }
1156                        OptionThreadSendMsg::Some(ThreadSendMsg::Custom(_)) => {
1157                            $ticks.fetch_add(1, AtomicOrd::SeqCst);
1158                        }
1159                        OptionThreadSendMsg::None => {
1160                            let _slept = thread_sleep_ms(1);
1161                        }
1162                    }
1163                }
1164            }
1165        };
1166    }
1167
1168    terminating_worker!(worker_wait_a, WAIT_A_TICKS, WAIT_A_TERMINATED);
1169    terminating_worker!(worker_wait_b, WAIT_B_TICKS, WAIT_B_TERMINATED);
1170    terminating_worker!(worker_wait_c, WAIT_C_TICKS, WAIT_C_TERMINATED);
1171    terminating_worker!(worker_wait_d, WAIT_D_TICKS, WAIT_D_TERMINATED);
1172    terminating_worker!(worker_wait_e, WAIT_E_TICKS, WAIT_E_TERMINATED);
1173
1174    // ==================================================================
1175    // OptionThreadReceiveMsg — getters / predicates
1176    // ==================================================================
1177
1178    #[test]
1179    fn option_thread_receive_msg_none_into_option_is_none() {
1180        assert!(OptionThreadReceiveMsg::None.into_option().is_none());
1181        assert!(OptionThreadReceiveMsg::None.as_ref().is_none());
1182    }
1183
1184    #[test]
1185    fn option_thread_receive_msg_round_trips_through_from_and_into_option() {
1186        // Round-trip: Option -> OptionThreadReceiveMsg -> Option must be the identity.
1187        for update in [
1188            Update::DoNothing,
1189            Update::RefreshDom,
1190            Update::RefreshDomAllWindows,
1191        ] {
1192            let msg = ThreadReceiveMsg::Update(update);
1193            let ffi: OptionThreadReceiveMsg = Some(msg.clone()).into();
1194            assert_eq!(ffi.into_option(), Some(msg));
1195        }
1196        let empty: OptionThreadReceiveMsg = None.into();
1197        assert_eq!(empty, OptionThreadReceiveMsg::None);
1198        assert!(empty.into_option().is_none());
1199    }
1200
1201    #[test]
1202    fn option_thread_receive_msg_as_ref_does_not_consume() {
1203        let opt = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::RefreshDom));
1204        // as_ref() borrows: calling it repeatedly must keep returning the same payload.
1205        for _ in 0..3 {
1206            assert_eq!(
1207                opt.as_ref(),
1208                Some(&ThreadReceiveMsg::Update(Update::RefreshDom))
1209            );
1210        }
1211        // ... and the value is still intact afterwards.
1212        assert_eq!(
1213            opt.into_option(),
1214            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
1215        );
1216    }
1217
1218    #[test]
1219    fn option_thread_receive_msg_as_ref_handles_writeback_variant() {
1220        let opt = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::WriteBack(
1221            ThreadWriteBackMsg::new(
1222                wb_do_nothing as WriteBackCallbackType,
1223                RefAny::new(1_usize),
1224            ),
1225        ));
1226        let Some(ThreadReceiveMsg::WriteBack(inner)) = opt.as_ref() else {
1227            panic!("as_ref() must expose the WriteBack payload");
1228        };
1229        assert_eq!(
1230            inner.callback.cb as *const () as usize,
1231            wb_do_nothing as *const () as usize
1232        );
1233        assert!(opt.into_option().is_some());
1234    }
1235
1236    #[test]
1237    fn option_thread_receive_msg_ord_and_hash_are_consistent() {
1238        let none = OptionThreadReceiveMsg::None;
1239        let some = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::DoNothing));
1240        // Declaration order: None < Some.
1241        assert!(none < some);
1242        assert_eq!(none.cmp(&none), core::cmp::Ordering::Equal);
1243        // Eq => equal hashes.
1244        assert_eq!(hash_of(&none), hash_of(&OptionThreadReceiveMsg::None));
1245        assert_eq!(
1246            hash_of(&some),
1247            hash_of(&OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(
1248                Update::DoNothing
1249            )))
1250        );
1251    }
1252
1253    #[test]
1254    fn thread_receive_msg_orders_writeback_before_update() {
1255        let wb = ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
1256            wb_do_nothing as WriteBackCallbackType,
1257            RefAny::new(0_usize),
1258        ));
1259        let up = ThreadReceiveMsg::Update(Update::DoNothing);
1260        assert!(wb < up, "variant order (WriteBack=0, Update=1) must decide");
1261        assert!(
1262            ThreadReceiveMsg::Update(Update::DoNothing)
1263                < ThreadReceiveMsg::Update(Update::RefreshDom)
1264        );
1265    }
1266
1267    // ==================================================================
1268    // ThreadWriteBackMsg — constructor invariants
1269    // ==================================================================
1270
1271    #[test]
1272    fn thread_write_back_msg_new_stores_both_fields() {
1273        let mut msg = ThreadWriteBackMsg::new(
1274            wb_do_nothing as WriteBackCallbackType,
1275            RefAny::new(0xDEAD_BEEF_usize),
1276        );
1277        assert_eq!(
1278            msg.callback.cb as *const () as usize,
1279            wb_do_nothing as *const () as usize
1280        );
1281        assert_eq!(
1282            msg.refany
1283                .downcast_ref::<usize>()
1284                .map(|v| *v)
1285                .expect("payload type must survive construction"),
1286            0xDEAD_BEEF_usize
1287        );
1288        // A fn-ptr-built callback carries no FFI ctx.
1289        assert_eq!(msg.callback.ctx, OptionRefAny::None);
1290    }
1291
1292    #[test]
1293    fn thread_write_back_msg_new_accepts_both_into_impls() {
1294        // `C: Into<WriteBackCallback>` must accept a bare fn pointer *and* an
1295        // already-built WriteBackCallback; both must land on the same cb.
1296        let from_fn = ThreadWriteBackMsg::new(
1297            wb_record as WriteBackCallbackType,
1298            RefAny::new(1_usize),
1299        );
1300        let from_struct =
1301            ThreadWriteBackMsg::new(WriteBackCallback::new(wb_record), RefAny::new(1_usize));
1302        assert_eq!(from_fn.callback, from_struct.callback);
1303    }
1304
1305    #[test]
1306    fn thread_write_back_msg_clone_shares_payload_and_compares_equal() {
1307        // FIXED (this test previously pinned the opposite). `RefAny`'s equality no
1308        // longer includes `instance_id` — it keys on `sharing_info` alone (see the
1309        // comment on `RefAny` in core/src/refany.rs). So every type that transitively
1310        // contains a `RefAny` and derives `PartialEq` (ThreadWriteBackMsg,
1311        // ThreadReceiveMsg, OptionThreadReceiveMsg, ThreadSendMsg) now honours the
1312        // `a.clone() == a` contract, matching the shared heap payload.
1313        let msg = ThreadWriteBackMsg::new(
1314            wb_do_nothing as WriteBackCallbackType,
1315            RefAny::new(5_usize),
1316        );
1317        let mut cloned = msg.clone();
1318
1319        // Same callback, same underlying data ...
1320        assert_eq!(msg.callback, cloned.callback);
1321        assert_eq!(
1322            cloned.refany.downcast_ref::<usize>().map(|v| *v),
1323            Some(5_usize)
1324        );
1325        // ... and therefore `==`.
1326        assert_eq!(msg, cloned);
1327        // Two clones of the same message are equal to each other as well.
1328        assert_eq!(msg.clone(), msg.clone());
1329    }
1330
1331    // ==================================================================
1332    // WriteBackCallback / ThreadCallback — fn-pointer identity semantics
1333    // ==================================================================
1334
1335    #[test]
1336    fn writeback_callback_new_has_no_ctx_and_matches_fn_ptr() {
1337        let cb = WriteBackCallback::new(wb_record);
1338        assert_eq!(cb.ctx, OptionRefAny::None);
1339        assert_eq!(cb.cb as *const () as usize, wb_record as *const () as usize);
1340        // The From<fn ptr> impl must be equivalent to ::new.
1341        assert_eq!(cb, WriteBackCallback::from(wb_record as WriteBackCallbackType));
1342    }
1343
1344    #[test]
1345    fn writeback_callback_eq_ord_hash_key_off_the_fn_pointer_only() {
1346        let a = WriteBackCallback::new(wb_record);
1347        let b = WriteBackCallback::new(wb_record);
1348        let c = WriteBackCallback::new(wb_do_nothing);
1349
1350        assert_eq!(a, b);
1351        assert_eq!(hash_of(&a), hash_of(&b));
1352        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1353
1354        assert_ne!(a, c);
1355        // Ord must be a strict total order: exactly one direction holds.
1356        assert!((a < c) ^ (c < a));
1357        assert_eq!(a.partial_cmp(&c), Some(a.cmp(&c)));
1358
1359        // Clone preserves identity (no RefAny in the compared fields).
1360        assert_eq!(a, a.clone());
1361        assert_eq!(hash_of(&a), hash_of(&a.clone()));
1362    }
1363
1364    #[test]
1365    fn writeback_callback_debug_does_not_panic() {
1366        let s = format!("{:?}", WriteBackCallback::new(wb_record));
1367        assert!(s.starts_with("WriteBackCallback {"), "got {s}");
1368    }
1369
1370    #[test]
1371    fn writeback_callback_invoke_forwards_args_and_returns_callback_update() {
1372        WB_THREAD_DATA.store(0, AtomicOrd::SeqCst);
1373        WB_WRITEBACK_DATA.store(0, AtomicOrd::SeqCst);
1374
1375        let cb = WriteBackCallback::new(wb_record);
1376        let update = with_callback_info(|info| {
1377            cb.invoke(RefAny::new(11_usize), RefAny::new(22_usize), info)
1378        });
1379
1380        // The return value must be whatever the callback returned, unmodified.
1381        assert_eq!(update, Update::RefreshDomAllWindows);
1382        // ... and the two RefAnys must arrive in the documented order (not swapped).
1383        assert_eq!(WB_THREAD_DATA.load(AtomicOrd::SeqCst), 11);
1384        assert_eq!(WB_WRITEBACK_DATA.load(AtomicOrd::SeqCst), 22);
1385    }
1386
1387    #[test]
1388    fn writeback_callback_invoke_is_repeatable() {
1389        let cb = WriteBackCallback::new(wb_do_nothing);
1390        with_callback_info(|info| {
1391            // CallbackInfo is Copy, so the same info can back several invocations.
1392            for _ in 0..4 {
1393                assert_eq!(
1394                    cb.invoke(RefAny::new(0_usize), RefAny::new(0_usize), info),
1395                    Update::DoNothing
1396                );
1397            }
1398        });
1399    }
1400
1401    #[test]
1402    fn thread_callback_new_has_no_ctx_and_orders_by_fn_ptr() {
1403        let a = ThreadCallback::new(worker_quiet);
1404        let b = ThreadCallback::from(worker_quiet as ThreadCallbackType);
1405        let c = ThreadCallback::new(worker_send_update);
1406
1407        assert_eq!(a.ctx, OptionRefAny::None);
1408        assert_eq!(a, b);
1409        assert_eq!(hash_of(&a), hash_of(&b));
1410        assert_ne!(a, c);
1411        assert!((a < c) ^ (c < a));
1412        assert_eq!(a, a.clone());
1413        assert!(format!("{a:?}").starts_with("ThreadCallback {"));
1414    }
1415
1416    #[test]
1417    fn thread_send_callback_wrapper_traits_are_consistent() {
1418        // impl_callback_traits! generated Clone/Eq/Ord/Hash for the FFI wrappers.
1419        let a = ThreadSendCallback {
1420            cb: default_send_thread_msg_fn,
1421        };
1422        let b = a.clone();
1423        assert_eq!(a, b);
1424        assert_eq!(hash_of(&a), hash_of(&b));
1425        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1426        assert!(format!("{a:?}").starts_with("ThreadSendCallback {"));
1427    }
1428
1429    // ==================================================================
1430    // ThreadSender
1431    // ==================================================================
1432
1433    #[test]
1434    fn thread_sender_send_delivers_the_exact_message() {
1435        let (rx, mut sender) = make_sender();
1436        assert!(sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
1437        assert_eq!(
1438            rx.try_recv().ok(),
1439            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
1440        );
1441        assert!(rx.try_recv().is_err(), "channel must now be empty");
1442    }
1443
1444    #[test]
1445    fn thread_sender_send_returns_false_when_receiver_is_gone() {
1446        let (rx, mut sender) = make_sender();
1447        drop(rx);
1448        // Disconnected channel: must report failure, not panic.
1449        assert!(!sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
1450        assert!(!sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
1451            wb_do_nothing as WriteBackCallbackType,
1452            RefAny::new(0_usize),
1453        ))));
1454    }
1455
1456    #[test]
1457    fn thread_sender_send_survives_a_poisoned_mutex() {
1458        let (rx, mut sender) = make_sender();
1459        let arc = Arc::clone(&*sender.ptr);
1460        let handle = std::thread::spawn(move || {
1461            let _guard = arc.lock().expect("mutex is fresh here");
1462            panic!("intentional poison");
1463        });
1464        assert!(handle.join().is_err(), "helper thread must have panicked");
1465
1466        // ThreadSender::send does `.lock().ok()` — a poisoned lock must degrade to
1467        // `false`, never to an unwrap panic.
1468        assert!(!sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
1469        // ... and nothing was actually pushed onto the channel.
1470        assert!(rx.try_recv().is_err());
1471    }
1472
1473    #[test]
1474    fn thread_sender_get_ctx_is_none_by_default_and_clones_the_payload() {
1475        let (_rx, mut sender) = make_sender();
1476        assert_eq!(sender.get_ctx(), OptionRefAny::None);
1477
1478        sender.ctx = OptionRefAny::Some(RefAny::new(9_usize));
1479        let OptionRefAny::Some(mut ctx) = sender.get_ctx() else {
1480            panic!("get_ctx must hand back the ctx that was set");
1481        };
1482        assert_eq!(ctx.downcast_ref::<usize>().map(|v| *v), Some(9_usize));
1483        // get_ctx clones rather than moves: the sender still holds its ctx.
1484        assert!(matches!(sender.get_ctx(), OptionRefAny::Some(_)));
1485    }
1486
1487    #[test]
1488    fn thread_sender_clone_shares_the_underlying_channel() {
1489        let (rx, mut sender) = make_sender();
1490        sender.ctx = OptionRefAny::Some(RefAny::new(3_usize));
1491        let mut cloned = sender.clone();
1492
1493        assert!(sender.send(ThreadReceiveMsg::Update(Update::DoNothing)));
1494        assert!(cloned.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
1495
1496        // Both endpoints feed the same channel, in order.
1497        assert_eq!(
1498            rx.try_recv().ok(),
1499            Some(ThreadReceiveMsg::Update(Update::DoNothing))
1500        );
1501        assert_eq!(
1502            rx.try_recv().ok(),
1503            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
1504        );
1505        // The FFI ctx survives the clone.
1506        assert!(matches!(cloned.get_ctx(), OptionRefAny::Some(_)));
1507    }
1508
1509    // ==================================================================
1510    // Private FFI callbacks — the raw-pointer trampolines
1511    // ==================================================================
1512
1513    #[test]
1514    fn default_send_thread_msg_fn_reports_disconnect_instead_of_panicking() {
1515        let (tx, rx) = channel::<ThreadReceiveMsg>();
1516        let tx_ptr = core::ptr::from_ref::<Sender<ThreadReceiveMsg>>(&tx).cast::<core::ffi::c_void>();
1517
1518        assert!(default_send_thread_msg_fn(
1519            tx_ptr,
1520            ThreadReceiveMsg::Update(Update::RefreshDom)
1521        ));
1522        assert_eq!(
1523            rx.try_recv().ok(),
1524            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
1525        );
1526
1527        drop(rx);
1528        assert!(!default_send_thread_msg_fn(
1529            tx_ptr,
1530            ThreadReceiveMsg::Update(Update::RefreshDom)
1531        ));
1532    }
1533
1534    #[test]
1535    fn library_send_thread_msg_fn_reports_disconnect_instead_of_panicking() {
1536        let (tx, rx) = channel::<ThreadSendMsg>();
1537        let tx_ptr = core::ptr::from_ref::<Sender<ThreadSendMsg>>(&tx).cast::<core::ffi::c_void>();
1538
1539        assert!(library_send_thread_msg_fn(tx_ptr, ThreadSendMsg::Tick));
1540        assert!(library_send_thread_msg_fn(
1541            tx_ptr,
1542            ThreadSendMsg::Custom(RefAny::new(4_usize))
1543        ));
1544        assert_eq!(rx.try_recv().ok(), Some(ThreadSendMsg::Tick));
1545        assert!(matches!(rx.try_recv(), Ok(ThreadSendMsg::Custom(_))));
1546
1547        drop(rx);
1548        assert!(!library_send_thread_msg_fn(
1549            tx_ptr,
1550            ThreadSendMsg::TerminateThread
1551        ));
1552    }
1553
1554    #[test]
1555    fn library_receive_thread_msg_fn_is_non_blocking_on_empty_and_disconnected() {
1556        let (tx, rx) = channel::<ThreadReceiveMsg>();
1557        let rx_ptr =
1558            core::ptr::from_ref::<Receiver<ThreadReceiveMsg>>(&rx).cast::<core::ffi::c_void>();
1559
1560        // Empty but connected: must return immediately with None (not block).
1561        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
1562
1563        tx.send(ThreadReceiveMsg::Update(Update::RefreshDomAllWindows))
1564            .expect("receiver is alive");
1565        assert_eq!(
1566            library_receive_thread_msg_fn(rx_ptr),
1567            OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::RefreshDomAllWindows))
1568        );
1569
1570        // Disconnected: still None, still no panic, and it stays None.
1571        drop(tx);
1572        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
1573        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
1574    }
1575
1576    #[test]
1577    fn default_receive_thread_msg_fn_is_non_blocking_on_empty_and_disconnected() {
1578        let (tx, rx) = channel::<ThreadSendMsg>();
1579        let rx_ptr = core::ptr::from_ref::<Receiver<ThreadSendMsg>>(&rx).cast::<core::ffi::c_void>();
1580
1581        assert_eq!(default_receive_thread_msg_fn(rx_ptr), OptionThreadSendMsg::None);
1582
1583        tx.send(ThreadSendMsg::TerminateThread)
1584            .expect("receiver is alive");
1585        assert_eq!(
1586            default_receive_thread_msg_fn(rx_ptr),
1587            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
1588        );
1589
1590        drop(tx);
1591        assert_eq!(default_receive_thread_msg_fn(rx_ptr), OptionThreadSendMsg::None);
1592    }
1593
1594    #[test]
1595    fn default_check_thread_finished_tracks_the_dropcheck_arc() {
1596        let alive = Arc::new(());
1597        let weak = Arc::downgrade(&alive);
1598        let weak_ptr =
1599            core::ptr::from_ref::<alloc::sync::Weak<()>>(&weak).cast::<core::ffi::c_void>();
1600
1601        // Strong ref still held by the (simulated) worker => not finished.
1602        assert!(!default_check_thread_finished(weak_ptr));
1603        drop(alive);
1604        // Worker gone => finished, and the answer is stable across calls.
1605        assert!(default_check_thread_finished(weak_ptr));
1606        assert!(default_check_thread_finished(weak_ptr));
1607    }
1608
1609    #[test]
1610    fn sender_and_receiver_drop_stubs_ignore_their_argument() {
1611        // Both destructors are documented no-ops: they must never dereference the
1612        // pointer, so even a null one is safe to hand them.
1613        thread_sender_drop(core::ptr::null_mut::<ThreadSenderInner>());
1614        thread_receiver_drop(core::ptr::null_mut::<ThreadReceiverInner>());
1615    }
1616
1617    // ==================================================================
1618    // Thread / create_thread_libstd — the live-worker paths
1619    // ==================================================================
1620
1621    #[test]
1622    fn create_thread_libstd_runs_the_callback_and_delivers_its_message() {
1623        let t = create_thread_libstd(
1624            RefAny::new(0_usize),
1625            RefAny::new(0_usize),
1626            ThreadCallback::new(worker_send_update),
1627        );
1628        join_worker(&t);
1629
1630        let mut guard = t.ptr.lock().expect("not poisoned");
1631        assert!(
1632            guard.is_finished(),
1633            "after join the dropcheck Arc must be gone"
1634        );
1635        assert_eq!(
1636            drain(&mut guard),
1637            vec![ThreadReceiveMsg::Update(Update::RefreshDom)]
1638        );
1639    }
1640
1641    #[test]
1642    fn thread_create_delivers_a_writeback_message_intact() {
1643        let t = Thread::create(
1644            RefAny::new(0_usize),
1645            RefAny::new(0_usize),
1646            worker_send_writeback as ThreadCallbackType,
1647        );
1648        join_worker(&t);
1649
1650        let mut guard = t.ptr.lock().expect("not poisoned");
1651        let msgs = drain(&mut guard);
1652        assert_eq!(msgs.len(), 1);
1653        let ThreadReceiveMsg::WriteBack(wb) = &msgs[0] else {
1654            panic!("expected a WriteBack message, got {:?}", msgs[0]);
1655        };
1656        assert_eq!(
1657            wb.callback.cb as *const () as usize,
1658            wb_do_nothing as *const () as usize
1659        );
1660        // The RefAny payload survived the channel hop between threads.
1661        let mut payload = wb.refany.clone();
1662        assert_eq!(payload.downcast_ref::<usize>().map(|v| *v), Some(77_usize));
1663    }
1664
1665    #[test]
1666    fn quiet_worker_leaves_the_receive_queue_empty() {
1667        let t = Thread::create(
1668            RefAny::new(0_usize),
1669            RefAny::new(0_usize),
1670            worker_quiet as ThreadCallbackType,
1671        );
1672        join_worker(&t);
1673
1674        let mut guard = t.ptr.lock().expect("not poisoned");
1675        // try_recv on a worker that sent nothing must be None, never a block/panic.
1676        assert!(drain(&mut guard).is_empty());
1677        assert_eq!(guard.receiver_try_recv(), OptionThreadReceiveMsg::None);
1678    }
1679
1680    #[test]
1681    fn thread_destructor_is_idempotent() {
1682        let t = create_thread_libstd(
1683            RefAny::new(0_usize),
1684            RefAny::new(0_usize),
1685            ThreadCallback::new(worker_send_update),
1686        );
1687        // Running the destructor twice by hand must not double-join (which would
1688        // panic / abort); `thread_handle.take()` makes the second call a no-op.
1689        join_worker(&t);
1690        join_worker(&t);
1691        // ... and the real Drop impl will run it a third time when `t` goes away.
1692        drop(t);
1693    }
1694
1695    #[test]
1696    fn thread_send_message_reaches_the_worker_in_order() {
1697        let t = Thread::create(
1698            RefAny::new(0_usize),
1699            RefAny::new(0_usize),
1700            worker_wait_a as ThreadCallbackType,
1701        );
1702
1703        // The worker holds its ThreadReceiver alive, so every send must succeed.
1704        for _ in 0..3 {
1705            assert!(t.send_message(ThreadSendMsg::Tick));
1706        }
1707        assert!(t.send_message(ThreadSendMsg::Custom(RefAny::new(1_usize))));
1708
1709        join_worker(&t); // queues TerminateThread behind the 4 messages, then joins
1710
1711        assert!(WAIT_A_TERMINATED.load(AtomicOrd::SeqCst));
1712        assert_eq!(WAIT_A_TICKS.load(AtomicOrd::SeqCst), 4);
1713
1714        let mut guard = t.ptr.lock().expect("not poisoned");
1715        assert!(guard.is_finished());
1716        // 3 Ticks echoed back; Custom is counted but not echoed.
1717        assert_eq!(drain(&mut guard).len(), 3);
1718    }
1719
1720    #[test]
1721    fn thread_send_message_returns_false_once_the_worker_is_gone() {
1722        let t = Thread::create(
1723            RefAny::new(0_usize),
1724            RefAny::new(0_usize),
1725            worker_wait_b as ThreadCallbackType,
1726        );
1727        assert!(t.send_message(ThreadSendMsg::Tick));
1728
1729        join_worker(&t); // worker exits, dropping its Receiver<ThreadSendMsg>
1730
1731        assert!(WAIT_B_TERMINATED.load(AtomicOrd::SeqCst));
1732        // Disconnected channel: report false rather than panicking.
1733        assert!(!t.send_message(ThreadSendMsg::Tick));
1734        assert!(!t.send_message(ThreadSendMsg::TerminateThread));
1735    }
1736
1737    #[test]
1738    fn thread_is_finished_is_false_while_the_worker_is_alive() {
1739        let t = Thread::create(
1740            RefAny::new(0_usize),
1741            RefAny::new(0_usize),
1742            worker_wait_c as ThreadCallbackType,
1743        );
1744        {
1745            // The dropcheck Arc is moved into the closure before spawn, so it is
1746            // alive from creation until the worker body returns: deterministic false.
1747            let guard = t.ptr.lock().expect("not poisoned");
1748            assert!(!guard.is_finished());
1749        }
1750        join_worker(&t);
1751        assert!(t.ptr.lock().expect("not poisoned").is_finished());
1752        assert!(WAIT_C_TERMINATED.load(AtomicOrd::SeqCst));
1753    }
1754
1755    #[test]
1756    fn thread_clone_sender_shares_the_worker_channel() {
1757        let t = Thread::create(
1758            RefAny::new(0_usize),
1759            RefAny::new(0_usize),
1760            worker_wait_d as ThreadCallbackType,
1761        );
1762        let sender = t.clone_sender().expect("std build must hand back a Sender");
1763        // Same channel as `send_message`: both reach the worker's receiver, which
1764        // stays alive until it is told to terminate.
1765        assert!(sender.send(ThreadSendMsg::Tick).is_ok());
1766        assert!(t.send_message(ThreadSendMsg::Tick));
1767        drop(sender);
1768
1769        join_worker(&t);
1770        assert!(WAIT_D_TERMINATED.load(AtomicOrd::SeqCst));
1771        assert_eq!(WAIT_D_TICKS.load(AtomicOrd::SeqCst), 2);
1772    }
1773
1774    #[test]
1775    fn thread_send_message_and_clone_sender_survive_a_poisoned_mutex() {
1776        let t = Thread::create(
1777            RefAny::new(0_usize),
1778            RefAny::new(0_usize),
1779            worker_wait_e as ThreadCallbackType,
1780        );
1781        let arc = Arc::clone(&*t.ptr);
1782        let handle = std::thread::spawn(move || {
1783            let _guard = arc.lock().expect("mutex is fresh here");
1784            panic!("intentional poison");
1785        });
1786        assert!(handle.join().is_err(), "helper thread must have panicked");
1787
1788        // Both accessors are `.lock()`-fallible by design; poison must degrade
1789        // gracefully, not unwind through the FFI boundary.
1790        assert!(!t.send_message(ThreadSendMsg::Tick));
1791        assert!(t.clone_sender().is_none());
1792
1793        // Teardown still works: Mutex::drop hands out the inner value regardless of
1794        // poison, so the Drop impl can still terminate + join the worker.
1795        drop(t);
1796        assert!(WAIT_E_TERMINATED.load(AtomicOrd::SeqCst));
1797    }
1798
1799    #[test]
1800    fn thread_clone_shares_the_same_inner_state() {
1801        let t = create_thread_libstd(
1802            RefAny::new(0_usize),
1803            RefAny::new(0_usize),
1804            ThreadCallback::new(worker_quiet),
1805        );
1806        let cloned = t.clone();
1807        assert_eq!(Arc::strong_count(&*t.ptr), 2, "clone must be shallow");
1808        assert!(cloned.run_destructor);
1809
1810        join_worker(&t);
1811        // The clone sees the same (now finished) ThreadInner.
1812        assert!(cloned.ptr.lock().expect("not poisoned").is_finished());
1813        drop(cloned);
1814        drop(t);
1815    }
1816
1817    #[test]
1818    fn option_thread_into_option_round_trips() {
1819        assert!(OptionThread::None.into_option().is_none());
1820
1821        let t = create_thread_libstd(
1822            RefAny::new(0_usize),
1823            RefAny::new(0_usize),
1824            ThreadCallback::new(worker_quiet),
1825        );
1826        let opt: OptionThread = Some(t).into();
1827        let recovered = opt.into_option().expect("Some must round-trip to Some");
1828        join_worker(&recovered);
1829        assert!(recovered.ptr.lock().expect("not poisoned").is_finished());
1830    }
1831
1832    // ==================================================================
1833    // thread_sleep_* — numeric boundaries
1834    // ==================================================================
1835
1836    #[test]
1837    fn thread_sleep_zero_returns_immediately_for_every_unit() {
1838        let start = StdInstant::now();
1839        assert_eq!(thread_sleep_ms(0), EmptyStruct::new());
1840        assert_eq!(thread_sleep_us(0), EmptyStruct::new());
1841        assert_eq!(thread_sleep_ns(0), EmptyStruct::new());
1842        // A zero sleep must not become an unbounded one.
1843        assert!(start.elapsed() < core::time::Duration::from_secs(5));
1844        assert_eq!(EmptyStruct::new()._reserved, 0);
1845    }
1846
1847    #[test]
1848    fn thread_sleep_sleeps_at_least_the_requested_duration() {
1849        // std::thread::sleep guarantees *at least* the requested time.
1850        let start = StdInstant::now();
1851        let _slept = thread_sleep_ms(5);
1852        assert!(start.elapsed() >= core::time::Duration::from_millis(5));
1853
1854        let start = StdInstant::now();
1855        let _slept = thread_sleep_us(5_000);
1856        assert!(start.elapsed() >= core::time::Duration::from_micros(5_000));
1857
1858        let start = StdInstant::now();
1859        let _slept = thread_sleep_ns(5_000_000);
1860        assert!(start.elapsed() >= core::time::Duration::from_nanos(5_000_000));
1861    }
1862
1863    #[test]
1864    fn thread_sleep_one_unit_does_not_panic() {
1865        // Smallest non-zero input in each unit: no truncation panic, no overflow.
1866        let _ms = thread_sleep_ms(1);
1867        let _us = thread_sleep_us(1);
1868        let _ns = thread_sleep_ns(1);
1869    }
1870
1871    static MAX_SLEEP_ENTERED: AtomicBool = AtomicBool::new(false);
1872    static MAX_SLEEP_PANICKED: AtomicBool = AtomicBool::new(false);
1873
1874    #[test]
1875    fn thread_sleep_max_converts_without_overflow() {
1876        // u64::MAX is representable in every Duration constructor these fns use, so
1877        // the conversion itself must not overflow-panic ...
1878        let _d_ms = core::time::Duration::from_millis(u64::MAX);
1879        let _d_us = core::time::Duration::from_micros(u64::MAX);
1880        let _d_ns = core::time::Duration::from_nanos(u64::MAX);
1881
1882        // ... but the *sleep* is genuinely unbounded (~584 million years at MAX), so
1883        // it can only be exercised on a detached thread: assert it reaches the sleep
1884        // rather than unwinding. Nothing ever joins this thread by design.
1885        let _detached = std::thread::spawn(|| {
1886            MAX_SLEEP_ENTERED.store(true, AtomicOrd::SeqCst);
1887            if std::panic::catch_unwind(|| {
1888                let _slept = thread_sleep_ms(u64::MAX);
1889            })
1890            .is_err()
1891            {
1892                MAX_SLEEP_PANICKED.store(true, AtomicOrd::SeqCst);
1893            }
1894        });
1895
1896        for _ in 0..200 {
1897            if MAX_SLEEP_ENTERED.load(AtomicOrd::SeqCst) {
1898                break;
1899            }
1900            let _slept = thread_sleep_ms(10);
1901        }
1902        assert!(
1903            MAX_SLEEP_ENTERED.load(AtomicOrd::SeqCst),
1904            "detached sleeper never started"
1905        );
1906        assert!(
1907            !MAX_SLEEP_PANICKED.load(AtomicOrd::SeqCst),
1908            "thread_sleep_ms(u64::MAX) must not panic"
1909        );
1910    }
1911}