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}