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")]
7
use alloc::sync::Arc;
8
#[cfg(feature = "std")]
9
use std::sync::{
10
    mpsc::{channel, Receiver, Sender},
11
    Mutex,
12
};
13
#[cfg(feature = "std")]
14
use std::thread::{self, JoinHandle};
15

            
16
use 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

            
26
use crate::callbacks::CallbackInfo;
27

            
28
macro_rules! impl_callback_traits {
29
    ($name:ident) => {
30
        impl core::fmt::Debug for $name {
31
1
            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32
1
                write!(f, concat!(stringify!($name), " {{ cb: {:p} }}"), self.cb as *const ())
33
1
            }
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
1
            fn clone(&self) -> Self { Self { cb: self.cb } }
40
        }
41
        impl PartialEq for $name {
42
1
            fn eq(&self, other: &Self) -> bool {
43
1
                self.cb as *const () as usize == other.cb as *const () as usize
44
1
            }
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
1
            fn cmp(&self, other: &Self) -> core::cmp::Ordering {
54
1
                (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
55
1
            }
56
        }
57
        impl core::hash::Hash for $name {
58
2
            fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
59
2
                (self.cb as *const () as usize).hash(state);
60
2
            }
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)]
70
pub 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)]
77
pub enum OptionThreadReceiveMsg {
78
    None,
79
    Some(ThreadReceiveMsg),
80
}
81

            
82
impl From<Option<ThreadReceiveMsg>> for OptionThreadReceiveMsg {
83
23
    fn from(inner: Option<ThreadReceiveMsg>) -> Self {
84
23
        inner.map_or_else(|| Self::None, Self::Some)
85
23
    }
86
}
87

            
88
impl OptionThreadReceiveMsg {
89
7
    #[must_use] pub fn into_option(self) -> Option<ThreadReceiveMsg> {
90
7
        match self {
91
2
            Self::None => None,
92
5
            Self::Some(v) => Some(v),
93
        }
94
7
    }
95

            
96
5
    #[must_use] pub const fn as_ref(&self) -> Option<&ThreadReceiveMsg> {
97
5
        match self {
98
1
            Self::None => None,
99
4
            Self::Some(v) => Some(v),
100
        }
101
5
    }
102
}
103

            
104
/// Message containing writeback data and callback
105
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
106
#[repr(C)]
107
pub struct ThreadWriteBackMsg {
108
    pub refany: RefAny,
109
    pub callback: WriteBackCallback,
110
}
111

            
112
impl ThreadWriteBackMsg {
113
38
    pub fn new<C: Into<WriteBackCallback>>(callback: C, data: RefAny) -> Self {
114
38
        Self {
115
38
            refany: data,
116
38
            callback: callback.into(),
117
38
        }
118
38
    }
119
}
120

            
121
/// `ThreadSender` allows sending messages from the background thread to the main thread
122
#[derive(Debug)]
123
#[repr(C)]
124
pub 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

            
134
impl Clone for ThreadSender {
135
1
    fn clone(&self) -> Self {
136
1
        Self {
137
1
            ptr: self.ptr.clone(),
138
1
            run_destructor: true,
139
1
            ctx: self.ctx.clone(),
140
1
        }
141
1
    }
142
}
143

            
144
impl Drop for ThreadSender {
145
70
    fn drop(&mut self) {
146
70
        self.run_destructor = false;
147
70
    }
148
}
149

            
150
impl 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
69
    #[must_use] pub fn new(t: ThreadSenderInner) -> Self {
162
69
        Self {
163
69
            ptr: Box::new(Arc::new(Mutex::new(t))),
164
69
            run_destructor: true,
165
69
            ctx: OptionRefAny::None,
166
69
        }
167
69
    }
168

            
169
    /// Get the FFI context (e.g., Python callable)
170
4
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
171
4
        self.ctx.clone()
172
4
    }
173

            
174
    #[cfg(not(feature = "std"))]
175
    pub fn send(&mut self, _msg: ThreadReceiveMsg) -> bool {
176
        false
177
    }
178

            
179
    #[cfg(feature = "std")]
180
45
    pub fn send(&mut self, msg: ThreadReceiveMsg) -> bool {
181
45
        let Some(ts) = self.ptr.lock().ok() else {
182
1
            return false;
183
        };
184
44
        (ts.send_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()).cast::<core::ffi::c_void>(), msg)
185
45
    }
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)]
192
pub 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"))]
202
unsafe impl Send for ThreadSenderInner {}
203

            
204
#[cfg(feature = "std")]
205
impl 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")]
212
impl 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")]
219
impl Eq for ThreadSenderInner {}
220

            
221
#[cfg(feature = "std")]
222
impl 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")]
232
impl 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

            
238
impl Drop for ThreadSenderInner {
239
69
    fn drop(&mut self) {
240
69
        (self.destructor.cb)(self);
241
69
    }
242
}
243

            
244
/// Callback for sending messages from thread to main thread
245
pub 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)]
249
pub struct ThreadSendCallback {
250
    pub cb: ThreadSendCallbackType,
251
}
252

            
253
impl_callback_traits!(ThreadSendCallback);
254

            
255
/// Destructor callback for `ThreadSender`
256
pub 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)]
260
pub struct ThreadSenderDestructorCallback {
261
    pub cb: ThreadSenderDestructorCallbackType,
262
}
263

            
264
impl_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
272
pub 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)]
280
pub 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

            
287
impl WriteBackCallback {
288
    /// Create a new `WriteBackCallback`
289
37
    pub fn new(cb: WriteBackCallbackType) -> Self {
290
37
        Self {
291
37
            cb,
292
37
            ctx: OptionRefAny::None,
293
37
        }
294
37
    }
295

            
296
    /// Invoke the callback
297
5
    #[must_use] pub fn invoke(
298
5
        &self,
299
5
        thread_data: RefAny,
300
5
        writeback_data: RefAny,
301
5
        callback_info: CallbackInfo,
302
5
    ) -> Update {
303
5
        (self.cb)(thread_data, writeback_data, callback_info)
304
5
    }
305
}
306

            
307
impl core::fmt::Debug for WriteBackCallback {
308
1
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
309
1
        write!(f, "WriteBackCallback {{ cb: {:p} }}", self.cb as *const ())
310
1
    }
311
}
312

            
313
impl Clone for WriteBackCallback {
314
6
    fn clone(&self) -> Self {
315
6
        Self {
316
6
            cb: self.cb,
317
6
            ctx: self.ctx.clone(),
318
6
        }
319
6
    }
320
}
321

            
322
impl From<WriteBackCallbackType> for WriteBackCallback {
323
11
    fn from(cb: WriteBackCallbackType) -> Self {
324
11
        Self {
325
11
            cb,
326
11
            ctx: OptionRefAny::None,
327
11
        }
328
11
    }
329
}
330

            
331
impl PartialEq for WriteBackCallback {
332
9
    fn eq(&self, other: &Self) -> bool {
333
9
        std::ptr::eq(self.cb as *const (), other.cb as *const ())
334
9
    }
335
}
336

            
337
impl Eq for WriteBackCallback {}
338

            
339
impl PartialOrd for WriteBackCallback {
340
3
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
341
3
        Some(self.cmp(other))
342
3
    }
343
}
344

            
345
impl Ord for WriteBackCallback {
346
5
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
347
5
        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
348
5
    }
349
}
350

            
351
impl core::hash::Hash for WriteBackCallback {
352
4
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
353
4
        (self.cb as *const () as usize).hash(state);
354
4
    }
355
}
356

            
357
/// Callback type for the function that runs in the background thread
358
pub type ThreadCallbackType = extern "C" fn(RefAny, ThreadSender, ThreadReceiver);
359

            
360
#[repr(C)]
361
pub 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

            
368
impl ThreadCallback {
369
    /// Create a new `ThreadCallback`
370
23
    pub fn new(cb: ThreadCallbackType) -> Self {
371
23
        Self {
372
23
            cb,
373
23
            ctx: OptionRefAny::None,
374
23
        }
375
23
    }
376
}
377

            
378
impl core::fmt::Debug for ThreadCallback {
379
1
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
380
1
        write!(f, "ThreadCallback {{ cb: {:p} }}", self.cb as *const ())
381
1
    }
382
}
383

            
384
impl Clone for ThreadCallback {
385
27
    fn clone(&self) -> Self {
386
27
        Self {
387
27
            cb: self.cb,
388
27
            ctx: self.ctx.clone(),
389
27
        }
390
27
    }
391
}
392

            
393
impl From<ThreadCallbackType> for ThreadCallback {
394
10
    fn from(cb: ThreadCallbackType) -> Self {
395
10
        Self {
396
10
            cb,
397
10
            ctx: OptionRefAny::None,
398
10
        }
399
10
    }
400
}
401

            
402
impl PartialEq for ThreadCallback {
403
3
    fn eq(&self, other: &Self) -> bool {
404
3
        std::ptr::eq(self.cb as *const (), other.cb as *const ())
405
3
    }
406
}
407

            
408
impl Eq for ThreadCallback {}
409

            
410
impl PartialOrd for ThreadCallback {
411
2
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
412
2
        Some(self.cmp(other))
413
2
    }
414
}
415

            
416
impl Ord for ThreadCallback {
417
2
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
418
2
        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
419
2
    }
420
}
421

            
422
impl core::hash::Hash for ThreadCallback {
423
2
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
424
2
        (self.cb as *const () as usize).hash(state);
425
2
    }
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.
443
azul_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
459
pub 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)]
464
pub struct LibraryReceiveThreadMsgCallback {
465
    pub cb: LibraryReceiveThreadMsgCallbackType,
466
}
467

            
468
impl_callback_traits!(LibraryReceiveThreadMsgCallback);
469

            
470
/// Callback type for the destructor that cleans up a `ThreadInner`
471
pub 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)]
475
pub struct ThreadDestructorCallback {
476
    pub cb: ThreadDestructorCallbackType,
477
}
478

            
479
impl_callback_traits!(ThreadDestructorCallback);
480

            
481
/// Wrapper around Thread because Thread needs to be clone-able
482
#[derive(Debug)]
483
#[repr(C)]
484
pub 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

            
492
impl Clone for Thread {
493
1
    fn clone(&self) -> Self {
494
1
        Self {
495
1
            ptr: self.ptr.clone(),
496
1
            run_destructor: true,
497
1
        }
498
1
    }
499
}
500

            
501
impl Drop for Thread {
502
42
    fn drop(&mut self) {
503
42
        self.run_destructor = false;
504
42
    }
505
}
506

            
507
impl Thread {
508
    #[cfg(feature = "std")]
509
41
    #[must_use] pub fn new(ti: ThreadInner) -> Self {
510
41
        Self {
511
41
            ptr: Box::new(Arc::new(Mutex::new(ti))),
512
41
            run_destructor: true,
513
41
        }
514
41
    }
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
37
    pub fn create<C: Into<ThreadCallback>>(
534
37
        thread_initialize_data: RefAny,
535
37
        writeback_data: RefAny,
536
37
        callback: C,
537
37
    ) -> Self {
538
37
        create_thread_libstd(thread_initialize_data, writeback_data, callback.into())
539
37
    }
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
12
    #[must_use] pub fn send_message(&self, msg: ThreadSendMsg) -> bool {
548
12
        self.ptr.lock().is_ok_and(|inner| inner.sender.send(msg).is_ok())
549
12
    }
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
5
    #[must_use] pub fn clone_sender(&self) -> Option<Sender<ThreadSendMsg>> {
561
5
        self.ptr.lock().ok().map(|inner| (*inner.sender).clone())
562
5
    }
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)]
576
pub 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")]
605
impl ThreadInner {
606
    /// Returns true if the Thread has been finished, false otherwise
607
20
    #[must_use] pub fn is_finished(&self) -> bool {
608
20
        (self.check_thread_finished_fn.cb)(
609
20
            std::ptr::from_ref(self.dropcheck.as_ref()).cast::<core::ffi::c_void>()
610
20
        )
611
20
    }
612

            
613
    /// Send a message to the thread
614
2
    pub fn sender_send(&mut self, msg: ThreadSendMsg) -> bool {
615
2
        (self.send_thread_msg_fn.cb)(
616
2
            std::ptr::from_ref(self.sender.as_ref()).cast::<core::ffi::c_void>(),
617
2
            msg,
618
2
        )
619
2
    }
620

            
621
    /// Try to receive a message from the thread (non-blocking)
622
15
    pub fn receiver_try_recv(&mut self) -> OptionThreadReceiveMsg {
623
15
        (self.receive_thread_msg_fn.cb)(
624
15
            std::ptr::from_ref(self.receiver.as_ref()).cast::<core::ffi::c_void>()
625
15
        )
626
15
    }
627
}
628

            
629
#[cfg(not(feature = "std"))]
630
impl 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

            
647
impl Drop for ThreadInner {
648
41
    fn drop(&mut self) {
649
41
        (self.thread_destructor_fn.cb)(self);
650
41
    }
651
}
652

            
653
/// How long `default_thread_destructor_fn` waits for a worker to acknowledge
654
/// `TerminateThread` before detaching it: 200 steps x 10ms = 2s.
655
///
656
/// Module scope rather than inside the function because clippy's
657
/// `items_after_statements` is denied here — an item declared mid-body reads as
658
/// if it were scoped to that point when it is not.
659
#[cfg(feature = "std")]
660
const THREAD_TERMINATE_GRACE_STEPS: u32 = 200;
661

            
662
// Default callback implementations for std
663
#[cfg(feature = "std")]
664
52
extern "C" fn default_thread_destructor_fn(thread: *mut ThreadInner) {
665
52
    let thread = unsafe { &mut *thread };
666

            
667
52
    if let Some(thread_handle) = thread.thread_handle.take() {
668
41
        drop(thread.sender.send(ThreadSendMsg::TerminateThread));
669

            
670
        // BOUNDED wait, then DETACH. This was an unconditional
671
        // `thread_handle.join()`, which hangs forever whenever the worker does
672
        // not observe `TerminateThread` — and a worker blocked in a device read
673
        // (v4l2, ALSA) or on a different channel never does.
674
        //
675
        // Measured on the published 0.2.0 azul-self-test-linux: after the
676
        // window closed and the Wayland display was disconnected, the main
677
        // thread sat in `pthread_join` on an unnamed worker while 22 other
678
        // threads idled. `App::run()` never returned, so the process never
679
        // reached `std::process::exit()` in main and had to be killed. The demo
680
        // advertises "exits on its own (~4s)".
681
        //
682
        // `dropcheck` already tells us what we need: the worker holds an Arc
683
        // whose Weak lives here, so `strong_count() == 0` means it has actually
684
        // finished and the join will return immediately. If it has not finished
685
        // within the grace period we DROP the handle instead, which detaches the
686
        // thread. A detached thread cannot block `process::exit`, and shutting
687
        // down is precisely when refusing to wait is correct: the alternative on
688
        // display is a hang, which users read as a crash.
689
        //
690
        // Spin on a sleep rather than `Instant`: this file is compiled for wasm
691
        // and the clockless targets, where the strict `Instant` gate is 0.
692
41
        let mut finished = thread.dropcheck.strong_count() == 0;
693
41
        let mut waited = 0_u32;
694
59
        while !finished && waited < THREAD_TERMINATE_GRACE_STEPS {
695
18
            thread::sleep(core::time::Duration::from_millis(10));
696
18
            waited += 1;
697
18
            finished = thread.dropcheck.strong_count() == 0;
698
18
        }
699

            
700
41
        if finished {
701
41
            drop(thread_handle.join()); // returns immediately; ignore the result
702
41
        } else {
703
            // Detached. Say so — a silently leaked worker is how a "why is this
704
            // slow to exit" question becomes unanswerable.
705
            eprintln!(
706
                "[azul][thread] a background thread did not acknowledge \
707
                 TerminateThread within {}ms and was DETACHED rather than \
708
                 joined. It will be torn down by process exit. If this recurs, \
709
                 that worker is blocking on something it cannot be interrupted \
710
                 from (a device read, or a channel other than its terminate \
711
                 channel).",
712
                THREAD_TERMINATE_GRACE_STEPS * 10,
713
            );
714
            drop(thread_handle);
715
        }
716
11
    }
717
52
}
718

            
719
#[cfg(not(feature = "std"))]
720
extern "C" fn default_thread_destructor_fn(_thread: *mut ThreadInner) {}
721

            
722
#[cfg(feature = "std")]
723
5
extern "C" fn library_send_thread_msg_fn(
724
5
    sender: *const core::ffi::c_void,
725
5
    msg: ThreadSendMsg,
726
5
) -> bool {
727
5
    unsafe { &*sender.cast::<Sender<ThreadSendMsg>>() }
728
5
        .send(msg)
729
5
        .is_ok()
730
5
}
731

            
732
#[cfg(not(feature = "std"))]
733
extern "C" fn library_send_thread_msg_fn(
734
    _sender: *const core::ffi::c_void,
735
    _msg: ThreadSendMsg,
736
) -> bool {
737
    false
738
}
739

            
740
#[cfg(feature = "std")]
741
19
extern "C" fn library_receive_thread_msg_fn(
742
19
    receiver: *const core::ffi::c_void,
743
19
) -> OptionThreadReceiveMsg {
744
19
    unsafe { &*receiver.cast::<Receiver<ThreadReceiveMsg>>() }
745
19
        .try_recv()
746
19
        .ok()
747
19
        .into()
748
19
}
749

            
750
#[cfg(not(feature = "std"))]
751
extern "C" fn library_receive_thread_msg_fn(
752
    _receiver: *const core::ffi::c_void,
753
) -> OptionThreadReceiveMsg {
754
    None.into()
755
}
756

            
757
#[cfg(feature = "std")]
758
19
extern "C" fn default_send_thread_msg_fn(
759
19
    sender: *const core::ffi::c_void,
760
19
    msg: ThreadReceiveMsg,
761
19
) -> bool {
762
19
    unsafe { &*sender.cast::<Sender<ThreadReceiveMsg>>() }
763
19
        .send(msg)
764
19
        .is_ok()
765
19
}
766

            
767
#[cfg(not(feature = "std"))]
768
extern "C" fn default_send_thread_msg_fn(
769
    _sender: *const core::ffi::c_void,
770
    _msg: ThreadReceiveMsg,
771
) -> bool {
772
    false
773
}
774

            
775
#[cfg(feature = "std")]
776
19
extern "C" fn default_receive_thread_msg_fn(
777
19
    receiver: *const core::ffi::c_void,
778
19
) -> OptionThreadSendMsg {
779
19
    unsafe { &*receiver.cast::<Receiver<ThreadSendMsg>>() }
780
19
        .try_recv()
781
19
        .ok()
782
19
        .into()
783
19
}
784

            
785
#[cfg(not(feature = "std"))]
786
extern "C" fn default_receive_thread_msg_fn(
787
    _receiver: *const core::ffi::c_void,
788
) -> OptionThreadSendMsg {
789
    None.into()
790
}
791

            
792
#[cfg(feature = "std")]
793
23
extern "C" fn default_check_thread_finished(dropcheck: *const core::ffi::c_void) -> bool {
794
23
    let weak = unsafe { &*dropcheck.cast::<alloc::sync::Weak<()>>() };
795
23
    weak.upgrade().is_none()
796
23
}
797

            
798
#[cfg(not(feature = "std"))]
799
extern "C" fn default_check_thread_finished(_dropcheck: *const core::ffi::c_void) -> bool {
800
    true
801
}
802

            
803
#[cfg(feature = "std")]
804
47
const extern "C" fn thread_sender_drop(_: *mut ThreadSenderInner) {}
805

            
806
#[cfg(not(feature = "std"))]
807
extern "C" fn thread_sender_drop(_: *mut ThreadSenderInner) {}
808

            
809
#[cfg(feature = "std")]
810
42
const extern "C" fn thread_receiver_drop(_: *mut ThreadReceiverInner) {}
811

            
812
#[cfg(not(feature = "std"))]
813
extern "C" fn thread_receiver_drop(_: *mut ThreadReceiverInner) {}
814

            
815
/// Function that creates a new Thread object
816
pub type CreateThreadCallbackType = extern "C" fn(RefAny, RefAny, ThreadCallback) -> Thread;
817

            
818
#[repr(C)]
819
pub struct CreateThreadCallback {
820
    pub cb: CreateThreadCallbackType,
821
}
822

            
823
impl_callback_traits!(CreateThreadCallback);
824
impl Copy for CreateThreadCallback {}
825

            
826
/// Create a new thread using the standard library
827
#[cfg(feature = "std")]
828
41
#[must_use] pub extern "C" fn create_thread_libstd(
829
41
    thread_initialize_data: RefAny,
830
41
    writeback_data: RefAny,
831
41
    callback: ThreadCallback,
832
41
) -> Thread {
833
41
    let (sender_receiver, receiver_receiver) = channel::<ThreadReceiveMsg>();
834
41
    let mut sender_receiver = ThreadSender::new(ThreadSenderInner {
835
41
        ptr: Box::new(sender_receiver),
836
41
        send_fn: ThreadSendCallback {
837
41
            cb: default_send_thread_msg_fn,
838
41
        },
839
41
        destructor: ThreadSenderDestructorCallback {
840
41
            cb: thread_sender_drop,
841
41
        },
842
41
    });
843
    // Set the ctx from the callback for FFI
844
41
    sender_receiver.ctx = callback.ctx.clone();
845

            
846
41
    let (sender_sender, receiver_sender) = channel::<ThreadSendMsg>();
847
41
    let mut receiver_sender = ThreadReceiver::new(ThreadReceiverInner {
848
41
        ptr: Box::new(receiver_sender),
849
41
        recv_fn: ThreadRecvCallback {
850
41
            cb: default_receive_thread_msg_fn,
851
41
        },
852
41
        destructor: ThreadReceiverDestructorCallback {
853
41
            cb: thread_receiver_drop,
854
41
        },
855
41
    });
856
    // Set the ctx from the callback for FFI
857
41
    receiver_sender.ctx = callback.ctx.clone();
858

            
859
41
    let thread_check = Arc::new(());
860
41
    let dropcheck = Arc::downgrade(&thread_check);
861

            
862
41
    let thread_handle = Some(thread::spawn(move || {
863
        // Keep thread_check alive for the entire duration of the thread
864
        // by binding it to a named variable (not `_` which drops immediately)
865
41
        let _thread_check_guard = thread_check;
866
41
        (callback.cb)(thread_initialize_data, sender_receiver, receiver_sender);
867
        // _thread_check_guard gets dropped here, signals that the thread has finished
868
41
    }));
869

            
870
41
    let thread_handle: Box<Option<JoinHandle<()>>> =
871
41
        Box::new(thread_handle);
872
41
    let sender: Box<Sender<ThreadSendMsg>> = Box::new(sender_sender);
873
41
    let receiver: Box<Receiver<ThreadReceiveMsg>> =
874
41
        Box::new(receiver_receiver);
875
41
    let dropcheck: Box<alloc::sync::Weak<()>> = Box::new(dropcheck);
876

            
877
41
    Thread::new(ThreadInner {
878
41
        thread_handle,
879
41
        sender,
880
41
        receiver,
881
41
        writeback_data,
882
41
        dropcheck,
883
41
        thread_destructor_fn: ThreadDestructorCallback {
884
41
            cb: default_thread_destructor_fn,
885
41
        },
886
41
        check_thread_finished_fn: CheckThreadFinishedCallback {
887
41
            cb: default_check_thread_finished,
888
41
        },
889
41
        send_thread_msg_fn: LibrarySendThreadMsgCallback {
890
41
            cb: library_send_thread_msg_fn,
891
41
        },
892
41
        receive_thread_msg_fn: LibraryReceiveThreadMsgCallback {
893
41
            cb: library_receive_thread_msg_fn,
894
41
        },
895
41
    })
896
41
}
897

            
898
#[cfg(not(feature = "std"))]
899
pub extern "C" fn create_thread_libstd(
900
    _thread_initialize_data: RefAny,
901
    _writeback_data: RefAny,
902
    _callback: ThreadCallback,
903
) -> Thread {
904
    Thread {
905
        ptr: core::ptr::null(),
906
        run_destructor: false,
907
    }
908
}
909

            
910
#[cfg(test)]
911
mod tests {
912
    use super::*;
913

            
914
    extern "C" fn test_writeback_callback(
915
        _thread_data: RefAny,
916
        _writeback_data: RefAny,
917
        _callback_info: CallbackInfo,
918
    ) -> Update {
919
        Update::DoNothing
920
    }
921

            
922
    #[test]
923
1
    fn test_writeback_callback_creation() {
924
1
        let callback = WriteBackCallback::new(test_writeback_callback);
925
1
        assert_eq!(callback.cb as *const () as usize, test_writeback_callback as *const () as usize);
926
1
    }
927

            
928
    #[test]
929
1
    fn test_writeback_callback_clone() {
930
1
        let callback = WriteBackCallback::new(test_writeback_callback);
931
1
        let cloned = callback.clone();
932
1
        assert_eq!(callback, cloned);
933
1
    }
934
}
935
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
936
/// Optional Thread type for API compatibility
937
#[derive(Debug, Clone)]
938
#[repr(C, u8)]
939
pub enum OptionThread {
940
    None,
941
    Some(Thread),
942
}
943

            
944
impl From<Option<Thread>> for OptionThread {
945
1
    fn from(o: Option<Thread>) -> Self {
946
1
        o.map_or_else(|| Self::None, Self::Some)
947
1
    }
948
}
949

            
950
impl OptionThread {
951
2
    #[must_use] pub fn into_option(self) -> Option<Thread> {
952
2
        match self {
953
1
            Self::None => None,
954
1
            Self::Some(t) => Some(t),
955
        }
956
2
    }
957
}
958

            
959
// ============================================================================
960
// Sleep utilities
961
// ============================================================================
962

            
963
/// Sleeps the current thread for the specified number of milliseconds.
964
///
965
/// This is a cross-platform utility that can be called from C/C++/Python.
966
///
967
/// # Arguments
968
/// * `milliseconds` - Number of milliseconds to sleep
969
#[cfg(feature = "std")]
970
6
#[must_use] pub fn thread_sleep_ms(milliseconds: u64) -> azul_css::corety::EmptyStruct {
971
6
    thread::sleep(std::time::Duration::from_millis(milliseconds));
972
6
    azul_css::corety::EmptyStruct::new()
973
6
}
974

            
975
/// Sleeps the current thread for the specified number of milliseconds (no-op on no_std).
976
#[cfg(not(feature = "std"))]
977
pub fn thread_sleep_ms(_milliseconds: u64) -> azul_css::corety::EmptyStruct {
978
    // No-op on no_std - can't sleep without OS
979
    azul_css::corety::EmptyStruct::new()
980
}
981

            
982
/// Sleeps the current thread for the specified number of microseconds.
983
///
984
/// # Arguments
985
/// * `microseconds` - Number of microseconds to sleep
986
#[cfg(feature = "std")]
987
3
#[must_use] pub fn thread_sleep_us(microseconds: u64) -> azul_css::corety::EmptyStruct {
988
3
    thread::sleep(std::time::Duration::from_micros(microseconds));
989
3
    azul_css::corety::EmptyStruct::new()
990
3
}
991

            
992
/// Sleeps the current thread for the specified number of microseconds (no-op on no_std).
993
#[cfg(not(feature = "std"))]
994
pub fn thread_sleep_us(_microseconds: u64) -> azul_css::corety::EmptyStruct {
995
    // No-op on no_std - can't sleep without OS
996
    azul_css::corety::EmptyStruct::new()
997
}
998

            
999
/// Sleeps the current thread for the specified number of nanoseconds.
///
/// # Arguments
/// * `nanoseconds` - Number of nanoseconds to sleep
#[cfg(feature = "std")]
3
#[must_use] pub fn thread_sleep_ns(nanoseconds: u64) -> azul_css::corety::EmptyStruct {
3
    thread::sleep(std::time::Duration::from_nanos(nanoseconds));
3
    azul_css::corety::EmptyStruct::new()
3
}
/// Sleeps the current thread for the specified number of nanoseconds (no-op on no_std).
#[cfg(not(feature = "std"))]
pub fn thread_sleep_ns(_nanoseconds: u64) -> azul_css::corety::EmptyStruct {
    // No-op on no_std - can't sleep without OS
    azul_css::corety::EmptyStruct::new()
}
// ============================================================================
// Generated adversarial tests
// ============================================================================
#[cfg(all(test, feature = "std"))]
#[allow(clippy::too_many_lines, clippy::unreadable_literal)]
mod autotest_generated {
    use core::{
        hash::{Hash, Hasher},
        sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrd},
    };
    use std::{
        collections::{hash_map::DefaultHasher, BTreeMap},
        sync::{Arc, Mutex},
        time::Instant as StdInstant,
    };
    use azul_core::{
        dom::{DomId, DomNodeId},
        geom::OptionLogicalPosition,
        gl::OptionGlContextPtr,
        hit_test::ScrollPosition,
        resources::RendererResources,
        styled_dom::NodeHierarchyItemId,
        window::{MonitorVec, RawWindowHandle},
    };
    use azul_css::{corety::EmptyStruct, system::SystemStyle};
    use rust_fontconfig::FcFontCache;
    use super::*;
    #[cfg(feature = "icu")]
    use crate::icu::IcuLocalizerHandle;
    use crate::{
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
        window::LayoutWindow,
        window_state::FullWindowState,
    };
    // ------------------------------------------------------------------
    // Harness
    // ------------------------------------------------------------------
    /// Upper bound on a worker's non-blocking poll loop: `ThreadReceiver::recv` is
    /// `try_recv` under the hood, so a worker that waits for `TerminateThread` MUST
    /// be bounded or a lost message would hang the whole test binary.
    const MAX_WORKER_POLLS: usize = 10_000;
    fn hash_of<T: Hash>(value: &T) -> u64 {
        let mut hasher = DefaultHasher::new();
        value.hash(&mut hasher);
        hasher.finish()
    }
    /// A live `ThreadSender` plus the receiving end of its channel.
    fn make_sender() -> (Receiver<ThreadReceiveMsg>, ThreadSender) {
        let (tx, rx) = channel::<ThreadReceiveMsg>();
        let sender = ThreadSender::new(ThreadSenderInner {
            ptr: Box::new(tx),
            send_fn: ThreadSendCallback {
                cb: default_send_thread_msg_fn,
            },
            destructor: ThreadSenderDestructorCallback {
                cb: thread_sender_drop,
            },
        });
        (rx, sender)
    }
    /// Drain every message currently queued on the main->worker side.
    fn drain(inner: &mut ThreadInner) -> Vec<ThreadReceiveMsg> {
        let mut out = Vec::new();
        while let OptionThreadReceiveMsg::Some(msg) = inner.receiver_try_recv() {
            out.push(msg);
        }
        out
    }
    /// Runs the thread destructor by hand (terminate + join), so every assertion
    /// after it observes a *finished* worker instead of racing one.
    fn join_worker(t: &Thread) {
        let mut guard = t.ptr.lock().expect("thread mutex must not be poisoned");
        default_thread_destructor_fn(core::ptr::from_mut::<ThreadInner>(&mut guard));
    }
    /// Builds a real `CallbackInfo` (the only way to exercise `WriteBackCallback::invoke`)
    /// over an otherwise-empty `LayoutWindow`.
    fn with_callback_info<R>(f: impl FnOnce(CallbackInfo) -> R) -> R {
        let layout_window =
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
        let renderer_resources = RendererResources::default();
        let previous_window_state: Option<FullWindowState> = None;
        let current_window_state = FullWindowState::default();
        let gl_context = OptionGlContextPtr::None;
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
            BTreeMap::new();
        let window_handle = RawWindowHandle::Unsupported;
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
        let ref_data = CallbackInfoRefData {
            layout_window: &layout_window,
            renderer_resources: &renderer_resources,
            previous_window_state: &previous_window_state,
            current_window_state: &current_window_state,
            gl_context: &gl_context,
            current_scroll_manager: &scroll_states,
            current_window_handle: &window_handle,
            system_callbacks: &system_callbacks,
            system_style: Arc::new(SystemStyle::default()),
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
            #[cfg(feature = "icu")]
            icu_localizer: IcuLocalizerHandle::default(),
            ctx: OptionRefAny::None,
        };
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
        let info = CallbackInfo::new(
            &ref_data,
            &changes,
            DomNodeId {
                dom: DomId::ROOT_ID,
                node: NodeHierarchyItemId::NONE,
            },
            OptionLogicalPosition::None,
            OptionLogicalPosition::None,
        );
        f(info)
    }
    // ------------------------------------------------------------------
    // Callback fixtures
    // ------------------------------------------------------------------
    static WB_THREAD_DATA: AtomicUsize = AtomicUsize::new(0);
    static WB_WRITEBACK_DATA: AtomicUsize = AtomicUsize::new(0);
    extern "C" fn wb_record(
        mut thread_data: RefAny,
        mut writeback_data: RefAny,
        _callback_info: CallbackInfo,
    ) -> Update {
        if let Some(v) = thread_data.downcast_ref::<usize>() {
            WB_THREAD_DATA.store(*v, AtomicOrd::SeqCst);
        }
        if let Some(v) = writeback_data.downcast_ref::<usize>() {
            WB_WRITEBACK_DATA.store(*v, AtomicOrd::SeqCst);
        }
        Update::RefreshDomAllWindows
    }
    extern "C" fn wb_do_nothing(
        _thread_data: RefAny,
        _writeback_data: RefAny,
        _callback_info: CallbackInfo,
    ) -> Update {
        Update::DoNothing
    }
    /// A worker that exits immediately without ever touching its channels.
    extern "C" fn worker_quiet(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
    /// A worker that pushes exactly one `Update` back to the main thread, then exits.
    extern "C" fn worker_send_update(_d: RefAny, mut sender: ThreadSender, _r: ThreadReceiver) {
        let _sent = sender.send(ThreadReceiveMsg::Update(Update::RefreshDom));
    }
    /// A worker that pushes one `WriteBack` message (the RefAny-carrying variant).
    extern "C" fn worker_send_writeback(_d: RefAny, mut sender: ThreadSender, _r: ThreadReceiver) {
        let _sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
            wb_do_nothing as WriteBackCallbackType,
            RefAny::new(77_usize),
        )));
    }
    /// Generates a worker that echoes `Tick`s back until it is told to terminate.
    /// Each instance gets its own statics so tests can run in parallel without racing.
    macro_rules! terminating_worker {
        ($fn_name:ident, $ticks:ident, $terminated:ident) => {
            static $ticks: AtomicUsize = AtomicUsize::new(0);
            static $terminated: AtomicBool = AtomicBool::new(false);
            extern "C" fn $fn_name(
                _d: RefAny,
                mut sender: ThreadSender,
                mut receiver: ThreadReceiver,
            ) {
                for _ in 0..MAX_WORKER_POLLS {
                    match receiver.recv() {
                        OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread) => {
                            $terminated.store(true, AtomicOrd::SeqCst);
                            break;
                        }
                        OptionThreadSendMsg::Some(ThreadSendMsg::Tick) => {
                            $ticks.fetch_add(1, AtomicOrd::SeqCst);
                            let _sent = sender.send(ThreadReceiveMsg::Update(Update::RefreshDom));
                        }
                        OptionThreadSendMsg::Some(ThreadSendMsg::Custom(_)) => {
                            $ticks.fetch_add(1, AtomicOrd::SeqCst);
                        }
                        OptionThreadSendMsg::None => {
                            let _slept = thread_sleep_ms(1);
                        }
                    }
                }
            }
        };
    }
    terminating_worker!(worker_wait_a, WAIT_A_TICKS, WAIT_A_TERMINATED);
    terminating_worker!(worker_wait_b, WAIT_B_TICKS, WAIT_B_TERMINATED);
    terminating_worker!(worker_wait_c, WAIT_C_TICKS, WAIT_C_TERMINATED);
    terminating_worker!(worker_wait_d, WAIT_D_TICKS, WAIT_D_TERMINATED);
    terminating_worker!(worker_wait_e, WAIT_E_TICKS, WAIT_E_TERMINATED);
    // ==================================================================
    // OptionThreadReceiveMsg — getters / predicates
    // ==================================================================
    #[test]
    fn option_thread_receive_msg_none_into_option_is_none() {
        assert!(OptionThreadReceiveMsg::None.into_option().is_none());
        assert!(OptionThreadReceiveMsg::None.as_ref().is_none());
    }
    #[test]
    fn option_thread_receive_msg_round_trips_through_from_and_into_option() {
        // Round-trip: Option -> OptionThreadReceiveMsg -> Option must be the identity.
        for update in [
            Update::DoNothing,
            Update::RefreshDom,
            Update::RefreshDomAllWindows,
        ] {
            let msg = ThreadReceiveMsg::Update(update);
            let ffi: OptionThreadReceiveMsg = Some(msg.clone()).into();
            assert_eq!(ffi.into_option(), Some(msg));
        }
        let empty: OptionThreadReceiveMsg = None.into();
        assert_eq!(empty, OptionThreadReceiveMsg::None);
        assert!(empty.into_option().is_none());
    }
    #[test]
    fn option_thread_receive_msg_as_ref_does_not_consume() {
        let opt = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::RefreshDom));
        // as_ref() borrows: calling it repeatedly must keep returning the same payload.
        for _ in 0..3 {
            assert_eq!(
                opt.as_ref(),
                Some(&ThreadReceiveMsg::Update(Update::RefreshDom))
            );
        }
        // ... and the value is still intact afterwards.
        assert_eq!(
            opt.into_option(),
            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
        );
    }
    #[test]
    fn option_thread_receive_msg_as_ref_handles_writeback_variant() {
        let opt = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::WriteBack(
            ThreadWriteBackMsg::new(
                wb_do_nothing as WriteBackCallbackType,
                RefAny::new(1_usize),
            ),
        ));
        let Some(ThreadReceiveMsg::WriteBack(inner)) = opt.as_ref() else {
            panic!("as_ref() must expose the WriteBack payload");
        };
        assert_eq!(
            inner.callback.cb as *const () as usize,
            wb_do_nothing as *const () as usize
        );
        assert!(opt.into_option().is_some());
    }
    #[test]
    fn option_thread_receive_msg_ord_and_hash_are_consistent() {
        let none = OptionThreadReceiveMsg::None;
        let some = OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::DoNothing));
        // Declaration order: None < Some.
        assert!(none < some);
        assert_eq!(none.cmp(&none), core::cmp::Ordering::Equal);
        // Eq => equal hashes.
        assert_eq!(hash_of(&none), hash_of(&OptionThreadReceiveMsg::None));
        assert_eq!(
            hash_of(&some),
            hash_of(&OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(
                Update::DoNothing
            )))
        );
    }
    #[test]
    fn thread_receive_msg_orders_writeback_before_update() {
        let wb = ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
            wb_do_nothing as WriteBackCallbackType,
            RefAny::new(0_usize),
        ));
        let up = ThreadReceiveMsg::Update(Update::DoNothing);
        assert!(wb < up, "variant order (WriteBack=0, Update=1) must decide");
        assert!(
            ThreadReceiveMsg::Update(Update::DoNothing)
                < ThreadReceiveMsg::Update(Update::RefreshDom)
        );
    }
    // ==================================================================
    // ThreadWriteBackMsg — constructor invariants
    // ==================================================================
    #[test]
    fn thread_write_back_msg_new_stores_both_fields() {
        let mut msg = ThreadWriteBackMsg::new(
            wb_do_nothing as WriteBackCallbackType,
            RefAny::new(0xDEAD_BEEF_usize),
        );
        assert_eq!(
            msg.callback.cb as *const () as usize,
            wb_do_nothing as *const () as usize
        );
        assert_eq!(
            msg.refany
                .downcast_ref::<usize>()
                .map(|v| *v)
                .expect("payload type must survive construction"),
            0xDEAD_BEEF_usize
        );
        // A fn-ptr-built callback carries no FFI ctx.
        assert_eq!(msg.callback.ctx, OptionRefAny::None);
    }
    #[test]
    fn thread_write_back_msg_new_accepts_both_into_impls() {
        // `C: Into<WriteBackCallback>` must accept a bare fn pointer *and* an
        // already-built WriteBackCallback; both must land on the same cb.
        let from_fn = ThreadWriteBackMsg::new(
            wb_record as WriteBackCallbackType,
            RefAny::new(1_usize),
        );
        let from_struct =
            ThreadWriteBackMsg::new(WriteBackCallback::new(wb_record), RefAny::new(1_usize));
        assert_eq!(from_fn.callback, from_struct.callback);
    }
    #[test]
    fn thread_write_back_msg_clone_shares_payload_and_compares_equal() {
        // FIXED (this test previously pinned the opposite). `RefAny`'s equality no
        // longer includes `instance_id` — it keys on `sharing_info` alone (see the
        // comment on `RefAny` in core/src/refany.rs). So every type that transitively
        // contains a `RefAny` and derives `PartialEq` (ThreadWriteBackMsg,
        // ThreadReceiveMsg, OptionThreadReceiveMsg, ThreadSendMsg) now honours the
        // `a.clone() == a` contract, matching the shared heap payload.
        let msg = ThreadWriteBackMsg::new(
            wb_do_nothing as WriteBackCallbackType,
            RefAny::new(5_usize),
        );
        let mut cloned = msg.clone();
        // Same callback, same underlying data ...
        assert_eq!(msg.callback, cloned.callback);
        assert_eq!(
            cloned.refany.downcast_ref::<usize>().map(|v| *v),
            Some(5_usize)
        );
        // ... and therefore `==`.
        assert_eq!(msg, cloned);
        // Two clones of the same message are equal to each other as well.
        assert_eq!(msg.clone(), msg.clone());
    }
    // ==================================================================
    // WriteBackCallback / ThreadCallback — fn-pointer identity semantics
    // ==================================================================
    #[test]
    fn writeback_callback_new_has_no_ctx_and_matches_fn_ptr() {
        let cb = WriteBackCallback::new(wb_record);
        assert_eq!(cb.ctx, OptionRefAny::None);
        assert_eq!(cb.cb as *const () as usize, wb_record as *const () as usize);
        // The From<fn ptr> impl must be equivalent to ::new.
        assert_eq!(cb, WriteBackCallback::from(wb_record as WriteBackCallbackType));
    }
    #[test]
    fn writeback_callback_eq_ord_hash_key_off_the_fn_pointer_only() {
        let a = WriteBackCallback::new(wb_record);
        let b = WriteBackCallback::new(wb_record);
        let c = WriteBackCallback::new(wb_do_nothing);
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
        assert_ne!(a, c);
        // Ord must be a strict total order: exactly one direction holds.
        assert!((a < c) ^ (c < a));
        assert_eq!(a.partial_cmp(&c), Some(a.cmp(&c)));
        // Clone preserves identity (no RefAny in the compared fields).
        assert_eq!(a, a.clone());
        assert_eq!(hash_of(&a), hash_of(&a.clone()));
    }
    #[test]
    fn writeback_callback_debug_does_not_panic() {
        let s = format!("{:?}", WriteBackCallback::new(wb_record));
        assert!(s.starts_with("WriteBackCallback {"), "got {s}");
    }
    #[test]
    fn writeback_callback_invoke_forwards_args_and_returns_callback_update() {
        WB_THREAD_DATA.store(0, AtomicOrd::SeqCst);
        WB_WRITEBACK_DATA.store(0, AtomicOrd::SeqCst);
        let cb = WriteBackCallback::new(wb_record);
        let update = with_callback_info(|info| {
            cb.invoke(RefAny::new(11_usize), RefAny::new(22_usize), info)
        });
        // The return value must be whatever the callback returned, unmodified.
        assert_eq!(update, Update::RefreshDomAllWindows);
        // ... and the two RefAnys must arrive in the documented order (not swapped).
        assert_eq!(WB_THREAD_DATA.load(AtomicOrd::SeqCst), 11);
        assert_eq!(WB_WRITEBACK_DATA.load(AtomicOrd::SeqCst), 22);
    }
    #[test]
    fn writeback_callback_invoke_is_repeatable() {
        let cb = WriteBackCallback::new(wb_do_nothing);
        with_callback_info(|info| {
            // CallbackInfo is Copy, so the same info can back several invocations.
            for _ in 0..4 {
                assert_eq!(
                    cb.invoke(RefAny::new(0_usize), RefAny::new(0_usize), info),
                    Update::DoNothing
                );
            }
        });
    }
    #[test]
    fn thread_callback_new_has_no_ctx_and_orders_by_fn_ptr() {
        let a = ThreadCallback::new(worker_quiet);
        let b = ThreadCallback::from(worker_quiet as ThreadCallbackType);
        let c = ThreadCallback::new(worker_send_update);
        assert_eq!(a.ctx, OptionRefAny::None);
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        assert_ne!(a, c);
        assert!((a < c) ^ (c < a));
        assert_eq!(a, a.clone());
        assert!(format!("{a:?}").starts_with("ThreadCallback {"));
    }
    #[test]
    fn thread_send_callback_wrapper_traits_are_consistent() {
        // impl_callback_traits! generated Clone/Eq/Ord/Hash for the FFI wrappers.
        let a = ThreadSendCallback {
            cb: default_send_thread_msg_fn,
        };
        let b = a.clone();
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
        assert!(format!("{a:?}").starts_with("ThreadSendCallback {"));
    }
    // ==================================================================
    // ThreadSender
    // ==================================================================
    #[test]
    fn thread_sender_send_delivers_the_exact_message() {
        let (rx, mut sender) = make_sender();
        assert!(sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
        assert_eq!(
            rx.try_recv().ok(),
            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
        );
        assert!(rx.try_recv().is_err(), "channel must now be empty");
    }
    #[test]
    fn thread_sender_send_returns_false_when_receiver_is_gone() {
        let (rx, mut sender) = make_sender();
        drop(rx);
        // Disconnected channel: must report failure, not panic.
        assert!(!sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
        assert!(!sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
            wb_do_nothing as WriteBackCallbackType,
            RefAny::new(0_usize),
        ))));
    }
    #[test]
    fn thread_sender_send_survives_a_poisoned_mutex() {
        let (rx, mut sender) = make_sender();
        let arc = Arc::clone(&*sender.ptr);
        let handle = thread::spawn(move || {
            let _guard = arc.lock().expect("mutex is fresh here");
            panic!("intentional poison");
        });
        assert!(handle.join().is_err(), "helper thread must have panicked");
        // ThreadSender::send does `.lock().ok()` — a poisoned lock must degrade to
        // `false`, never to an unwrap panic.
        assert!(!sender.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
        // ... and nothing was actually pushed onto the channel.
        assert!(rx.try_recv().is_err());
    }
    #[test]
    fn thread_sender_get_ctx_is_none_by_default_and_clones_the_payload() {
        let (_rx, mut sender) = make_sender();
        assert_eq!(sender.get_ctx(), OptionRefAny::None);
        sender.ctx = OptionRefAny::Some(RefAny::new(9_usize));
        let OptionRefAny::Some(mut ctx) = sender.get_ctx() else {
            panic!("get_ctx must hand back the ctx that was set");
        };
        assert_eq!(ctx.downcast_ref::<usize>().map(|v| *v), Some(9_usize));
        // get_ctx clones rather than moves: the sender still holds its ctx.
        assert!(matches!(sender.get_ctx(), OptionRefAny::Some(_)));
    }
    #[test]
    fn thread_sender_clone_shares_the_underlying_channel() {
        let (rx, mut sender) = make_sender();
        sender.ctx = OptionRefAny::Some(RefAny::new(3_usize));
        let mut cloned = sender.clone();
        assert!(sender.send(ThreadReceiveMsg::Update(Update::DoNothing)));
        assert!(cloned.send(ThreadReceiveMsg::Update(Update::RefreshDom)));
        // Both endpoints feed the same channel, in order.
        assert_eq!(
            rx.try_recv().ok(),
            Some(ThreadReceiveMsg::Update(Update::DoNothing))
        );
        assert_eq!(
            rx.try_recv().ok(),
            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
        );
        // The FFI ctx survives the clone.
        assert!(matches!(cloned.get_ctx(), OptionRefAny::Some(_)));
    }
    // ==================================================================
    // Private FFI callbacks — the raw-pointer trampolines
    // ==================================================================
    #[test]
    fn default_send_thread_msg_fn_reports_disconnect_instead_of_panicking() {
        let (tx, rx) = channel::<ThreadReceiveMsg>();
        let tx_ptr = core::ptr::from_ref::<Sender<ThreadReceiveMsg>>(&tx).cast::<core::ffi::c_void>();
        assert!(default_send_thread_msg_fn(
            tx_ptr,
            ThreadReceiveMsg::Update(Update::RefreshDom)
        ));
        assert_eq!(
            rx.try_recv().ok(),
            Some(ThreadReceiveMsg::Update(Update::RefreshDom))
        );
        drop(rx);
        assert!(!default_send_thread_msg_fn(
            tx_ptr,
            ThreadReceiveMsg::Update(Update::RefreshDom)
        ));
    }
    #[test]
    fn library_send_thread_msg_fn_reports_disconnect_instead_of_panicking() {
        let (tx, rx) = channel::<ThreadSendMsg>();
        let tx_ptr = core::ptr::from_ref::<Sender<ThreadSendMsg>>(&tx).cast::<core::ffi::c_void>();
        assert!(library_send_thread_msg_fn(tx_ptr, ThreadSendMsg::Tick));
        assert!(library_send_thread_msg_fn(
            tx_ptr,
            ThreadSendMsg::Custom(RefAny::new(4_usize))
        ));
        assert_eq!(rx.try_recv().ok(), Some(ThreadSendMsg::Tick));
        assert!(matches!(rx.try_recv(), Ok(ThreadSendMsg::Custom(_))));
        drop(rx);
        assert!(!library_send_thread_msg_fn(
            tx_ptr,
            ThreadSendMsg::TerminateThread
        ));
    }
    #[test]
    fn library_receive_thread_msg_fn_is_non_blocking_on_empty_and_disconnected() {
        let (tx, rx) = channel::<ThreadReceiveMsg>();
        let rx_ptr =
            core::ptr::from_ref::<Receiver<ThreadReceiveMsg>>(&rx).cast::<core::ffi::c_void>();
        // Empty but connected: must return immediately with None (not block).
        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
        tx.send(ThreadReceiveMsg::Update(Update::RefreshDomAllWindows))
            .expect("receiver is alive");
        assert_eq!(
            library_receive_thread_msg_fn(rx_ptr),
            OptionThreadReceiveMsg::Some(ThreadReceiveMsg::Update(Update::RefreshDomAllWindows))
        );
        // Disconnected: still None, still no panic, and it stays None.
        drop(tx);
        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
        assert_eq!(library_receive_thread_msg_fn(rx_ptr), OptionThreadReceiveMsg::None);
    }
    #[test]
    fn default_receive_thread_msg_fn_is_non_blocking_on_empty_and_disconnected() {
        let (tx, rx) = channel::<ThreadSendMsg>();
        let rx_ptr = core::ptr::from_ref::<Receiver<ThreadSendMsg>>(&rx).cast::<core::ffi::c_void>();
        assert_eq!(default_receive_thread_msg_fn(rx_ptr), OptionThreadSendMsg::None);
        tx.send(ThreadSendMsg::TerminateThread)
            .expect("receiver is alive");
        assert_eq!(
            default_receive_thread_msg_fn(rx_ptr),
            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
        );
        drop(tx);
        assert_eq!(default_receive_thread_msg_fn(rx_ptr), OptionThreadSendMsg::None);
    }
    #[test]
    fn default_check_thread_finished_tracks_the_dropcheck_arc() {
        let alive = Arc::new(());
        let weak = Arc::downgrade(&alive);
        let weak_ptr =
            core::ptr::from_ref::<alloc::sync::Weak<()>>(&weak).cast::<core::ffi::c_void>();
        // Strong ref still held by the (simulated) worker => not finished.
        assert!(!default_check_thread_finished(weak_ptr));
        drop(alive);
        // Worker gone => finished, and the answer is stable across calls.
        assert!(default_check_thread_finished(weak_ptr));
        assert!(default_check_thread_finished(weak_ptr));
    }
    #[test]
    fn sender_and_receiver_drop_stubs_ignore_their_argument() {
        // Both destructors are documented no-ops: they must never dereference the
        // pointer, so even a null one is safe to hand them.
        thread_sender_drop(core::ptr::null_mut::<ThreadSenderInner>());
        thread_receiver_drop(core::ptr::null_mut::<ThreadReceiverInner>());
    }
    // ==================================================================
    // Thread / create_thread_libstd — the live-worker paths
    // ==================================================================
    #[test]
    fn create_thread_libstd_runs_the_callback_and_delivers_its_message() {
        let t = create_thread_libstd(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            ThreadCallback::new(worker_send_update),
        );
        join_worker(&t);
        let mut guard = t.ptr.lock().expect("not poisoned");
        assert!(
            guard.is_finished(),
            "after join the dropcheck Arc must be gone"
        );
        assert_eq!(
            drain(&mut guard),
            vec![ThreadReceiveMsg::Update(Update::RefreshDom)]
        );
    }
    #[test]
    fn thread_create_delivers_a_writeback_message_intact() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_send_writeback as ThreadCallbackType,
        );
        join_worker(&t);
        let mut guard = t.ptr.lock().expect("not poisoned");
        let msgs = drain(&mut guard);
        assert_eq!(msgs.len(), 1);
        let ThreadReceiveMsg::WriteBack(wb) = &msgs[0] else {
            panic!("expected a WriteBack message, got {:?}", msgs[0]);
        };
        assert_eq!(
            wb.callback.cb as *const () as usize,
            wb_do_nothing as *const () as usize
        );
        // The RefAny payload survived the channel hop between threads.
        let mut payload = wb.refany.clone();
        assert_eq!(payload.downcast_ref::<usize>().map(|v| *v), Some(77_usize));
    }
    #[test]
    fn quiet_worker_leaves_the_receive_queue_empty() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_quiet as ThreadCallbackType,
        );
        join_worker(&t);
        let mut guard = t.ptr.lock().expect("not poisoned");
        // try_recv on a worker that sent nothing must be None, never a block/panic.
        assert!(drain(&mut guard).is_empty());
        assert_eq!(guard.receiver_try_recv(), OptionThreadReceiveMsg::None);
    }
    #[test]
    fn thread_destructor_is_idempotent() {
        let t = create_thread_libstd(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            ThreadCallback::new(worker_send_update),
        );
        // Running the destructor twice by hand must not double-join (which would
        // panic / abort); `thread_handle.take()` makes the second call a no-op.
        join_worker(&t);
        join_worker(&t);
        // ... and the real Drop impl will run it a third time when `t` goes away.
        drop(t);
    }
    #[test]
    fn thread_send_message_reaches_the_worker_in_order() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_wait_a as ThreadCallbackType,
        );
        // The worker holds its ThreadReceiver alive, so every send must succeed.
        for _ in 0..3 {
            assert!(t.send_message(ThreadSendMsg::Tick));
        }
        assert!(t.send_message(ThreadSendMsg::Custom(RefAny::new(1_usize))));
        join_worker(&t); // queues TerminateThread behind the 4 messages, then joins
        assert!(WAIT_A_TERMINATED.load(AtomicOrd::SeqCst));
        assert_eq!(WAIT_A_TICKS.load(AtomicOrd::SeqCst), 4);
        let mut guard = t.ptr.lock().expect("not poisoned");
        assert!(guard.is_finished());
        // 3 Ticks echoed back; Custom is counted but not echoed.
        assert_eq!(drain(&mut guard).len(), 3);
    }
    #[test]
    fn thread_send_message_returns_false_once_the_worker_is_gone() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_wait_b as ThreadCallbackType,
        );
        assert!(t.send_message(ThreadSendMsg::Tick));
        join_worker(&t); // worker exits, dropping its Receiver<ThreadSendMsg>
        assert!(WAIT_B_TERMINATED.load(AtomicOrd::SeqCst));
        // Disconnected channel: report false rather than panicking.
        assert!(!t.send_message(ThreadSendMsg::Tick));
        assert!(!t.send_message(ThreadSendMsg::TerminateThread));
    }
    #[test]
    fn thread_is_finished_is_false_while_the_worker_is_alive() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_wait_c as ThreadCallbackType,
        );
        {
            // The dropcheck Arc is moved into the closure before spawn, so it is
            // alive from creation until the worker body returns: deterministic false.
            let guard = t.ptr.lock().expect("not poisoned");
            assert!(!guard.is_finished());
        }
        join_worker(&t);
        assert!(t.ptr.lock().expect("not poisoned").is_finished());
        assert!(WAIT_C_TERMINATED.load(AtomicOrd::SeqCst));
    }
    #[test]
    fn thread_clone_sender_shares_the_worker_channel() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_wait_d as ThreadCallbackType,
        );
        let sender = t.clone_sender().expect("std build must hand back a Sender");
        // Same channel as `send_message`: both reach the worker's receiver, which
        // stays alive until it is told to terminate.
        assert!(sender.send(ThreadSendMsg::Tick).is_ok());
        assert!(t.send_message(ThreadSendMsg::Tick));
        drop(sender);
        join_worker(&t);
        assert!(WAIT_D_TERMINATED.load(AtomicOrd::SeqCst));
        assert_eq!(WAIT_D_TICKS.load(AtomicOrd::SeqCst), 2);
    }
    #[test]
    fn thread_send_message_and_clone_sender_survive_a_poisoned_mutex() {
        let t = Thread::create(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            worker_wait_e as ThreadCallbackType,
        );
        let arc = Arc::clone(&*t.ptr);
        let handle = thread::spawn(move || {
            let _guard = arc.lock().expect("mutex is fresh here");
            panic!("intentional poison");
        });
        assert!(handle.join().is_err(), "helper thread must have panicked");
        // Both accessors are `.lock()`-fallible by design; poison must degrade
        // gracefully, not unwind through the FFI boundary.
        assert!(!t.send_message(ThreadSendMsg::Tick));
        assert!(t.clone_sender().is_none());
        // Teardown still works: Mutex::drop hands out the inner value regardless of
        // poison, so the Drop impl can still terminate + join the worker.
        drop(t);
        assert!(WAIT_E_TERMINATED.load(AtomicOrd::SeqCst));
    }
    #[test]
    fn thread_clone_shares_the_same_inner_state() {
        let t = create_thread_libstd(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            ThreadCallback::new(worker_quiet),
        );
        let cloned = t.clone();
        assert_eq!(Arc::strong_count(&*t.ptr), 2, "clone must be shallow");
        assert!(cloned.run_destructor);
        join_worker(&t);
        // The clone sees the same (now finished) ThreadInner.
        assert!(cloned.ptr.lock().expect("not poisoned").is_finished());
        drop(cloned);
        drop(t);
    }
    #[test]
    fn option_thread_into_option_round_trips() {
        assert!(OptionThread::None.into_option().is_none());
        let t = create_thread_libstd(
            RefAny::new(0_usize),
            RefAny::new(0_usize),
            ThreadCallback::new(worker_quiet),
        );
        let opt: OptionThread = Some(t).into();
        let recovered = opt.into_option().expect("Some must round-trip to Some");
        join_worker(&recovered);
        assert!(recovered.ptr.lock().expect("not poisoned").is_finished());
    }
    // ==================================================================
    // thread_sleep_* — numeric boundaries
    // ==================================================================
    #[test]
    fn thread_sleep_zero_returns_immediately_for_every_unit() {
        let start = StdInstant::now();
        assert_eq!(thread_sleep_ms(0), EmptyStruct::new());
        assert_eq!(thread_sleep_us(0), EmptyStruct::new());
        assert_eq!(thread_sleep_ns(0), EmptyStruct::new());
        // A zero sleep must not become an unbounded one.
        assert!(start.elapsed() < core::time::Duration::from_secs(5));
        assert_eq!(EmptyStruct::new()._reserved, 0);
    }
    #[test]
    fn thread_sleep_sleeps_at_least_the_requested_duration() {
        // std::thread::sleep guarantees *at least* the requested time.
        let start = StdInstant::now();
        let _slept = thread_sleep_ms(5);
        assert!(start.elapsed() >= core::time::Duration::from_millis(5));
        let start = StdInstant::now();
        let _slept = thread_sleep_us(5_000);
        assert!(start.elapsed() >= core::time::Duration::from_micros(5_000));
        let start = StdInstant::now();
        let _slept = thread_sleep_ns(5_000_000);
        assert!(start.elapsed() >= core::time::Duration::from_nanos(5_000_000));
    }
    #[test]
    fn thread_sleep_one_unit_does_not_panic() {
        // Smallest non-zero input in each unit: no truncation panic, no overflow.
        let _ms = thread_sleep_ms(1);
        let _us = thread_sleep_us(1);
        let _ns = thread_sleep_ns(1);
    }
    static MAX_SLEEP_ENTERED: AtomicBool = AtomicBool::new(false);
    static MAX_SLEEP_PANICKED: AtomicBool = AtomicBool::new(false);
    #[test]
    fn thread_sleep_max_converts_without_overflow() {
        // u64::MAX is representable in every Duration constructor these fns use, so
        // the conversion itself must not overflow-panic ...
        let _d_ms = core::time::Duration::from_millis(u64::MAX);
        let _d_us = core::time::Duration::from_micros(u64::MAX);
        let _d_ns = core::time::Duration::from_nanos(u64::MAX);
        // ... but the *sleep* is genuinely unbounded (~584 million years at MAX), so
        // it can only be exercised on a detached thread: assert it reaches the sleep
        // rather than unwinding. Nothing ever joins this thread by design.
        let _detached = thread::spawn(|| {
            MAX_SLEEP_ENTERED.store(true, AtomicOrd::SeqCst);
            if std::panic::catch_unwind(|| {
                let _slept = thread_sleep_ms(u64::MAX);
            })
            .is_err()
            {
                MAX_SLEEP_PANICKED.store(true, AtomicOrd::SeqCst);
            }
        });
        for _ in 0..200 {
            if MAX_SLEEP_ENTERED.load(AtomicOrd::SeqCst) {
                break;
            }
            let _slept = thread_sleep_ms(10);
        }
        assert!(
            MAX_SLEEP_ENTERED.load(AtomicOrd::SeqCst),
            "detached sleeper never started"
        );
        assert!(
            !MAX_SLEEP_PANICKED.load(AtomicOrd::SeqCst),
            "thread_sleep_ms(u64::MAX) must not panic"
        );
    }
}