Skip to main content

qcode_jit/
jit.rs

1//! Owning, caching and running compiled blocks.
2
3use cranelift::prelude::*;
4use cranelift_jit::{JITBuilder, JITModule};
5use cranelift_module::{Linkage, Module};
6use qcode::{
7    context::Context,
8    value::{
9        BlockId, ValueId,
10        insn::{InstructionId, Mnemonic},
11    },
12};
13use qcode_emulator::{EmulatorErrorKind, SizedValue, StandaloneEmulator};
14use qcode_vm::{
15    BlockExecutor, Executed, VmMemory, qcode_jit_load, qcode_jit_sdiv128, qcode_jit_srem128,
16    qcode_jit_store, qcode_jit_udiv128, qcode_jit_urem128,
17};
18
19use crate::compile::{BLOCK_OK, BlockTranslator, Export, Helpers, SpaceTable, Unsupported};
20
21/// What is known about one block: the index of its native code, or the reason
22/// the compiler declined it, together with the instruction count that answer
23/// was reached for.
24type CacheEntry = Option<(usize, Result<usize, Unsupported>)>;
25
26/// A block that has been compiled to native code.
27struct Compiled {
28    /// The compiled body. Its arguments are the base of an array of space base
29    /// pointers, in the order [`SpaceTable`] records, the base of the export
30    /// buffer (one `u64` slot per entry of `exports`), the base of the guest's
31    /// software TLB, and the `VmMemory` its slow paths call back into. It
32    /// returns [`BLOCK_OK`], or [`BLOCK_FAULT`](crate::compile::BLOCK_FAULT) if
33    /// an access faulted and the block stopped there.
34    entry: extern "C" fn(*const *mut u8, *mut u64, *mut u8, *mut VmMemory) -> i32,
35    table: SpaceTable,
36    /// The terminator operands this block computes, in slot order.
37    exports: Vec<Export>,
38    /// How many body instructions this block has — everything but the
39    /// terminator. The caller needs it to position the interpreter, and taking
40    /// it from here saves resolving the block through the module arena again.
41    body_len: usize,
42    /// Where each of `table`'s spaces lives in the machine's flat storage.
43    ///
44    /// Resolved on first execution and kept: a slot is stable for the life of
45    /// the spaces, so re-entering a hot block costs an array index per space
46    /// rather than a map lookup.
47    slots: Vec<usize>,
48}
49
50/// How much work the JIT is taking, and how much it is declining.
51#[derive(Debug, Default, Clone)]
52pub struct JitStats {
53    /// Blocks translated to native code.
54    pub compiled: u64,
55    /// Blocks the compiler declined; these run on the interpreter.
56    pub declined: u64,
57    /// Executions that ran as native code.
58    pub native_runs: u64,
59}
60
61/// A JIT backend: compiles blocks on first use and runs them thereafter.
62///
63/// Holding the [`JITModule`] means compiled code lives as long as this does.
64pub struct Jit {
65    module: JITModule,
66    /// The runtime's slow-path accessors, declared once and referenced by every
67    /// compiled block.
68    helpers: Helpers,
69    /// Compiled blocks, indexed by the handles in `cache`.
70    compiled: Vec<Compiled>,
71    /// What is known about each block: an index into `compiled`, or the reason
72    /// it was declined. Declining is cached too, so a block the compiler cannot
73    /// take is only examined once.
74    ///
75    /// Each entry records the instruction count it was made for, because a
76    /// block is *not* immutable here: a VM that lifts on demand first presents
77    /// an empty placeholder (which the compiler rightly declines), then fills
78    /// it, then runs a cleanup pass over it. Trusting the entry regardless of
79    /// count would freeze that first decline forever and the block would never
80    /// be compiled.
81    ///
82    /// Stored as slots indexed by the block id's two components rather than in
83    /// a map: this is read on *every* block execution, and hashing a
84    /// `(function, local)` pair each time was a measurable share of run time.
85    /// Sparse ids cost only an unused slot.
86    cache: Vec<Vec<CacheEntry>>,
87    /// Reused across runs so a hot block does not allocate to be entered.
88    scratch: Vec<*mut u8>,
89    /// Likewise for the export buffer compiled code writes its terminator
90    /// operands into.
91    exports: Vec<u64>,
92    pub stats: JitStats,
93}
94
95impl Default for Jit {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl Jit {
102    pub fn new() -> Self {
103        let mut flags = settings::builder();
104        // Compilation happens on the guest's critical path, so favour getting
105        // through it over the last few percent of code quality.
106        flags
107            .set("opt_level", "speed")
108            .expect("opt_level is a known flag");
109        // The verifier re-checks Cranelift IR this backend has just built, on
110        // the guest's critical path, for every block. It is a development aid
111        // for the compiler itself; what guards *this* translation is the
112        // divergence harness, which compares compiled code against the
113        // interpreter block by block over whole programs.
114        flags
115            .set("enable_verifier", "false")
116            .expect("enable_verifier is a known flag");
117        let isa = cranelift_native::builder()
118            .expect("host is a supported target")
119            .finish(settings::Flags::new(flags))
120            .expect("isa builds for the host");
121        let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
122        // The two calls compiled code makes. Registered by name because that is
123        // how Cranelift resolves an external function; the addresses are this
124        // process's own, so there is no dynamic loading involved.
125        builder.symbol("qcode_jit_load", qcode_jit_load as *const u8);
126        builder.symbol("qcode_jit_store", qcode_jit_store as *const u8);
127        builder.symbol("qcode_jit_udiv128", qcode_jit_udiv128 as *const u8);
128        builder.symbol("qcode_jit_urem128", qcode_jit_urem128 as *const u8);
129        builder.symbol("qcode_jit_sdiv128", qcode_jit_sdiv128 as *const u8);
130        builder.symbol("qcode_jit_srem128", qcode_jit_srem128 as *const u8);
131        let mut module = JITModule::new(builder);
132
133        let mut load_sig = module.make_signature();
134        // (memory, address, size, out) -> status
135        load_sig.params.push(AbiParam::new(types::I64));
136        load_sig.params.push(AbiParam::new(types::I64));
137        load_sig.params.push(AbiParam::new(types::I32));
138        load_sig.params.push(AbiParam::new(types::I64));
139        load_sig.returns.push(AbiParam::new(types::I32));
140        let load = module
141            .declare_function("qcode_jit_load", Linkage::Import, &load_sig)
142            .expect("the load helper declares once");
143
144        let mut store_sig = module.make_signature();
145        // (memory, address, size, value) -> status
146        store_sig.params.push(AbiParam::new(types::I64));
147        store_sig.params.push(AbiParam::new(types::I64));
148        store_sig.params.push(AbiParam::new(types::I32));
149        store_sig.params.push(AbiParam::new(types::I64));
150        store_sig.returns.push(AbiParam::new(types::I32));
151        let store = module
152            .declare_function("qcode_jit_store", Linkage::Import, &store_sig)
153            .expect("the store helper declares once");
154
155        // (a low, a high, b low, b high, out) -> ()
156        let mut divide_sig = module.make_signature();
157        for _ in 0..5 {
158            divide_sig.params.push(AbiParam::new(types::I64));
159        }
160        let mut wide_division = |name: &str| {
161            module
162                .declare_function(name, Linkage::Import, &divide_sig)
163                .expect("a division helper declares once")
164        };
165        let divisions = [
166            wide_division("qcode_jit_udiv128"),
167            wide_division("qcode_jit_urem128"),
168            wide_division("qcode_jit_sdiv128"),
169            wide_division("qcode_jit_srem128"),
170        ];
171
172        Self {
173            module,
174            helpers: Helpers {
175                load,
176                store,
177                divisions,
178            },
179            compiled: Vec::new(),
180            cache: Vec::new(),
181            scratch: Vec::new(),
182            exports: Vec::new(),
183            stats: JitStats::default(),
184        }
185    }
186
187    /// Whether `block` has native code, compiling it on first sight.
188    ///
189    /// A decline is remembered, so an unsupported block costs one compilation
190    /// attempt over the life of the machine rather than one per execution.
191    fn resolve(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<usize, Unsupported> {
192        let count = ctx.block(block).instruction_ids().len();
193        let func: usize = block.func.into();
194        let local: usize = block.local.into();
195        if let Some(Some((cached_count, known))) =
196            self.cache.get(func).and_then(|slots| slots.get(local))
197            && *cached_count == count
198        {
199            return known.clone();
200        }
201
202        let outcome = self.compile(ctx, block);
203        match &outcome {
204            Ok(_) => self.stats.compiled += 1,
205            Err(_) => self.stats.declined += 1,
206        }
207        if func >= self.cache.len() {
208            self.cache.resize_with(func + 1, Vec::new);
209        }
210        let slots = &mut self.cache[func];
211        if local >= slots.len() {
212            slots.resize(local + 1, None);
213        }
214        slots[local] = Some((count, outcome.clone()));
215        outcome
216    }
217
218    fn compile(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<usize, Unsupported> {
219        let body_len = ctx.block(block).instruction_ids().len().saturating_sub(1);
220        let mut signature = self.module.make_signature();
221        // spaces, exports, tlb, memory.
222        for _ in 0..4 {
223            signature.params.push(AbiParam::new(types::I64));
224        }
225        signature.returns.push(AbiParam::new(types::I32));
226
227        let name = format!("qcode_block_{}_{}", self.compiled.len(), self.cache.len());
228        let id = self
229            .module
230            .declare_function(&name, Linkage::Export, &signature)
231            .map_err(|_| Unsupported::Mnemonic("function declaration failed"))?;
232
233        let mut context = self.module.make_context();
234        context.func.signature = signature;
235        let helpers = crate::compile::HelperRefs {
236            load: self
237                .module
238                .declare_func_in_func(self.helpers.load, &mut context.func),
239            store: self
240                .module
241                .declare_func_in_func(self.helpers.store, &mut context.func),
242            divisions: self
243                .helpers
244                .divisions
245                .map(|id| self.module.declare_func_in_func(id, &mut context.func)),
246        };
247
248        // A fresh builder context per attempt: a declined block abandons its
249        // half-built function, which would leave a shared context dirty and
250        // trip Cranelift's emptiness assertion on the next compilation.
251        let mut builder_ctx = FunctionBuilderContext::new();
252        let (table, exports) = {
253            let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_ctx);
254            let entry = builder.create_block();
255            builder.append_block_params_for_function_params(entry);
256            builder.switch_to_block(entry);
257            builder.seal_block(entry);
258
259            let mut translator = BlockTranslator::new(ctx, builder, entry, helpers);
260            match translator.translate_body(block) {
261                Ok(()) => {
262                    let compiled = (translator.table.clone(), translator.exports.clone());
263                    translator.finish();
264                    compiled
265                }
266                Err(unsupported) => {
267                    // The half-built function is simply dropped; nothing was
268                    // defined in the module, so there is nothing to undo.
269                    self.module.clear_context(&mut context);
270                    return Err(unsupported);
271                }
272            }
273        };
274
275        self.module
276            .define_function(id, &mut context)
277            .map_err(|_| Unsupported::Mnemonic("function definition failed"))?;
278        self.module.clear_context(&mut context);
279        self.module
280            .finalize_definitions()
281            .map_err(|_| Unsupported::Mnemonic("finalization failed"))?;
282
283        let code = self.module.get_finalized_function(id);
284        // SAFETY: `code` is the entry point Cranelift just finalized for the
285        // signature declared above — four pointer-width arguments and a 32-bit
286        // status result.
287        let entry = unsafe {
288            std::mem::transmute::<
289                *const u8,
290                extern "C" fn(*const *mut u8, *mut u64, *mut u8, *mut VmMemory) -> i32,
291            >(code)
292        };
293
294        self.compiled.push(Compiled {
295            entry,
296            table,
297            exports,
298            body_len,
299            slots: Vec::new(),
300        });
301        Ok(self.compiled.len() - 1)
302    }
303
304    /// Compiles `block` without running it, reporting why if it is declined.
305    ///
306    /// For tooling that wants to report coverage over a module.
307    pub fn try_compile(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<(), Unsupported> {
308        self.resolve(ctx, block).map(|_| ())
309    }
310
311    /// Runs `block` as native code, if it has any.
312    ///
313    /// Returns `Ok(None)` when the block is not compiled, which is the caller's
314    /// signal to run it on the interpreter instead, and `Ok(Some(n))` when it
315    /// ran, where `n` is the number of body instructions it retired.
316    pub fn run_block(
317        &mut self,
318        ctx: &Context<'_>,
319        emu: &mut StandaloneEmulator<VmMemory>,
320        block: BlockId,
321        chain: bool,
322    ) -> Result<Option<Executed>, EmulatorErrorKind> {
323        let mut current = block;
324        let mut retired = 0;
325        loop {
326            let Ok(index) = self.resolve(ctx, current) else {
327                // Nothing compiled here. If earlier blocks ran, the machine is
328                // already at `current`'s start and the interpreter takes over
329                // from there; otherwise this call did nothing at all.
330                return Ok((retired > 0).then_some(Executed {
331                    block: current,
332                    body: 0,
333                    retired,
334                }));
335            };
336            let body = self.enter(ctx, emu, index)?;
337            retired += body as u64;
338            self.stats.native_runs += 1;
339
340            // Only continue while the successor is one this backend can also
341            // run: deciding the branch here is what keeps control inside
342            // compiled code, and the interpreter would otherwise redo it.
343            let next = if chain {
344                self.next_block(ctx, emu, current)
345            } else {
346                None
347            };
348            let Some(next) = next.filter(|&next| self.is_compiled(ctx, next)) else {
349                return Ok(Some(Executed {
350                    block: current,
351                    body,
352                    retired,
353                }));
354            };
355            // Chaining means this block's terminator was decided here rather
356            // than by the interpreter, so it is retired work nobody else will
357            // count. Only the *last* block's terminator is left to the caller.
358            retired += 1;
359            current = next;
360        }
361    }
362
363    /// Whether `block` has native code, without compiling it.
364    fn is_compiled(&mut self, ctx: &Context<'_>, block: BlockId) -> bool {
365        self.resolve(ctx, block).is_ok()
366    }
367
368    /// The successor this block's terminator selects, when that is a decision
369    /// the backend can make: an argument-less branch, or a conditional one
370    /// whose condition the compiled body has just exported.
371    ///
372    /// `None` means "leave it to the interpreter" — an indirect branch, a call,
373    /// a return, or any edge that binds block arguments, all of which stay in
374    /// one implementation.
375    fn next_block(
376        &self,
377        ctx: &Context<'_>,
378        emu: &StandaloneEmulator<VmMemory>,
379        block: BlockId,
380    ) -> Option<BlockId> {
381        let &terminator = ctx.block(block).instruction_ids().last()?;
382        let terminator = InstructionId::new(block.func, terminator);
383        let insn = qcode::value::Instruction::from_id(ctx, terminator);
384        let target = match insn.mnemonic() {
385            Mnemonic::Branch(branch) if branch.args.is_empty() => branch.target,
386            Mnemonic::CBranch(cbranch)
387                if cbranch.success_args.is_empty() && cbranch.failure_args.is_empty() =>
388            {
389                let ValueId::Instruction(condition) = cbranch.condition.qualify(block.func) else {
390                    return None;
391                };
392                let taken = emu.insn_values.get(&condition)?.as_bits() != 0;
393                if taken {
394                    cbranch.success_block
395                } else {
396                    cbranch.failure_block
397                }
398            }
399            _ => return None,
400        };
401        Some(BlockId::new(block.func, target))
402    }
403
404    /// Runs one compiled block, leaving its terminator's operands where the
405    /// interpreter would have put them. Returns how many body instructions it
406    /// retired.
407    fn enter(
408        &mut self,
409        _ctx: &Context<'_>,
410        emu: &mut StandaloneEmulator<VmMemory>,
411        index: usize,
412    ) -> Result<usize, EmulatorErrorKind> {
413        let compiled = &mut self.compiled[index];
414        // Taken as a raw pointer, and everything below derived from it: the
415        // compiled block holds base pointers into the flat spaces *while*
416        // calling back into this same `VmMemory` for the RAM accesses it could
417        // not settle inline. A live `&mut` spanning the call would make those
418        // base pointers ones the compiler is entitled to assume nothing else
419        // reaches.
420        let memory: *mut VmMemory = &raw mut emu.memory;
421
422        // SAFETY: for every dereference of `memory` here — it points to the
423        // emulator's own memory, which outlives this call, and no reference to
424        // it is held across any of them.
425        if compiled.slots.is_empty() {
426            compiled.slots = compiled
427                .table
428                .entries()
429                .iter()
430                .map(|&(space, _)| unsafe { (*memory).flat_mut().slot(space) })
431                .collect();
432        }
433
434        // Each space is grown to the size the block needs *before* its base
435        // pointer is taken: growing reallocates, and compiled code holds these
436        // pointers for the duration of the call.
437        self.scratch.clear();
438        for (&slot, &(_, required)) in compiled.slots.iter().zip(compiled.table.entries()) {
439            self.scratch
440                .push(unsafe { (*memory).flat_mut().base_ptr_at(slot, required)? });
441        }
442        self.exports.clear();
443        self.exports.resize(compiled.exports.len(), 0);
444        // The TLB moves only with the memory itself, which is pinned for the
445        // duration of the call.
446        let tlb = unsafe { (*memory).mmu.tlb_ptr() };
447
448        // SAFETY: the function was compiled from this block and reads and writes
449        // only within the byte ranges recorded in its space table, each of which
450        // has just been made addressable, plus the export buffer, which has just
451        // been sized to the slot count that same compilation recorded, plus
452        // guest RAM through the TLB and the memory it is handed.
453        let status = (compiled.entry)(
454            self.scratch.as_ptr(),
455            self.exports.as_mut_ptr(),
456            tlb,
457            memory,
458        );
459
460        // A faulting access stopped the block where it happened. The fault is
461        // left where an interpreted one would be, for the VM to turn into an
462        // exit; what goes back from here is only the error the interpreter's
463        // own signature can carry.
464        if status != BLOCK_OK as i32 {
465            // SAFETY: as above.
466            let fault = unsafe { (*memory).fault() };
467            let fault = fault.ok_or(EmulatorErrorKind::MemoryReadError(0))?;
468            return Err(if fault.is_write() {
469                EmulatorErrorKind::MemoryWriteError(fault.addr)
470            } else {
471                EmulatorErrorKind::MemoryReadError(fault.addr)
472            });
473        }
474
475        // The terminator is still the interpreter's to run, so the operands it
476        // reads have to look as though the interpreter had computed them.
477        for (export, &bits) in compiled.exports.iter().zip(&self.exports) {
478            emu.insn_values
479                .insert(export.insn, SizedValue::new(bits, export.size));
480        }
481
482        Ok(compiled.body_len)
483    }
484}
485
486/// Lets a [`Jit`] be installed on a machine as its block executor.
487impl BlockExecutor for Jit {
488    fn run_block(
489        &mut self,
490        ctx: &Context<'_>,
491        emu: &mut StandaloneEmulator<VmMemory>,
492        block: BlockId,
493        chain: bool,
494    ) -> Result<Option<Executed>, EmulatorErrorKind> {
495        Jit::run_block(self, ctx, emu, block, chain)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn a_fresh_jit_has_compiled_nothing() {
505        let jit = Jit::new();
506        assert_eq!(jit.stats.compiled, 0);
507        assert_eq!(jit.stats.declined, 0);
508        assert_eq!(jit.stats.native_runs, 0);
509    }
510}