blob: beb5f93ad4d9f61eee15727b814c9b95e56b4a35 [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//! # pw_kernel User Space API
//!
//! ## Core Concepts
//!
//! ### Objects
//! Objects are the basic building block of functionality exposed to user space.
//! They are polymorphic and may be one of a limited set of types:
//!
//! - [Channel](#channel)
//! - [Wait Group](#wait-group)
//! - TODO: <https://pwbug.dev/496975012> - add other objects.
//!
//! ### Handles
//! All system calls reference objects through a u32 handle which indexes into
//! a process-local handle table.
//!
//! ### Signals and Waiting
//! Every kernel object has a set of signals that can be pending and waited
//! upon. The exact meaning and semantics of each signal vary between kernel
//! objects. The signal types are:
//! - `Signals::READABLE`: Object is readable.
//! - `Signals::WRITABLE`: Object is writable.
//! - `Signals::ERROR`: Object is in an error state.
//! - `Signals::USER`: User defined signal. Useful for out of band signaling
//! between peers on a [Channel](#channel).
//!
//! Any kernel object can be waited for signals to assert using the
//! [`object_wait()`] syscall. Multiple objects can be waited on simultaneously
//! using a [Wait Group](#wait-group) object.
//!
//! #### Open Questions
//! - What are the multi-threaded semantics of waiting.
//!
//! ### Object and Handle Creation
//! Initially only statically defined and allocated objects and handles are
//! supported. The creation of these will be driven through build time
//! configuration and the necessary code will be generated to allocate kernel
//! data structures as well as expose handle definitions to user space
//! processes.
//!
//! ## Object Types
//!
//! ### Channel
//! A channel is a unidirectional connection between two asymmetric peers: an
//! initiator and a handler. A channel allows the initiator peer to send a
//! buffer of data to the handler peer and wait for it's response. A channel
//! can have a maximum of one transaction pending at a time and is designed to
//! not require intermediate kernel buffers.
//!
//! Both a synchronous and asynchronous API is offered to the initiator
//! while the handler side API is strictly non-blocking. All data copies
//! between peers happen during the system call.
//!
//! The flow of a transaction is as follows:
//! - The initiator starts the transaction by providing send and receive
//! buffers to one of the two transact system calls ([`channel_transact()`] or
//! [`channel_async_transact()`]). This has the additional side effect of
//! clearing `Signals::READABLE` and `Signals::WRITABLE` on the initiator.
//! - The handler's `Signals::READABLE` will become asserted.
//! - The handler can now read the message in multiple calls to
//! [`channel_read()`] causing the kernel to copy the data from the
//! initiator's send buffer to the buffer provided to [`channel_read()`].
//! - The handler completes the transaction by calling [`channel_respond()`]
//! and providing a response. The kernel will immediately copy the response
//! from the handler's buffer to the initiator's receive buffer.
//! There is no built in mechanism for the handler to signal an error
//! to the initiator. This is left to the higher level protocol used to
//! communicate over the channel. This will clear `Signals::READABLE` and
//! `Signals::WRITABLE` on the handler and raise `Signals::READABLE` on
//! the initiator.
//!
//! The handler's only ways of communicating with the initiator are by
//! - responding to an initiated transaction
//! - raising `Signals::USER` on the initiator by calling
//! [`object_set_peer_user_signal()`]
//!
//! #### Initiator Signals
//! - `Signals::WRITABLE` indicates there is no pending transaction and one
//! can be started. Cleared on transaction initiation.
//! - `Signals::READABLE` indicates the handler has responded to the
//! pending transaction. Cleared on transaction initiation.
//! - `Signals::ERROR` indicates pending transaction has an error. Cleared
//! when the initiator is waited on.
//! - `Signals::USER` indicates the handler calls [`object_set_peer_user_signal()`].
//! Cleared when the handler calls [`object_set_peer_user_signal()`] with `set=false`,
//! or automatically by the kernel if the initiator handler terminates. Preserved on
//! initiator process termination.
//!
//! #### Handler Signals
//! - `Signals::READABLE` indicates there is a pending transaction. Cleared when
//! the handler calls [`channel_respond()`].
//! - `Signals::WRITABLE` indicates there is a pending transaction. Cleared when
//! the handler calls [`channel_respond()`].
//! - `Signals::ERROR` indicates a pending transaction error. No error states
//! are defined at the moment. In the future an error may be raised when
//! the remote peer closes.
//! - `Signals::USER` indicates the initiator calls [`object_set_peer_user_signal()`].
//! Cleared when the initiator calls [`object_set_peer_user_signal()`] with `set=false`,
//! or automatically by the kernel if the initiator process terminates. Preserved on
//! handler process termination.
//!
//! ### Wait Group
//! Wait groups provide a mechanism for waiting on multiple handles at once.
//! Handles can be added to and removed from a wait group with [`wait_group_add()`]
//! and [`wait_group_remove()`]. In addition to a set of signals to wait on,
//! an arbitrary `user_data` is provided to [`wait_group_add()`]. This value
//! is returned by [`object_wait()`], unmodified by the kernel, when the wait group is
//! waited on.
//!
//! #### Open questions:
//! - How is the wait group's member list allocated in there kernel. To support
//! a fully statically allocated kernel one of two approaches are being
//! considered:
//! - an object (or possibly handle) may only be in a single wait group at
//! one time. This allows a wait group to maintain an intrusive list of
//! objects with the list element storage being stored in the object.
//! - wait queues are statically sized at compile time and adding more objects
//! than there is space for will return an error.
//!
//! ### Interrupt
//! Interrupt objects provide a mechanism for handling hardware interrupts.
//! A single Interrupt object can be configured to handle multiple interrupt
//! sources (IRQs), up to a maximum of 16 per object.
//! Each interrupt source handled by an Interrupt object is mapped to a unique
//! signal bit within the higher 16 bits of `Signals`. These signals
//! range from `Signals::INTERRUPT_A` (bit 16) to `Signals::INTERRUPT_O` (bit 30).
//!
//! When a interrupt occurs for a source handled by an Interrupt object, the kernel
//! masks the interrupt and then signals the corresponding `INTERRUPT_` bit on the
//! object. Userspace threads can wait on the Interrupt object using the
//! [`object_wait()`] syscall.
//!
//! Upon waking from `object_wait()`, the returned `Signals` mask indicates which
//! interrupt(s) have triggered. After the userspace handler has serviced the
//! interrupt(s), it must call the [`interrupt_ack()`] syscall to allow the
//! interrupt(s) to be triggered again. This syscall must be provided with a
//! `Signals` mask containing the bits for the interrupts that have been
//! handled.
//!
//! [`interrupt_ack()`] accomplishes two things:
//! 1. It clears the specified signal bits on the Interrupt object, allowing it to
//! receive new signals for those interrupts.
//! 2. It signals to the underlying hardware interrupt controller (e.g., PLIC or
//! NVIC) that the associated IRQ(s) have been acknowledged, re-enabling them
//! at the hardware level.
//!
//! ### Futex
//! In design
//!
//! ## System Calls
//! The C ABI system calls listed here are not intended to be called directly
//! by user space code and instead be accessed through language idiomatic
//! wrapper libraries.
//!
//! ### Generic Syscalls
//! - [`object_wait()`]
//! - [`object_set_peer_user_signal()`]
//!
//! ### Channel Initiator Syscalls
//! - [`channel_transact()`]
//! - [`channel_async_transact()`]
//! - [`channel_async_transact_complete()`]
//! - [`channel_async_cancel()`]
//!
//! ### Channel Handler Syscalls
//! - [`channel_read()`]
//! - [`channel_respond()`]
//!
//! ### Wait Group Syscalls
//! - [`wait_group_add()`]
//! - [`wait_group_remove()`]
#![no_std]
use bitflags::bitflags;
use pw_status::{Error, Result};
pub struct SysCallReturnValue {
// The return value representing the two return registers.
pub value: [usize; 2],
}
// Errors are encoded as negative values in the first return value of syscalls.
// This requires that:
// 1) Error variants values are no greater than isize::MAX (satisfied by inspection)
// 2) Error fits in a single usize. (satisfied by the assertion below)
const _: () = assert!(core::mem::size_of::<Error>() <= core::mem::size_of::<usize>());
impl From<SysCallReturnValue> for Result<()> {
fn from(ret_value: SysCallReturnValue) -> Result<()> {
let val = ret_value.value[0].cast_signed();
if val < 0 {
// TODO debug assert if error number is out of range
let val = (-val).cast_unsigned();
// TODO(421404517): Avoid the lossy cast
#[allow(clippy::cast_possible_truncation)]
Err(unsafe { core::mem::transmute::<u32, Error>(val as u32) })
} else {
Ok(())
}
}
}
impl From<SysCallReturnValue> for Result<u32> {
fn from(ret_value: SysCallReturnValue) -> Result<u32> {
let val = ret_value.value[0].cast_signed();
if val < 0 {
// TODO debug assert if error number is out of range
let val = (-val).cast_unsigned();
// TODO(421404517): Avoid the lossy cast
#[allow(clippy::cast_possible_truncation)]
Err(unsafe { core::mem::transmute::<u32, Error>(val as u32) })
} else {
// TODO(421404517): Avoid the lossy cast
#[allow(clippy::cast_possible_truncation)]
Ok(ret_value.value[0] as u32)
}
}
}
impl From<i64> for SysCallReturnValue {
fn from(value: i64) -> Self {
let unsigned_value = value.cast_unsigned();
Self {
#[expect(clippy::cast_possible_truncation)]
value: [
unsigned_value as u32 as usize,
(unsigned_value >> 32) as u32 as usize,
],
}
}
}
impl From<u64> for SysCallReturnValue {
fn from(value: u64) -> Self {
Self {
#[expect(clippy::cast_possible_truncation)]
value: [value as u32 as usize, (value >> 32) as u32 as usize],
}
}
}
impl From<SysCallReturnValue> for u64 {
fn from(ret_value: SysCallReturnValue) -> u64 {
#[allow(clippy::cast_possible_truncation)]
let low = ret_value.value[0] as u32;
#[allow(clippy::cast_possible_truncation)]
let high = ret_value.value[1] as u32;
(u64::from(high) << 32) | u64::from(low)
}
}
impl From<Result<u64>> for SysCallReturnValue {
fn from(value: Result<u64>) -> Self {
match value {
Ok(val) => {
#[expect(clippy::cast_possible_truncation)]
let low = val as u32 as usize;
let high = (val >> 32) as u32 as usize;
Self { value: [low, high] }
}
Err(error) => Self::from(-(error as i64)),
}
}
}
impl From<Result<()>> for SysCallReturnValue {
fn from(value: Result<()>) -> Self {
match value {
Ok(()) => Self { value: [0, 0] },
Err(error) => Self::from(-(error as i64)),
}
}
}
impl From<SysCallReturnValue> for Result<WaitReturn> {
fn from(ret: SysCallReturnValue) -> Result<WaitReturn> {
// TODO(421404517): Avoid the lossy cast
#[allow(clippy::cast_possible_truncation)]
let val = ret.value[1] as u32;
let signals = Signals::from_bits_truncate(val);
// Encode that there was an error in the reserved high bit of
// the signal mask, and store the error in the data field.
if signals.contains(Signals::RESERVED) {
// TODO(421404517): Avoid the lossy cast
#[allow(clippy::cast_possible_truncation)]
Err(unsafe { core::mem::transmute::<u32, Error>(ret.value[0] as u32) })
} else {
Ok(WaitReturn {
user_data: ret.value[0],
pending_signals: signals,
})
}
}
}
impl From<Result<WaitReturn>> for SysCallReturnValue {
fn from(value: Result<WaitReturn>) -> Self {
match value {
Ok(val) => Self {
value: [val.user_data, val.pending_signals.bits() as usize],
},
Err(error) => Self {
// Error value is saved in the data field and the reserved bit
// is used to indicate an error in the signal mask.
value: [error as usize, Signals::RESERVED.bits() as usize],
},
}
}
}
impl From<Result<ExitStatus>> for SysCallReturnValue {
fn from(value: Result<ExitStatus>) -> Self {
match value {
Ok(status) => Self {
// SAFETY: ExitStatus is repr(C, usize) and fits exactly in [usize; 2]
// with no padding, as verified by the static assert.
value: unsafe { core::mem::transmute::<ExitStatus, [usize; 2]>(status) },
},
Err(error) => Self::from(-(error as i64)),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u16)]
pub enum SysCallId {
// IDs are not ABI stable yet and are subject to change.
SysCallLowerBound = 0x0000,
ChannelAsyncCancel = 0x0001,
ChannelAsyncTransact = 0x0002,
ChannelAsyncTransactComplete = 0x0003,
ChannelRead = 0x0004,
ChannelRespond = 0x0005,
ChannelTransact = 0x0006,
InterruptAck = 0x0007,
ObjectWait = 0x0008,
ProcessExit = 0x0009,
ProcessStart = 0x000a,
RaisePeerUserSignal = 0x000b,
TaskJoin = 0x000c,
TaskTerminate = 0x000d,
ThreadExit = 0x000e,
ThreadStart = 0x000f,
WaitGroupAdd = 0x0010,
WaitGroupRemove = 0x0011,
// Upper bound for standard system calls.
SysCallUpperBound = 0x0012,
// System calls prefixed with 0xF000 are reserved development/debugging use.
DebugSysCallLowerBound = 0xf000,
DebugPutc = 0xf001,
DebugShutdown = 0xf002,
DebugLog = 0xf003,
DebugNop = 0xf004,
DebugTriggerInterrupt = 0xf005,
DebugClockNow = 0xf006,
// Upper bound for debug system calls.
DebugSysCallUpperBound = 0xf007,
}
impl TryFrom<u16> for SysCallId {
type Error = Error;
fn try_from(value: u16) -> Result<Self> {
if (value > SysCallId::SysCallLowerBound as u16
&& value < SysCallId::SysCallUpperBound as u16)
|| (value > SysCallId::DebugSysCallLowerBound as u16
&& value < SysCallId::DebugSysCallUpperBound as u16)
{
// SAFETY: The value is guaranteed to be a valid SysCallId variant
// because it falls within the valid ranges of defined variants.
Ok(unsafe { core::mem::transmute::<u16, SysCallId>(value) })
} else {
Err(Error::InvalidArgument)
}
}
}
/// A set of object signals
#[derive(Copy, Clone, PartialEq)]
#[repr(C)]
pub struct Signals(u32);
bitflags! {
impl Signals: u32 {
/// Object is readable.
const READABLE = 1 << 0;
/// Object is writeable.
const WRITEABLE = 1 << 1;
/// Object is in an error state.
const ERROR = 1 << 2;
/// Thread or Process has terminated and can be joined.
const JOINABLE = 1 << 3;
/// Object has a protocol specific user signal pending.
const USER = 1 << 15;
/// Bits 16-30 are used to denote which interrupt on the
/// interrupt object was signaled. They are intentionally
/// named by letter not number so as not to confuse the
/// position within an object mask to the IRQ number.
const INTERRUPT_A = 1 << 16;
const INTERRUPT_B = 1 << 17;
const INTERRUPT_C = 1 << 18;
const INTERRUPT_D = 1 << 19;
const INTERRUPT_E = 1 << 20;
const INTERRUPT_F = 1 << 21;
const INTERRUPT_G = 1 << 22;
const INTERRUPT_H = 1 << 23;
const INTERRUPT_I = 1 << 24;
const INTERRUPT_J = 1 << 25;
const INTERRUPT_K = 1 << 26;
const INTERRUPT_L = 1 << 27;
const INTERRUPT_M = 1 << 28;
const INTERRUPT_N = 1 << 29;
const INTERRUPT_O = 1 << 30;
/// Reserved for internal kernel use.
const RESERVED = 1 << 31;
}
}
impl Signals {
#[must_use]
pub const fn no_active() -> Self {
Self(0)
}
}
/// Return value from the [`object_wait()`] syscall.
#[derive(Copy, Clone, PartialEq)]
#[repr(C)]
pub struct WaitReturn {
/// `user_data` of the wait group member.
pub user_data: usize,
/// Signals pending on the object.
pub pending_signals: Signals,
}
pub use exit_status::ExitStatus;
impl SysCallReturnValue {
/// Converts a `SysCallReturnValue` to a `Result<ExitStatus>`.
///
/// # Safety
/// The caller must guarantee that if the `SysCallReturnValue` does not represent
/// a system error (i.e., `value[0]` interpreted as signed is positive), then
/// `value` contains a valid bit pattern for `ExitStatus` (the tag in
/// `value[0]` corresponds to a valid variant of `ExitStatus`). If the value
/// does represent a system error, the caller guarantees that it is a valid
/// value of `Error`
///
/// These preconditions are satisfied the return values of the `thread_terminate()`
/// and `process_terminate()` system calls.
pub unsafe fn to_exit_status(self) -> Result<ExitStatus> {
let val = self.value[0].cast_signed();
if val < 0 {
let val = (-val).cast_unsigned();
// SAFETY: Caller guarantees a valid `Error` value.
#[allow(clippy::cast_possible_truncation)]
Err(unsafe { core::mem::transmute::<u32, Error>(val as u32) })
} else {
// SAFETY: The caller guarantees that if it is not an error, the value
// corresponds to a valid variant of `ExitStatus`.
Ok(unsafe { core::mem::transmute::<[usize; 2], ExitStatus>(self.value) })
}
}
}
unsafe extern "C" {
/// Perform a synchronous channel transaction
///
/// Performs a transaction from the initiator side of a channel and blocks
/// until the handler side has completed the transaction. `send_data` and
/// `recv_data` may overlap or be the same buffer.
///
/// While non-block and infinite blocking semantics are not explicitly
/// supported, they can be effectively achieved with:
/// - Non-blocking: `deadline` == 0
/// - Infinite blocking: `deadline` == `u64::MAX`
///
/// This call will cause `Signals::READABLE` to be cleared on the initiator
/// channel object at the beginning of execution. However by the time it
/// returns without error, `Signals::READABLE` will be set again.
///
/// The maximum size of buffers that may be passed is `isize::MAX`.
///
/// # Returns
/// - `>=0`: Number of bytes received from the handler side.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid initiator
/// channel object.
/// - [`Error::ResourceExhausted`]: The channel already has a pending transaction.
/// - [`Error::PermissionDenied`]: `send_data` or `recv_data` do not reference
/// valid memory regions in this processes' address space.
/// - [`Error::DeadlineExceeded`]: The handler side did not respond before
/// `deadline` was exceeded.
pub fn channel_transact(
object_handle: u32,
send_data: *mut u8,
send_len: usize,
recv_data: *mut u8,
recv_len: usize,
deadline: u64,
) -> isize;
/// Perform an asynchronous channel transaction
///
/// Initiates a transaction from the initiator side of a channel.
/// send_data` must remain valid and readable and `recv_data` must remain
/// valid and writeable for the duration of the transaction (completed or
/// canceled.) `send_data` and `recv_data` may overlap or be the same
/// buffer.
///
/// This call will cause `Signals::READABLE` to be cleared on the initiator
/// channel object. It will be signaled by the handler side when it
/// responds.
///
/// # Returns
/// - `0`: Transaction was successfully initiated.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid initiator
/// channel object.
/// - [`Error::ResourceExhausted`]: The channel already has a pending transaction.
/// - [`Error::PermissionDenied`]: `send_data` or `recv_data` do not reference
/// valid memory regions in this processes' address space.
pub fn channel_async_transact(
object_handle: u32,
send_data: *const u8,
send_len: usize,
recv_data: *mut u8,
recv_len: usize,
) -> isize;
/// Completes an asynchronous channel transaction
///
/// # Returns
/// - `>=0`: Number of bytes received from the handler side.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid initiator
/// channel object.
/// - [`Error::Unavailable`]: No transaction was pending on the channel.
pub fn channel_async_transact_complete(object_handle: u32) -> isize;
/// Cancels a pending transaction on a channel
///
/// # Returns
/// - `0`: Pending transaction was successfully canceled.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid initiator
/// channel object.
/// - [`Error::Unavailable`]: No transaction was pending on the channel.
pub fn channel_async_cancel(object_handle: u32) -> isize;
/// Perform a non-blocking read from a pending transaction.
///
/// Attempts to read up to `buf_len` bytes from the `send_buffer` of the
/// pending transaction starting from `offset`. The kernel will copy the
/// data from the initiators `send_buffer` into `buffer` before returning.
///
/// The maximum size of buffer that may be passed is `isize::MAX`.
///
/// # Returns
/// - `>=0`: Number of bytes read from the send_buffer.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid handler
/// channel object.
/// - [`Error::OutOfRange`]: A read was requested outside the bound of the
/// initiator's `send_buffer`.
/// - [`Error::Unavailable`]: No transaction was pending on the channel.
/// This can happen in the middle of handling a transaction if the initiator
/// cancels the transaction.
/// - [`Error::PermissionDenied`]: `buffer` does not reference a valid memory
/// region in this processes' address space.
/// - TODO: What error should be returned if the initiator's `send_buffer`
/// is invalid. Is that also permission denied?
pub fn channel_read(
object_handle: u32,
offset: usize,
buffer: *mut u8,
buffer_len: usize,
) -> isize;
/// Respond to and complete a pending transaction
///
/// Causes the kernel to copy `buffer` into the initiator's `recv_buffer`
/// and set `Signals::READABLE` on the initiator channel object.
///
/// The maximum size of buffer that may be passed is `isize::MAX`.
///
/// # Returns
/// - `0`: On success.
/// - [`Error::OutOfRange`]: The initiator's `recv_buffer` is not large enough
/// to fit the provided `buffer`.
/// - [`Error::Unavailable`]: No transaction was pending on the channel.
/// This can happen in the middle of handling a transaction if the initiator
/// cancels the transaction.
/// - [`Error::PermissionDenied`]: `buffer` does not reference a valid memory
/// region in this processes' address space.
pub fn channel_respond(object_handle: u32, buffer: *mut u8, buffer_len: usize) -> isize;
/// Set or clear `Signals::USER` on the paired peer.
///
/// Modeled after a level-triggered hardware interrupt: the signaler owns
/// the signal and controls it in both directions. `set != 0` raises USER
/// on the peer; `set == 0` lowers USER on the peer. The receiver does
/// not clear the signal — only the sender does.
///
/// If the signaler process terminates or is reset, the kernel automatically
/// lowers `Signals::USER` on the peer object to prevent signal leaks.
/// Symmetrically, if the receiver process terminates and restarts, its own
/// `Signals::USER` is preserved as long as the signaler remains alive and
/// has not lowered it.
///
/// # Returns
/// - `0`: On success.
/// - [`Error::Unimplemented`]: `object_handle` does not support peer signaling.
/// - [`Error::FailedPrecondition`]: No active peer (e.g., handler not yet wired).
pub fn object_set_peer_user_signal(object_handle: u32, set: u32) -> isize;
/// Acknowledges the signaled interrupts allowing them to be signaled again.
///
/// This must be called by the interrupt handler after processing the
/// interrupts received via an interrupt object. It signals to the
/// underlying interrupt controller that the interrupt handling is complete
/// and can be signaled again.
///
/// # Returns
/// - `0`: On success.
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid Interrupt
/// object, or the `signal_mask` is invalid.
pub fn interrupt_ack(object_handle: u32, signal_mask: Signals) -> isize;
//
// waiting
//
// Need to define multi threaded semantics
/// Wait on a single object
///
/// Waits for one of the signals in `signal_mask` to be pending on `object_handle`.
///
/// # Returns
/// - `WaitReturn.status >= 0`: Returns object specific metadata
/// - [`Error::InvalidArgument`]: `object_handle` is not a valid object or the object is a
/// wait group and there are no members in the wait group.
/// - [`Error::DeadlineExceeded`]: The handler side did not respond before
/// `deadline` was exceeded.
pub fn object_wait(object_handle: u32, signal_mask: Signals, deadline: u64) -> WaitReturn;
/// Adds an object to a wait group
///
/// Add `object` to `wait_group`. `wait_group` will signal when one of the
/// signals in `signal_mask` is raised on `object`. `user_data` is passed,
/// untouched by the kernel to the return value of `object_wait()`.
///
/// The order in which the object is added does not affect the order in
/// which objects in the `wait_group` are signaled.
///
/// # Returns
/// `0`: Success
/// - [`Error::InvalidArgument`]: `wait_group` is not a valid wait group or
/// `object` is not a valid object.
/// - [`Error::ResourceExhausted`]: `object` is already in a wait group.
pub fn wait_group_add(
wait_group: u32,
object: u32,
signal_mask: Signals,
user_data: usize,
) -> isize;
/// Removes an object from a wait group
///
/// # Returns
/// `0`: Success
/// - [`Error::InvalidArgument`]: `wait_group` is not a valid wait group or
/// `object` is not a valid object.
/// - [`Error::NotFound`]: `object` is not in `wait_group`.
pub fn wait_group_remove(wait_group: u32, object: u32) -> isize;
}
pub trait SysCallInterface {
fn object_wait(handle: u32, signal_mask: u32, deadline: u64) -> Result<WaitReturn>;
fn wait_group_add(
wait_group: u32,
object: u32,
signal_mask: Signals,
user_data: usize,
) -> Result<()>;
fn wait_group_remove(wait_group: u32, object: u32) -> Result<()>;
#[expect(clippy::missing_safety_doc)]
unsafe fn channel_transact(
handle: u32,
send_data: *const u8,
send_len: usize,
recv_data: *mut u8,
recv_len: usize,
deadline: u64,
) -> Result<u32>;
#[expect(clippy::missing_safety_doc)]
unsafe fn channel_async_transact(
handle: u32,
send_data: *const u8,
send_len: usize,
recv_data: *mut u8,
recv_len: usize,
) -> Result<()>;
fn channel_async_transact_complete(handle: u32) -> Result<u32>;
fn channel_async_cancel(handle: u32) -> Result<()>;
#[expect(clippy::missing_safety_doc)]
unsafe fn channel_read(
object_handle: u32,
offset: usize,
buffer: *mut u8,
buffer_len: usize,
) -> Result<u32>;
#[expect(clippy::missing_safety_doc)]
unsafe fn channel_respond(
object_handle: u32,
buffer: *const u8,
buffer_len: usize,
) -> Result<()>;
fn interrupt_ack(object_handle: u32, signal_mask: Signals) -> Result<()>;
fn thread_start(object_handle: u32, initial_pc: usize, initial_sp: usize) -> Result<()>;
fn task_terminate(object_handle: u32) -> Result<()>;
fn task_join(object_handle: u32) -> Result<ExitStatus>;
fn thread_exit(exit_code: u32) -> !;
fn process_start(object_handle: u32) -> Result<()>;
fn process_exit(exit_code: u32) -> !;
/// Set or clear `Signals::USER` on the paired peer (level-triggered model).
///
/// If the signaler process terminates or is reset, the kernel automatically
/// lowers `Signals::USER` on the peer object to prevent signal leaks.
/// Symmetrically, if the receiver process terminates and restarts, its own
/// `Signals::USER` is preserved as long as the signaler remains alive and
/// has not lowered it.
fn object_set_peer_user_signal(object_handle: u32, set: bool) -> Result<()>;
fn debug_putc(a: u32) -> Result<u32>;
// TODO: Consider adding an feature flagged PowerManager object and move
// this shutdown call to it.
fn debug_shutdown(a: u32) -> Result<()>;
#[expect(clippy::missing_safety_doc)]
unsafe fn debug_log(buffer: *const u8, buffer_len: usize) -> Result<()>;
fn debug_nop() -> Result<()>;
fn debug_trigger_interrupt(irq: u32) -> Result<()>;
fn debug_clock_now() -> u64;
}
#[cfg(test)]
mod tests {
use unittest::test;
use super::*;
#[test]
fn test_syscall_id_try_from_valid() -> unittest::Result<()> {
for i in (SysCallId::SysCallLowerBound as u16 + 1)..(SysCallId::SysCallUpperBound as u16) {
// SAFETY: The loop range guarantees `i` is a valid standard SysCallId.
let expected = unsafe { core::mem::transmute::<u16, SysCallId>(i) };
unittest::assert_eq!(SysCallId::try_from(i), Ok(expected));
}
Ok(())
}
#[test]
fn test_syscall_id_try_from_valid_debug() -> unittest::Result<()> {
for i in (SysCallId::DebugSysCallLowerBound as u16 + 1)
..(SysCallId::DebugSysCallUpperBound as u16)
{
// SAFETY: The loop range guarantees `i` is a valid debug SysCallId.
let expected = unsafe { core::mem::transmute::<u16, SysCallId>(i) };
unittest::assert_eq!(SysCallId::try_from(i), Ok(expected));
}
Ok(())
}
#[test]
fn test_syscall_id_try_from_invalid() -> unittest::Result<()> {
unittest::assert_eq!(
SysCallId::try_from(SysCallId::SysCallLowerBound as u16),
Err(Error::InvalidArgument)
);
unittest::assert_eq!(
SysCallId::try_from(SysCallId::SysCallUpperBound as u16),
Err(Error::InvalidArgument)
);
unittest::assert_eq!(
SysCallId::try_from(SysCallId::DebugSysCallLowerBound as u16),
Err(Error::InvalidArgument)
);
unittest::assert_eq!(
SysCallId::try_from(SysCallId::DebugSysCallUpperBound as u16),
Err(Error::InvalidArgument)
);
unittest::assert_eq!(
SysCallId::try_from(SysCallId::DebugSysCallUpperBound as u16 + 1),
Err(Error::InvalidArgument)
);
Ok(())
}
}