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