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