nub_arch_x86_abi/lib.rs
1//! Wire format for the host ↔ guest "run this PVM program" RPC.
2//!
3//! Personality-agnostic: the host pre-publishes each state object it
4//! wants the guest to see via the [`FN_ID_NUB_PUT_CAP`] RPC — the
5//! payload is opaque bytes whose encoding the guest personality
6//! defines (JAVM: an rkyv-archived `javm_cap::Cap` landing in
7//! `javm-guest-x86`'s heap-resident directory) — then ships a
8//! fixed-size [`InvokePacket`] referencing the published root object
9//! by hash on every call. The invoke packet is `#[repr(C)]` bytes (no
10//! codec); the response is rkyv-archived ([`InvocationResult`]).
11
12#![cfg_attr(not(feature = "std"), no_std)]
13
14extern crate alloc;
15
16use core::sync::atomic::{AtomicU32, AtomicU64};
17
18/// Maximum fixed execution lanes the guest runtime can address. The production
19/// default vCPU pool is capped lower, but host configuration must not exceed
20/// this ABI-visible lane table size.
21pub const MAX_EXECUTION_LANES: usize = 64;
22
23/// `fn_id` for the `nub_heap_stats` diagnostic. Payload is empty;
24/// response is 32 bytes packing four LE u64s (allocated_bytes,
25/// allocation_count, fragment_count, available_bytes).
26pub const FN_ID_NUB_HEAP_STATS: u32 = 2;
27
28/// `fn_id` for the cache-based RPC. Payload is a
29/// [`InvokePacket`] (host-side `#[repr(C)]` bytes, no rkyv); the
30/// guest dereferences cache VAs by `root_hash` lookup, runs the
31/// JIT, and replies with rkyv-archived [`InvocationResult`].
32pub const FN_ID_NUB_INVOKE_CACHED: u32 = 3;
33
34/// `fn_id` for the "publish a state object" RPC.
35///
36/// Payload: opaque personality-encoded bytes. The guest personality's
37/// `GuestStore::put_object` decodes, validates, content-hashes, and
38/// inserts the object into its store, then replies with the raw
39/// 32-byte content hash. All-`0xFF` is reserved as the error sentinel
40/// (decode/validation failure) — see `GuestStore::put_object` in
41/// `nub-arch-x86` for the contract. JAVM's personality decodes an
42/// rkyv-archived `javm_cap::Cap` into `javm-guest-x86`'s
43/// heap-resident directory.
44pub const FN_ID_NUB_PUT_CAP: u32 = 4;
45
46// fn_id 5 was FN_ID_NUB_GET_BOOT_INFO — the boot-info-read RPC that
47// fed the host's direct dereference of the guest cap directory. That
48// host path was deleted (host/guest hashbrown deref is unsound; see
49// `nub-host-kvm::MultiUseSandbox::published_blobs`), and the RPC with
50// it. The id stays reserved so old blobs and new hosts never disagree
51// about what 5 means.
52
53/// `fn_id` for the bench-only "evict the entire JIT compile cache"
54/// RPC. Empty payload; empty response. Used by `javm-bench` to force
55/// each criterion iteration to pay the recompile cost (otherwise the
56/// JIT cache turns the loop into pure warm-cache execute, which isn't
57/// what we want to measure for PolkaVM-shaped workloads).
58pub const FN_ID_NUB_EVICT_JIT_ALL: u32 = 6;
59/// `fn_id` for a long-lived per-vCPU invoke worker. Payload is a little-endian
60/// `u32` lane index. The function does not use the legacy rkyv response ring;
61/// it polls that lane's [`ParallelInvokeSlot`] in scratch memory, runs invokes
62/// with `run_top_on_lane`, and writes results back into the same slot.
63pub const FN_ID_NUB_INVOKE_WORKER: u32 = 7;
64
65/// 32-byte content hash of a published state object (the legacy name
66/// from when the only personality was the JAVM cap system — matches
67/// `nub_kernel::ObjHash` and, byte-wise, `javm_cap::CapHash`; kept as
68/// a local alias so this crate stays dependency-free).
69pub type CapHash = [u8; 32];
70
71/// Fixed-layout invocation packet. Sent as raw `#[repr(C)]` bytes via
72/// the existing rkyv `Request` envelope (its `payload` field). The
73/// guest reads the bytes directly with `core::ptr::read_unaligned`.
74///
75/// `root_hash` keys the object graph root to invoke (JAVM: a published `Cap::Instance`).
76/// `endpoint_idx` selects the entry within `ImageCap.endpoints`.
77/// `args` overlay φ[7..=10] on top of the endpoint's `initial_regs`.
78#[repr(C)]
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct InvokePacket {
81 pub root_hash: CapHash,
82 pub endpoint_idx: u32,
83 pub _pad: u32,
84 pub args: [u64; 4],
85 pub initial_gas: u64,
86}
87
88impl InvokePacket {
89 /// Size of the packet in bytes — what the host writes to the
90 /// `Request.payload` and what the guest reads back.
91 pub const SIZE: usize = core::mem::size_of::<Self>();
92
93 /// Cast the packet to its raw bytes.
94 pub fn as_bytes(&self) -> &[u8] {
95 unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) }
96 }
97
98 /// Parse a packet from raw bytes (length-checked).
99 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
100 if bytes.len() != Self::SIZE {
101 return None;
102 }
103 Some(unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const Self) })
104 }
105}
106
107/// Bytes of the running Instance's scratchpad (`slot[0]`) region surfaced at the
108/// top-level HALT — a fixed-size **head** of the returned DataCap's effective
109/// content. The guest writes its result into the scratchpad-mapped memory
110/// region during the run (CoW into the cap); at top HALT the engine reads the
111/// region's effective bytes back out here, so the host observes the full,
112/// uncompressed result without a separate data-flow event.
113///
114/// V1 surfaces a fixed-size window (enough for the fuzz differential's 13-slot
115/// register signature: 13 × 8 = 104 ≤ 128). The full variable-length DataCap
116/// return is deferred to the YieldMarker/YieldCatcher kernel design — see
117/// `kernel-assisted-instances.md`. Zero-filled when the Instance maps no
118/// scratchpad region (every non-fuzz path today).
119pub const SCRATCHPAD_HEAD_LEN: usize = 128;
120
121/// Invocation result. Both backends produce this shape on completion;
122/// rkyv-archived on the wire from the cached path's response.
123#[repr(C)]
124#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize)]
125#[rkyv(derive(Debug, PartialEq, Eq))]
126pub struct InvocationResult {
127 pub exit_reason: u32,
128 pub exit_arg: u32,
129 pub return_value: u64,
130 pub gas_remaining: u64,
131 /// Effective bytes of the running Instance's scratchpad (`slot[0]`) region
132 /// head at top HALT (see [`SCRATCHPAD_HEAD_LEN`]). Zero when no scratchpad
133 /// region is mapped.
134 pub scratchpad_head: [u8; SCRATCHPAD_HEAD_LEN],
135}
136
137pub const PARALLEL_INVOKE_SLOT_BYTES: usize = 512;
138
139pub const PARALLEL_INVOKE_STATUS_EMPTY: u32 = 0;
140pub const PARALLEL_INVOKE_STATUS_READY: u32 = 1;
141pub const PARALLEL_INVOKE_STATUS_RUNNING: u32 = 2;
142pub const PARALLEL_INVOKE_STATUS_DONE: u32 = 3;
143pub const PARALLEL_INVOKE_STATUS_STOP: u32 = 4;
144pub const PARALLEL_INVOKE_STATUS_STARTING: u32 = 5;
145pub const PARALLEL_INVOKE_STATUS_EVICT_JIT_READY: u32 = 6;
146
147/// One host<->guest invoke slot. Slots are addressed by lane index at
148/// `parallel_slot_base + lane * PARALLEL_INVOKE_SLOT_BYTES`.
149///
150/// Synchronization protocol:
151/// - host writes `job_id` and `packet`, then stores `READY` with release;
152/// - guest CASes `READY -> RUNNING`, runs the invoke, writes `result`, then
153/// stores `DONE` with release;
154/// - host reads `DONE` with acquire, copies `result`, then stores `EMPTY`.
155///
156/// Bench-only control commands, such as `EVICT_JIT_READY`, use the same
157/// `RUNNING -> DONE -> EMPTY` completion protocol after the host reserves all
158/// lanes.
159#[repr(C, align(64))]
160pub struct ParallelInvokeSlot {
161 pub status: AtomicU32,
162 pub _pad0: u32,
163 pub job_id: AtomicU64,
164 pub packet: InvokePacket,
165 pub result: InvocationResult,
166}
167
168const _: () = assert!(core::mem::size_of::<ParallelInvokeSlot>() <= PARALLEL_INVOKE_SLOT_BYTES);
169const _: () = assert!(core::mem::align_of::<ParallelInvokeSlot>() <= 64);