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