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//! [`run_instance`] is the in-process counterpart to nub-arch-x86's
6//! JIT-driven `enter_frame` / `build_frame_runtime`: takes a published
7//! [`javm_cap::cap::instance::InstanceCap`] + its referenced
8//! [`javm_cap::cap::image::ImageCap`] (both `Global`-allocated
9//! locally), wires the bytecode + memory layout to
10//! [`javm_exec::interp::Interpreter::run`], and produces an
11//! [`InvocationResult`].
12
13use javm_cap::cap::image::ImageCap;
14use javm_cap::cap::instance::InstanceCap;
15use javm_exec::{
16    Access, CopyingMemory, EcallHandler, EcallKind, EcallResult, ExitReason, GasCounter, PAGE_SIZE,
17    Regs, gas_const, interp::Interpreter, predecode::predecode_rv_with_mem_cycles,
18};
19use nub_arch_x86_abi::{InvocationResult, SCRATCHPAD_HEAD_LEN};
20use nub_kernel::{Arch, CapHash, InstanceRef, InvokeOptions, InvokeOutcome};
21
22/// In-process Arch backend.
23#[derive(Default)]
24pub struct LocalArch {
25    state_root: CapHash,
26}
27
28impl LocalArch {
29    pub fn new() -> Self {
30        Self::default()
31    }
32}
33
34/// Stub error type for the skeleton — the local backend cannot fail
35/// today. Replace with a real error enum when invocation lands.
36#[derive(Debug)]
37pub enum LocalArchError {}
38
39impl Arch for LocalArch {
40    type Error = LocalArchError;
41
42    fn invoke(
43        &mut self,
44        _target: InstanceRef,
45        _endpoint: u16,
46        _args: &[u8],
47        _opts: InvokeOptions,
48    ) -> Result<InvokeOutcome, Self::Error> {
49        Ok(InvokeOutcome {
50            return_value: 42,
51            gas_used: 0,
52        })
53    }
54
55    fn state_root(&self) -> CapHash {
56        self.state_root
57    }
58}
59
60/// Run an Instance through the PVM2 (RISC-V) interpreter, returning the
61/// same `InvocationResult` shape `nub-arch-x86`'s JIT path produces.
62/// The exit-reason mapping matches the JIT exit codes (HostCall=4,
63/// Trap=7, etc.) so the two backends agree on a well-formed program.
64///
65/// Endpoint dispatch: `endpoint_idx` selects
66/// `image.endpoints[endpoint_idx]`; the endpoint's `entry_pc` is used
67/// as the start PC. Caller-supplied `args` overlay φ[7..=10] on top
68/// of the endpoint's `initial_regs`. Memory is seeded from the
69/// Instance's `mem` DataCap (the whole RW extent), with pinned mappings
70/// re-laid read-only.
71pub fn run_instance(
72    instance: &InstanceCap,
73    image: &ImageCap,
74    endpoint_idx: u8,
75    args: [u64; 4],
76    initial_gas: u64,
77) -> InvocationResult {
78    // Data lives at [DATA_BASE, mem_size); base the flat buffer at
79    // DATA_BASE so [0, DATA_BASE) (null guard + code) faults, matching
80    // the recompiler's page table.
81    let mut mem = CopyingMemory::new();
82    mem.base = javm_cap::layout::DATA_BASE;
83    let data_extent = instance.mem.content_len();
84    let mut mem_image = vec![0u8; data_extent as usize];
85    if data_extent > 0 {
86        // Seed the whole extent from the Instance's memory image (the immutable
87        // backing — both initial and pinned content). No cache lookup needed.
88        instance.mem.copy_into(0, &mut mem_image);
89        mem.map_region(
90            javm_cap::layout::DATA_BASE as u64,
91            data_extent,
92            Access::ReadWrite,
93            Some(&mem_image),
94        )
95        .expect("map base RW region");
96    }
97    // Re-lay pinned mappings read-only (same bytes, from the seeded image) so a
98    // guest store faults, matching the recompiler's PinnedCapRo direct map.
99    let data_base = javm_cap::layout::DATA_BASE as u64;
100    for m in image.mappings.iter() {
101        if m.path().is_empty() || !image.mapping_is_pinned(m.start as u32) {
102            continue;
103        }
104        let off = (m.start.saturating_sub(data_base)) as usize;
105        let len = (m.size as usize).min(mem_image.len().saturating_sub(off));
106        if len > 0 {
107            overlay(
108                &mut mem,
109                m.start as u32,
110                &mem_image[off..off + len],
111                Access::ReadOnly,
112            );
113        }
114    }
115
116    // V1 single-byte ABI: the endpoint selector is a single-byte Key into the
117    // sparse endpoint list.
118    let target = javm_cap::Key::from(endpoint_idx);
119    let (_, endpoint) = image
120        .endpoints
121        .iter()
122        .find(|(k, _)| *k == target)
123        .expect("endpoint key not defined");
124
125    let mut regs = Regs::new();
126    regs.pc = endpoint.entry_pc;
127    // Endpoint baseline first, then layer the InstanceCap's persisted
128    // regs on top (publish_instance writes them; subsequent invokes
129    // observe them). Args overlay φ[7..=10] last.
130    // Persisted file is the 13 host-mapped slots; x3/x4 (slots 13/14) start
131    // at 0 (Regs::new zeros them), matching the recompiler.
132    regs.gpr[..javm_cap::NUM_REGS].copy_from_slice(&endpoint.initial_regs);
133    for (i, v) in instance.regs.iter().enumerate() {
134        if *v != 0 {
135            regs.gpr[i] = *v;
136        }
137    }
138    for (i, v) in args.iter().enumerate() {
139        regs.gpr[7 + i] = *v;
140    }
141
142    let mut gas = GasCounter::new(initial_gas);
143    let mut handler = LocalEcallHandler;
144
145    // The executable code region, mapped RO at the fixed CODE_BASE
146    // (PC = CODE_BASE + byte_offset).
147    let (code_base, code_bytes) = image
148        .code_mapping()
149        .expect("image has no executable code mapping");
150
151    // Category #3: guest PIC data loads of the program's own bytecode
152    // page-in the touched code page(s) on first read (read-only forever),
153    // identical to the recompiler's lazy code materialization.
154    mem.set_code_region(code_base, code_bytes.len() as u32);
155
156    // Category #2: the load/store base latency (mem_cycles) is scaled
157    // ×1..4 by the Instance's declared memory footprint. `mem_size`
158    // (high-water-mark over the Image's memory_mappings) is the same
159    // value the recompiler derives, so both engines pick the same tier.
160    let mem_cycles = gas_const::mem_cycles_for(gas_const::accessible_pages(
161        instance.mem_size(),
162        javm_cap::layout::DATA_BASE,
163    ));
164    let predecode = predecode_rv_with_mem_cycles(code_bytes, mem_cycles);
165    let exit = Interpreter::run(
166        &predecode,
167        code_bytes,
168        code_base,
169        &mut regs,
170        &mut mem,
171        &mut gas,
172        &mut handler,
173    );
174
175    let (exit_reason, exit_arg) = match exit {
176        ExitReason::Halt => (0, 0),
177        ExitReason::Panic => (1, 0),
178        ExitReason::OutOfGas => (2, 0),
179        ExitReason::PageFault(addr) => (3, addr),
180        ExitReason::HostCall(imm) => (4, imm),
181        ExitReason::Ecall => (6, 0),
182        ExitReason::Trap => (7, 0),
183    };
184
185    // Surface the running Instance's scratchpad (slot[0]) region head — the
186    // effective bytes of `[DATA_BASE, DATA_BASE + SCRATCHPAD_HEAD_LEN)` from the
187    // final flat memory (the guest's writes landed here during the run). The
188    // recompiler reads the identical window from its post-run CoW pages, so the
189    // two engines surface byte-identical result data.
190    let mut scratchpad_head = [0u8; SCRATCHPAD_HEAD_LEN];
191    let sig_base = javm_cap::layout::DATA_BASE;
192    for (i, byte) in scratchpad_head.iter_mut().enumerate() {
193        *byte = mem.read_u8(sig_base + i as u32).unwrap_or(0);
194    }
195
196    InvocationResult {
197        exit_reason,
198        exit_arg,
199        return_value: regs.gpr[7],
200        gas_remaining: gas.remaining(),
201        scratchpad_head,
202    }
203}
204
205fn page_round_up_u64(n: u64) -> u64 {
206    let p = PAGE_SIZE as u64;
207    n.div_ceil(p) * p
208}
209
210/// Overlay a sub-region of mem with a permission + initial bytes. No-op
211/// if `data` is empty.
212fn overlay(mem: &mut CopyingMemory, start: u32, data: &[u8], access: Access) {
213    if data.is_empty() {
214        return;
215    }
216    let size = page_round_up_u64(data.len() as u64);
217    mem.map_region(start as u64, size, access, Some(data))
218        .expect("map_region overlay");
219}
220
221/// Minimal `EcallHandler` for the local backend: every `ecall` /
222/// `ecalli` ends the run by surfacing the corresponding `ExitReason`,
223/// matching the JIT trampoline's exit shape.
224struct LocalEcallHandler;
225
226impl EcallHandler for LocalEcallHandler {
227    fn handle(
228        &mut self,
229        kind: EcallKind,
230        _regs: &mut Regs,
231        _mem: &mut dyn javm_exec::Memory,
232    ) -> EcallResult {
233        match kind {
234            EcallKind::Ecalli(imm) => EcallResult::Exit(ExitReason::HostCall(imm)),
235            EcallKind::Ecall => EcallResult::Exit(ExitReason::Ecall),
236        }
237    }
238}