Skip to main content

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}