Repository: bytecodealliance/wasm-tools (rust)

Issue:
wit-component: support resource exports from adapters
#1084 added support for resources to `wit-component`, but appears to only support exporting resources from the main module -- not from adapters.  Given that `wasm-tools component link` represents all dynamic libraries as "adapters", and `componentize-py` uses that to create components, I'd like to add support for exporting resources from adapters.

I'm planning to start working on a PR tomorrow unless there are objections.

Pull request description:
add support for exporting resources from adapters
Previously, you could only export resources from the main module -- not from adapters. Given that wasm-tools component link represents all dynamic libraries as "adapters", and `componentize-py` uses that to create components, we'd like to support both.

Fixes #1200

Patch:
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 4b7a7a15..598aa85a 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -74,7 +74,8 @@
 use crate::encoding::world::WorldAdapter;
 use crate::metadata::{self, Bindgen, ModuleMetadata};
 use crate::validation::{
-    ValidatedModule, BARE_FUNC_MODULE_NAME, MAIN_MODULE_IMPORT_NAME, POST_RETURN_PREFIX,
+    ResourceInfo, ValidatedModule, BARE_FUNC_MODULE_NAME, MAIN_MODULE_IMPORT_NAME,
+    POST_RETURN_PREFIX,
 };
 use crate::StringEncoding;
 use anyhow::{anyhow, bail, Context, Result};
@@ -585,43 +586,12 @@ impl<'a> EncodingState<'a> {
             args.push((*adapter, ModuleArg::Instance(index)));
         }
 
-        for (import, info) in info.required_resource_funcs.iter() {
-            let mut exports = Vec::new();
-            for (resource, info) in info {
-                // Destructors for resources live on the shim module previously
-                // created, so if one is specified create the resource with
-                // the shim module that currently exists. The shim will get
-                // filled in later with the actual destructor after the main
-                // module is instantiated.
-                let dtor = info.dtor_export.map(|_| {
-                    self.component.core_alias_export(
-                        self.shim_instance_index.unwrap(),
-                        &shims.shim_names[&ShimKind::ResourceDtor { import, resource }],
-                        ExportKind::Func,
-                    )
-                });
-                let resource_idx = self.component.type_resource(ValType::I32, dtor);
-                let prev = self.export_type_map.insert(info.id, resource_idx);
-                assert!(prev.is_none());
-
-                if let Some(name) = info.drop_import {
-                    let index = self.component.resource_drop(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-                if let Some(name) = info.rep_import {
-                    let index = self.component.resource_rep(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-                if let Some(name) = info.new_import {
-                    let index = self.component.resource_new(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-            }
-            if !exports.is_empty() {
-                let index = self.component.core_instantiate_exports(exports);
-                args.push((import.as_str(), ModuleArg::Instance(index)));
-            }
-        }
+        self.add_resource_funcs(
+            CustomModule::Main,
+            &info.required_resource_funcs,
+            &shims,
+            &mut args,
+        );
 
         // Instantiate the main module now that all of its arguments have been
         // prepared. With this we now have the main linear memory for
@@ -1189,6 +1159,14 @@ impl<'a> EncodingState<'a> {
                     &mut signatures,
                 );
             }
+
+            self.encode_resource_dtors(
+                CustomModule::Adapter(adapter_name),
+                &adapter.info.required_resource_funcs,
+                &mut signatures,
+                &mut ret,
+            );
+
             let funcs = match self.info.info.adapters_required.get(adapter_name) {
                 Some(funcs) => funcs,
                 None => continue,
@@ -1217,31 +1195,12 @@ impl<'a> EncodingState<'a> {
             }
         }
 
-        // Any resource destructors are encoded through the shim module. The
-        // core wasm probably imports resource intrinsics which requires the
-        // resource definition, but the resource definition requires
-        // the destructor to be available. The shim module breaks this
-        // circular dependency.
-        for (import, info) in self.info.info.required_resource_funcs.iter() {
-            for (resource, info) in info {
-                if info.dtor_export.is_none() {
-                    continue;
-                }
-                signatures.push(WasmSignature {
-                    params: vec![WasmType::I32],
-                    results: Vec::new(),
-                    indirect_params: false,
-                    retptr: false,
-                });
-                let name = ret.list.len().to_string();
-                ret.list.push(Shim {
-                    name,
-                    debug_name: format!("dtor-{import}-{resource}"),
-                    options: RequiredOptions::empty(),
-                    kind: ShimKind::ResourceDtor { import, resource },
-                });
-            }
-        }
+        self.encode_resource_dtors(
+            CustomModule::Main,
+            &self.info.info.required_resource_funcs,
+            &mut signatures,
+            &mut ret,
+        );
 
         if ret.list.is_empty() {
             return ret;
@@ -1445,13 +1404,24 @@ impl<'a> EncodingState<'a> {
                 // meaning that any destructor for the resource must be called
                 // indirectly due to the otherwise circular dependency between
                 // the module and the resource itself.
-                ShimKind::ResourceDtor { import, resource } => self.component.core_alias_export(
-                    self.instance_index.unwrap(),
-                    self.info.info.required_resource_funcs[*import][*resource]
-                        .dtor_export
-                        .unwrap(),
-                    ExportKind::Func,
-                ),
+                ShimKind::ResourceDtor {
+                    module,
+                    import,
+                    resource,
+                } => {
+                    let funcs = match module {
+                        CustomModule::Main => &self.info.info.required_resource_funcs,
+                        CustomModule::Adapter(name) => {
+                            &self.info.adapters[name].info.required_resource_funcs
+                        }
+                    };
+
+                    self.component.core_alias_export(
+                        self.instance_index.unwrap(),
+                        funcs[*import][*resource].dtor_export.as_deref().unwrap(),
+                        ExportKind::Func,
+                    )
+                }
             };
 
             exports.push((shim.name.as_str(), ExportKind::Func, core_func_index));
@@ -1495,6 +1465,94 @@ impl<'a> EncodingState<'a> {
         self.instance_index = Some(instance_index);
     }
 
+    fn encode_resource_dtors<'b>(
+        &mut self,
+        module: CustomModule<'b>,
+        funcs: &'b IndexMap<String, IndexMap<String, ResourceInfo>>,
+        signatures: &mut Vec<WasmSignature>,
+        shims: &mut Shims<'b>,
+    ) {
+        // Any resource destructors are encoded through the shim module. The
+        // core wasm probably imports resource intrinsics which requires the
+        // resource definition, but the resource definition requires
+        // the destructor to be available. The shim module breaks this
+        // circular dependency.
+        for (import, info) in funcs.iter() {
+            for (resource, info) in info {
+                if info.dtor_export.is_none() {
+                    continue;
+                }
+                signatures.push(WasmSignature {
+                    params: vec![WasmType::I32],
+                    results: Vec::new(),
+                    indirect_params: false,
+                    retptr: false,
+                });
+                let name = shims.list.len().to_string();
+                shims.list.push(Shim {
+                    name,
+                    debug_name: format!("dtor-{import}-{resource}"),
+                    options: RequiredOptions::empty(),
+                    kind: ShimKind::ResourceDtor {
+                        module,
+                        import,
+                        resource,
+                    },
+                });
+            }
+        }
+    }
+
+    fn add_resource_funcs<'b>(
+        &mut self,
+        module: CustomModule<'b>,
+        funcs: &'b IndexMap<String, IndexMap<String, ResourceInfo>>,
+        shims: &Shims,
+        args: &mut Vec<(&'b str, ModuleArg)>,
+    ) {
+        for (import, info) in funcs {
+            let mut exports = Vec::new();
+            for (resource, info) in info {
+                // Destructors for resources live on the shim module previously
+                // created, so if one is specified create the resource with
+                // the shim module that currently exists. The shim will get
+                // filled in later with the actual destructor after the main
+                // module is instantiated.
+                let dtor = info.dtor_export.as_deref().map(|_| {
+                    self.component.core_alias_export(
+                        self.shim_instance_index.unwrap(),
+                        &shims.shim_names[&ShimKind::ResourceDtor {
+                            module,
+                            import,
+                            resource,
+                        }],
+                        ExportKind::Func,
+                    )
+                });
+                let resource_idx = self.component.type_resource(ValType::I32, dtor);
+                let prev = self.export_type_map.insert(info.id, resource_idx);
+                assert!(prev.is_none());
+
+                if let Some(name) = info.drop_import.as_deref() {
+                    let index = self.component.resource_drop(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+                if let Some(name) = info.rep_import.as_deref() {
+                    let index = self.component.resource_rep(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+                if let Some(name) = info.new_import.as_deref() {
+                    let index = self.component.resource_new(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+            }
+            if !exports.is_empty() {
+                let index = self.component.core_instantiate_exports(exports);
+                args.push((import.as_str(), ModuleArg::Instance(index)));
+            }
+        }
+    }
+
     /// This function will instantiate the specified adapter module, which may
     /// depend on previously-instantiated modules.
     fn instantiate_adapter_module(
@@ -1597,6 +1655,14 @@ impl<'a> EncodingState<'a> {
             );
             args.push((import_name, ModuleArg::Instance(instance)));
         }
+
+        self.add_resource_funcs(
+            CustomModule::Adapter(name),
+            &adapter.info.required_resource_funcs,
+            shims,
+            &mut args,
+        );
+
         let instance = self
             .component
             .core_instantiate(self.adapter_modules[name], args);
@@ -1685,6 +1751,8 @@ enum ShimKind<'a> {
     /// A shim used as the destructor for a resource which allows defining the
     /// resource before the core module being instantiated.
     ResourceDtor {
+        /// Which instance to pull the destructor function from.
+        module: CustomModule<'a>,
         /// The import that the resource was defined for.
         import: &'a str,
         /// The name of the resource being destroyed.
diff --git a/crates/wit-component/src/encoding/types.rs b/crates/wit-component/src/encoding/types.rs
index 00140673..d660612e 100644
--- a/crates/wit-component/src/encoding/types.rs
+++ b/crates/wit-component/src/encoding/types.rs
@@ -131,7 +131,7 @@ pub trait ValtypeEncoder<'a> {
                 // If this type is imported from another interface then return
                 // it as it was bound here with an alias.
                 let ty = &resolve.types[id];
-                log::trace!("encode type name={:?}", ty.name);
+                log::trace!("encode type name={:?} {:?}", ty.name, &ty.kind);
                 if let Some(index) = self.maybe_import_type(resolve, id) {
                     self.type_map().insert(id, index);
                     return Ok(ComponentValType::Type(index));
diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs
index 39120c9f..79c9c52e 100644
--- a/crates/wit-component/src/encoding/world.rs
+++ b/crates/wit-component/src/encoding/world.rs
@@ -113,14 +113,29 @@ impl<'a> ComponentWorld<'a> {
         ) in self.encoder.adapters.iter()
         {
             let required_by_import = self.info.adapters_required.get(name.as_str());
-            let required =
-                self.required_adapter_exports(resolve, world, required_exports, required_by_import);
-            if required.is_empty() && library_info.is_none() {
+            let no_required_by_import = || required_by_import.map(|m| m.is_empty()).unwrap_or(true);
+            let no_required_exports = || {
+                required_exports
+                    .iter()
+                    .all(|name| match &resolve.worlds[world].exports[name] {
+                        WorldItem::Function(_) => false,
+                        WorldItem::Interface(id) => resolve.interfaces[*id].functions.is_empty(),
+                        WorldItem::Type(_) => true,
+                    })
+            };
+            if no_required_by_import() && no_required_exports() && library_info.is_none() {
                 continue;
             }
             let wasm = if library_info.is_some() {
                 Cow::Borrowed(wasm as &[u8])
             } else {
+                let required = self.required_adapter_exports(
+                    resolve,
+                    world,
+                    required_exports,
+                    required_by_import,
+                );
+
                 Cow::Owned(
                     crate::gc::run(wasm, &required, self.info.realloc)
                         .context("failed to reduce input adapter module to its minimal size")?,
@@ -131,7 +146,8 @@ impl<'a> ComponentWorld<'a> {
                 resolve,
                 world,
                 metadata,
-                &required,
+                required_by_import,
+                required_exports,
                 library_info.is_some(),
             )
             .context("failed to validate the imports of the minimized adapter module")?;
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index c7bd5d33..1262add2 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -80,7 +80,7 @@ pub struct ValidatedModule<'a> {
     /// imported resources and this is only for exported resources. Exported
     /// resources still require intrinsics to be imported into the core module
     /// itself.
-    pub required_resource_funcs: IndexMap<String, IndexMap<&'a str, ResourceInfo<'a>>>,
+    pub required_resource_funcs: IndexMap<String, IndexMap<String, ResourceInfo>>,
 
     /// Whether or not this module exported a linear memory.
     pub has_memory: bool,
@@ -105,11 +105,11 @@ pub struct RequiredImports {
     pub resources: IndexSet<String>,
 }
 
-pub struct ResourceInfo<'a> {
-    pub drop_import: Option<&'a str>,
-    pub new_import: Option<&'a str>,
-    pub rep_import: Option<&'a str>,
-    pub dtor_export: Option<&'a str>,
+pub struct ResourceInfo {
+    pub drop_import: Option<String>,
+    pub new_import: Option<String>,
+    pub rep_import: Option<String>,
+    pub dtor_export: Option<String>,
     pub id: TypeId,
 }
 
@@ -255,7 +255,8 @@ pub fn validate_module<'a>(
             &metadata.resolve.name_world_key(name),
             &export_funcs,
             &types,
-            &mut ret,
+            &mut ret.post_returns,
+            &mut ret.required_resource_funcs,
         )?;
     }
 
@@ -269,7 +270,7 @@ pub fn validate_module<'a>(
                     name,
                     funcs,
                     &types,
-                    &mut ret,
+                    &mut ret.required_resource_funcs,
                 )?;
             }
             _ => bail!("import from `{name}` does not correspond to exported interface"),
@@ -285,7 +286,7 @@ fn validate_exported_interface_resource_imports<'a>(
     import_module: &str,
     funcs: &IndexMap<&'a str, u32>,
     types: &Types,
-    info: &mut ValidatedModule<'a>,
+    required_resource_funcs: &mut IndexMap<String, IndexMap<String, ResourceInfo>>,
 ) -> Result<()> {
     let is_resource = |name: &str| match resolve.interfaces[interface].types.get(name) {
         Some(ty) => matches!(resolve.types[*ty].kind, TypeDefKind::Resource),
@@ -295,17 +296,17 @@ fn validate_exported_interface_resource_imports<'a>(
         if valid_exported_resource_func(func_name, *ty, types, is_resource)?.is_none() {
             bail!("import of `{func_name}` is not a valid resource function");
         }
-        let info = info.required_resource_funcs.get_mut(import_module).unwrap();
+        let info = required_resource_funcs.get_mut(import_module).unwrap();
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_DROP) {
-            info[resource_name].drop_import = Some(func_name);
+            info[resource_name].drop_import = Some(func_name.to_string());
             continue;
         }
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_NEW) {
-            info[resource_name].new_import = Some(func_name);
+            info[resource_name].new_import = Some(func_name.to_string());
             continue;
         }
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_REP) {
-            info[resource_name].rep_import = Some(func_name);
+            info[resource_name].rep_import = Some(func_name.to_string());
             continue;
         }
 
@@ -324,6 +325,15 @@ pub struct ValidatedAdapter<'a> {
     /// imported interface.
     pub required_imports: IndexMap<String, RequiredImports>,
 
+    /// Resource-related functions required and imported which work over
+    /// exported resources from the final component.
+    ///
+    /// Note that this is disjoint from `required_imports` which handles
+    /// imported resources and this is only for exported resources. Exported
+    /// resources still require intrinsics to be imported into the core module
+    /// itself.
+    pub required_resource_funcs: IndexMap<String, IndexMap<String, ResourceInfo>>,
+
     /// This is the module and field name of the memory import, if one is
     /// specified.
     ///
@@ -373,7 +383,8 @@ pub fn validate_adapter_module<'a>(
     resolve: &'a Resolve,
     world: WorldId,
     metadata: &'a ModuleMetadata,
-    required: &IndexMap<String, (FuncType, Option<&Function>)>,
+    required_by_import: Option<&IndexMap<&str, FuncType>>,
+    exports: &IndexSet<WorldKey>,
     is_library: bool,
 ) -> Result<ValidatedAdapter<'a>> {
     let mut validator = Validator::new();
@@ -383,6 +394,7 @@ pub fn validate_adapter_module<'a>(
     let mut funcs = Vec::new();
     let mut ret = ValidatedAdapter {
         required_imports: Default::default(),
+        required_resource_funcs: Default::default(),
         needs_memory: None,
         needs_core_exports: Default::default(),
         import_realloc: None,
@@ -467,8 +479,9 @@ pub fn validate_adapter_module<'a>(
     }
 
     let types = types.unwrap();
-    for (name, funcs) in import_funcs {
-        if name == MAIN_MODULE_IMPORT_NAME {
+    let mut exported_resource_funcs = Vec::new();
+    for (name, funcs) in &import_funcs {
+        if *name == MAIN_MODULE_IMPORT_NAME {
             ret.needs_core_exports
                 .extend(funcs.iter().map(|(name, _ty)| name.to_string()));
             continue;
@@ -476,17 +489,22 @@ pub fn validate_adapter_module<'a>(
 
         // An empty module name is indicative of the top-level import namespace,
         // so look for top-level functions here.
-        if name == BARE_FUNC_MODULE_NAME {
-            let required = validate_imports_top_level(resolve, world, &funcs, &types)?;
+        if *name == BARE_FUNC_MODULE_NAME {
+            let required = validate_imports_top_level(resolve, world, funcs, &types)?;
             ret.required_imports
                 .insert(BARE_FUNC_MODULE_NAME.to_string(), required);
             continue;
         }
 
+        if let Some(interface_name) = name.strip_prefix("[export]") {
+            exported_resource_funcs.push((name, interface_name, &import_funcs[name]));
+            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)
+                    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());
@@ -502,24 +520,46 @@ pub fn validate_adapter_module<'a>(
         }
     }
 
-    for (name, (ty, func)) in required {
-        let idx = match export_funcs.get(name.as_str()) {
-            Some(idx) => *idx,
-            None => bail!("adapter module did not export `{name}`"),
-        };
-        let id = types.function_at(idx);
-        let actual = types[id].unwrap_func();
-        validate_func_sig(name, ty, actual)?;
-
-        if let Some(func) = func {
-            let post_return = format!("{POST_RETURN_PREFIX}{name}");
-            if let Some(idx) = export_funcs.get(post_return.as_str()) {
-                let id = types.function_at(*idx);
-                let actual = types[id].unwrap_func();
-                validate_post_return(resolve, &actual, func)?;
-                let ok = ret.post_returns.insert(post_return);
-                assert!(ok);
+    if let Some(required) = required_by_import {
+        for (name, ty) in required {
+            let idx = match export_funcs.get(name) {
+                Some(idx) => *idx,
+                None => bail!("adapter module did not export `{name}`"),
+            };
+            let id = types.function_at(idx);
+            let actual = types[id].unwrap_func();
+            validate_func_sig(name, ty, actual)?;
+        }
+    }
+
+    let world = &resolve.worlds[world];
+
+    for name in exports {
+        validate_exported_item(
+            resolve,
+            &world.exports[name],
+            &resolve.name_world_key(name),
+            &export_funcs,
+            &types,
+            &mut ret.post_returns,
+            &mut ret.required_resource_funcs,
+        )?;
+    }
+
+    for (name, interface_name, funcs) in exported_resource_funcs {
+        let world_key = world_key(resolve, interface_name);
+        match world.exports.get(&world_key) {
+            Some(WorldItem::Interface(i)) => {
+                validate_exported_interface_resource_imports(
+                    resolve,
+                    *i,
+                    name,
+                    funcs,
+                    &types,
+                    &mut ret.required_resource_funcs,
+                )?;
             }
+            _ => bail!("import from `{name}` does not correspond to exported interface"),
         }
     }
 
@@ -722,9 +762,10 @@ fn validate_exported_item<'a>(
     resolve: &'a Resolve,
     item: &'a WorldItem,
     export_name: &str,
-    exports: &IndexMap<&'a str, u32>,
+    exports: &IndexMap<&str, u32>,
     types: &Types,
-    info: &mut ValidatedModule<'a>,
+    post_returns: &mut IndexSet<String>,
+    required_resource_funcs: &mut IndexMap<String, IndexMap<String, ResourceInfo>>,
 ) -> Result<()> {
     let mut validate = |func: &Function, name: Option<&str>| {
         let expected_export_name = func.core_export_name(name);
@@ -741,7 +782,7 @@ fn validate_exported_item<'a>(
 
         let post_return = format!("{POST_RETURN_PREFIX}{expected_export_name}");
         if let Some(index) = exports.get(&post_return[..]) {
-            let ok = info.post_returns.insert(post_return);
+            let ok = post_returns.insert(post_return);
             assert!(ok);
             let id = types.function_at(*index);
             let ty = types[id].unwrap_func();
@@ -776,14 +817,12 @@ fn validate_exported_item<'a>(
                     let ty = types[id].unwrap_func();
                     let expected = FuncType::new([ValType::I32], []);
                     validate_func_sig(name, &expected, ty)?;
-                    info.dtor_export = Some(name);
+                    info.dtor_export = Some(name.to_string());
                 }
-                let prev = map.insert(name.as_str(), info);
+                let prev = map.insert(name.to_string(), info);
                 assert!(prev.is_none());
             }
-            let prev = info
-                .required_resource_funcs
-                .insert(format!("[export]{export_name}"), map);
+            let prev = required_resource_funcs.insert(format!("[export]{export_name}"), map);
             assert!(prev.is_none());
         }
         // not required to have anything exported in the core wasm module


Test patch:
diff --git a/crates/wit-component/tests/components/link-resources/component.wat b/crates/wit-component/tests/components/link-resources/component.wat
new file mode 100644
index 00000000..8794c067
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/component.wat
@@ -0,0 +1,196 @@
+(component
+  (core module (;0;)
+    (table (;0;) 0 funcref)
+    (memory (;0;) 17)
+    (global (;0;) (mut i32) i32.const 1048576)
+    (global (;1;) i32 i32.const 1048592)
+    (global (;2;) i32 i32.const 0)
+    (global (;3;) i32 i32.const 1048592)
+    (global (;4;) i32 i32.const 0)
+    (global (;5;) (mut i32) i32.const 1048592)
+    (global (;6;) (mut i32) i32.const 1114112)
+    (export "__stack_pointer" (global 0))
+    (export "bar:memory_base" (global 1))
+    (export "bar:table_base" (global 2))
+    (export "foo:memory_base" (global 3))
+    (export "foo:table_base" (global 4))
+    (export "__heap_base" (global 5))
+    (export "__heap_end" (global 6))
+    (export "__indirect_function_table" (table 0))
+    (export "memory" (memory 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;1;)
+    (@dylink.0
+      (mem-info (memory 0 4))
+    )
+    (type (;0;) (func (param i32) (result i32)))
+    (func $foo (;0;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_ctor (;1;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_get (;2;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (export "test:test/foo#[constructor]x" (func $x_ctor))
+    (export "test:test/foo#[method]x.get" (func $x_get))
+  )
+  (core module (;2;)
+    (@dylink.0
+      (mem-info (memory 0 4))
+      (needed "foo")
+    )
+    (type (;0;) (func (param i32) (result i32)))
+    (func $x_ctor (;0;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_get (;1;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (export "test:test/bar#[constructor]x" (func $x_ctor))
+    (export "test:test/bar#[method]x.get" (func $x_get))
+  )
+  (core module (;3;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32)))
+    (import "env" "memory" (memory (;0;) 0))
+    (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
+    (func (;0;) (type 0))
+    (start 0)
+    (elem (;0;) (i32.const 0) func)
+    (elem (;1;) (i32.const 0) func)
+    (data (;0;) (i32.const 1048576) "\00\00\00\00\00\00\10\00")
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 0))
+  (alias core export 0 "memory" (core memory (;0;)))
+  (alias core export 0 "__heap_base" (core global (;0;)))
+  (alias core export 0 "__heap_end" (core global (;1;)))
+  (core instance (;1;)
+    (export "__heap_base" (global 0))
+    (export "__heap_end" (global 1))
+  )
+  (core instance (;2;))
+  (alias core export 0 "memory" (core memory (;1;)))
+  (alias core export 0 "__indirect_function_table" (core table (;0;)))
+  (alias core export 0 "__stack_pointer" (core global (;2;)))
+  (alias core export 0 "foo:memory_base" (core global (;3;)))
+  (alias core export 0 "foo:table_base" (core global (;4;)))
+  (core instance (;3;)
+    (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))
+  )
+  (type (;0;) (resource (rep i32)))
+  (core instance (;4;) (instantiate 1
+      (with "GOT.mem" (instance 1))
+      (with "GOT.func" (instance 2))
+      (with "env" (instance 3))
+    )
+  )
+  (alias core export 0 "__heap_base" (core global (;5;)))
+  (alias core export 0 "__heap_end" (core global (;6;)))
+  (core instance (;5;)
+    (export "__heap_base" (global 5))
+    (export "__heap_end" (global 6))
+  )
+  (core instance (;6;))
+  (alias core export 0 "memory" (core memory (;2;)))
+  (alias core export 0 "__indirect_function_table" (core table (;1;)))
+  (alias core export 0 "__stack_pointer" (core global (;7;)))
+  (alias core export 0 "bar:memory_base" (core global (;8;)))
+  (alias core export 0 "bar:table_base" (core global (;9;)))
+  (core instance (;7;)
+    (export "memory" (memory 2))
+    (export "__indirect_function_table" (table 1))
+    (export "__stack_pointer" (global 7))
+    (export "__memory_base" (global 8))
+    (export "__table_base" (global 9))
+  )
+  (type (;1;) (resource (rep i32)))
+  (core instance (;8;) (instantiate 2
+      (with "GOT.mem" (instance 5))
+      (with "GOT.func" (instance 6))
+      (with "env" (instance 7))
+    )
+  )
+  (core instance (;9;) (instantiate 3
+      (with "env" (instance 0))
+      (with "bar" (instance 8))
+      (with "foo" (instance 4))
+    )
+  )
+  (type (;2;) (own 0))
+  (type (;3;) (func (param "v" u32) (result 2)))
+  (alias core export 4 "test:test/foo#[constructor]x" (core func (;0;)))
+  (func (;0;) (type 3) (canon lift (core func 0)))
+  (type (;4;) (borrow 0))
+  (type (;5;) (func (param "self" 4) (result u32)))
+  (alias core export 4 "test:test/foo#[method]x.get" (core func (;1;)))
+  (func (;1;) (type 5) (canon lift (core func 1)))
+  (component (;0;)
+    (import "import-type-x" (type (;0;) (sub resource)))
+    (type (;1;) (own 0))
+    (type (;2;) (func (param "v" u32) (result 1)))
+    (import "import-constructor-x" (func (;0;) (type 2)))
+    (type (;3;) (borrow 0))
+    (type (;4;) (func (param "self" 3) (result u32)))
+    (import "import-method-x-get" (func (;1;) (type 4)))
+    (export (;5;) "x" (type 0))
+    (type (;6;) (own 5))
+    (type (;7;) (func (param "v" u32) (result 6)))
+    (export (;2;) "[constructor]x" (func 0) (func (type 7)))
+    (type (;8;) (borrow 5))
+    (type (;9;) (func (param "self" 8) (result u32)))
+    (export (;3;) "[method]x.get" (func 1) (func (type 9)))
+  )
+  (instance (;0;) (instantiate 0
+      (with "import-constructor-x" (func 0))
+      (with "import-method-x-get" (func 1))
+      (with "import-type-x" (type 0))
+    )
+  )
+  (export (;1;) (interface "test:test/foo") (instance 0))
+  (type (;6;) (own 1))
+  (type (;7;) (func (param "v" u32) (result 6)))
+  (alias core export 8 "test:test/bar#[constructor]x" (core func (;2;)))
+  (func (;2;) (type 7) (canon lift (core func 2)))
+  (type (;8;) (borrow 1))
+  (type (;9;) (func (param "self" 8) (result u32)))
+  (alias core export 8 "test:test/bar#[method]x.get" (core func (;3;)))
+  (func (;3;) (type 9) (canon lift (core func 3)))
+  (component (;1;)
+    (import "import-type-x" (type (;0;) (sub resource)))
+    (type (;1;) (own 0))
+    (type (;2;) (func (param "v" u32) (result 1)))
+    (import "import-constructor-x" (func (;0;) (type 2)))
+    (type (;3;) (borrow 0))
+    (type (;4;) (func (param "self" 3) (result u32)))
+    (import "import-method-x-get" (func (;1;) (type 4)))
+    (export (;5;) "x" (type 0))
+    (type (;6;) (own 5))
+    (type (;7;) (func (param "v" u32) (result 6)))
+    (export (;2;) "[constructor]x" (func 0) (func (type 7)))
+    (type (;8;) (borrow 5))
+    (type (;9;) (func (param "self" 8) (result u32)))
+    (export (;3;) "[method]x.get" (func 1) (func (type 9)))
+  )
+  (instance (;2;) (instantiate 1
+      (with "import-constructor-x" (func 2))
+      (with "import-method-x-get" (func 3))
+      (with "import-type-x" (type 1))
+    )
+  )
+  (export (;3;) (interface "test:test/bar") (instance 2))
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
\ No newline at end of file
diff --git a/crates/wit-component/tests/components/link-resources/component.wit.print b/crates/wit-component/tests/components/link-resources/component.wit.print
new file mode 100644
index 00000000..273010a4
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/component.wit.print
@@ -0,0 +1,6 @@
+package root:component
+
+world root {
+  export test:test/foo
+  export test:test/bar
+}
diff --git a/crates/wit-component/tests/components/link-resources/lib-bar.wat b/crates/wit-component/tests/components/link-resources/lib-bar.wat
new file mode 100644
index 00000000..53ce67f6
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-bar.wat
@@ -0,0 +1,15 @@
+(module
+  (@dylink.0
+    (mem-info (memory 0 4))
+    (needed "foo")
+  )
+  (type (func (param i32) (result i32)))
+  (func $x_ctor (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_get (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (export "test:test/bar#[constructor]x" (func $x_ctor))
+  (export "test:test/bar#[method]x.get" (func $x_get))
+)
diff --git a/crates/wit-component/tests/components/link-resources/lib-bar.wit b/crates/wit-component/tests/components/link-resources/lib-bar.wit
new file mode 100644
index 00000000..3fae33dd
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-bar.wit
@@ -0,0 +1,13 @@
+package test:test;
+
+interface bar {
+  resource x {
+    constructor(v: u32);
+    get: func() -> u32;
+  }
+}
+
+world lib-bar {
+    import bar;
+    export bar;
+}
diff --git a/crates/wit-component/tests/components/link-resources/lib-foo.wat b/crates/wit-component/tests/components/link-resources/lib-foo.wat
new file mode 100644
index 00000000..f561b00e
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-foo.wat
@@ -0,0 +1,17 @@
+(module
+  (@dylink.0
+    (mem-info (memory 0 4))
+  )
+  (type (func (param i32) (result i32)))
+  (func $foo (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_ctor (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_get (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (export "test:test/foo#[constructor]x" (func $x_ctor))
+  (export "test:test/foo#[method]x.get" (func $x_get))
+)
diff --git a/crates/wit-component/tests/components/link-resources/lib-foo.wit b/crates/wit-component/tests/components/link-resources/lib-foo.wit
new file mode 100644
index 00000000..47bd48c1
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-foo.wit
@@ -0,0 +1,13 @@
+package test:test;
+
+interface foo {
+  resource x {
+    constructor(v: u32);
+    get: func() -> u32;
+  }
+}
+
+world lib-foo {
+  import foo;
+  export foo;
+}