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") {
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 => { }
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"),
132 ModuleVersionStrategy::Custom(c) => c,
133 ModuleVersionStrategy::None => "",
134 };
135 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 memory_reservation_for_growth: _,
293
294 generate_address_map: _,
299
300 debug_adapter_modules: _,
302 } = self.tunables;
303
304 Self::check_collector(collector, other.collector)?;
305 Self::check_int(
306 memory_reservation,
307 other.memory_reservation,
308 "memory reservation",
309 )?;
310 Self::check_int(
311 memory_guard_size,
312 other.memory_guard_size,
313 "memory guard size",
314 )?;
315 Self::check_bool(
316 generate_native_debuginfo,
317 other.generate_native_debuginfo,
318 "debug information support",
319 )?;
320 Self::check_bool(
321 parse_wasm_debuginfo,
322 other.parse_wasm_debuginfo,
323 "WebAssembly backtrace support",
324 )?;
325 Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
326 Self::check_bool(
327 epoch_interruption,
328 other.epoch_interruption,
329 "epoch interruption",
330 )?;
331 Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
332 Self::check_bool(
333 guard_before_linear_memory,
334 other.guard_before_linear_memory,
335 "guard before linear memory",
336 )?;
337 Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
338 Self::check_bool(
339 relaxed_simd_deterministic,
340 other.relaxed_simd_deterministic,
341 "relaxed simd deterministic semantics",
342 )?;
343 Self::check_bool(
344 winch_callable,
345 other.winch_callable,
346 "Winch calling convention",
347 )?;
348 Self::check_bool(
349 signals_based_traps,
350 other.signals_based_traps,
351 "Signals-based traps",
352 )?;
353 Self::check_bool(
354 memory_init_cow,
355 other.memory_init_cow,
356 "memory initialization with CoW",
357 )?;
358
359 Ok(())
360 }
361
362 fn check_cfg_bool(
363 cfg: bool,
364 cfg_str: &str,
365 found: bool,
366 expected: bool,
367 feature: impl fmt::Display,
368 ) -> Result<()> {
369 if cfg {
370 Self::check_bool(found, expected, feature)
371 } else {
372 assert!(!expected);
373 ensure!(
374 !found,
375 "Module was compiled with {feature} but support in the host \
376 was disabled at compile time because the `{cfg_str}` Cargo \
377 feature was not enabled",
378 );
379 Ok(())
380 }
381 }
382
383 fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
384 let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
385 let difference = *other ^ module_features;
386 for (name, flag) in difference.iter_names() {
387 let found = module_features.contains(flag);
388 let expected = other.contains(flag);
389 if flag == wasmparser::WasmFeatures::GC_TYPES {
393 Self::check_cfg_bool(
394 cfg!(feature = "gc"),
395 "gc",
396 found,
397 expected,
398 WasmFeature(name),
399 )?;
400 } else {
401 Self::check_bool(found, expected, WasmFeature(name))?;
402 }
403 }
404
405 return Ok(());
406
407 struct WasmFeature<'a>(&'a str);
408
409 impl fmt::Display for WasmFeature<'_> {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411 write!(f, "support for WebAssembly feature `")?;
412 for c in self.0.chars().map(|c| c.to_lowercase()) {
413 write!(f, "{c}")?;
414 }
415 write!(f, "`")?;
416 Ok(())
417 }
418 }
419 }
420
421 fn check_collector(
422 module: Option<wasmtime_environ::Collector>,
423 host: Option<wasmtime_environ::Collector>,
424 ) -> Result<()> {
425 match (module, host) {
426 (None, None) => Ok(()),
427 (Some(module), Some(host)) if module == host => Ok(()),
428
429 (None, Some(_)) => {
430 bail!("module was compiled without GC but GC is enabled in the host")
431 }
432 (Some(_), None) => {
433 bail!("module was compiled with GC however GC is disabled in the host")
434 }
435
436 (Some(module), Some(host)) => {
437 bail!(
438 "module was compiled for the {module} collector but \
439 the host is configured to use the {host} collector",
440 )
441 }
442 }
443 }
444}
445
446#[cfg(test)]
447mod test {
448 use super::*;
449 use crate::{Cache, Config, Module, OptLevel};
450 use std::{
451 collections::hash_map::DefaultHasher,
452 hash::{Hash, Hasher},
453 };
454 use tempfile::TempDir;
455
456 #[test]
457 fn test_architecture_mismatch() -> Result<()> {
458 let engine = Engine::default();
459 let mut metadata = Metadata::new(&engine);
460 metadata.target = "unknown-generic-linux".to_string();
461
462 match metadata.check_compatible(&engine) {
463 Ok(_) => unreachable!(),
464 Err(e) => assert_eq!(
465 e.to_string(),
466 "Module was compiled for architecture 'unknown'",
467 ),
468 }
469
470 Ok(())
471 }
472
473 #[test]
474 #[cfg(target_arch = "x86_64")] fn test_os_mismatch() -> Result<()> {
477 let engine = Engine::default();
478 let mut metadata = Metadata::new(&engine);
479
480 metadata.target = format!(
481 "{}-generic-unknown",
482 target_lexicon::Triple::host().architecture
483 );
484
485 match metadata.check_compatible(&engine) {
486 Ok(_) => unreachable!(),
487 Err(e) => assert_eq!(
488 e.to_string(),
489 "Module was compiled for operating system 'unknown'",
490 ),
491 }
492
493 Ok(())
494 }
495
496 #[test]
497 fn test_cranelift_flags_mismatch() -> Result<()> {
498 let engine = Engine::default();
499 let mut metadata = Metadata::new(&engine);
500
501 metadata
502 .shared_flags
503 .push(("preserve_frame_pointers", FlagValue::Bool(false)));
504
505 match metadata.check_compatible(&engine) {
506 Ok(_) => unreachable!(),
507 Err(e) => assert!(format!("{e:?}").starts_with(
508 "\
509compilation settings of module incompatible with native host
510
511Caused by:
512 setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported"
513 )),
514 }
515
516 Ok(())
517 }
518
519 #[test]
520 fn test_isa_flags_mismatch() -> Result<()> {
521 let engine = Engine::default();
522 let mut metadata = Metadata::new(&engine);
523
524 metadata
525 .isa_flags
526 .push(("not_a_flag", FlagValue::Bool(true)));
527
528 match metadata.check_compatible(&engine) {
529 Ok(_) => unreachable!(),
530 Err(e) => assert!(
531 format!("{e:?}").starts_with(
532 "\
533compilation settings of module incompatible with native host
534
535Caused by:
536 don't know how to test for target-specific flag \"not_a_flag\" at runtime",
537 ),
538 "bad error {e:?}",
539 ),
540 }
541
542 Ok(())
543 }
544
545 #[test]
546 #[cfg_attr(miri, ignore)]
547 #[cfg(target_pointer_width = "64")] fn test_tunables_int_mismatch() -> Result<()> {
549 let engine = Engine::default();
550 let mut metadata = Metadata::new(&engine);
551
552 metadata.tunables.memory_guard_size = 0;
553
554 match metadata.check_compatible(&engine) {
555 Ok(_) => unreachable!(),
556 Err(e) => assert_eq!(
557 e.to_string(),
558 "Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
559 ),
560 }
561
562 Ok(())
563 }
564
565 #[test]
566 fn test_tunables_bool_mismatch() -> Result<()> {
567 let mut config = Config::new();
568 config.epoch_interruption(true);
569
570 let engine = Engine::new(&config)?;
571 let mut metadata = Metadata::new(&engine);
572 metadata.tunables.epoch_interruption = false;
573
574 match metadata.check_compatible(&engine) {
575 Ok(_) => unreachable!(),
576 Err(e) => assert_eq!(
577 e.to_string(),
578 "Module was compiled without epoch interruption but it is enabled for the host"
579 ),
580 }
581
582 let mut config = Config::new();
583 config.epoch_interruption(false);
584
585 let engine = Engine::new(&config)?;
586 let mut metadata = Metadata::new(&engine);
587 metadata.tunables.epoch_interruption = true;
588
589 match metadata.check_compatible(&engine) {
590 Ok(_) => unreachable!(),
591 Err(e) => assert_eq!(
592 e.to_string(),
593 "Module was compiled with epoch interruption but it is not enabled for the host"
594 ),
595 }
596
597 Ok(())
598 }
599
600 #[test]
601 #[cfg(target_arch = "x86_64")] fn test_feature_mismatch() -> Result<()> {
604 let mut config = Config::new();
605 config.wasm_threads(true);
606
607 let engine = Engine::new(&config)?;
608 let mut metadata = Metadata::new(&engine);
609 metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
610
611 match metadata.check_compatible(&engine) {
612 Ok(_) => unreachable!(),
613 Err(e) => assert_eq!(
614 e.to_string(),
615 "Module was compiled without support for WebAssembly feature \
616 `threads` but it is enabled for the host"
617 ),
618 }
619
620 let mut config = Config::new();
621 config.wasm_threads(false);
622
623 let engine = Engine::new(&config)?;
624 let mut metadata = Metadata::new(&engine);
625 metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
626
627 match metadata.check_compatible(&engine) {
628 Ok(_) => unreachable!(),
629 Err(e) => assert_eq!(
630 e.to_string(),
631 "Module was compiled with support for WebAssembly feature \
632 `threads` but it is not enabled for the host"
633 ),
634 }
635
636 Ok(())
637 }
638
639 #[test]
640 fn engine_weak_upgrades() {
641 let engine = Engine::default();
642 let weak = engine.weak();
643 weak.upgrade()
644 .expect("engine is still alive, so weak reference can upgrade");
645 drop(engine);
646 assert!(
647 weak.upgrade().is_none(),
648 "engine was dropped, so weak reference cannot upgrade"
649 );
650 }
651
652 #[test]
653 #[cfg_attr(miri, ignore)]
654 fn cache_accounts_for_opt_level() -> Result<()> {
655 let td = TempDir::new()?;
656 let config_path = td.path().join("config.toml");
657 std::fs::write(
658 &config_path,
659 &format!(
660 "
661 [cache]
662 directory = '{}'
663 ",
664 td.path().join("cache").display()
665 ),
666 )?;
667 let mut cfg = Config::new();
668 cfg.cranelift_opt_level(OptLevel::None)
669 .cache(Some(Cache::from_file(Some(&config_path))?));
670 let engine = Engine::new(&cfg)?;
671 Module::new(&engine, "(module (func))")?;
672 let cache_config = engine
673 .config()
674 .cache
675 .as_ref()
676 .expect("Missing cache config");
677 assert_eq!(cache_config.cache_hits(), 0);
678 assert_eq!(cache_config.cache_misses(), 1);
679 Module::new(&engine, "(module (func))")?;
680 assert_eq!(cache_config.cache_hits(), 1);
681 assert_eq!(cache_config.cache_misses(), 1);
682
683 let mut cfg = Config::new();
684 cfg.cranelift_opt_level(OptLevel::Speed)
685 .cache(Some(Cache::from_file(Some(&config_path))?));
686 let engine = Engine::new(&cfg)?;
687 let cache_config = engine
688 .config()
689 .cache
690 .as_ref()
691 .expect("Missing cache config");
692 Module::new(&engine, "(module (func))")?;
693 assert_eq!(cache_config.cache_hits(), 0);
694 assert_eq!(cache_config.cache_misses(), 1);
695 Module::new(&engine, "(module (func))")?;
696 assert_eq!(cache_config.cache_hits(), 1);
697 assert_eq!(cache_config.cache_misses(), 1);
698
699 let mut cfg = Config::new();
700 cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
701 .cache(Some(Cache::from_file(Some(&config_path))?));
702 let engine = Engine::new(&cfg)?;
703 let cache_config = engine
704 .config()
705 .cache
706 .as_ref()
707 .expect("Missing cache config");
708 Module::new(&engine, "(module (func))")?;
709 assert_eq!(cache_config.cache_hits(), 0);
710 assert_eq!(cache_config.cache_misses(), 1);
711 Module::new(&engine, "(module (func))")?;
712 assert_eq!(cache_config.cache_hits(), 1);
713 assert_eq!(cache_config.cache_misses(), 1);
714
715 let mut cfg = Config::new();
716 cfg.debug_info(true)
717 .cache(Some(Cache::from_file(Some(&config_path))?));
718 let engine = Engine::new(&cfg)?;
719 let cache_config = engine
720 .config()
721 .cache
722 .as_ref()
723 .expect("Missing cache config");
724 Module::new(&engine, "(module (func))")?;
725 assert_eq!(cache_config.cache_hits(), 0);
726 assert_eq!(cache_config.cache_misses(), 1);
727 Module::new(&engine, "(module (func))")?;
728 assert_eq!(cache_config.cache_hits(), 1);
729 assert_eq!(cache_config.cache_misses(), 1);
730
731 Ok(())
732 }
733
734 #[test]
735 fn precompile_compatibility_key_accounts_for_opt_level() {
736 fn hash_for_config(cfg: &Config) -> u64 {
737 let engine = Engine::new(cfg).expect("Config should be valid");
738 let mut hasher = DefaultHasher::new();
739 engine.precompile_compatibility_hash().hash(&mut hasher);
740 hasher.finish()
741 }
742 let mut cfg = Config::new();
743 cfg.cranelift_opt_level(OptLevel::None);
744 let opt_none_hash = hash_for_config(&cfg);
745 cfg.cranelift_opt_level(OptLevel::Speed);
746 let opt_speed_hash = hash_for_config(&cfg);
747 assert_ne!(opt_none_hash, opt_speed_hash)
748 }
749
750 #[test]
751 fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
752 fn hash_for_config(cfg: &Config) -> u64 {
753 let engine = Engine::new(cfg).expect("Config should be valid");
754 let mut hasher = DefaultHasher::new();
755 engine.precompile_compatibility_hash().hash(&mut hasher);
756 hasher.finish()
757 }
758 let mut cfg_custom_version = Config::new();
759 cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
760 let custom_version_hash = hash_for_config(&cfg_custom_version);
761
762 let mut cfg_default_version = Config::new();
763 cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
764 let default_version_hash = hash_for_config(&cfg_default_version);
765
766 let mut cfg_none_version = Config::new();
767 cfg_none_version.module_version(ModuleVersionStrategy::None)?;
768 let none_version_hash = hash_for_config(&cfg_none_version);
769
770 assert_ne!(custom_version_hash, default_version_hash);
771 assert_ne!(custom_version_hash, none_version_hash);
772 assert_ne!(default_version_hash, none_version_hash);
773
774 Ok(())
775 }
776
777 #[test]
778 #[cfg_attr(miri, ignore)]
779 #[cfg(feature = "component-model")]
780 fn components_are_cached() -> Result<()> {
781 use crate::component::Component;
782
783 let td = TempDir::new()?;
784 let config_path = td.path().join("config.toml");
785 std::fs::write(
786 &config_path,
787 &format!(
788 "
789 [cache]
790 directory = '{}'
791 ",
792 td.path().join("cache").display()
793 ),
794 )?;
795 let mut cfg = Config::new();
796 cfg.cache(Some(Cache::from_file(Some(&config_path))?));
797 let engine = Engine::new(&cfg)?;
798 let cache_config = engine
799 .config()
800 .cache
801 .as_ref()
802 .expect("Missing cache config");
803 Component::new(&engine, "(component (core module (func)))")?;
804 assert_eq!(cache_config.cache_hits(), 0);
805 assert_eq!(cache_config.cache_misses(), 1);
806 Component::new(&engine, "(component (core module (func)))")?;
807 assert_eq!(cache_config.cache_hits(), 1);
808 assert_eq!(cache_config.cache_misses(), 1);
809
810 Ok(())
811 }
812}