Skip to main content

thruput_syscall/
lib.rs

1//! # Thruput syscall dispatch
2//!
3//! The raw guest-exit boundary for Thruput syscalls. This crate contains the
4//! single dispatch chokepoint, `syscall_wrapper`, which leaves the guest and
5//! executes a system call. It speaks only in integer words (`SyscallWord`) and
6//! raw `i32` error codes — the typed `SyscallNo` / `SysErr` layer lives in
7//! `thruput-xdr`.
8//!
9//! A dispatch path exists only when the target is `riscv32`, or when the
10//! `syscalls_mock` feature is enabled. On any other target (a plain host
11//! without the mock) there is no way to leave the guest, so the dispatch
12//! chokepoint `syscall_wrapper` is not exported. This keeps a feature-less host
13//! build (and `cargo build -p`, `cargo hack`, rust-analyzer) clean without a
14//! `compile_error!` guard. The `SyscallWord` type alias, by contrast, is a pure
15//! pointer-width type with no dispatch dependency, so it is always defined.
16//!
17//! There are two guest dispatch paths, selected by target:
18//!
19//! - **riscv32**: `syscall_wrapper` issues the RISC-V `ecall` trap instruction
20//!   directly, passing the syscall number in `a0` and arguments in `a1..a7`
21//!   (the host returns the error code in `a0`).
22//! - **native mock** (`syscalls_mock`): `syscall_wrapper` calls a native
23//!   `syscall_wrapper_impl` symbol, used by `native_exec` in the SDK and by the
24//!   syscall tests.
25//!
26//! You should never need to invoke `syscall_wrapper` directly; instead use the
27//! typed bindings generated in `thruput-xdr`.
28#![no_std]
29
30/// The integer width of a single syscall argument slot.
31///
32/// On `riscv32` each slot is a 32-bit value, carried in a register (`a1..a7`)
33/// across the `ecall` trap. On every other target each slot is a 64-bit
34/// value, wide enough to carry a 64-bit host pointer (this is the path used
35/// by `native_exec` in the SDK). The width is spelled out per target rather
36/// than taken from `usize`, so the syscall ABI is decoupled from any
37/// incidental use of `usize` elsewhere.
38///
39/// This is a pure type alias with no dispatch dependency, so it is always
40/// defined — even on targets where `syscall_wrapper` is unavailable.
41#[cfg(target_arch = "riscv32")]
42pub type SyscallWord = u32;
43#[cfg(not(target_arch = "riscv32"))]
44pub type SyscallWord = u64;
45
46// On riscv32 the slot must be exactly pointer/register width (32 bits); a 64-bit
47// slot would be split across a register pair and break the `ecall` ABI.
48#[cfg(target_arch = "riscv32")]
49const _: () = assert!(core::mem::size_of::<SyscallWord>() == 4);
50
51#[cfg(any(target_arch = "riscv32", feature = "syscalls_mock"))]
52pub use dispatch::syscall_wrapper;
53
54#[cfg(any(target_arch = "riscv32", feature = "syscalls_mock"))]
55mod dispatch {
56    use super::SyscallWord;
57
58    // The syscall entrypoint is declared with the *concrete* slot type for each
59    // target, written out explicitly rather than via the `SyscallWord` alias. Since
60    // `syscall_wrapper` (below) passes `SyscallWord`-typed values into it, any
61    // disagreement between these declarations and `SyscallWord` is a compile error
62    // at the call site — a tripwire if `SyscallWord` is ever defined incorrectly.
63
64    // Native mock entrypoint (used by `native_exec` and the syscall tests): always
65    // native, so the slots are concretely 64-bit.
66    #[cfg(feature = "syscalls_mock")]
67    unsafe extern "C-unwind" {
68        fn syscall_wrapper_impl(
69            syscall_no: u64,
70            arg1: u64,
71            arg2: u64,
72            arg3: u64,
73            arg4: u64,
74            arg5: u64,
75            arg6: u64,
76            arg7: u64,
77        ) -> i32;
78    }
79
80    // riscv32 (bare-metal): no host import — issue the `ecall` trap directly. The
81    // syscall number is passed in `a0` and arguments in `a1..a7`; the host leaves
82    // the error code in `a0`. Per the RISC-V calling convention `a0..a7` are
83    // caller-saved argument registers, so each is declared as clobbered
84    // (`inlateout(reg) val => _`), except `a0`, whose post-state is the result.
85    // The `u32` parameters match `SyscallWord` on riscv32; if `SyscallWord` were
86    // ever mis-defined wider, the `syscall_wrapper` call site would fail to compile.
87    #[cfg(all(not(feature = "syscalls_mock"), target_arch = "riscv32"))]
88    #[inline]
89    unsafe fn syscall_wrapper_impl(
90        syscall_no: u32,
91        arg1: u32,
92        arg2: u32,
93        arg3: u32,
94        arg4: u32,
95        arg5: u32,
96        arg6: u32,
97        arg7: u32,
98    ) -> i32 {
99        let result: i32;
100        // SAFETY: `ecall` traps to the host with the documented register ABI and
101        // touches neither the stack nor memory through the stack pointer.
102        unsafe {
103            core::arch::asm!(
104                "ecall",
105                inlateout("a0") syscall_no => result,
106                inlateout("a1") arg1 => _,
107                inlateout("a2") arg2 => _,
108                inlateout("a3") arg3 => _,
109                inlateout("a4") arg4 => _,
110                inlateout("a5") arg5 => _,
111                inlateout("a6") arg6 => _,
112                inlateout("a7") arg7 => _,
113                options(nostack),
114            );
115        }
116        result
117    }
118
119    /// The single dispatch chokepoint: leaves the guest and executes a syscall.
120    ///
121    /// Takes the syscall number and arguments as raw pointer-width words and returns
122    /// the raw `i32` error code. The typed `SyscallNo` / `SysErr` wrapping is applied
123    /// by the generated bindings in `thruput-xdr`.
124    #[inline(always)]
125    pub fn syscall_wrapper(
126        syscall_no: SyscallWord,
127        arg1: SyscallWord,
128        arg2: SyscallWord,
129        arg3: SyscallWord,
130        arg4: SyscallWord,
131        arg5: SyscallWord,
132        arg6: SyscallWord,
133        arg7: SyscallWord,
134    ) -> i32 {
135        // SAFETY: `syscall_wrapper_impl` requires only correctly-typed integer
136        // arguments (it has no further preconditions); the typed `SyscallWord`
137        // parameters guarantee that.
138        unsafe {
139            syscall_wrapper_impl(
140                syscall_no, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
141            )
142        }
143    }
144}