Skip to main content

wasmtime/engine/
serialization.rs

1//! This module implements serialization and deserialization of `Engine`
2//! configuration data which is embedded into compiled artifacts of Wasmtime.
3//!
4//! The data serialized here is used to double-check that when a module is
5//! loaded from one host onto another that it's compatible with the target host.
6//! Additionally though this data is the first data read from a precompiled
7//! artifact so it's "extra hardened" to provide reasonable-ish error messages
8//! for mismatching wasmtime versions. Once something successfully deserializes
9//! here it's assumed it's meant for this wasmtime so error messages are in
10//! general much worse afterwards.
11//!
12//! Wasmtime AOT artifacts are ELF files so the data for the engine here is
13//! stored into a section of the output file. The structure of this section is:
14//!
15//! 1. A version byte, currently `VERSION`.
16//! 2. A byte indicating how long the next field is.
17//! 3. A version string of the length of the previous byte value.
18//! 4. A `postcard`-encoded `Metadata` structure.
19//!
20//! This is hoped to help distinguish easily Wasmtime-based ELF files from
21//! other random ELF files, as well as provide better error messages for
22//! using wasmtime artifacts across versions.
23
24use crate::prelude::*;
25use crate::{Engine, ModuleVersionStrategy, Precompiled};
26use core::fmt;
27use core::str::FromStr;
28use object::endian::Endianness;
29#[cfg(any(feature = "cranelift", feature = "winch"))]
30use object::write::{Object, StandardSegment};
31use object::{
32    FileFlags, Object as _,
33    elf::FileHeader64,
34    read::elf::{ElfFile64, FileHeader, SectionHeader},
35};
36use serde_derive::{Deserialize, Serialize};
37use wasmtime_environ::{FlagValue, ObjectKind, OperatorCostStrategy, Tunables, obj};
38
39const VERSION: u8 = 0;
40
41/// Verifies that the serialized engine in `mmap` is compatible with the
42/// `engine` provided.
43///
44/// This function will verify that the `mmap` provided can be deserialized
45/// successfully and that the contents are all compatible with the `engine`
46/// provided here, notably compatible wasm features are enabled, compatible
47/// compiler options, etc. If a mismatch is found and the compilation metadata
48/// specified is incompatible then an error is returned.
49pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> Result<()> {
50    // Parse the input `mmap` as an ELF file and see if the header matches the
51    // Wasmtime-generated header. This includes a Wasmtime-specific `os_abi` and
52    // the `e_flags` field should indicate whether `expected` matches or not.
53    //
54    // Note that errors generated here could mean that a precompiled module was
55    // loaded as a component, or vice versa, both of which aren't supposed to
56    // work.
57    //
58    // Ideally we'd only `File::parse` once and avoid the linear
59    // `section_by_name` search here but the general serialization code isn't
60    // structured well enough to make this easy and additionally it's not really
61    // a perf issue right now so doing that is left for another day's
62    // refactoring.
63    let header = FileHeader64::<Endianness>::parse(mmap)
64        .map_err(obj::ObjectCrateErrorWrapper)
65        .context("failed to parse precompiled artifact as an ELF")?;
66    let endian = header
67        .endian()
68        .context("failed to parse header endianness")?;
69
70    let expected_e_flags = match expected {
71        ObjectKind::Module => obj::EF_WASMTIME_MODULE,
72        ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
73    };
74    ensure!(
75        header.e_flags(endian).contains(expected_e_flags),
76        "incompatible object file format"
77    );
78
79    let section_headers = header
80        .section_headers(endian, mmap)
81        .context("failed to parse section headers")?;
82    let strings = header
83        .section_strings(endian, mmap, section_headers)
84        .context("failed to parse strings table")?;
85    let sections = header
86        .sections(endian, mmap)
87        .context("failed to parse sections table")?;
88
89    let mut section_header = None;
90    for s in sections.iter() {
91        let name = s.name(endian, strings)?;
92        if name == obj::ELF_WASM_ENGINE.as_bytes() {
93            section_header = Some(s);
94        }
95    }
96    let Some(section_header) = section_header else {
97        bail!("failed to find section `{}`", obj::ELF_WASM_ENGINE)
98    };
99    let data = section_header
100        .data(endian, mmap)
101        .map_err(obj::ObjectCrateErrorWrapper)?;
102    let (first, data) = data
103        .split_first()
104        .ok_or_else(|| format_err!("invalid engine section"))?;
105    if *first != VERSION {
106        bail!("mismatched version in engine section");
107    }
108    let (len, data) = data
109        .split_first()
110        .ok_or_else(|| format_err!("invalid engine section"))?;
111    let len = usize::from(*len);
112    let (version, data) = if data.len() < len + 1 {
113        bail!("engine section too small")
114    } else {
115        data.split_at(len)
116    };
117
118    match &engine.config().module_version {
119        ModuleVersionStrategy::None => { /* ignore the version info, accept all */ }
120        _ => {
121            let version = core::str::from_utf8(&version)?;
122            if version != engine.config().module_version.as_str() {
123                bail!("Module was compiled with incompatible version '{version}'");
124            }
125        }
126    }
127    postcard::from_bytes::<Metadata<'_>>(data)?.check_compatible(engine)
128}
129
130#[cfg(any(feature = "cranelift", feature = "winch"))]
131pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Metadata<'_>) {
132    let section = obj.add_section(
133        obj.segment_name(StandardSegment::Data).to_vec(),
134        obj::ELF_WASM_ENGINE.as_bytes().to_vec(),
135        object::SectionKind::ReadOnlyData,
136    );
137    let mut data = Vec::new();
138    data.push(VERSION);
139    let version = engine.config().module_version.as_str();
140    // This precondition is checked in Config::module_version:
141    assert!(
142        version.len() < 256,
143        "package version must be less than 256 bytes"
144    );
145    data.push(version.len() as u8);
146    data.extend_from_slice(version.as_bytes());
147    data.extend(postcard::to_allocvec(metadata).unwrap());
148    obj.set_section_data(section, data, 1);
149}
150
151fn detect_precompiled<'data, R: object::ReadRef<'data>>(
152    obj: ElfFile64<'data, Endianness, R>,
153) -> Option<Precompiled> {
154    match obj.flags() {
155        FileFlags::Elf {
156            os_abi: obj::ELFOSABI_WASMTIME,
157            abi_version: 0,
158            e_flags,
159        } if e_flags.contains(obj::EF_WASMTIME_MODULE) => Some(Precompiled::Module),
160        FileFlags::Elf {
161            os_abi: obj::ELFOSABI_WASMTIME,
162            abi_version: 0,
163            e_flags,
164        } if e_flags.contains(obj::EF_WASMTIME_COMPONENT) => Some(Precompiled::Component),
165        _ => None,
166    }
167}
168
169pub fn detect_precompiled_bytes(bytes: &[u8]) -> Option<Precompiled> {
170    detect_precompiled(ElfFile64::parse(bytes).ok()?)
171}
172
173#[cfg(feature = "std")]
174pub fn detect_precompiled_file(path: impl AsRef<std::path::Path>) -> Result<Option<Precompiled>> {
175    let read_cache = object::ReadCache::new(std::fs::File::open(path)?);
176    let obj = ElfFile64::parse(&read_cache)?;
177    Ok(detect_precompiled(obj))
178}
179
180#[derive(Serialize, Deserialize)]
181pub struct Metadata<'a> {
182    target: TryString,
183    #[serde(borrow)]
184    shared_flags: TryVec<(&'a str, FlagValue<'a>)>,
185    #[serde(borrow)]
186    isa_flags: TryVec<(&'a str, FlagValue<'a>)>,
187    tunables: Tunables,
188    features: u64,
189}
190
191impl Metadata<'_> {
192    #[cfg(any(feature = "cranelift", feature = "winch"))]
193    pub fn new(engine: &Engine) -> Result<Metadata<'static>> {
194        let compiler = engine.try_compiler()?;
195        Ok(Metadata {
196            target: compiler.triple().to_string().into(),
197            shared_flags: compiler.flags().into(),
198            isa_flags: compiler.isa_flags().into(),
199            tunables: engine.tunables().clone(),
200            features: engine.features().bits(),
201        })
202    }
203
204    fn check_compatible(mut self, engine: &Engine) -> Result<()> {
205        self.check_triple(engine)?;
206        self.check_shared_flags(engine)?;
207        self.check_isa_flags(engine)?;
208        self.check_tunables(&engine.tunables())?;
209        self.check_features(&engine.features())?;
210        Ok(())
211    }
212
213    fn check_triple(&self, engine: &Engine) -> Result<()> {
214        let engine_target = engine.target();
215        let module_target =
216            target_lexicon::Triple::from_str(&self.target).map_err(|e| format_err!(e))?;
217
218        if module_target.architecture != engine_target.architecture {
219            bail!(
220                "Module was compiled for architecture '{}'",
221                module_target.architecture
222            );
223        }
224
225        if module_target.operating_system != engine_target.operating_system {
226            bail!(
227                "Module was compiled for operating system '{}'",
228                module_target.operating_system
229            );
230        }
231
232        Ok(())
233    }
234
235    fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
236        for (name, val) in self.shared_flags.iter() {
237            engine
238                .check_compatible_with_shared_flag(name, val)
239                .map_err(|s| crate::Error::msg(s))
240                .context("compilation settings of module incompatible with native host")?;
241        }
242        Ok(())
243    }
244
245    fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
246        for (name, val) in self.isa_flags.iter() {
247            engine
248                .check_compatible_with_isa_flag(name, val)
249                .map_err(|s| crate::Error::msg(s))
250                .context("compilation settings of module incompatible with native host")?;
251        }
252        Ok(())
253    }
254
255    fn check_int<T: Eq + fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
256        if found == expected {
257            return Ok(());
258        }
259
260        bail!(
261            "Module was compiled with a {feature} of '{found}' but '{expected}' is expected for the host"
262        );
263    }
264
265    fn check_bool(found: bool, expected: bool, feature: impl fmt::Display) -> Result<()> {
266        if found == expected {
267            return Ok(());
268        }
269
270        bail!(
271            "Module was compiled {} {} but it {} enabled for the host",
272            if found { "with" } else { "without" },
273            feature,
274            if expected { "is" } else { "is not" }
275        );
276    }
277
278    fn check_cost(
279        consume_fuel: bool,
280        found: &OperatorCostStrategy,
281        expected: &OperatorCostStrategy,
282    ) -> Result<()> {
283        if !consume_fuel {
284            return Ok(());
285        }
286
287        if found != expected {
288            bail!("Module costs are incompatible");
289        }
290
291        Ok(())
292    }
293
294    fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
295        let Tunables {
296            collector,
297            memory_reservation,
298            memory_guard_size,
299            debug_native,
300            debug_guest,
301            debug_symbols,
302            parse_wasm_debuginfo,
303            consume_fuel,
304            ref operator_cost,
305            epoch_interruption,
306            mmu_interruption,
307            memory_may_move,
308            guard_before_linear_memory,
309            table_lazy_init,
310            relaxed_simd_deterministic,
311            winch_callable,
312            signals_based_traps,
313            memory_init_cow,
314            inlining,
315            inlining_small_callee_size,
316            inlining_sum_size_threshold,
317            concurrency_support,
318            recording,
319            asan_stack_switching,
320            memory_watchpoints,
321
322            // This doesn't affect compilation, it's just a runtime setting.
323            memory_reservation_for_growth: _,
324
325            // This does technically affect compilation but modules with/without
326            // trap information can be loaded into engines with the opposite
327            // setting just fine (it's just a section in the compiled file and
328            // whether it's present or not)
329            generate_address_map: _,
330
331            // Just a debugging aid, doesn't affect functionality at all.
332            debug_adapter_modules: _,
333
334            // This is a runtime GC debugging setting, doesn't affect compilation.
335            gc_zeal_alloc_counter: _,
336
337            gc_heap_reservation,
338            gc_heap_guard_size,
339            gc_heap_may_move,
340            gc_heap_initial_size,
341
342            // This doesn't affect compilation, it's just a runtime setting.
343            gc_heap_reservation_for_growth: _,
344
345            // No need to match whether or not this metadata is emitted, if it
346            // is or isn't then that's fine, the runtime handles it the same
347            // way.
348            metadata_for_internal_asserts: _,
349            metadata_for_gc_heap_corruption: _,
350
351            // Only affects cold-block layout; a compiled artifact loads into an
352            // engine configured either way.
353            branch_hinting: _,
354        } = self.tunables;
355
356        Self::check_collector(collector, other.collector)?;
357        Self::check_int(
358            memory_reservation,
359            other.memory_reservation,
360            "memory reservation",
361        )?;
362        Self::check_int(
363            memory_guard_size,
364            other.memory_guard_size,
365            "memory guard size",
366        )?;
367        Self::check_bool(
368            debug_native,
369            other.debug_native,
370            "native debug information support",
371        )?;
372        Self::check_bool(debug_guest, other.debug_guest, "guest debug")?;
373        Self::check_bool(debug_symbols, other.debug_symbols, "debug symbols")?;
374        Self::check_bool(
375            parse_wasm_debuginfo,
376            other.parse_wasm_debuginfo,
377            "WebAssembly backtrace support",
378        )?;
379        Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
380        Self::check_cost(consume_fuel, operator_cost, &other.operator_cost)?;
381        Self::check_bool(
382            epoch_interruption,
383            other.epoch_interruption,
384            "epoch interruption",
385        )?;
386        Self::check_bool(mmu_interruption, other.mmu_interruption, "MMU interruption")?;
387        Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
388        Self::check_bool(
389            guard_before_linear_memory,
390            other.guard_before_linear_memory,
391            "guard before linear memory",
392        )?;
393        Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
394        Self::check_bool(
395            relaxed_simd_deterministic,
396            other.relaxed_simd_deterministic,
397            "relaxed simd deterministic semantics",
398        )?;
399        Self::check_bool(
400            winch_callable,
401            other.winch_callable,
402            "Winch calling convention",
403        )?;
404        Self::check_bool(
405            signals_based_traps,
406            other.signals_based_traps,
407            "Signals-based traps",
408        )?;
409        Self::check_bool(
410            memory_init_cow,
411            other.memory_init_cow,
412            "memory initialization with CoW",
413        )?;
414        Self::check_int(
415            inlining_small_callee_size,
416            other.inlining_small_callee_size,
417            "function inlining small-callee size",
418        )?;
419        Self::check_int(
420            inlining_sum_size_threshold,
421            other.inlining_sum_size_threshold,
422            "function inlining sum-size threshold",
423        )?;
424        Self::check_bool(
425            concurrency_support,
426            other.concurrency_support,
427            "concurrency support",
428        )?;
429        Self::check_bool(recording, other.recording, "RR recording support")?;
430        Self::check_bool(
431            asan_stack_switching,
432            other.asan_stack_switching,
433            "AddressSanitizer stack-switching support",
434        )?;
435        Self::check_bool(
436            memory_watchpoints,
437            other.memory_watchpoints,
438            "memory watchpoint support",
439        )?;
440        Self::check_inlining(inlining, other.inlining)?;
441        Self::check_int(
442            gc_heap_reservation,
443            other.gc_heap_reservation,
444            "GC heap reservation",
445        )?;
446        Self::check_int(
447            gc_heap_guard_size,
448            other.gc_heap_guard_size,
449            "GC heap guard size",
450        )?;
451        Self::check_int(
452            gc_heap_initial_size,
453            other.gc_heap_initial_size,
454            "GC heap initial size",
455        )?;
456        Self::check_bool(gc_heap_may_move, other.gc_heap_may_move, "GC heap may move")?;
457
458        Ok(())
459    }
460
461    fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
462        let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
463        let missing_features = (*other & module_features) ^ module_features;
464        for (name, _) in missing_features.iter_names() {
465            let name = name.to_ascii_lowercase();
466            bail!(
467                "Module was compiled with support for WebAssembly feature \
468                `{name}` but it is not enabled for the host",
469            );
470        }
471        Ok(())
472    }
473
474    fn check_collector(
475        module: Option<wasmtime_environ::Collector>,
476        host: Option<wasmtime_environ::Collector>,
477    ) -> Result<()> {
478        match (module, host) {
479            // If the module doesn't require GC support it doesn't matter
480            // whether the host has GC support enabled or not.
481            (None, _) => Ok(()),
482            (Some(module), Some(host)) if module == host => Ok(()),
483
484            (Some(_), None) => {
485                bail!("module was compiled with GC however GC is disabled in the host")
486            }
487
488            (Some(module), Some(host)) => {
489                bail!(
490                    "module was compiled for the {module} collector but \
491                     the host is configured to use the {host} collector",
492                )
493            }
494        }
495    }
496
497    fn check_inlining(
498        module: wasmtime_environ::Inlining,
499        host: wasmtime_environ::Inlining,
500    ) -> Result<()> {
501        if module == host {
502            return Ok(());
503        }
504
505        let desc = |cfg| match cfg {
506            wasmtime_environ::Inlining::No => "without intra-module inlining",
507            wasmtime_environ::Inlining::Yes => "with intra-module inlining",
508            wasmtime_environ::Inlining::InterModuleAndIntraGc => {
509                "with intra-module inlining only when using GC"
510            }
511            wasmtime_environ::Inlining::Intrinsics => "with intrinsic inlining",
512            wasmtime_environ::Inlining::InterModule => "with inter-module inlining",
513        };
514
515        let module = desc(module);
516        let host = desc(host);
517
518        bail!("module was compiled {module} however the host is configured {host}")
519    }
520}
521
522#[cfg(test)]
523mod test {
524    use super::*;
525    use crate::{Cache, Config, Module, OptLevel};
526    use std::{
527        collections::hash_map::DefaultHasher,
528        hash::{Hash, Hasher},
529    };
530    use tempfile::TempDir;
531
532    #[test]
533    fn test_architecture_mismatch() -> Result<()> {
534        let engine = Engine::default();
535        let mut metadata = Metadata::new(&engine)?;
536        metadata.target = "unknown-generic-linux".to_string().into();
537
538        match metadata.check_compatible(&engine) {
539            Ok(_) => unreachable!(),
540            Err(e) => assert_eq!(
541                e.to_string(),
542                "Module was compiled for architecture 'unknown'",
543            ),
544        }
545
546        Ok(())
547    }
548
549    // Note that this test runs on a platform that is known to use Cranelift
550    #[test]
551    #[cfg(all(target_arch = "x86_64", not(miri)))]
552    fn test_os_mismatch() -> Result<()> {
553        let engine = Engine::default();
554        let mut metadata = Metadata::new(&engine)?;
555
556        metadata.target = format!(
557            "{}-generic-unknown",
558            target_lexicon::Triple::host().architecture
559        )
560        .into();
561
562        match metadata.check_compatible(&engine) {
563            Ok(_) => unreachable!(),
564            Err(e) => assert_eq!(
565                e.to_string(),
566                "Module was compiled for operating system 'unknown'",
567            ),
568        }
569
570        Ok(())
571    }
572
573    fn assert_contains(error: &Error, msg: &str) {
574        let msg = msg.trim();
575        if error.chain().any(|e| e.to_string().contains(msg)) {
576            return;
577        }
578
579        panic!("failed to find:\n\n'''{msg}\n'''\n\nwithin error message:\n\n'''{error:?}'''")
580    }
581
582    #[test]
583    fn test_cranelift_flags_mismatch() -> Result<()> {
584        let engine = Engine::default();
585        let mut metadata = Metadata::new(&engine)?;
586
587        metadata
588            .shared_flags
589            .push(("preserve_frame_pointers", FlagValue::Bool(false)))?;
590
591        match metadata.check_compatible(&engine) {
592            Ok(_) => unreachable!(),
593            Err(e) => {
594                assert_contains(
595                    &e,
596                    "compilation settings of module incompatible with native host",
597                );
598                assert_contains(
599                    &e,
600                    "setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported",
601                );
602            }
603        }
604
605        Ok(())
606    }
607
608    #[test]
609    fn test_isa_flags_mismatch() -> Result<()> {
610        let engine = Engine::default();
611        let mut metadata = Metadata::new(&engine)?;
612
613        metadata
614            .isa_flags
615            .push(("not_a_flag", FlagValue::Bool(true)))?;
616
617        match metadata.check_compatible(&engine) {
618            Ok(_) => unreachable!(),
619            Err(e) => {
620                assert_contains(
621                    &e,
622                    "compilation settings of module incompatible with native host",
623                );
624                assert_contains(
625                    &e,
626                    "don't know how to test for target-specific flag \"not_a_flag\" at runtime",
627                );
628            }
629        }
630
631        Ok(())
632    }
633
634    #[test]
635    #[cfg_attr(any(miri, not(has_native_signals)), ignore)]
636    #[cfg(target_pointer_width = "64")] // different defaults on 32-bit platforms
637    fn test_tunables_int_mismatch() -> Result<()> {
638        let engine = Engine::default();
639        let mut metadata = Metadata::new(&engine)?;
640
641        metadata.tunables.memory_guard_size = 0;
642
643        match metadata.check_compatible(&engine) {
644            Ok(_) => unreachable!(),
645            Err(e) => assert_eq!(
646                e.to_string(),
647                "Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
648            ),
649        }
650
651        Ok(())
652    }
653
654    #[test]
655    fn test_tunables_bool_mismatch() -> Result<()> {
656        let mut config = Config::new();
657        config.epoch_interruption(true);
658
659        let engine = Engine::new(&config)?;
660        let mut metadata = Metadata::new(&engine)?;
661        metadata.tunables.epoch_interruption = false;
662
663        match metadata.check_compatible(&engine) {
664            Ok(_) => unreachable!(),
665            Err(e) => assert_eq!(
666                e.to_string(),
667                "Module was compiled without epoch interruption but it is enabled for the host"
668            ),
669        }
670
671        let mut config = Config::new();
672        config.epoch_interruption(false);
673
674        let engine = Engine::new(&config)?;
675        let mut metadata = Metadata::new(&engine)?;
676        metadata.tunables.epoch_interruption = true;
677
678        match metadata.check_compatible(&engine) {
679            Ok(_) => unreachable!(),
680            Err(e) => assert_eq!(
681                e.to_string(),
682                "Module was compiled with epoch interruption but it is not enabled for the host"
683            ),
684        }
685
686        Ok(())
687    }
688
689    /// This test is only run a platform that is known to implement threads
690    #[test]
691    #[cfg(all(target_arch = "x86_64", not(miri)))]
692    fn test_feature_mismatch() -> Result<()> {
693        let mut config = Config::new();
694        config.wasm_threads(true);
695
696        let engine = Engine::new(&config)?;
697        let mut metadata = Metadata::new(&engine)?;
698        metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
699
700        // If a feature is disabled in the module and enabled in the host,
701        // that's always ok.
702        metadata.check_compatible(&engine)?;
703
704        let mut config = Config::new();
705        config.wasm_threads(false);
706
707        let engine = Engine::new(&config)?;
708        let mut metadata = Metadata::new(&engine)?;
709        metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
710
711        match metadata.check_compatible(&engine) {
712            Ok(_) => unreachable!(),
713            Err(e) => assert_eq!(
714                e.to_string(),
715                "Module was compiled with support for WebAssembly feature \
716                `threads` but it is not enabled for the host"
717            ),
718        }
719
720        Ok(())
721    }
722
723    #[test]
724    fn engine_weak_upgrades() {
725        let engine = Engine::default();
726        let weak = engine.weak();
727        weak.upgrade()
728            .expect("engine is still alive, so weak reference can upgrade");
729        drop(engine);
730        assert!(
731            weak.upgrade().is_none(),
732            "engine was dropped, so weak reference cannot upgrade"
733        );
734    }
735
736    #[test]
737    #[cfg_attr(miri, ignore)]
738    fn cache_accounts_for_opt_level() -> Result<()> {
739        let _ = env_logger::try_init();
740
741        let td = TempDir::new()?;
742        let config_path = td.path().join("config.toml");
743        std::fs::write(
744            &config_path,
745            &format!(
746                "
747                    [cache]
748                    directory = '{}'
749                ",
750                td.path().join("cache").display()
751            ),
752        )?;
753        let mut cfg = Config::new();
754        cfg.cranelift_opt_level(OptLevel::None)
755            .cache(Some(Cache::from_file(Some(&config_path))?));
756        let engine = Engine::new(&cfg)?;
757        Module::new(&engine, "(module (func))")?;
758        let cache_config = engine
759            .config()
760            .cache
761            .as_ref()
762            .expect("Missing cache config");
763        assert_eq!(cache_config.cache_hits(), 0);
764        assert_eq!(cache_config.cache_misses(), 1);
765        Module::new(&engine, "(module (func))")?;
766        assert_eq!(cache_config.cache_hits(), 1);
767        assert_eq!(cache_config.cache_misses(), 1);
768
769        let mut cfg = Config::new();
770        cfg.cranelift_opt_level(OptLevel::Speed)
771            .cache(Some(Cache::from_file(Some(&config_path))?));
772        let engine = Engine::new(&cfg)?;
773        let cache_config = engine
774            .config()
775            .cache
776            .as_ref()
777            .expect("Missing cache config");
778        Module::new(&engine, "(module (func))")?;
779        assert_eq!(cache_config.cache_hits(), 0);
780        assert_eq!(cache_config.cache_misses(), 1);
781        Module::new(&engine, "(module (func))")?;
782        assert_eq!(cache_config.cache_hits(), 1);
783        assert_eq!(cache_config.cache_misses(), 1);
784
785        let mut cfg = Config::new();
786        cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
787            .cache(Some(Cache::from_file(Some(&config_path))?));
788        let engine = Engine::new(&cfg)?;
789        let cache_config = engine
790            .config()
791            .cache
792            .as_ref()
793            .expect("Missing cache config");
794        Module::new(&engine, "(module (func))")?;
795        assert_eq!(cache_config.cache_hits(), 0);
796        assert_eq!(cache_config.cache_misses(), 1);
797        Module::new(&engine, "(module (func))")?;
798        assert_eq!(cache_config.cache_hits(), 1);
799        assert_eq!(cache_config.cache_misses(), 1);
800
801        let mut cfg = Config::new();
802        cfg.debug_info(true)
803            .cache(Some(Cache::from_file(Some(&config_path))?));
804        let engine = Engine::new(&cfg)?;
805        let cache_config = engine
806            .config()
807            .cache
808            .as_ref()
809            .expect("Missing cache config");
810        Module::new(&engine, "(module (func))")?;
811        assert_eq!(cache_config.cache_hits(), 0);
812        assert_eq!(cache_config.cache_misses(), 1);
813        Module::new(&engine, "(module (func))")?;
814        assert_eq!(cache_config.cache_hits(), 1);
815        assert_eq!(cache_config.cache_misses(), 1);
816
817        Ok(())
818    }
819
820    #[test]
821    fn precompile_compatibility_key_accounts_for_opt_level() {
822        fn hash_for_config(cfg: &Config) -> u64 {
823            let engine = Engine::new(cfg).expect("Config should be valid");
824            let mut hasher = DefaultHasher::new();
825            engine.precompile_compatibility_hash().hash(&mut hasher);
826            hasher.finish()
827        }
828        let mut cfg = Config::new();
829        cfg.cranelift_opt_level(OptLevel::None);
830        let opt_none_hash = hash_for_config(&cfg);
831        cfg.cranelift_opt_level(OptLevel::Speed);
832        let opt_speed_hash = hash_for_config(&cfg);
833        assert_ne!(opt_none_hash, opt_speed_hash)
834    }
835
836    #[test]
837    fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
838        fn hash_for_config(cfg: &Config) -> u64 {
839            let engine = Engine::new(cfg).expect("Config should be valid");
840            let mut hasher = DefaultHasher::new();
841            engine.precompile_compatibility_hash().hash(&mut hasher);
842            hasher.finish()
843        }
844        let mut cfg_custom_version = Config::new();
845        cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
846        let custom_version_hash = hash_for_config(&cfg_custom_version);
847
848        let mut cfg_default_version = Config::new();
849        cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
850        let default_version_hash = hash_for_config(&cfg_default_version);
851
852        let mut cfg_none_version = Config::new();
853        cfg_none_version.module_version(ModuleVersionStrategy::None)?;
854        let none_version_hash = hash_for_config(&cfg_none_version);
855
856        assert_ne!(custom_version_hash, default_version_hash);
857        assert_ne!(custom_version_hash, none_version_hash);
858        assert_ne!(default_version_hash, none_version_hash);
859
860        Ok(())
861    }
862
863    #[test]
864    #[cfg_attr(miri, ignore)]
865    #[cfg(feature = "component-model")]
866    fn components_are_cached() -> Result<()> {
867        use crate::component::Component;
868
869        let td = TempDir::new()?;
870        let config_path = td.path().join("config.toml");
871        std::fs::write(
872            &config_path,
873            &format!(
874                "
875                    [cache]
876                    directory = '{}'
877                ",
878                td.path().join("cache").display()
879            ),
880        )?;
881        let mut cfg = Config::new();
882        cfg.cache(Some(Cache::from_file(Some(&config_path))?));
883        let engine = Engine::new(&cfg)?;
884        let cache_config = engine
885            .config()
886            .cache
887            .as_ref()
888            .expect("Missing cache config");
889        Component::new(&engine, "(component (core module (func)))")?;
890        assert_eq!(cache_config.cache_hits(), 0);
891        assert_eq!(cache_config.cache_misses(), 1);
892        Component::new(&engine, "(component (core module (func)))")?;
893        assert_eq!(cache_config.cache_hits(), 1);
894        assert_eq!(cache_config.cache_misses(), 1);
895
896        Ok(())
897    }
898}