Skip to main content

ax_runtime/thread/
runtime_impl.rs

1use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
2
3use ax_task::runtime::cpu::{LocalIrqState, PreemptGuardToken};
4
5use super::*;
6
7static SCHED_SWITCH_TRACE_HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
8static SCHED_SWITCH_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
9
10unsafe fn membarrier_ipi_memory_barrier(_arg: *mut ()) {
11    core::sync::atomic::fence(Ordering::SeqCst);
12}
13
14unsafe fn membarrier_ipi_refresh_run_queue(_arg: *mut ()) {
15    ax_task::sync::membarrier::refresh_current_membarrier_run_queue()
16        .unwrap_or_else(|error| panic!("membarrier rq refresh failed in IPI: {error}"));
17}
18
19/// Allocation-free scheduler-switch capture hook installed by an OS layer.
20///
21/// Capture must not acquire task/rq locks. Its optional returned notification
22/// runs in incoming switch completion after those locks have been released.
23pub type SchedSwitchTraceHook = fn(SchedSwitchRecord) -> Option<fn()>;
24
25/// Installs the process-wide scheduler-switch diagnostic consumer.
26///
27/// Reinstalling the same function is harmless; replacing a live consumer is an
28/// invariant violation because switches may concurrently execute the hook.
29pub fn install_sched_switch_trace_hook(hook: SchedSwitchTraceHook) {
30    let hook = hook as *mut ();
31    match SCHED_SWITCH_TRACE_HOOK.compare_exchange(
32        core::ptr::null_mut(),
33        hook,
34        Ordering::AcqRel,
35        Ordering::Acquire,
36    ) {
37        Ok(_) => {}
38        Err(installed) => assert_eq!(installed, hook, "scheduler trace hook already installed"),
39    }
40}
41
42/// Publishes whether the scheduler-switch trace site has active consumers.
43pub fn publish_sched_switch_trace_gate(enabled: bool) {
44    SCHED_SWITCH_TRACE_ENABLED.store(enabled, Ordering::Release);
45}
46
47struct ArceOsTaskRuntime;
48
49impl_task_runtime! {
50    impl TaskRuntime for ArceOsTaskRuntime {
51        unsafe fn task_system_handle() -> TaskSystemHandle {
52            runtime_task_system_handle()
53        }
54
55        unsafe fn current_cpu_owner_handles() -> CurrentCpuOwnerHandles {
56            // SAFETY: the ax-task caller already owns a migration pin. The
57            // callback captures both paired endpoints from that same CPU area
58            // in one transaction.
59            unsafe { with_current_cpu_pin(current_cpu_owner_handles) }
60        }
61
62        unsafe fn current_cpu_remote_handle() -> CpuRemoteHandle {
63            // SAFETY: the ax-task caller keeps the scheduler-owned current
64            // thread fixed. Bootstrap cached this CPU's Arc-backed endpoint
65            // before online publication and retains its TaskSystem owner.
66            unsafe { scheduler_current_cpu_remote_handle() }
67        }
68
69        fn current_thread_identity() -> ThreadIdentityV1 {
70            scheduler_current_thread_identity()
71        }
72
73        fn current_thread_publication() -> CurrentThreadPublication {
74            scheduler_current_thread_publication()
75        }
76
77        fn current_preemption_pending() -> bool {
78            cpu_local::current_preemption_pending().unwrap_or_else(|error| {
79                panic!("current preemption state is unavailable: {error}")
80            })
81        }
82
83        unsafe fn cpu_remote_handle(cpu: RuntimeCpuId) -> CpuRemoteHandle {
84            cpu_remote(cpu).map_or(CpuRemoteHandle::NONE, |cpu| {
85                // SAFETY: TaskSystem owns this Arc-backed CpuRemote endpoint
86                // through shutdown and the lookup preserves its CPU identity.
87                unsafe {
88                    CpuRemoteHandle::from_raw((cpu as *const CpuRemote).expose_provenance())
89                }
90            })
91        }
92
93        unsafe fn current_cpu_id() -> RuntimeCpuId {
94            // SAFETY: the TaskRuntime caller retains a migration pin for the
95            // complete owner-CPU observation.
96            let cpu = unsafe { cpu_local::current_cpu_index() }
97                .unwrap_or_else(|error| panic!("task runtime CPU index is invalid: {error}"));
98            RuntimeCpuId::new(cpu.as_u32())
99        }
100
101        fn prepare_cpu_online(cpu: RuntimeCpuId) -> RuntimeStatus {
102            // SAFETY: this hook runs on the IRQ-excluded owner CPU before
103            // scheduler publication.
104            if cpu != unsafe { Self::current_cpu_id() } {
105                return RuntimeStatus::InvalidArgument;
106            }
107            crate::clock_event_runtime::init_timer();
108            RuntimeStatus::Success
109        }
110
111        fn prepare_cpu_offline(cpu: RuntimeCpuId) -> RuntimeStatus {
112            // SAFETY: this hook runs on the IRQ-excluded owner CPU after
113            // remote admission has closed.
114            if cpu != unsafe { Self::current_cpu_id() } {
115                return RuntimeStatus::InvalidArgument;
116            }
117            // Global TLB quarantine retains its own frames until acknowledged.
118            // Its retries belong to ordinary MM mutation/reclaim paths: taking
119            // kernel_aspace or waiting for remote shootdowns here would invert
120            // the IRQ-off scheduler registry transaction against another CPU.
121            // No recoverable work may follow this publication: it installs the
122            // safe root, clears the CPU-local active handle, and releases the
123            // logical address-space lease in one direction.
124            release_current_active_address_space();
125            crate::clock_event_runtime::take_current_clock_event_offline();
126            RuntimeStatus::Success
127        }
128
129        fn local_irq_save_and_disable() -> LocalIrqState {
130            #[cfg(any(test, feature = "host-test"))]
131            {
132                // Host-only runtime tests have no ArceOS hardware IRQ source.
133                // This empty-domain token belongs to the runtime adapter, not
134                // to a simulated CPU interrupt-enable register.
135                // SAFETY: the matching restore accepts exactly this zero token.
136                unsafe { LocalIrqState::from_raw(0) }
137            }
138            #[cfg(not(any(test, feature = "host-test")))]
139            {
140                let was_enabled = ax_cpu::interrupt::irqs_enabled();
141                ax_cpu::interrupt::disable_irqs();
142                // SAFETY: the matching restore operation interprets only this
143                // boolean state while the caller retains the same CPU.
144                unsafe { LocalIrqState::from_raw(usize::from(was_enabled)) }
145            }
146        }
147
148        unsafe fn local_irq_restore(state: LocalIrqState) {
149            #[cfg(any(test, feature = "host-test"))]
150            {
151                debug_assert_eq!(state.into_raw(), 0);
152            }
153            #[cfg(not(any(test, feature = "host-test")))]
154            {
155                if state.into_raw() != 0 {
156                    ax_cpu::interrupt::enable_irqs();
157                } else {
158                    ax_cpu::interrupt::disable_irqs();
159                }
160            }
161        }
162
163        fn irq_guard_enter() -> IrqGuardToken {
164            #[cfg(any(test, feature = "host-test"))]
165            {
166                // SAFETY: host-test mode models one balanced runtime IRQ token.
167                unsafe { IrqGuardToken::from_raw(1) }
168            }
169            #[cfg(not(any(test, feature = "host-test")))]
170            {
171                if crate::guard::inherits_hardirq_cpu_owner() {
172                    return IrqGuardToken::NONE;
173                }
174                crate::guard::enter_irq();
175                // SAFETY: enter_irq established the matching live guard state.
176                unsafe { IrqGuardToken::from_raw(1) }
177            }
178        }
179
180        unsafe fn irq_guard_exit(token: IrqGuardToken) {
181            #[cfg(not(any(test, feature = "host-test")))]
182            if !token.is_none() {
183                crate::guard::exit_irq("task runtime");
184            }
185            #[cfg(any(test, feature = "host-test"))]
186            let _ = token;
187        }
188
189        fn preempt_guard_enter() -> PreemptGuardToken {
190            #[cfg(any(test, feature = "host-test"))]
191            {
192                // SAFETY: host-test mode models one balanced runtime preemption token.
193                unsafe { PreemptGuardToken::from_raw(1) }
194            }
195            #[cfg(not(any(test, feature = "host-test")))]
196            {
197                match crate::guard::enter_lock_preempt() {
198                    Some(token) => {
199                        // SAFETY: the architecture owner identifies the live
200                        // depth established by enter_lock_preempt.
201                        unsafe { PreemptGuardToken::from_raw(token.into_raw()) }
202                    }
203                    None => PreemptGuardToken::NONE,
204                }
205            }
206        }
207
208        unsafe fn preempt_guard_exit(token: PreemptGuardToken) {
209            assert!(
210                !token.is_none(),
211                "inherited owner scope passed to ordinary preemption exit"
212            );
213            #[cfg(not(any(test, feature = "host-test")))]
214            {
215                let token = unsafe { cpu_local::PreemptionToken::from_raw(token.into_raw()) }
216                .expect("task preemption token must retain its architecture owner");
217                crate::guard::exit_preempt(token);
218            }
219        }
220
221        unsafe fn preempt_guard_exit_irq_return(token: PreemptGuardToken) {
222            assert!(
223                !token.is_none(),
224                "inherited owner scope passed to IRQ-return preemption exit"
225            );
226            #[cfg(not(any(test, feature = "host-test")))]
227            {
228                let token = unsafe { cpu_local::PreemptionToken::from_raw(token.into_raw()) }
229                .expect("IRQ-return token must retain its architecture owner");
230                crate::guard::exit_preempt_from_irq_return(token);
231            }
232        }
233
234        fn hardirq_enter() {
235            #[cfg(feature = "irq-time-accounting")]
236            crate::irq_time::enter();
237        }
238
239        fn hardirq_exit() {
240            #[cfg(feature = "irq-time-accounting")]
241            crate::irq_time::exit();
242        }
243
244        fn publish_local_scheduler_work() -> bool {
245            #[cfg(any(test, feature = "host-test"))]
246            {
247                false
248            }
249            #[cfg(not(any(test, feature = "host-test")))]
250            {
251                crate::guard::publish_local_scheduler_work()
252            }
253        }
254
255        fn finish_context_switch_tail() -> bool {
256            finish_runtime_context_switch_tail()
257        }
258
259        fn finish_initial_context_switch() {
260            crate::guard::finish_initial_context_switch();
261        }
262
263        fn scheduler_frame_guard_enter(
264            origin: ax_task::runtime::switch::RuntimeScheduleOrigin,
265            entry: ax_task::runtime::switch::RuntimeSchedulerEntry,
266        ) -> RuntimeSchedulerFrameEnterResult {
267            crate::guard::enter_scheduler_frame_guard(origin, entry)
268        }
269
270        fn scheduler_frame_guard_exit(
271            return_to: ax_task::runtime::switch::RuntimeSchedulerReturn,
272            needs_reschedule: bool,
273        ) -> bool {
274            crate::guard::exit_scheduler_frame_guard(return_to, needs_reschedule)
275        }
276
277        fn in_hard_irq() -> bool {
278            #[cfg(any(test, feature = "host-test"))]
279            {
280                false
281            }
282            #[cfg(not(any(test, feature = "host-test")))]
283            {
284                ax_hal::irq::in_irq_context()
285            }
286        }
287
288        fn validate_schedule_context(
289            origin: ax_task::runtime::switch::RuntimeScheduleOrigin,
290        ) -> RuntimeStatus {
291            crate::guard::validate_schedule_context(origin)
292        }
293
294        fn validate_owner_cpu_context() -> RuntimeStatus {
295            crate::guard::validate_owner_cpu_context()
296        }
297
298        fn monotonic_now() -> ax_task::time::MonotonicInstant {
299            ax_task::time::MonotonicInstant::from_nanos(
300                ax_hal::time::monotonic_time_nanos(),
301            )
302            .expect("platform monotonic clock exceeded the signed ktime domain")
303        }
304
305        fn rq_clock_sample() -> ax_task::runtime::cpu::RqClockSample {
306            #[cfg(feature = "qperf-metrics")]
307            let clock_started_ns = ax_hal::time::monotonic_time_nanos();
308            // SAFETY: ax-task holds the target runqueue IRQ-save lock, which
309            // pins its owner CPU for this complete local clock sample.
310            let clock_ns = unsafe { ax_hal::time::scheduler_clock_source() }
311                .unwrap_or_else(|error| {
312                    panic!("current scheduler clock source is unavailable: {error}")
313                });
314            #[cfg(feature = "qperf-metrics")]
315            let irq_time_started_ns = ax_hal::time::monotonic_time_nanos();
316            #[cfg(feature = "irq-time-accounting")]
317            let irq_time_ns = crate::irq_time::total_current();
318            #[cfg(feature = "qperf-metrics")]
319            {
320                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
321                    15,
322                    clock_started_ns,
323                    irq_time_started_ns,
324                );
325            }
326            #[cfg(all(feature = "qperf-metrics", feature = "irq-time-accounting"))]
327            {
328                let irq_time_finished_ns = ax_hal::time::monotonic_time_nanos();
329                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
330                    16,
331                    irq_time_started_ns,
332                    irq_time_finished_ns,
333                );
334            }
335            #[cfg(feature = "irq-time-accounting")]
336            return ax_task::runtime::cpu::RqClockSample::new(
337                ax_task::sched::SchedulerTimestamp::from_nanos(clock_ns),
338                irq_time_ns,
339            );
340            #[cfg(not(feature = "irq-time-accounting"))]
341            ax_task::runtime::cpu::RqClockSample::without_irq_time_accounting(
342                ax_task::sched::SchedulerTimestamp::from_nanos(clock_ns),
343            )
344        }
345
346        fn publish_scheduler_deadline(update: ax_task::runtime::cpu::SchedulerDeadlineUpdate) {
347            crate::clock_event_runtime::publish_local_scheduler_deadline(update);
348        }
349
350        fn publish_scheduler_runtime_deadline(
351            update: ax_task::runtime::cpu::SchedulerRuntimeDeadline,
352        ) {
353            crate::clock_event_runtime::publish_local_scheduler_runtime_deadline(update);
354        }
355
356        fn idle_exit_restart_scheduler_tick() {
357            crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(
358                crate::clock_event_runtime::monotonic_now(),
359            );
360        }
361
362        fn notify_scheduler_cpu(cpu: RuntimeCpuId) -> RuntimeStatus {
363            #[cfg(any(feature = "ipi", feature = "wake-ipi"))]
364            {
365                let cpu_id = cpu.as_u32() as usize;
366                if cpu_id >= ax_hal::cpu_num() {
367                    return RuntimeStatus::InvalidArgument;
368                }
369                match ax_ipi::notify_cpu(ax_hal::irq::CpuId(cpu_id)) {
370                    Ok(notification) => {
371                        if notification == ax_ipi::IpiNotification::Sent {
372                            #[cfg(feature = "qperf-metrics")]
373                            record_scheduler_ipi_send();
374                        }
375                        RuntimeStatus::Success
376                    }
377                    Err(ax_hal::irq::IrqError::InvalidCpu) => RuntimeStatus::InvalidArgument,
378                    Err(ax_hal::irq::IrqError::CpuOffline) => RuntimeStatus::NotInitialized,
379                    Err(ax_hal::irq::IrqError::Busy) => RuntimeStatus::Busy,
380                    Err(ax_hal::irq::IrqError::NoMemory) => RuntimeStatus::NoMemory,
381                    Err(ax_hal::irq::IrqError::Unsupported) => RuntimeStatus::Unsupported,
382                    Err(_) => RuntimeStatus::Platform,
383                }
384            }
385            #[cfg(not(any(feature = "ipi", feature = "wake-ipi")))]
386            {
387                let _ = cpu;
388                RuntimeStatus::Unsupported
389            }
390        }
391
392        fn wait_for_interrupt() {
393            // Linux keeps the idle task non-preemptible from do_idle() through
394            // tick_nohz_idle_exit() and enters schedule_idle() only afterwards.
395            // Keep the same ownership across the IRQ-enabled WFI window: an
396            // interrupt may publish need-resched, but its return path cannot
397            // switch away before this scope restores the stopped tick.
398            let idle_exit_guard = crate::task::sync::PreemptGuard::new();
399            ax_cpu::interrupt::disable_irqs();
400            unsafe {
401                // SAFETY: local IRQs remain disabled through the immediately
402                // following task-work and clockevent recheck, matching Linux
403                // `current_clr_polling_and_test()`.
404                ax_task::runtime::cpu::finish_current_cpu_idle_polling()
405            }
406            .expect("idle handoff requires an initialized current CPU");
407            #[cfg(feature = "fault-injection")]
408            let injected_probe = super::creation_probe::publish_idle_probe_at_wait();
409            #[cfg(feature = "fault-injection")]
410            if super::creation_probe::idle_probe_pending() {
411                // The probe mailbox is persistent work, like Linux's
412                // need_resched condition. An already-consumed IPI is not a
413                // substitute for this final IRQ-off observation before WFI.
414                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(
415                    crate::clock_event_runtime::monotonic_now(),
416                );
417                ax_cpu::interrupt::enable_irqs();
418                drop(idle_exit_guard);
419                return;
420            }
421            let mut now = crate::clock_event_runtime::monotonic_now();
422            let mut needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
423                .expect("idle handoff requires an initialized current CPU");
424            if needs_reschedule
425                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
426            {
427                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
428                ax_cpu::interrupt::enable_irqs();
429                drop(idle_exit_guard);
430                return;
431            }
432
433            // Linux NOHZ removes the periodic scheduler tick only after idle
434            // polling is withdrawn. Task deadlines remain selected by the
435            // same physical clockevent and are reprogrammed in this IRQ-off
436            // transaction.
437            crate::clock_event_runtime::stop_current_scheduler_tick_for_idle();
438            now = crate::clock_event_runtime::monotonic_now();
439            needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
440                .expect("idle handoff requires an initialized current CPU");
441            if needs_reschedule
442                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
443            {
444                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
445                ax_cpu::interrupt::enable_irqs();
446                drop(idle_exit_guard);
447                return;
448            }
449
450            #[cfg(feature = "fault-injection")]
451            assert!(!injected_probe || !super::creation_probe::idle_probe_pending(),
452                "idle must recheck pending owner probe before WFI");
453            ax_cpu::interrupt::wait_for_irqs_disabled();
454
455            // A non-scheduling interrupt may leave the CPU in the idle loop,
456            // in which case the tick stays stopped just as in Linux do_idle().
457            // Work that makes the idle thread yield restarts the tick before
458            // the scheduler can select a non-idle thread.
459            let irq_guard = crate::task::sync::IrqSaveGuard::new();
460            now = crate::clock_event_runtime::monotonic_now();
461            needs_reschedule = ax_task::runtime::cpu::current_cpu_needs_resched()
462                .expect("idle wake requires an initialized current CPU");
463            if needs_reschedule
464                || crate::clock_event_runtime::local_clock_event_has_immediate_work(now)
465            {
466                crate::clock_event_runtime::restart_current_scheduler_tick_after_idle(now);
467            }
468            drop(irq_guard);
469            drop(idle_exit_guard);
470        }
471
472        fn allocate_stack(_request: StackRequest) -> RuntimeHandleResult {
473            match allocate_runtime_stack(_request) {
474                Ok(handle) => RuntimeHandleResult::success(handle.into_raw()),
475                Err(status) => RuntimeHandleResult::failure(status),
476            }
477        }
478
479        fn deallocate_stack(_stack: StackHandle) {
480            assert_eq!(
481                deallocate_runtime_stack(_stack),
482                RuntimeStatus::Success,
483                "reclaimable task stack destruction failed"
484            );
485        }
486
487        fn allocate_kernel_tls() -> RuntimeHandleResult {
488            allocate_runtime_tls()
489        }
490
491        fn deallocate_tls(_tls: TlsHandle) {
492            assert_eq!(
493                deallocate_runtime_tls(_tls),
494                RuntimeStatus::Success,
495                "reclaimable task TLS destruction failed"
496            );
497        }
498
499        fn create_kernel_context(_request: KernelContextRequest) -> RuntimeHandleResult {
500            create_runtime_context(_request)
501        }
502
503        fn create_user_context(_request: UserContextRequest) -> RuntimeHandleResult {
504            create_user_runtime_context(_request)
505        }
506
507        fn bind_context_thread(binding: ContextThreadBinding) -> RuntimeStatus {
508            bind_runtime_context_thread(binding)
509        }
510
511        fn destroy_context(_context: ExecutionContextHandle) {
512            assert_eq!(
513                destroy_runtime_context(_context),
514                RuntimeStatus::Success,
515                "task context remained live after scheduler switch tail"
516            );
517        }
518
519        fn destroy_address_space(
520            address_space: AddressSpaceHandle,
521        ) -> AddressSpaceDestroyOutcome {
522            destroy_runtime_address_space(address_space)
523        }
524
525        fn arm_address_space_reclaim(
526            address_space: AddressSpaceHandle,
527        ) -> AddressSpaceReclaimArmOutcome {
528            arm_runtime_address_space_reclaim(address_space)
529        }
530
531        fn address_space_membarrier_state(
532            address_space: AddressSpaceHandle,
533        ) -> AddressSpaceMembarrierState {
534            runtime_address_space_membarrier_state(address_space)
535        }
536
537        fn update_address_space_membarrier_state(
538            address_space: AddressSpaceHandle,
539            registration: MembarrierRegistration,
540            phase: MembarrierRegistrationPhase,
541        ) -> AddressSpaceMembarrierState {
542            update_runtime_address_space_membarrier_state(address_space, registration, phase)
543        }
544
545        fn synchronize_membarrier_cpu(
546            cpu: RuntimeCpuId,
547            action: RuntimeMembarrierAction,
548        ) -> RuntimeStatus {
549            let cpu = cpu.as_u32() as usize;
550            let callback = match action {
551                RuntimeMembarrierAction::MemoryBarrier => membarrier_ipi_memory_barrier,
552                RuntimeMembarrierAction::RefreshRunQueue => {
553                    membarrier_ipi_refresh_run_queue
554                }
555            };
556            #[cfg(feature = "ipi")]
557            {
558                // SAFETY: both callbacks are fixed, allocation-free hard-IRQ
559                // operations and carry no argument lifetime.
560                match unsafe {
561                    crate::ipi_delivery::run_on_cpu_sync(cpu, callback, core::ptr::null_mut())
562                } {
563                    Ok(()) => RuntimeStatus::Success,
564                    Err(ax_hal::irq::IrqError::CpuOffline) => RuntimeStatus::Busy,
565                    Err(ax_hal::irq::IrqError::InvalidCpu) => RuntimeStatus::InvalidArgument,
566                    Err(_) => RuntimeStatus::Platform,
567                }
568            }
569            #[cfg(not(feature = "ipi"))]
570            {
571                // A UP runtime executes the same hard-call ABI locally. SMP
572                // membarrier requires the explicit `ipi` capability.
573                let current = unsafe { Self::current_cpu_id() }.as_u32() as usize;
574                if cpu != current {
575                    return RuntimeStatus::Unsupported;
576                }
577                // SAFETY: the selected fixed callback takes no argument.
578                unsafe { callback(core::ptr::null_mut()) };
579                RuntimeStatus::Success
580            }
581        }
582
583        unsafe fn switch_context(plan: RuntimeSwitchPlan) {
584            // SAFETY: the TaskRuntime contract passes one committed move-only
585            // switch transaction under the active scheduler baton.
586            unsafe { switch_runtime_context(plan) };
587        }
588
589        fn flush_tlb_local(_start: usize, _size: usize) {
590            ax_cpu::mmu::flush_tlb(None);
591        }
592
593        fn trace_sched_switch(record: SchedSwitchRecord) -> Option<fn()> {
594            if !SCHED_SWITCH_TRACE_ENABLED.load(Ordering::Acquire) {
595                return None;
596            }
597            let hook = SCHED_SWITCH_TRACE_HOOK.load(Ordering::Acquire);
598            if hook.is_null() {
599                return None;
600            }
601            // SAFETY: installation accepts exactly this function-pointer type,
602            // and the process-wide hook is never replaced or removed.
603            let hook = unsafe { core::mem::transmute::<*mut (), SchedSwitchTraceHook>(hook) };
604            hook(record)
605        }
606
607        fn emergency_console_write(message: &str) {
608            ax_hal::console::write_bytes(message.as_bytes());
609        }
610
611        fn fatal_invariant(code: u32, argument: usize) -> ! {
612            panic!("ax-task invariant {code} failed with argument {argument:#x}")
613        }
614    }
615}