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