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