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