Repository: bytecodealliance/wasm-tools
Pull request #1459 (2024-03-18 22:48:19)

allow implementing world imports via adapter
Previously, it was not possible to supply an adapter which implements e.g. `wasi:cli/environment@0.2.0` such that `wit-component` would replace the main (or library) module's import with the adapter's version.  This commit makes that possible by prefering the adapter version over the import whenever there's a match (i.e. an adapter with a name matching the WIT interface name and version).

This enables a form of lightweight virtualization in cases where we want to replace imported functions with e.g. trapping stubs or a few lines of WAT -- without adding component composition to the mix.  This approach also works with e.g. `wasmtime-py`, which as of this writing does not support composed components.


Patch:
diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs
index e9e2242c..66a61a39 100644
--- a/crates/wit-component/src/encoding/world.rs
+++ b/crates/wit-component/src/encoding/world.rs
@@ -87,7 +87,7 @@ impl<'a> ComponentWorld<'a> {
             exports_used: HashMap::new(),
         };
 
-        ret.process_adapters()?;
+        ret.process_adapters(&adapters)?;
         ret.process_imports()?;
         ret.process_exports_used();
         ret.process_live_type_imports();
@@ -99,7 +99,7 @@ impl<'a> ComponentWorld<'a> {
     /// adapters and figure out what functions are required from the
     /// adapter itself, either because the functions are imported by the
     /// main module or they're part of the adapter's exports.
-    fn process_adapters(&mut self) -> Result<()> {
+    fn process_adapters(&mut self, adapters: &IndexSet<&str>) -> Result<()> {
         let resolve = &self.encoder.metadata.resolve;
         let world = self.encoder.metadata.world;
         for (
@@ -157,6 +157,7 @@ impl<'a> ComponentWorld<'a> {
                 required_by_import,
                 required_exports,
                 library_info.is_some(),
+                adapters,
             )
             .context("failed to validate the imports of the minimized adapter module")?;
             self.adapters.insert(
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index b410443d..4ce35f9f 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -229,25 +229,31 @@ pub fn validate_module<'a>(
             continue;
         }
 
-        match world.imports.get(&world_key(&metadata.resolve, name)) {
-            Some(WorldItem::Interface(interface)) => {
-                let required =
-                    validate_imported_interface(&metadata.resolve, *interface, name, funcs, &types)
-                        .with_context(|| format!("failed to validate import interface `{name}`"))?;
-                let prev = ret.required_imports.insert(name, required);
-                assert!(prev.is_none());
+        if adapters.contains(name) {
+            let map = ret.adapters_required.entry(name).or_default();
+            for (func, ty) in funcs {
+                let ty = types[types.core_type_at(*ty).unwrap_sub()].unwrap_func();
+                map.insert(func, ty.clone());
             }
-            None if adapters.contains(name) => {
-                let map = ret.adapters_required.entry(name).or_default();
-                for (func, ty) in funcs {
-                    let ty = types[types.core_type_at(*ty).unwrap_sub()].unwrap_func();
-                    map.insert(func, ty.clone());
+        } else {
+            match world.imports.get(&world_key(&metadata.resolve, name)) {
+                Some(WorldItem::Interface(interface)) => {
+                    let required = validate_imported_interface(
+                        &metadata.resolve,
+                        *interface,
+                        name,
+                        funcs,
+                        &types,
+                    )
+                    .with_context(|| format!("failed to validate import interface `{name}`"))?;
+                    let prev = ret.required_imports.insert(name, required);
+                    assert!(prev.is_none());
                 }
+                Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
+                    bail!("import `{}` is not an interface", name)
+                }
+                None => bail!("module requires an import interface named `{name}`"),
             }
-            Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
-                bail!("import `{}` is not an interface", name)
-            }
-            None => bail!("module requires an import interface named `{name}`"),
         }
     }
 
@@ -389,6 +395,7 @@ pub fn validate_adapter_module<'a>(
     required_by_import: Option<&IndexMap<&str, FuncType>>,
     exports: &IndexSet<WorldKey>,
     is_library: bool,
+    adapters: &IndexSet<&str>,
 ) -> Result<ValidatedAdapter<'a>> {
     let mut validator = Validator::new();
     let mut import_funcs = IndexMap::new();
@@ -517,20 +524,24 @@ pub fn validate_adapter_module<'a>(
             continue;
         }
 
-        match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
-            Some(WorldItem::Interface(interface)) => {
-                let required =
-                    validate_imported_interface(resolve, *interface, name, funcs, &types)
-                        .with_context(|| format!("failed to validate import interface `{name}`"))?;
-                let prev = ret.required_imports.insert(name.to_string(), required);
-                assert!(prev.is_none());
-            }
-            None | Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
-                if !is_library {
-                    bail!(
-                        "adapter module requires an import interface named `{}`",
-                        name
-                    )
+        if !(is_library && adapters.contains(name)) {
+            match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
+                Some(WorldItem::Interface(interface)) => {
+                    let required =
+                        validate_imported_interface(resolve, *interface, name, funcs, &types)
+                            .with_context(|| {
+                                format!("failed to validate import interface `{name}`")
+                            })?;
+                    let prev = ret.required_imports.insert(name.to_string(), required);
+                    assert!(prev.is_none());
+                }
+                None | Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
+                    if !is_library {
+                        bail!(
+                            "adapter module requires an import interface named `{}`",
+                            name
+                        )
+                    }
                 }
             }
         }


Test patch:
diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs
index 84cdfa41..43d1ab50 100644
--- a/crates/wit-component/tests/components.rs
+++ b/crates/wit-component/tests/components.rs
@@ -22,7 +22,10 @@ use wit_parser::{PackageId, Resolve, UnresolvedPackage};
 ///   interfaces of the `module.wat` or `lib-$name.wat` and
 ///   `dlopen-lib-$name.wat` files. Must have a `default world`
 /// * [optional] `adapt-$name.wat` - optional adapter for the module name
-///   `$name`, can be specified for multiple `$name`s
+///   `$name`, can be specified for multiple `$name`s.  Alternatively, if $name
+///   doesn't work as part of a filename (e.g. contains forward slashes), it may
+///   be specified on the first line of the file with the prefix `;; module name:
+///   `, e.g. `;; module name: wasi:cli/environment@0.2.0`.
 /// * [optional] `adapt-$name.wit` - required for each `*.wat` adapter to
 ///   describe imports/exports of the adapter.
 /// * [optional] `stub-missing-functions` - if linking libraries and this file
@@ -184,7 +187,15 @@ fn read_name_and_module(
 ) -> Result<(String, Vec<u8>)> {
     let wasm = read_core_module(path, resolve, pkg)?;
     let stem = path.file_stem().unwrap().to_str().unwrap();
-    let name = stem.trim_start_matches(prefix).to_owned();
+    let name = if let Some(name) = fs::read_to_string(path)?
+        .lines()
+        .next()
+        .and_then(|line| line.strip_prefix(";; module name: "))
+    {
+        name.to_owned()
+    } else {
+        stem.trim_start_matches(prefix).to_owned()
+    };
     Ok((name, wasm))
 }
 
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wat b/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wat
new file mode 100644
index 00000000..56c63a14
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wat
@@ -0,0 +1,8 @@
+;; module name: wasi:cli/environment@0.2.0
+(module
+  (type (func (param i32)))
+  (func $get-environment (type 0)
+    unreachable
+  )
+  (export "get-environment" (func $get-environment))
+)
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wit b/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wit
new file mode 100644
index 00000000..5a465bef
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/adapt-wasip2.wit
@@ -0,0 +1,1 @@
+world adapt-wasip2 { }
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/component.wat b/crates/wit-component/tests/components/adapt-stub-wasip2/component.wat
new file mode 100644
index 00000000..8a65bd58
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/component.wat
@@ -0,0 +1,75 @@
+(component
+  (core module (;0;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32)))
+    (type (;2;) (func (param i32 i32 i32 i32) (result i32)))
+    (import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (;0;) (type 1)))
+    (func $start (;1;) (type 0))
+    (func $realloc (;2;) (type 2) (param i32 i32 i32 i32) (result i32)
+      unreachable
+    )
+    (memory (;0;) 1)
+    (export "cabi_realloc" (func $realloc))
+    (export "memory" (memory 0))
+    (start $start)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32)))
+    (func $get-environment (;0;) (type 0) (param i32)
+      unreachable
+    )
+    (export "get-environment" (func $get-environment))
+  )
+  (core module (;2;)
+    (type (;0;) (func (param i32)))
+    (func $adapt-wasi:cli/environment@0.2.0-get-environment (;0;) (type 0) (param i32)
+      local.get 0
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (table (;0;) 1 1 funcref)
+    (export "0" (func $adapt-wasi:cli/environment@0.2.0-get-environment))
+    (export "$imports" (table 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;3;)
+    (type (;0;) (func (param i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 2))
+  (alias core export 0 "0" (core func (;0;)))
+  (core instance (;1;)
+    (export "get-environment" (func 0))
+  )
+  (core instance (;2;) (instantiate 0
+      (with "wasi:cli/environment@0.2.0" (instance 1))
+    )
+  )
+  (alias core export 2 "memory" (core memory (;0;)))
+  (alias core export 2 "cabi_realloc" (core func (;1;)))
+  (core instance (;3;) (instantiate 1))
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias core export 3 "get-environment" (core func (;2;)))
+  (core instance (;4;)
+    (export "$imports" (table 0))
+    (export "0" (func 2))
+  )
+  (core instance (;5;) (instantiate 3
+      (with "" (instance 4))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/component.wit.print b/crates/wit-component/tests/components/adapt-stub-wasip2/component.wit.print
new file mode 100644
index 00000000..b4548abf
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/component.wit.print
@@ -0,0 +1,4 @@
+package root:component;
+
+world root {
+}
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/deps/cli/environment.wit b/crates/wit-component/tests/components/adapt-stub-wasip2/deps/cli/environment.wit
new file mode 100644
index 00000000..bb0c8c4e
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/deps/cli/environment.wit
@@ -0,0 +1,5 @@
+package wasi:cli@0.2.0;
+
+interface environment {
+  get-environment: func() -> list<tuple<string, string>>;
+}
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/module.wat b/crates/wit-component/tests/components/adapt-stub-wasip2/module.wat
new file mode 100644
index 00000000..a86fbbd6
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/module.wat
@@ -0,0 +1,11 @@
+(module
+  (type (func))
+  (type (func (param i32)))
+  (type (func (param i32 i32 i32 i32) (result i32)))
+  (import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (type 1)))
+  (func $start (type 0))
+  (func $realloc (type 2) unreachable)
+  (export "cabi_realloc" (func $realloc))
+  (memory (export "memory") 1)
+  (start $start)
+)
diff --git a/crates/wit-component/tests/components/adapt-stub-wasip2/module.wit b/crates/wit-component/tests/components/adapt-stub-wasip2/module.wit
new file mode 100644
index 00000000..782fa4cf
--- /dev/null
+++ b/crates/wit-component/tests/components/adapt-stub-wasip2/module.wit
@@ -0,0 +1,5 @@
+package test:test;
+
+world module {
+  import wasi:cli/environment@0.2.0;
+}
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wat b/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wat
new file mode 100644
index 00000000..56c63a14
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wat
@@ -0,0 +1,8 @@
+;; module name: wasi:cli/environment@0.2.0
+(module
+  (type (func (param i32)))
+  (func $get-environment (type 0)
+    unreachable
+  )
+  (export "get-environment" (func $get-environment))
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wit b/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wit
new file mode 100644
index 00000000..5a465bef
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/adapt-wasip2.wit
@@ -0,0 +1,1 @@
+world adapt-wasip2 { }
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/component.wat b/crates/wit-component/tests/components/link-stub-wasip2/component.wat
new file mode 100644
index 00000000..244efb5c
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/component.wat
@@ -0,0 +1,359 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) (func (param "v" s32) (result s32)))
+      (export (;0;) "bar" (func (type 0)))
+    )
+  )
+  (import "test:test/test" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func (param i32)))
+    (import "wasi:cli/environment@0.2.0" "get-environment" (func (;0;) (type 0)))
+    (table (;0;) 1 funcref)
+    (memory (;0;) 17)
+    (global (;0;) (mut i32) i32.const 1048576)
+    (global (;1;) i32 i32.const 1048592)
+    (global (;2;) i32 i32.const 1)
+    (global (;3;) (mut i32) i32.const 0)
+    (global (;4;) i32 i32.const 1048624)
+    (global (;5;) i32 i32.const 1)
+    (global (;6;) i32 i32.const 1048624)
+    (global (;7;) i32 i32.const 1)
+    (global (;8;) (mut i32) i32.const 0)
+    (global (;9;) (mut i32) i32.const 1048640)
+    (global (;10;) (mut i32) i32.const 1114112)
+    (export "__stack_pointer" (global 0))
+    (export "bar:memory_base" (global 1))
+    (export "bar:table_base" (global 2))
+    (export "bar:well" (global 3))
+    (export "c:memory_base" (global 4))
+    (export "c:table_base" (global 5))
+    (export "foo:memory_base" (global 6))
+    (export "foo:table_base" (global 7))
+    (export "foo:um" (global 8))
+    (export "__heap_base" (global 9))
+    (export "__heap_end" (global 10))
+    (export "wasi:cli/environment@0.2.0:get-environment" (func 0))
+    (export "__indirect_function_table" (table 0))
+    (export "memory" (memory 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32)))
+    (func $get-environment (;0;) (type 0) (param i32)
+      unreachable
+    )
+    (export "get-environment" (func $get-environment))
+  )
+  (core module (;2;)
+    (@dylink.0
+      (mem-info (memory 0 4))
+    )
+    (type (;0;) (func))
+    (type (;1;) (func (param i32) (result i32)))
+    (type (;2;) (func (param i32)))
+    (import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (;0;) (type 2)))
+    (import "GOT.mem" "__heap_base" (global $__heap_base (;0;) (mut i32)))
+    (import "GOT.mem" "__heap_end" (global $__heap_end (;1;) (mut i32)))
+    (func $start (;1;) (type 0)
+      global.get $__heap_base
+      global.set $heap
+    )
+    (func $malloc (;2;) (type 1) (param i32) (result i32)
+      global.get $heap
+      global.get $heap
+      local.get 0
+      i32.add
+      global.set $heap
+    )
+    (func $abort (;3;) (type 0)
+      unreachable
+    )
+    (global $heap (;2;) (mut i32) i32.const 0)
+    (export "malloc" (func $malloc))
+    (export "abort" (func $abort))
+    (start $start)
+  )
+  (core module (;3;)
+    (@dylink.0
+      (mem-info (memory 4 4))
+      (needed "c")
+    )
+    (type (;0;) (func))
+    (type (;1;) (func (param i32) (result i32)))
+    (import "env" "memory" (memory (;0;) 1))
+    (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
+    (import "env" "__stack_pointer" (global $__stack_pointer (;0;) (mut i32)))
+    (import "env" "__memory_base" (global $__memory_base (;1;) i32))
+    (import "env" "__table_base" (global $__table_base (;2;) i32))
+    (import "env" "malloc" (func $malloc (;0;) (type 1)))
+    (import "env" "abort" (func $abort (;1;) (type 0)))
+    (import "GOT.mem" "um" (global $um (;3;) (mut i32)))
+    (import "test:test/test" "bar" (func $bar (;2;) (type 1)))
+    (func $__wasm_call_ctors (;3;) (type 0))
+    (func $__wasm_apply_data_relocs (;4;) (type 0))
+    (func $foo (;5;) (type 1) (param i32) (result i32)
+      global.get $__stack_pointer
+      i32.const 16
+      i32.sub
+      global.set $__stack_pointer
+      i32.const 4
+      call $malloc
+      i32.const 0
+      i32.eq
+      if ;; label = @1
+        call $abort
+        unreachable
+      end
+      local.get 0
+      global.get $um
+      i32.load offset=16
+      i32.add
+      i32.const 42
+      i32.add
+      call $bar
+      global.get $__stack_pointer
+      i32.const 16
+      i32.add
+      global.set $__stack_pointer
+    )
+    (global (;4;) i32 i32.const 0)
+    (export "__wasm_call_ctors" (func $__wasm_call_ctors))
+    (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
+    (export "foo" (func $foo))
+    (export "well" (global 4))
+    (data $.data (;0;) (global.get $__memory_base) "\04\00\00\00")
+  )
+  (core module (;4;)
+    (@dylink.0
+      (mem-info (memory 20 4))
+      (needed "foo")
+    )
+    (type (;0;) (func (param i32) (result i32)))
+    (type (;1;) (func))
+    (import "env" "memory" (memory (;0;) 1))
+    (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
+    (import "env" "__memory_base" (global $__memory_base (;0;) i32))
+    (import "env" "__table_base" (global $__table_base (;1;) i32))
+    (import "env" "foo" (func $foo (;0;) (type 0)))
+    (import "GOT.mem" "well" (global $well (;2;) (mut i32)))
+    (func $__wasm_call_ctors (;1;) (type 1))
+    (func $__wasm_apply_data_relocs (;2;) (type 1))
+    (func $bar (;3;) (type 0) (param i32) (result i32)
+      local.get 0
+      call $foo
+      global.get $well
+      i32.load
+      i32.add
+    )
+    (global (;3;) i32 i32.const 0)
+    (export "__wasm_call_ctors" (func $__wasm_call_ctors))
+    (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
+    (export "test:test/test#bar" (func $bar))
+    (export "um" (global 3))
+    (data $.data (;0;) (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
+  )
+  (core module (;5;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32)))
+    (import "env" "memory" (memory (;0;) 0))
+    (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
+    (import "bar" "__wasm_apply_data_relocs" (func (;0;) (type 0)))
+    (import "bar" "__wasm_call_ctors" (func (;1;) (type 0)))
+    (import "env" "foo:memory_base" (global (;0;) i32))
+    (import "foo" "well" (global (;1;) i32))
+    (import "env" "bar:well" (global (;2;) (mut i32)))
+    (import "foo" "__wasm_apply_data_relocs" (func (;2;) (type 0)))
+    (import "foo" "__wasm_call_ctors" (func (;3;) (type 0)))
+    (import "env" "bar:memory_base" (global (;3;) i32))
+    (import "bar" "um" (global (;4;) i32))
+    (import "env" "foo:um" (global (;5;) (mut i32)))
+    (func (;4;) (type 0)
+      global.get 0
+      global.get 1
+      i32.add
+      global.set 2
+      global.get 3
+      global.get 4
+      i32.add
+      global.set 5
+      call 0
+      call 2
+      call 1
+      call 3
+    )
+    (start 4)
+    (elem (;0;) (i32.const 1) func)
+    (elem (;1;) (i32.const 1) func)
+    (data (;0;) (i32.const 1048576) "\00\00\00\00\00\00\10\00")
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;6;)
+    (type (;0;) (func (param i32)))
+    (func $adapt-wasi:cli/environment@0.2.0-get-environment (;0;) (type 0) (param i32)
+      local.get 0
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (table (;0;) 1 1 funcref)
+    (export "0" (func $adapt-wasi:cli/environment@0.2.0-get-environment))
+    (export "$imports" (table 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;7;)
+    (type (;0;) (func (param i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 6))
+  (alias core export 0 "0" (core func (;0;)))
+  (core instance (;1;)
+    (export "get-environment" (func 0))
+  )
+  (core instance (;2;) (instantiate 0
+      (with "wasi:cli/environment@0.2.0" (instance 1))
+    )
+  )
+  (alias core export 2 "memory" (core memory (;0;)))
+  (core instance (;3;) (instantiate 1))
+  (alias core export 2 "__heap_base" (core global (;0;)))
+  (alias core export 2 "__heap_end" (core global (;1;)))
+  (core instance (;4;)
+    (export "__heap_base" (global 0))
+    (export "__heap_end" (global 1))
+  )
+  (core instance (;5;))
+  (alias core export 2 "memory" (core memory (;1;)))
+  (alias core export 2 "__indirect_function_table" (core table (;0;)))
+  (alias core export 2 "__stack_pointer" (core global (;2;)))
+  (alias core export 2 "c:memory_base" (core global (;3;)))
+  (alias core export 2 "c:table_base" (core global (;4;)))
+  (core instance (;6;)
+    (export "memory" (memory 1))
+    (export "__indirect_function_table" (table 0))
+    (export "__stack_pointer" (global 2))
+    (export "__memory_base" (global 3))
+    (export "__table_base" (global 4))
+  )
+  (alias core export 2 "wasi:cli/environment@0.2.0:get-environment" (core func (;1;)))
+  (core instance (;7;)
+    (export "get-environment" (func 1))
+  )
+  (core instance (;8;) (instantiate 2
+      (with "GOT.mem" (instance 4))
+      (with "GOT.func" (instance 5))
+      (with "env" (instance 6))
+      (with "wasi:cli/environment@0.2.0" (instance 7))
+    )
+  )
+  (alias core export 2 "foo:um" (core global (;5;)))
+  (alias core export 2 "__heap_base" (core global (;6;)))
+  (alias core export 2 "__heap_end" (core global (;7;)))
+  (core instance (;9;)
+    (export "um" (global 5))
+    (export "__heap_base" (global 6))
+    (export "__heap_end" (global 7))
+  )
+  (core instance (;10;))
+  (alias core export 2 "memory" (core memory (;2;)))
+  (alias core export 2 "__indirect_function_table" (core table (;1;)))
+  (alias core export 2 "__stack_pointer" (core global (;8;)))
+  (alias core export 2 "foo:memory_base" (core global (;9;)))
+  (alias core export 2 "foo:table_base" (core global (;10;)))
+  (alias core export 8 "abort" (core func (;2;)))
+  (alias core export 8 "malloc" (core func (;3;)))
+  (core instance (;11;)
+    (export "memory" (memory 2))
+    (export "__indirect_function_table" (table 1))
+    (export "__stack_pointer" (global 8))
+    (export "__memory_base" (global 9))
+    (export "__table_base" (global 10))
+    (export "abort" (func 2))
+    (export "malloc" (func 3))
+  )
+  (alias export 0 "bar" (func (;0;)))
+  (core func (;4;) (canon lower (func 0)))
+  (core instance (;12;)
+    (export "bar" (func 4))
+  )
+  (core instance (;13;) (instantiate 3
+      (with "GOT.mem" (instance 9))
+      (with "GOT.func" (instance 10))
+      (with "env" (instance 11))
+      (with "test:test/test" (instance 12))
+    )
+  )
+  (alias core export 2 "bar:well" (core global (;11;)))
+  (alias core export 2 "__heap_base" (core global (;12;)))
+  (alias core export 2 "__heap_end" (core global (;13;)))
+  (core instance (;14;)
+    (export "well" (global 11))
+    (export "__heap_base" (global 12))
+    (export "__heap_end" (global 13))
+  )
+  (core instance (;15;))
+  (alias core export 2 "memory" (core memory (;3;)))
+  (alias core export 2 "__indirect_function_table" (core table (;2;)))
+  (alias core export 2 "__stack_pointer" (core global (;14;)))
+  (alias core export 2 "bar:memory_base" (core global (;15;)))
+  (alias core export 2 "bar:table_base" (core global (;16;)))
+  (alias core export 13 "foo" (core func (;5;)))
+  (core instance (;16;)
+    (export "memory" (memory 3))
+    (export "__indirect_function_table" (table 2))
+    (export "__stack_pointer" (global 14))
+    (export "__memory_base" (global 15))
+    (export "__table_base" (global 16))
+    (export "foo" (func 5))
+  )
+  (core instance (;17;) (instantiate 4
+      (with "GOT.mem" (instance 14))
+      (with "GOT.func" (instance 15))
+      (with "env" (instance 16))
+    )
+  )
+  (alias core export 0 "$imports" (core table (;3;)))
+  (alias core export 3 "get-environment" (core func (;6;)))
+  (core instance (;18;)
+    (export "$imports" (table 3))
+    (export "0" (func 6))
+  )
+  (core instance (;19;) (instantiate 7
+      (with "" (instance 18))
+    )
+  )
+  (core instance (;20;) (instantiate 5
+      (with "env" (instance 2))
+      (with "bar" (instance 17))
+      (with "c" (instance 8))
+      (with "foo" (instance 13))
+    )
+  )
+  (type (;1;) (func (param "v" s32) (result s32)))
+  (alias core export 17 "test:test/test#bar" (core func (;7;)))
+  (func (;1;) (type 1) (canon lift (core func 7)))
+  (component (;0;)
+    (type (;0;) (func (param "v" s32) (result s32)))
+    (import "import-func-bar" (func (;0;) (type 0)))
+    (type (;1;) (func (param "v" s32) (result s32)))
+    (export (;1;) "bar" (func 0) (func (type 1)))
+  )
+  (instance (;1;) (instantiate 0
+      (with "import-func-bar" (func 1))
+    )
+  )
+  (export (;2;) "test:test/test" (instance 1))
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/component.wit.print b/crates/wit-component/tests/components/link-stub-wasip2/component.wit.print
new file mode 100644
index 00000000..769889d7
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/component.wit.print
@@ -0,0 +1,7 @@
+package root:component;
+
+world root {
+  import test:test/test;
+
+  export test:test/test;
+}
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/deps/cli/environment.wit b/crates/wit-component/tests/components/link-stub-wasip2/deps/cli/environment.wit
new file mode 100644
index 00000000..bb0c8c4e
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/deps/cli/environment.wit
@@ -0,0 +1,5 @@
+package wasi:cli@0.2.0;
+
+interface environment {
+  get-environment: func() -> list<tuple<string, string>>;
+}
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wat b/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wat
new file mode 100644
index 00000000..d06fe9ba
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wat
@@ -0,0 +1,29 @@
+(module
+  (@dylink.0
+    (mem-info (memory 20 4))
+    (needed "foo")
+  )
+  (type (func (param i32) (result i32)))
+  (type (func))
+  (import "env" "memory" (memory 1))
+  (import "env" "__indirect_function_table" (table 0 funcref))
+  (import "env" "__memory_base" (global $__memory_base i32))
+  (import "env" "__table_base" (global $__table_base i32))
+  (import "env" "foo" (func $foo (type 0)))
+  (import "GOT.mem" "well" (global $well (mut i32)))
+  (func $__wasm_call_ctors (type 1))
+  (func $__wasm_apply_data_relocs (type 1))
+  (func $bar (type 0) (param i32) (result i32)
+    local.get 0
+    call $foo
+    global.get $well
+    i32.load
+    i32.add
+  )
+  (global i32 i32.const 0)
+  (export "__wasm_call_ctors" (func $__wasm_call_ctors))
+  (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
+  (export "test:test/test#bar" (func $bar))
+  (export "um" (global 3))
+  (data $.data (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wit b/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wit
new file mode 100644
index 00000000..9cab0b5e
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-bar.wit
@@ -0,0 +1,10 @@
+package test:test;
+
+interface test {
+   bar: func(v: s32) -> s32;
+}
+
+world lib-bar {
+    import test;
+    export test;
+}
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wat b/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wat
new file mode 100644
index 00000000..5ffc8cd3
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wat
@@ -0,0 +1,29 @@
+(module
+  (@dylink.0
+    (mem-info (memory 0 4))
+  )
+  (type (func))
+  (type (func (param i32) (result i32)))
+  (type (func (param i32)))
+  (import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (type 2)))
+  (import "GOT.mem" "__heap_base" (global $__heap_base (mut i32)))
+  (import "GOT.mem" "__heap_end" (global $__heap_end (mut i32)))
+  (global $heap (mut i32) i32.const 0)
+  (func $start (type 0)
+    global.get $__heap_base
+    global.set $heap
+  )
+  (func $malloc (type 1) (param i32) (result i32)
+    global.get $heap
+    global.get $heap
+    local.get 0
+    i32.add
+    global.set $heap
+  )
+  (func $abort (type 0)
+    unreachable
+  )
+  (export "malloc" (func $malloc))
+  (export "abort" (func $abort))
+  (start $start)
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wit b/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wit
new file mode 100644
index 00000000..bb276f61
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-c.wit
@@ -0,0 +1,5 @@
+package test:test;
+
+world lib-c {
+  import wasi:cli/environment@0.2.0;
+}
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wat b/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wat
new file mode 100644
index 00000000..e681ea20
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wat
@@ -0,0 +1,55 @@
+(module
+  (@dylink.0
+    (mem-info (memory 4 4))
+    (needed "c")
+  )
+  (type (func))
+  (type (func (param i32) (result i32)))
+  (import "env" "memory" (memory 1))
+  (import "env" "__indirect_function_table" (table 0 funcref))
+  (import "env" "__stack_pointer" (global $__stack_pointer (mut i32)))
+  (import "env" "__memory_base" (global $__memory_base i32))
+  (import "env" "__table_base" (global $__table_base i32))
+  (import "env" "malloc" (func $malloc (type 1)))
+  (import "env" "abort" (func $abort (type 0)))
+  (import "GOT.mem" "um" (global $um (mut i32)))
+  (import "test:test/test" "bar" (func $bar (type 1)))
+  (func $__wasm_call_ctors (type 0))
+  (func $__wasm_apply_data_relocs (type 0))
+  (func $foo (type 1) (param i32) (result i32)
+    global.get $__stack_pointer
+    i32.const 16
+    i32.sub
+    global.set $__stack_pointer
+
+    i32.const 4
+    call $malloc
+
+    i32.const 0
+    i32.eq
+    if
+      call $abort
+      unreachable
+    end
+
+    local.get 0
+    global.get $um
+    i32.load offset=16
+    i32.add
+    i32.const 42
+    i32.add
+
+    call $bar
+
+    global.get $__stack_pointer
+    i32.const 16
+    i32.add
+    global.set $__stack_pointer
+  )
+  (global i32 i32.const 0)
+  (export "__wasm_call_ctors" (func $__wasm_call_ctors))
+  (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
+  (export "foo" (func $foo))
+  (export "well" (global 4))
+  (data $.data (global.get $__memory_base) "\04\00\00\00")
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wit b/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wit
new file mode 100644
index 00000000..3171fc99
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-foo.wit
@@ -0,0 +1,3 @@
+package test:test;
+
+world lib-foo { }
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wat b/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wat
new file mode 100644
index 00000000..8818e20f
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wat
@@ -0,0 +1,55 @@
+(module
+  (@dylink.0
+    (mem-info (memory 4 4))
+    (needed "c")
+  )
+  (type (func))
+  (type (func (param i32) (result i32)))
+  (import "env" "memory" (memory 1))
+  (import "env" "__indirect_function_table" (table 0 funcref))
+  (import "env" "__stack_pointer" (global $__stack_pointer (mut i32)))
+  (import "env" "__memory_base" (global $__memory_base i32))
+  (import "env" "__table_base" (global $__table_base i32))
+  (import "env" "malloc" (func $malloc (type 1)))
+  (import "env" "abort" (func $abort (type 0)))
+  (import "GOT.mem" "um" (global $um (mut i32)))
+  (import "test:test/test" "bar" (func $bar (type 1)))
+  (func $__wasm_call_ctors (type 0))
+  (func $__wasm_apply_data_relocs (type 0))
+  (func $foo (type 1) (param i32) (result i32)
+    global.get $__stack_pointer
+    i32.const 16
+    i32.sub
+    global.set $__stack_pointer
+
+    i32.const 4
+    call $malloc
+
+    i32.const 0
+    i32.eq
+    if
+      call $abort
+      unreachable
+    end
+
+    local.get 0
+    global.get $um
+    i32.load offset=16
+    i32.add
+    i32.const 42
+    i32.add
+
+    call $bar
+
+    global.get $__stack_pointer
+    i32.const 16
+    i32.add
+    global.set $__stack_pointer
+  )
+  (global i32 i32.const 0)
+  (export "__wasm_call_ctors" (func $__wasm_call_ctors))
+  (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
+  (export "foo2" (func $foo))
+  (export "well2" (global 4))
+  (data $.data (global.get $__memory_base) "\04\00\00\00")
+)
diff --git a/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wit b/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wit
new file mode 100644
index 00000000..73d286f7
--- /dev/null
+++ b/crates/wit-component/tests/components/link-stub-wasip2/lib-unused.wit
@@ -0,0 +1,3 @@
+package test:test;
+
+world lib-unused { }
diff --git a/crates/wit-parser/tests/ui/types.wit b/crates/wit-parser/tests/ui/types.wit
index bb391f91..f21b0119 100644
--- a/crates/wit-parser/tests/ui/types.wit
+++ b/crates/wit-parser/tests/ui/types.wit
@@ -54,7 +54,7 @@ interface types {
   type t52 = future<u32>;
   type t53 = future;
 
-  // type order doesn't matter
+  /// type order doesn't matter
   type foo = bar;
   type bar = u32;
 }
diff --git a/crates/wit-parser/tests/ui/wasi.wit b/crates/wit-parser/tests/ui/wasi.wit
index cf084a3f..db780a05 100644
--- a/crates/wit-parser/tests/ui/wasi.wit
+++ b/crates/wit-parser/tests/ui/wasi.wit
@@ -2,177 +2,177 @@ package wasi:filesystem;
 
 interface wasi {
   enum clockid {
-    // The clock measuring real time. Time value zero corresponds with
-    // 1970-01-01T00:00:00Z.
+    /// The clock measuring real time. Time value zero corresponds with
+    /// 1970-01-01T00:00:00Z.
     realtime,
-    // The store-wide monotonic clock, which is defined as a clock measuring
-    // real time, whose value cannot be adjusted and which cannot have negative
-    // clock jumps. The epoch of this clock is undefined. The absolute time
-    // value of this clock therefore has no meaning.
+    /// The store-wide monotonic clock, which is defined as a clock measuring
+    /// real time, whose value cannot be adjusted and which cannot have negative
+    /// clock jumps. The epoch of this clock is undefined. The absolute time
+    /// value of this clock therefore has no meaning.
     monotonic,
   }
 
-  // Timestamp in nanoseconds.
+  /// Timestamp in nanoseconds.
   type timestamp = u64;
 
-  // Error codes returned by functions.
-  // Not all of these error codes are returned by the functions provided by this
-  // API/ some are used in higher-level library layers, and others are provided
-  // merely for alignment with POSIX.
+  /// Error codes returned by functions.
+  /// Not all of these error codes are returned by the functions provided by this
+  /// API/ some are used in higher-level library layers, and others are provided
+  /// merely for alignment with POSIX.
   enum errno {
-    // No error occurred. System call completed successfully.
+    /// No error occurred. System call completed successfully.
     success,
-    // Argument list too long.
+    /// Argument list too long.
     toobig,
-    // Permission denied.
+    /// Permission denied.
     access,
-    // Address in use.
+    /// Address in use.
     addrinuse,
-    // Address not available.
+    /// Address not available.
     addrnotavail,
-    // Address family not supported.
+    /// Address family not supported.
     afnosupport,
-    // Resource unavailable, or operation would block.
+    /// Resource unavailable, or operation would block.
     again,
-    // Connection already in progress.
+    /// Connection already in progress.
     already,
-    // Bad file descriptor.
+    /// Bad file descriptor.
     badf,
-    // Bad message.
+    /// Bad message.
     badmsg,
-    // Device or resource busy.
+    /// Device or resource busy.
     busy,
-    // Operation canceled.
+    /// Operation canceled.
     canceled,
-    // No child processes.
+    /// No child processes.
     child,
-    // Connection aborted.
+    /// Connection aborted.
     connaborted,
-    // Connection refused.
+    /// Connection refused.
     connrefused,
-    // Connection reset.
+    /// Connection reset.
     connreset,
-    // Resource deadlock would occur.
+    /// Resource deadlock would occur.
     deadlk,
-    // Destination address required.
+    /// Destination address required.
     destaddrreq,
-    // Mathematics argument out of domain of function.
+    /// Mathematics argument out of domain of function.
     dom,
-    // Reserved.
+    /// Reserved.
     dquot,
-    // File exists.
+    /// File exists.
     exist,
-    // Bad address.
+    /// Bad address.
     fault,
-    // File too large.
+    /// File too large.
     fbig,
-    // Host is unreachable.
+    /// Host is unreachable.
     hostunreach,
-    // Identifier removed.
+    /// Identifier removed.
     idrm,
-    // Illegal byte sequence.
+    /// Illegal byte sequence.
     ilseq,
-    // Operation in progress.
+    /// Operation in progress.
     inprogress,
-    // Interrupted function.
+    /// Interrupted function.
     intr,
-    // Invalid argument.
+    /// Invalid argument.
     inval,
-    // I/O error.
+    /// I/O error.
     io,
-    // Socket is connected.
+    /// Socket is connected.
     isconn,
-    // Is a directory.
+    /// Is a directory.
     isdir,
-    // Too many levels of symbolic links.
+    /// Too many levels of symbolic links.
     loop,
-    // File descriptor value too large.
+    /// File descriptor value too large.
     mfile,
-    // Too many links.
+    /// Too many links.
     mlink,
-    // Message too large.
+    /// Message too large.
     msgsize,
-    // Reserved.
+    /// Reserved.
     multihop,
-    // Filename too long.
+    /// Filename too long.
     nametoolong,
-    // Network is down.
+    /// Network is down.
     netdown,
-    // Connection aborted by network.
+    /// Connection aborted by network.
     netreset,
-    // Network unreachable.
+    /// Network unreachable.
     netunreach,
-    // Too many files open in system.
+    /// Too many files open in system.
     nfile,
-    // No buffer space available.
+    /// No buffer space available.
     nobufs,
-    // No such device.
+    /// No such device.
     nodev,
-    // No such file or directory.
+    /// No such file or directory.
     noent,
-    // Executable file format error.
+    /// Executable file format error.
     noexec,
-    // No locks available.
+    /// No locks available.
     nolck,
-    // Reserved.
+    /// Reserved.
     nolink,
-    // Not enough space.
+    /// Not enough space.
     nomem,
-    // No message of the desired type.
+    /// No message of the desired type.
     nomsg,
-    // Protocol not available.
+    /// Protocol not available.
     noprotoopt,
-    // No space left on device.
+    /// No space left on device.
     nospc,
-    // Function not supported.
+    /// Function not supported.
     nosys,
-    // The socket is not connected.
+    /// The socket is not connected.
     notconn,
-    // Not a directory or a symbolic link to a directory.
+    /// Not a directory or a symbolic link to a directory.
     notdir,
-    // Directory not empty.
+    /// Directory not empty.
     notempty,
-    // State not recoverable.
+    /// State not recoverable.
     notrecoverable,
-    // Not a socket.
+    /// Not a socket.
     notsock,
-    // Not supported, or operation not supported on socket.
+    /// Not supported, or operation not supported on socket.
     notsup,
-    // Inappropriate I/O control operation.
+    /// Inappropriate I/O control operation.
     notty,
-    // No such device or address.
+    /// No such device or address.
     nxio,
-    // Value too large to be stored in data type.
+    /// Value too large to be stored in data type.
     overflow,
-    // Previous owner died.
+    /// Previous owner died.
     ownerdead,
-    // Operation not permitted.
+    /// Operation not permitted.
     perm,
-    // Broken pipe.
+    /// Broken pipe.
     pipe,
-    // Protocol error.
+    /// Protocol error.
     proto,
-    // Protocol not supported.
+    /// Protocol not supported.
     protonosupport,
-    // Protocol wrong type for socket.
+    /// Protocol wrong type for socket.
     prototype,
-    // Result too large.
+    /// Result too large.
     range,
-    // Read-only file system.
+    /// Read-only file system.
     rofs,
-    // Invalid seek.
+    /// Invalid seek.
     spipe,
-    // No such process.
+    /// No such process.
     srch,
-    // Reserved.
+    /// Reserved.
     stale,
-    // Connection timed out.
+    /// Connection timed out.
     timedout,
-    // Text file busy.
+    /// Text file busy.
     txtbsy,
-    // Cross-device link.
+    /// Cross-device link.
     xdev,
-    // Extension: Capabilities insufficient.
+    /// Extension: Capabilities insufficient.
     notcapable,
   }
 }