Skip to main content

ax_task/thread/
spawn.rs

1//! Runtime-backed construction and ownership of portable kernel threads.
2
3use alloc::{boxed::Box, string::String};
4use core::{
5    ptr,
6    sync::atomic::{AtomicBool, Ordering},
7};
8
9use crate::{
10    runtime::{
11        RuntimeStatus,
12        context::{RuntimeIrqGuard, runtime_current_cpu_mut, runtime_task_system},
13        lock::PreemptTicketLock,
14        resource::{
15            ExecutionContextHandle, KernelContextRequest, StackHandle, StackRequest,
16            ThreadResources, TlsHandle,
17        },
18        task_runtime,
19    },
20    sched::{CpuSet, SchedulePolicy},
21    sync::WaitQueue,
22    thread::{
23        SwitchReason, TaskError, ThreadExtension, ThreadExtensionOps, ThreadHandle, ThreadId,
24        ThreadSpec,
25    },
26};
27
28/// Default stack size used by portable kernel service threads.
29pub const DEFAULT_KERNEL_THREAD_STACK_SIZE: usize = 256 * 1024;
30
31/// Resource and diagnostic configuration for one kernel thread.
32#[derive(Debug)]
33pub struct ThreadBuilder {
34    name: String,
35    stack_size: usize,
36    stack_alignment: usize,
37    guard_size: usize,
38    policy: SchedulePolicy,
39    affinity: Option<CpuSet>,
40    os_extension: Option<ThreadExtension>,
41}
42
43impl ThreadBuilder {
44    /// Starts a builder with default portable stack requirements.
45    pub fn new(name: String) -> Self {
46        Self {
47            name,
48            stack_size: DEFAULT_KERNEL_THREAD_STACK_SIZE,
49            stack_alignment: 16,
50            guard_size: 0,
51            policy: SchedulePolicy::default(),
52            affinity: None,
53            os_extension: None,
54        }
55    }
56
57    /// Selects the usable stack size in bytes.
58    pub fn stack_size(mut self, stack_size: usize) -> Self {
59        self.stack_size = stack_size;
60        self
61    }
62
63    /// Selects the stack alignment in bytes.
64    pub fn stack_alignment(mut self, stack_alignment: usize) -> Self {
65        self.stack_alignment = stack_alignment;
66        self
67    }
68
69    /// Requests an inaccessible stack guard area from the runtime.
70    pub fn guard_size(mut self, guard_size: usize) -> Self {
71        self.guard_size = guard_size;
72        self
73    }
74
75    /// Selects the base scheduler policy.
76    pub fn policy(mut self, policy: SchedulePolicy) -> Self {
77        self.policy = policy;
78        self
79    }
80
81    /// Restricts placement to the supplied topology-sized CPU set.
82    pub fn affinity(mut self, affinity: CpuSet) -> Self {
83        self.affinity = Some(affinity);
84        self
85    }
86
87    /// Composes one OS-owned extension inside the portable thread wrapper.
88    ///
89    /// # Safety
90    ///
91    /// `extension` transfers unique callback-data ownership into this builder.
92    /// The caller must not install another copy or invoke its drop callback.
93    /// This builder must be spawned or dropped in ordinary task context.
94    pub unsafe fn extension(mut self, extension: ThreadExtension) -> Self {
95        self.os_extension = Some(extension);
96        self
97    }
98
99    /// Allocates, creates, and enqueues the configured thread.
100    ///
101    /// # Errors
102    ///
103    /// Returns scheduler validation or runtime resource errors from
104    /// runtime allocation and scheduler admission.
105    pub fn spawn<F>(self, entry: F) -> Result<KernelThreadHandle, TaskError>
106    where
107        F: FnOnce() + Send + 'static,
108    {
109        spawn_thread(self, entry)
110    }
111}
112
113/// Join capability for one runtime-backed kernel thread.
114///
115/// Callers must either [`join`](Self::join) a thread that may return or mark a
116/// shutdown-lifetime worker with [`detach_permanent`](Self::detach_permanent).
117#[derive(Debug)]
118#[must_use = "kernel threads must be joined or explicitly detached as permanent"]
119pub struct KernelThreadHandle {
120    thread: Option<ThreadHandle>,
121}
122
123impl KernelThreadHandle {
124    /// Returns the scheduler identity of this kernel thread.
125    pub fn id(&self) -> ThreadId {
126        self.thread
127            .as_ref()
128            .expect("kernel thread handle is consumed only by ownership methods")
129            .id()
130    }
131
132    /// Waits for logical thread exit and hands reclamation to the bounded reaper.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`TaskError::InvalidConfiguration`] when joining the current
137    /// thread and propagates scheduler wait or resource teardown errors.
138    pub fn join(mut self) -> Result<(), TaskError> {
139        let handle = self.thread.take().ok_or(TaskError::InvalidConfiguration)?;
140        if crate::thread::current::current_thread_id()? == handle.id() {
141            return Err(TaskError::InvalidConfiguration);
142        }
143        let data = kernel_thread_data(&handle)?;
144        data.join_wait
145            .try_wait_until(|| data.exit_completed.load(Ordering::Acquire))?;
146        reap_joined_thread(handle)
147    }
148
149    /// Marks a worker as intentionally live until scheduler shutdown.
150    ///
151    /// The entry closure must never return. Shutdown owns the remaining registry
152    /// record and runtime resources; this method performs no hidden reaping.
153    pub fn detach_permanent(mut self) {
154        let _thread = self.thread.take();
155    }
156}
157
158fn reap_joined_thread(mut handle: ThreadHandle) -> Result<(), TaskError> {
159    match runtime_task_system()?.reap_thread_handle(handle) {
160        Ok(()) => Ok(()),
161        Err(error)
162            if matches!(
163                error.task_error(),
164                TaskError::ThreadBusy | TaskError::NotExited
165            ) =>
166        {
167            handle = error.into_retry_handle();
168            drop(handle);
169            Ok(())
170        }
171        Err(error) => Err(error.task_error()),
172    }
173}
174
175impl ThreadBuilder {
176    fn stack_request(&self) -> StackRequest {
177        StackRequest {
178            usable_size: self.stack_size,
179            alignment: self.stack_alignment,
180            guard_size: self.guard_size,
181        }
182    }
183}
184
185/// Creates and enqueues a joinable kernel service thread.
186///
187/// The closure remains inside ax-task-owned extension data. Only opaque stack,
188/// TLS, and context handles cross [`crate::runtime::TaskRuntime`].
189///
190/// # Errors
191///
192/// Returns [`TaskError::NotInitialized`] before the runtime publishes scheduler
193/// objects, [`TaskError::InvalidConfiguration`] for invalid stack requirements,
194/// and [`TaskError::RuntimeFailure`] when a runtime resource operation fails.
195fn spawn_thread<F>(mut spec: ThreadBuilder, entry: F) -> Result<KernelThreadHandle, TaskError>
196where
197    F: FnOnce() + Send + 'static,
198{
199    validate_spec(&spec)?;
200    let system = runtime_task_system()?;
201    let resources = allocate_thread_resources(system, spec.stack_request())?;
202    let extension_data = Box::into_raw(Box::new(KernelThreadData::new(
203        entry,
204        core::mem::take(&mut spec.name),
205        spec.os_extension.take(),
206    )))
207    .expose_provenance();
208    // SAFETY: the boxed data remains live until the scheduler reaper invokes
209    // `kernel_thread_drop` through this exact callback-table identity.
210    let extension = unsafe { ThreadExtension::new(extension_data, &KERNEL_THREAD_OPS) };
211    let mut thread_spec = unsafe {
212        // SAFETY: allocation above created one live, uniquely owned resource
213        // bundle and this specification is its sole installation path.
214        ThreadSpec::new(spec.policy)
215            .with_extension(extension)
216            .with_resources(resources)
217    };
218    if let Some(affinity) = spec.affinity.take() {
219        thread_spec = thread_spec.with_affinity(affinity);
220    }
221    let handle = system.create_thread(thread_spec)?;
222
223    let mut irq_guard = RuntimeIrqGuard::enter();
224    let result = runtime_current_cpu_mut(&mut irq_guard)
225        .and_then(|mut cpu| system.start_thread(cpu.as_mut(), handle.id()));
226    drop(irq_guard);
227    if let Err(error) = result {
228        cleanup_unstarted_thread(system, handle);
229        return Err(error);
230    }
231    Ok(KernelThreadHandle {
232        thread: Some(handle),
233    })
234}
235
236type KernelThreadEntry = Box<dyn FnOnce() + Send + 'static>;
237
238struct KernelThreadData {
239    entry: PreemptTicketLock<Option<KernelThreadEntry>>,
240    join_wait: WaitQueue,
241    exit_completed: AtomicBool,
242    os_extension: Option<ThreadExtension>,
243    _name: String,
244}
245
246impl KernelThreadData {
247    fn new(
248        entry: impl FnOnce() + Send + 'static,
249        name: String,
250        os_extension: Option<ThreadExtension>,
251    ) -> Self {
252        Self {
253            entry: PreemptTicketLock::new(Some(Box::new(entry))),
254            join_wait: WaitQueue::new(),
255            exit_completed: AtomicBool::new(false),
256            os_extension,
257            _name: name,
258        }
259    }
260}
261
262static KERNEL_THREAD_OPS: ThreadExtensionOps = ThreadExtensionOps {
263    on_switch_in: kernel_thread_switch_in,
264    on_switch_out: kernel_thread_switch_out,
265    on_exit: kernel_thread_exit,
266    on_deadline_overrun: kernel_thread_deadline_overrun,
267    drop: kernel_thread_drop,
268};
269
270unsafe extern "Rust" fn kernel_thread_switch_in(
271    data: usize,
272    thread: ThreadId,
273    policy: SchedulePolicy,
274    charged_runtime_ns: u64,
275) {
276    let data = unsafe { kernel_thread_data_from_raw(data) };
277    if let Some(extension) = data.os_extension.as_ref() {
278        // SAFETY: the outer extension owns and forwards the inner callback.
279        unsafe {
280            (extension.ops().on_switch_in)(extension.data(), thread, policy, charged_runtime_ns)
281        };
282    }
283}
284
285unsafe extern "Rust" fn kernel_thread_switch_out(
286    data: usize,
287    thread: ThreadId,
288    reason: SwitchReason,
289) {
290    let data = unsafe { kernel_thread_data_from_raw(data) };
291    if let Some(extension) = data.os_extension.as_ref() {
292        // SAFETY: the outer extension owns and forwards the inner callback.
293        unsafe { (extension.ops().on_switch_out)(extension.data(), thread, reason) };
294    }
295}
296
297unsafe extern "Rust" fn kernel_thread_exit(data: usize, thread: ThreadId) {
298    let data = unsafe { kernel_thread_data_from_raw(data) };
299    if let Some(extension) = data.os_extension.as_ref() {
300        // SAFETY: exit is already deferred to ordinary task context.
301        unsafe { (extension.ops().on_exit)(extension.data(), thread) };
302    }
303    publish_kernel_thread_exit_completion(data);
304}
305
306fn publish_kernel_thread_exit_completion(data: &KernelThreadData) {
307    if data
308        .exit_completed
309        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
310        .is_ok()
311    {
312        data.join_wait.notify_all();
313    }
314}
315
316unsafe extern "Rust" fn kernel_thread_deadline_overrun(data: usize, thread: ThreadId) {
317    let data = unsafe { kernel_thread_data_from_raw(data) };
318    if let Some(extension) = data.os_extension.as_ref() {
319        // SAFETY: Deadline notification runs at a scheduler safe point.
320        unsafe { (extension.ops().on_deadline_overrun)(extension.data(), thread) };
321    }
322}
323
324unsafe extern "Rust" fn kernel_thread_drop(data: usize) {
325    // SAFETY: the extension owns the unique Box pointer until this callback.
326    drop(unsafe { Box::from_raw(ptr::with_exposed_provenance_mut::<KernelThreadData>(data)) });
327}
328
329unsafe fn kernel_thread_data_from_raw(data: usize) -> &'static KernelThreadData {
330    // SAFETY: every outer callback receives the live Box pointer installed with
331    // KERNEL_THREAD_OPS, which remains valid until its drop callback.
332    unsafe { &*ptr::with_exposed_provenance::<KernelThreadData>(data) }
333}
334
335unsafe extern "C" fn kernel_thread_entry() -> ! {
336    if let Err(error) = unsafe {
337        // SAFETY: this is the first operation in a fresh runtime context, which
338        // inherits exactly one scheduler switch guard and consumes it once.
339        crate::runtime::switch::finish_initial_context_switch()
340    } {
341        task_runtime::fatal_invariant(9, error_code(error));
342    }
343    let extension = crate::thread::current::current_thread_extension()
344        .unwrap_or_else(|error| task_runtime::fatal_invariant(10, error_code(error)))
345        .unwrap_or_else(|| task_runtime::fatal_invariant(11, 0));
346    if !core::ptr::eq(extension.ops(), &KERNEL_THREAD_OPS) {
347        task_runtime::fatal_invariant(12, extension.data());
348    }
349    let extension = unsafe {
350        // SAFETY: this trampoline is the running thread named by the lease;
351        // its registry record remains live until the non-returning exit below.
352        extension.release_for_current_thread_entry()
353    };
354    let data_raw = extension.data();
355    // SAFETY: the checked callback-table identity belongs only to
356    // `KernelThreadData`. The registry record retains the extension while the
357    // current thread runs, so the entry trampoline must release its temporary
358    // lease before entering a function that exits without unwinding.
359    let data = unsafe { &*ptr::with_exposed_provenance::<KernelThreadData>(data_raw) };
360    let Some(entry) = data.entry.lock().take() else {
361        task_runtime::fatal_invariant(13, data_raw);
362    };
363    entry();
364    let exit_permit = crate::thread::current::prepare_current_exit()
365        .unwrap_or_else(|error| task_runtime::fatal_invariant(15, error_code(error)));
366    // Logical completion is observable before the final non-returning
367    // schedule-out only after every recoverable scheduler precondition has
368    // been validated. Registry state, `on_cpu`, and the exit callback continue
369    // to gate physical reclamation independently.
370    publish_kernel_thread_exit_completion(data);
371    crate::thread::current::commit_current_exit(exit_permit)
372}
373
374fn validate_spec(spec: &ThreadBuilder) -> Result<(), TaskError> {
375    if spec.stack_size == 0 || spec.stack_alignment == 0 || !spec.stack_alignment.is_power_of_two()
376    {
377        Err(TaskError::InvalidConfiguration)
378    } else {
379        Ok(())
380    }
381}
382
383fn kernel_thread_data(handle: &ThreadHandle) -> Result<&KernelThreadData, TaskError> {
384    let extension = runtime_task_system()?
385        .thread_extension(handle)?
386        .ok_or(TaskError::InvalidConfiguration)?;
387    if !core::ptr::eq(extension.ops(), &KERNEL_THREAD_OPS) {
388        return Err(TaskError::InvalidConfiguration);
389    }
390    // SAFETY: the checked ops identity belongs only to KernelThreadData, and
391    // the returned borrow is bounded by `handle`, which keeps the registry
392    // record live until the caller is finished with the data.
393    Ok(unsafe { &*ptr::with_exposed_provenance::<KernelThreadData>(extension.data()) })
394}
395
396fn allocate_thread_resources(
397    system: &crate::runtime::TaskSystem,
398    request: StackRequest,
399) -> Result<ThreadResources, TaskError> {
400    let stack_result = task_runtime::allocate_stack(request);
401    if stack_result.status != RuntimeStatus::Success {
402        return Err(runtime_error(stack_result.status));
403    }
404    if stack_result.handle == 0 {
405        return Err(TaskError::InvalidRuntimeHandle);
406    }
407    // SAFETY: successful TaskRuntime stack allocation returns one non-zero,
408    // uniquely owned handle that remains live until deallocation.
409    let stack = unsafe { StackHandle::from_raw(stack_result.handle) };
410    let tls_result = task_runtime::allocate_kernel_tls();
411    let tls = match (tls_result.status, tls_result.handle) {
412        (RuntimeStatus::Success, 0) => {
413            return Err(release_partial_thread_resources(
414                system,
415                stack,
416                TlsHandle::NONE,
417                TaskError::InvalidRuntimeHandle,
418            ));
419        }
420        (RuntimeStatus::Success, handle) => {
421            // SAFETY: successful TaskRuntime TLS allocation returns one
422            // non-zero, uniquely owned handle live until deallocation.
423            unsafe { TlsHandle::from_raw(handle) }
424        }
425        (RuntimeStatus::Unsupported, _) => TlsHandle::NONE,
426        (status, _) => {
427            return Err(release_partial_thread_resources(
428                system,
429                stack,
430                TlsHandle::NONE,
431                runtime_error(status),
432            ));
433        }
434    };
435    let context_result = task_runtime::create_kernel_context(KernelContextRequest {
436        stack,
437        entry: kernel_thread_entry,
438        tls,
439    });
440    if context_result.status != RuntimeStatus::Success {
441        return Err(release_partial_thread_resources(
442            system,
443            stack,
444            tls,
445            runtime_error(context_result.status),
446        ));
447    }
448    if context_result.handle == 0 {
449        return Err(release_partial_thread_resources(
450            system,
451            stack,
452            tls,
453            TaskError::InvalidRuntimeHandle,
454        ));
455    }
456    Ok(unsafe {
457        // SAFETY: all handles were just created by the active runtime and their
458        // unique destruction rights move into the returned bundle.
459        ThreadResources::new(
460            ExecutionContextHandle::from_raw(context_result.handle),
461            stack,
462            tls,
463            crate::runtime::resource::AddressSpaceToken::NONE,
464        )
465    })
466}
467
468fn release_partial_thread_resources(
469    system: &crate::runtime::TaskSystem,
470    stack: StackHandle,
471    tls: TlsHandle,
472    creation_error: TaskError,
473) -> TaskError {
474    let resources = unsafe {
475        // SAFETY: this construction transaction uniquely owns both successful
476        // allocations and has not created an execution context.
477        ThreadResources::new(
478            ExecutionContextHandle::NONE,
479            stack,
480            tls,
481            crate::runtime::resource::AddressSpaceToken::NONE,
482        )
483    };
484    system.release_unpublished_resources(resources);
485    creation_error
486}
487
488fn cleanup_unstarted_thread(system: &crate::runtime::TaskSystem, handle: ThreadHandle) {
489    let thread = handle.id();
490    let _result = system.mark_exited(thread);
491    drop(handle);
492    let _result = system.reap_thread(thread);
493}
494
495const fn runtime_error(status: RuntimeStatus) -> TaskError {
496    TaskError::RuntimeFailure(status as u32)
497}
498
499const fn error_code(error: TaskError) -> usize {
500    match error {
501        TaskError::NotInitialized => 1,
502        TaskError::InvalidRuntimeHandle => 2,
503        TaskError::NoRunnableThread => 3,
504        TaskError::UnsafeContext => 4,
505        _ => 255,
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use core::sync::atomic::{AtomicUsize, Ordering};
512
513    use super::*;
514
515    static TEST_EXTENSION_OPS: ThreadExtensionOps = ThreadExtensionOps {
516        on_switch_in: test_extension_switch_in,
517        on_switch_out: test_extension_switch_out,
518        on_exit: test_extension_hook,
519        on_deadline_overrun: test_extension_hook,
520        drop: test_extension_drop,
521    };
522
523    #[test]
524    fn dropping_unspawned_builder_releases_owned_extension() {
525        let drops = AtomicUsize::new(0);
526        let extension = unsafe {
527            // SAFETY: the builder is dropped synchronously while `drops` lives.
528            ThreadExtension::new(
529                (&drops as *const AtomicUsize).expose_provenance(),
530                &TEST_EXTENSION_OPS,
531            )
532        };
533        let builder = unsafe {
534            // SAFETY: this test transfers the sole callback ownership.
535            ThreadBuilder::new(String::from("drop-test")).extension(extension)
536        };
537
538        drop(builder);
539
540        assert_eq!(drops.load(Ordering::Acquire), 1);
541    }
542
543    #[test]
544    fn invalid_spec_releases_extension_before_runtime_lookup() {
545        let drops = AtomicUsize::new(0);
546        let extension = unsafe {
547            // SAFETY: invalid-spec validation drops the extension synchronously.
548            ThreadExtension::new(
549                (&drops as *const AtomicUsize).expose_provenance(),
550                &TEST_EXTENSION_OPS,
551            )
552        };
553        let spec = unsafe {
554            // SAFETY: this test transfers the sole callback ownership.
555            ThreadBuilder::new(String::from("invalid-test"))
556                .stack_size(0)
557                .extension(extension)
558        };
559
560        let result = validate_spec(&spec);
561        drop(spec);
562
563        assert_eq!(result.unwrap_err(), TaskError::InvalidConfiguration);
564        assert_eq!(drops.load(Ordering::Acquire), 1);
565    }
566
567    unsafe extern "Rust" fn test_extension_hook(_data: usize, _thread: ThreadId) {}
568
569    unsafe extern "Rust" fn test_extension_switch_in(
570        _data: usize,
571        _thread: ThreadId,
572        _policy: SchedulePolicy,
573        _charged_runtime_ns: u64,
574    ) {
575    }
576
577    unsafe extern "Rust" fn test_extension_switch_out(
578        _data: usize,
579        _thread: ThreadId,
580        _reason: SwitchReason,
581    ) {
582    }
583
584    unsafe extern "Rust" fn test_extension_drop(data: usize) {
585        // SAFETY: each test supplies a live AtomicUsize for the synchronous drop.
586        let drops = unsafe { &*ptr::with_exposed_provenance::<AtomicUsize>(data) };
587        drops.fetch_add(1, Ordering::AcqRel);
588    }
589}