nub_arch_local/lib.rs
1//! In-process `Arch` impl: simulates the CPU + MMU substrate with
2//! Rust data structures. Runs directly in the host process; no
3//! sandbox, no cross-compilation.
4//!
5//! Personality-agnostic: [`run_program`] takes a prepared
6//! [`ProgramSpec`] (code, flat memory image, overlays, registers)
7//! plus an [`EcallHandler`], wires it to
8//! [`nub_exec::interp::Interpreter::run`], and produces an
9//! [`InvocationResult`]. This is the in-process counterpart to
10//! nub-arch-x86's JIT-driven `enter_frame` / `build_frame_runtime`.
11//! The personality lowers its own object types into a `ProgramSpec`
12//! (JAVM: `javm::JavmLocal`'s `run_instance`). For a program with no
13//! personality at all, [`program::PreparedProgram`] lowers a
14//! `nub_program::ProgramBlob` directly.
15
16pub mod program;
17
18pub use program::{PrepareError, PreparedProgram, run_blob};
19
20use nub_arch_x86_abi::{InvocationResult, SCRATCHPAD_HEAD_LEN};
21use nub_exec::{
22 Access, CopyingMemory, EcallHandler, EcallKind, EcallResult, ExitReason, GasCounter, PAGE_SIZE,
23 Regs, gas_const,
24 interp::Interpreter,
25 predecode::{Predecode, predecode_rv_with_mem_cycles},
26};
27use nub_kernel::{Arch, CapHash, InstanceRef, InvokeOptions, InvokeOutcome};
28
29/// In-process Arch backend.
30#[derive(Default)]
31pub struct LocalArch {
32 state_root: CapHash,
33}
34
35impl LocalArch {
36 pub fn new() -> Self {
37 Self::default()
38 }
39}
40
41/// Stub error type for the skeleton — the local backend cannot fail
42/// today. Replace with a real error enum when invocation lands.
43#[derive(Debug)]
44pub enum LocalArchError {}
45
46impl Arch for LocalArch {
47 type Error = LocalArchError;
48
49 fn invoke(
50 &mut self,
51 _target: InstanceRef,
52 _endpoint: u16,
53 _args: &[u8],
54 _opts: InvokeOptions,
55 ) -> Result<InvokeOutcome, Self::Error> {
56 Ok(InvokeOutcome {
57 return_value: 42,
58 gas_used: 0,
59 })
60 }
61
62 fn state_root(&self) -> CapHash {
63 self.state_root
64 }
65}
66
67/// A read-only overlay re-laid on top of the flat memory image: the
68/// bytes at `image[image_off..image_off + len]` become read-only at
69/// guest address `start`, so guest stores fault (matching the
70/// recompiler's pinned direct maps).
71#[derive(Clone, Copy, Debug)]
72pub struct RoOverlay {
73 pub start: u32,
74 pub image_off: usize,
75 pub len: usize,
76}
77
78/// Personality-agnostic program description for [`run_program`]: what
79/// to execute, over what memory, starting from which register file.
80/// The personality (JAVM: `javm::JavmLocal`'s `run_instance`) is
81/// responsible for lowering its own object types into this shape.
82pub struct ProgramSpec<'a> {
83 /// The executable code region, mapped RO with PC = `code_base` +
84 /// byte offset.
85 pub code_base: u32,
86 pub code: &'a [u8],
87 /// Base guest address of the flat RW data image. `[0, data_base)`
88 /// (null guard + code window) faults on data access.
89 pub data_base: u32,
90 /// Initial contents of the RW region `[data_base, data_base +
91 /// mem_image.len())`.
92 pub mem_image: &'a [u8],
93 /// Read-only re-lays over the seeded image.
94 pub ro_overlays: &'a [RoOverlay],
95 /// Declared memory footprint (high-water mark) used to pick the
96 /// load/store gas tier — must match what the JIT backend derives
97 /// so both engines charge identically.
98 pub declared_mem_size: u32,
99 /// Fully prepared register file (entry PC + initial GPRs).
100 pub regs: Regs,
101}
102
103/// A prepared address space plus predecoded code, ready to execute.
104///
105/// Splitting this out of [`run_program`] separates *setup* (build the
106/// flat memory, map the read-only overlays, predecode the code) from
107/// *execution* (interpret to an exit). Callers that measure execution
108/// need that split: every other engine a benchmark might compare
109/// against instantiates before the clock starts, so folding nub's
110/// setup into the timed region would understate it.
111///
112/// [`invoke`](Self::invoke) continues from the current memory state, so
113/// repeated calls observe the guest's own mutations. Build a fresh
114/// instance for a cold run.
115pub struct ProgramInstance {
116 mem: CopyingMemory,
117 predecode: Predecode,
118 code: Vec<u8>,
119 code_base: u32,
120 data_base: u32,
121 regs: Regs,
122}
123
124impl ProgramInstance {
125 /// Build the address space and predecode the code.
126 pub fn new(spec: &ProgramSpec<'_>) -> Self {
127 // Base the flat buffer at data_base so [0, data_base) faults,
128 // matching the recompiler's page table.
129 let mut mem = CopyingMemory::new();
130 mem.base = spec.data_base;
131 if !spec.mem_image.is_empty() {
132 mem.map_region(
133 spec.data_base as u64,
134 spec.mem_image.len() as u64,
135 Access::ReadWrite,
136 Some(spec.mem_image),
137 )
138 .expect("map base RW region");
139 }
140 for o in spec.ro_overlays {
141 overlay(
142 &mut mem,
143 o.start,
144 &spec.mem_image[o.image_off..o.image_off + o.len],
145 Access::ReadOnly,
146 );
147 }
148
149 // Category #3: guest PIC data loads of the program's own bytecode
150 // page-in the touched code page(s) on first read (read-only forever),
151 // identical to the recompiler's lazy code materialization.
152 mem.set_code_region(spec.code_base, spec.code.len() as u32);
153
154 // Category #2: the load/store base latency (mem_cycles) is scaled
155 // ×1..4 by the declared memory footprint, the same value the
156 // recompiler derives, so both engines pick the same tier.
157 let mem_cycles = gas_const::mem_cycles_for(gas_const::accessible_pages(
158 spec.declared_mem_size,
159 spec.data_base,
160 ));
161
162 ProgramInstance {
163 predecode: predecode_rv_with_mem_cycles(spec.code, mem_cycles),
164 code: spec.code.to_vec(),
165 mem,
166 code_base: spec.code_base,
167 data_base: spec.data_base,
168 regs: spec.regs.clone(),
169 }
170 }
171
172 /// Interpret from `regs` until the program exits.
173 ///
174 /// `handler` decides what ecall/ecalli mean;
175 /// [`ExitingEcallHandler`] surfaces them as exits, matching the JIT
176 /// trampoline.
177 pub fn invoke(&mut self, handler: &mut dyn EcallHandler, initial_gas: u64) -> InvocationResult {
178 let mut regs = self.regs.clone();
179 let mut gas = GasCounter::new(initial_gas);
180
181 let exit = Interpreter::run(
182 &self.predecode,
183 &self.code,
184 self.code_base,
185 &mut regs,
186 &mut self.mem,
187 &mut gas,
188 handler,
189 );
190
191 let (exit_reason, exit_arg) = match exit {
192 ExitReason::Halt => (0, 0),
193 ExitReason::Panic => (1, 0),
194 ExitReason::OutOfGas => (2, 0),
195 ExitReason::PageFault(addr) => (3, addr),
196 ExitReason::HostCall(imm) => (4, imm),
197 ExitReason::Ecall => (6, 0),
198 ExitReason::Trap => (7, 0),
199 };
200
201 // Surface the scratchpad head — the effective bytes of
202 // `[data_base, data_base + SCRATCHPAD_HEAD_LEN)` from the final
203 // flat memory. The recompiler reads the identical window from its
204 // post-run CoW pages, so the two engines surface byte-identical
205 // result data.
206 let mut scratchpad_head = [0u8; SCRATCHPAD_HEAD_LEN];
207 for (i, byte) in scratchpad_head.iter_mut().enumerate() {
208 *byte = self.mem.read_u8(self.data_base + i as u32).unwrap_or(0);
209 }
210
211 InvocationResult {
212 exit_reason,
213 exit_arg,
214 return_value: regs.gpr[7],
215 gas_remaining: gas.remaining(),
216 scratchpad_head,
217 }
218 }
219
220 /// Re-enter at a different PC on the next [`invoke`](Self::invoke).
221 pub fn set_entry_pc(&mut self, pc: u64) {
222 self.regs.pc = pc;
223 }
224}
225
226/// Run a prepared [`ProgramSpec`] through the PVM2 (RISC-V)
227/// interpreter, returning the same `InvocationResult` shape
228/// `nub-arch-x86`'s JIT path produces. The exit-reason mapping matches
229/// the JIT exit codes (HostCall=4, Trap=7, etc.) so the two backends
230/// agree on a well-formed program.
231///
232/// Equivalent to [`ProgramInstance::new`] followed by one
233/// [`invoke`](ProgramInstance::invoke) — same bytes, same gas.
234pub fn run_program(
235 spec: &ProgramSpec<'_>,
236 handler: &mut dyn EcallHandler,
237 initial_gas: u64,
238) -> InvocationResult {
239 ProgramInstance::new(spec).invoke(handler, initial_gas)
240}
241
242fn page_round_up_u64(n: u64) -> u64 {
243 let p = PAGE_SIZE as u64;
244 n.div_ceil(p) * p
245}
246
247/// Overlay a sub-region of mem with a permission + initial bytes. No-op
248/// if `data` is empty.
249fn overlay(mem: &mut CopyingMemory, start: u32, data: &[u8], access: Access) {
250 if data.is_empty() {
251 return;
252 }
253 let size = page_round_up_u64(data.len() as u64);
254 mem.map_region(start as u64, size, access, Some(data))
255 .expect("map_region overlay");
256}
257
258/// Minimal `EcallHandler`: every `ecall` / `ecalli` ends the run by
259/// surfacing the corresponding `ExitReason`, matching the JIT
260/// trampoline's exit shape.
261pub struct ExitingEcallHandler;
262
263impl EcallHandler for ExitingEcallHandler {
264 fn handle(
265 &mut self,
266 kind: EcallKind,
267 _regs: &mut Regs,
268 _mem: &mut dyn nub_exec::Memory,
269 ) -> EcallResult {
270 match kind {
271 EcallKind::Ecalli(imm) => EcallResult::Exit(ExitReason::HostCall(imm)),
272 EcallKind::Ecall => EcallResult::Exit(ExitReason::Ecall),
273 }
274 }
275}