Repository: bytecodealliance/wasmtime
Pull request #8078 (2024-03-11 17:30:45)

Add a component type accessor for `Component`
All the bits are already in place so all that's needed is to touch up some docs and fixup a minor case uncovered through testing. Otherwise this enables runtime reflection over the imports and exports of an uninstantiated component.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


github-actions[bot]: #### Subscribe to Label Action

cc @peterhuene

<details>
This issue or pull request has been labeled: "wasmtime:api"

Thus the following users have been cc'd because of the following labels:

* peterhuene: wasmtime:api

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e97968826..dd165d671 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -212,7 +212,7 @@ jobs:
         submodules: true
     - uses: ./.github/actions/install-rust
       with:
-        toolchain: nightly-2024-02-22
+        toolchain: nightly-2024-03-11
 
     # Build C API documentation
     - run: curl -L https://sourceforge.net/projects/doxygen/files/rel-1.9.3/doxygen-1.9.3.linux.bin.tar.gz/download | tar xzf -
@@ -397,7 +397,7 @@ jobs:
     # happen upstream. This is periodically updated through a PR.
     - uses: ./.github/actions/install-rust
       with:
-        toolchain: nightly-2024-02-22
+        toolchain: nightly-2024-03-11
 
     # Ensure that fuzzers sitll build.
     #
@@ -723,7 +723,7 @@ jobs:
         submodules: true
     - uses: ./.github/actions/install-rust
       with:
-        toolchain: nightly-2024-02-22
+        toolchain: nightly-2024-03-11
     - run: rustup component add rust-src miri
     - uses: actions/cache@v4
       with:
diff --git a/Cargo.toml b/Cargo.toml
index 9a8dcd502..d6972080e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -135,7 +135,7 @@ authors = ["The Wasmtime Project Developers"]
 edition = "2021"
 # Wasmtime's current policy is that this number can be no larger than the
 # current stable release of Rust minus 2.
-rust-version = "1.73.0"
+rust-version = "1.74.0"
 
 [workspace.lints.rust]
 # Turn on some lints which are otherwise allow-by-default in rustc.
diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs
index 489b5707e..eecf00cf9 100644
--- a/crates/environ/src/component/types.rs
+++ b/crates/environ/src/component/types.rs
@@ -1007,7 +1007,7 @@ pub enum TypeDef {
     ComponentInstance(TypeComponentInstanceIndex),
     /// A component function, not to be confused with a core wasm function.
     ComponentFunc(TypeFuncIndex),
-    /// An interface type.
+    /// An type in an interface.
     Interface(InterfaceType),
     /// A core wasm module and its type.
     Module(TypeModuleIndex),
@@ -1020,6 +1020,21 @@ pub enum TypeDef {
     Resource(TypeResourceTableIndex),
 }
 
+impl TypeDef {
+    /// A human readable description of what kind of type definition this is.
+    pub fn desc(&self) -> &str {
+        match self {
+            TypeDef::Component(_) => "component",
+            TypeDef::ComponentInstance(_) => "instance",
+            TypeDef::ComponentFunc(_) => "function",
+            TypeDef::Interface(_) => "type",
+            TypeDef::Module(_) => "core module",
+            TypeDef::CoreFunc(_) => "core function",
+            TypeDef::Resource(_) => "resource",
+        }
+    }
+}
+
 // NB: Note that maps below are stored as an `IndexMap` now but the order
 // typically does not matter. As a minor implementation detail we want the
 // serialization of this type to always be deterministic and using `IndexMap`
diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs
index af242d85d..25444ef6a 100644
--- a/crates/wasmtime/src/runtime/component/component.rs
+++ b/crates/wasmtime/src/runtime/component/component.rs
@@ -1,3 +1,5 @@
+use crate::component::matching::InstanceType;
+use crate::component::types;
 use crate::{
     code::CodeObject, code_memory::CodeMemory, instantiate::MmapVecWrapper,
     type_registry::TypeCollection, Engine, Module, ResourcesRequired,
@@ -284,6 +286,141 @@ impl Component {
         Component::from_parts(engine, code, None)
     }
 
+    /// Returns the type of this component as a [`types::Component`].
+    ///
+    /// This method enables runtime introspection of the type of a component
+    /// before instantiation, if necessary.
+    ///
+    /// ## Component types and Resources
+    ///
+    /// An important point to note here is that the precise type of imports and
+    /// exports of a component change when it is instantiated with respect to
+    /// resources. For example a [`Component`] represents an un-instantiated
+    /// component meaning that its imported resources are represeted as abstract
+    /// resource types. These abstract types are not equal to any other
+    /// component's types.
+    ///
+    /// For example:
+    ///
+    /// ```
+    /// # use wasmtime::Engine;
+    /// # use wasmtime::component::Component;
+    /// # use wasmtime::component::types::ComponentItem;
+    /// # fn main() -> wasmtime::Result<()> {
+    /// # let engine = Engine::default();
+    /// let a = Component::new(&engine, r#"
+    ///     (component (import "x" (type (sub resource))))
+    /// "#)?;
+    /// let b = Component::new(&engine, r#"
+    ///     (component (import "x" (type (sub resource))))
+    /// "#)?;
+    ///
+    /// let (_, a_ty) = a.component_type().imports(&engine).next().unwrap();
+    /// let (_, b_ty) = b.component_type().imports(&engine).next().unwrap();
+    ///
+    /// let a_ty = match a_ty {
+    ///     ComponentItem::Resource(ty) => ty,
+    ///     _ => unreachable!(),
+    /// };
+    /// let b_ty = match b_ty {
+    ///     ComponentItem::Resource(ty) => ty,
+    ///     _ => unreachable!(),
+    /// };
+    /// assert!(a_ty != b_ty);
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// Additionally, however, these abstract types are "substituted" during
+    /// instantiation meaning that a component type will appear to have changed
+    /// once it is instantiated.
+    ///
+    /// ```
+    /// # use wasmtime::{Engine, Store};
+    /// # use wasmtime::component::{Component, Linker, ResourceType};
+    /// # use wasmtime::component::types::ComponentItem;
+    /// # fn main() -> wasmtime::Result<()> {
+    /// # let engine = Engine::default();
+    /// // Here this component imports a resource and then exports it as-is
+    /// // which means that the export is equal to the import.
+    /// let a = Component::new(&engine, r#"
+    ///     (component
+    ///         (import "x" (type $x (sub resource)))
+    ///         (export "x" (type $x))
+    ///     )
+    /// "#)?;
+    ///
+    /// let (_, import) = a.component_type().imports(&engine).next().unwrap();
+    /// let (_, export) = a.component_type().exports(&engine).next().unwrap();
+    ///
+    /// let import = match import {
+    ///     ComponentItem::Resource(ty) => ty,
+    ///     _ => unreachable!(),
+    /// };
+    /// let export = match export {
+    ///     ComponentItem::Resource(ty) => ty,
+    ///     _ => unreachable!(),
+    /// };
+    /// assert_eq!(import, export);
+    ///
+    /// // However after instantiation the resource type "changes"
+    /// let mut store = Store::new(&engine, ());
+    /// let mut linker = Linker::new(&engine);
+    /// linker.root().resource("x", ResourceType::host::<()>(), |_, _| Ok(()))?;
+    /// let instance = linker.instantiate(&mut store, &a)?;
+    /// let instance_ty = instance.exports(&mut store).root().resource("x").unwrap();
+    ///
+    /// // Here `instance_ty` is not the same as either `import` or `export`,
+    /// // but it is equal to what we provided as an import.
+    /// assert!(instance_ty != import);
+    /// assert!(instance_ty != export);
+    /// assert!(instance_ty == ResourceType::host::<()>());
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// Finally, each instantiation of an exported resource from a component is
+    /// considered "fresh" for all instantiations meaning that different
+    /// instantiations will have different exported resource types:
+    ///
+    /// ```
+    /// # use wasmtime::{Engine, Store};
+    /// # use wasmtime::component::{Component, Linker};
+    /// # fn main() -> wasmtime::Result<()> {
+    /// # let engine = Engine::default();
+    /// let a = Component::new(&engine, r#"
+    ///     (component
+    ///         (type $x (resource (rep i32)))
+    ///         (export "x" (type $x))
+    ///     )
+    /// "#)?;
+    ///
+    /// let mut store = Store::new(&engine, ());
+    /// let linker = Linker::new(&engine);
+    /// let instance1 = linker.instantiate(&mut store, &a)?;
+    /// let instance2 = linker.instantiate(&mut store, &a)?;
+    ///
+    /// let x1 = instance1.exports(&mut store).root().resource("x").unwrap();
+    /// let x2 = instance2.exports(&mut store).root().resource("x").unwrap();
+    ///
+    /// // Despite these two resources being the same export of the same
+    /// // component they come from two different instances meaning that their
+    /// // types will be unique.
+    /// assert!(x1 != x2);
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn component_type(&self) -> types::Component {
+        let resources = Arc::new(PrimaryMap::new());
+        types::Component::from(
+            self.inner.ty,
+            &InstanceType {
+                types: self.types(),
+                resources: &resources,
+            },
+        )
+    }
+
     /// Final assembly step for a component from its in-memory representation.
     ///
     /// If the `artifacts` are specified as `None` here then they will be
diff --git a/crates/wasmtime/src/runtime/component/linker.rs b/crates/wasmtime/src/runtime/component/linker.rs
index 00bcb0a52..5c3847082 100644
--- a/crates/wasmtime/src/runtime/component/linker.rs
+++ b/crates/wasmtime/src/runtime/component/linker.rs
@@ -208,7 +208,7 @@ impl<T> Linker<T> {
         for (_idx, (name, ty)) in env_component.import_types.iter() {
             let import = self.map.get(name, &self.strings);
             cx.definition(ty, import)
-                .with_context(|| format!("import `{name}` has the wrong type"))?;
+                .with_context(|| format!("component imports {desc} `{name}`, but a matching implementation was not found in the linker", desc = ty.desc()))?;
         }
         Ok(cx)
     }
diff --git a/crates/wasmtime/src/runtime/component/matching.rs b/crates/wasmtime/src/runtime/component/matching.rs
index 7c47d98c9..8fb2c02c7 100644
--- a/crates/wasmtime/src/runtime/component/matching.rs
+++ b/crates/wasmtime/src/runtime/component/matching.rs
@@ -36,19 +36,27 @@ impl TypeChecker<'_> {
         match *expected {
             TypeDef::Module(t) => match actual {
                 Some(Definition::Module(actual)) => self.module(&self.types[t], actual),
-                _ => bail!("expected module found {}", desc(actual)),
+                Some(actual) => bail!("expected module found {}", actual.desc()),
+                None => bail!("module implementation is missing"),
             },
             TypeDef::ComponentInstance(t) => match actual {
                 Some(Definition::Instance(actual)) => self.instance(&self.types[t], Some(actual)),
                 None => self.instance(&self.types[t], None),
-                _ => bail!("expected instance found {}", desc(actual)),
+                Some(actual) => bail!("expected instance found {}", actual.desc()),
             },
             TypeDef::ComponentFunc(t) => match actual {
                 Some(Definition::Func(actual)) => self.func(t, actual),
-                _ => bail!("expected func found {}", desc(actual)),
+                Some(actual) => bail!("expected function found {}", actual.desc()),
+                None => bail!("function implementation is missing"),
+            },
+            TypeDef::Component(_) => match actual {
+                Some(actual) => bail!("expected component found {}", actual.desc()),
+                None => bail!("component implementation is missing"),
+            },
+            TypeDef::Interface(_) => match actual {
+                Some(actual) => bail!("expected type found {}", actual.desc()),
+                None => bail!("type implementation is missing"),
             },
-            TypeDef::Component(_) => bail!("expected component found {}", desc(actual)),
-            TypeDef::Interface(_) => bail!("expected type found {}", desc(actual)),
 
             TypeDef::Resource(i) => {
                 let i = self.types[i].ty;
@@ -63,7 +71,8 @@ impl TypeChecker<'_> {
                     // Wasmtime API.
                     None if self.imported_resources.get(i).is_some() => return Ok(()),
 
-                    _ => bail!("expected resource found {}", desc(actual)),
+                    Some(actual) => bail!("expected resource found {}", actual.desc()),
+                    None => bail!("resource implementation is missing"),
                 };
 
                 match self.imported_resources.get(i) {
@@ -165,13 +174,6 @@ impl TypeChecker<'_> {
     }
 }
 
-fn desc(def: Option<&Definition>) -> &'static str {
-    match def {
-        Some(def) => def.desc(),
-        None => "nothing",
-    }
-}
-
 impl Definition {
     fn desc(&self) -> &'static str {
         match self {
@@ -193,7 +195,10 @@ impl<'a> InstanceType<'a> {
 
     pub fn resource_type(&self, index: TypeResourceTableIndex) -> ResourceType {
         let index = self.types[index].ty;
-        self.resources[index]
+        self.resources
+            .get(index)
+            .copied()
+            .unwrap_or_else(|| ResourceType::uninstantiated(&self.types, index))
     }
 }
 


Test patch:
diff --git a/tests/all/component_model/aot.rs b/tests/all/component_model/aot.rs
index 5fc5c8890..b94e8f085 100644
--- a/tests/all/component_model/aot.rs
+++ b/tests/all/component_model/aot.rs
@@ -1,5 +1,6 @@
 use anyhow::Result;
-use wasmtime::component::{Component, Linker};
+use wasmtime::component::types::ComponentItem;
+use wasmtime::component::{Component, Linker, Type};
 use wasmtime::{Module, Precompiled, Store};
 
 #[test]
@@ -133,3 +134,33 @@ fn detect_precompiled() -> Result<()> {
     );
     Ok(())
 }
+
+#[test]
+fn reflect_resource_import() -> Result<()> {
+    let engine = super::engine();
+    let c = Component::new(
+        &engine,
+        r#"
+        (component
+            (import "x" (type $x (sub resource)))
+            (import "y" (func (result (own $x))))
+        )
+        "#,
+    )?;
+    let ty = c.component_type();
+    let mut imports = ty.imports(&engine);
+    let (_, x) = imports.next().unwrap();
+    let (_, y) = imports.next().unwrap();
+    let x = match x {
+        ComponentItem::Resource(t) => t,
+        _ => unreachable!(),
+    };
+    let y = match y {
+        ComponentItem::ComponentFunc(t) => t,
+        _ => unreachable!(),
+    };
+    let result = y.results().next().unwrap();
+    assert_eq!(result, Type::Own(x));
+
+    Ok(())
+}
diff --git a/tests/misc_testsuite/component-model/linking.wast b/tests/misc_testsuite/component-model/linking.wast
index 7cf4aad7d..966926f08 100644
--- a/tests/misc_testsuite/component-model/linking.wast
+++ b/tests/misc_testsuite/component-model/linking.wast
@@ -2,7 +2,7 @@
   (component
     (import "undefined-name" (core module))
   )
-  "expected module found nothing")
+  "was not found")
 (component $i)
 (component
   (import "i" (instance))
@@ -12,7 +12,7 @@
   "expected module found instance")
 (assert_unlinkable
   (component (import "i" (func)))
-  "expected func found instance")
+  "expected function found instance")
 (assert_unlinkable
   (component (import "i" (instance (export "x" (func)))))
-  "expected func found nothing")
+  "was not found")
diff --git a/tests/misc_testsuite/component-model/resources.wast b/tests/misc_testsuite/component-model/resources.wast
index 351ed85b3..c2868f838 100644
--- a/tests/misc_testsuite/component-model/resources.wast
+++ b/tests/misc_testsuite/component-model/resources.wast
@@ -167,7 +167,7 @@
       (export "missing" (type (sub resource)))
     ))
   )
-  "expected resource found nothing")
+  "was not found")
 (assert_unlinkable
   (component
     (import "host" (instance