From d6bea9cdee97e4209686f8295a0901e0f3da3e4c Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Mon, 13 Jul 2026 19:13:26 -0400 Subject: [PATCH] Plumb external-id on component import/export This plumbs the `(external-id "...")` annotation added in wasm-tools 0.253.0 through Wasmtime's type reflection, mirroring implements. The annotation is a free-form host-defined identifier which is ignored by type checking and is intended to tell the host how to satisfy an import (or interpret an export), e.g. distinguishing two imports of the same interface by a platform-native resource id. The field is added to `ComponentExternData` in wasmtime-environ and populated from wasmparser in both the type-conversion and nested instance exports, including func- and type-level annotations. New public API `ComponentExtern::external_id` field alongside `implements`. wasmparser validates `(external-id)` behind the same `cm-implements` gate as `(implements)`, so `Config::wasm_component_model_implements(true)` enables both. Note that adding a public field to `ComponentExtern` is a breaking change for any embedder constructing or exhaustively destructuring it. Signed-off-by: Bailey Hayes --- .../environ/src/component/translate/inline.rs | 1 + crates/environ/src/component/types.rs | 19 +++++-- crates/environ/src/component/types_builder.rs | 1 + .../wasmtime/src/runtime/component/types.rs | 6 +++ tests/all/component_model/aot.rs | 50 +++++++++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 7cada0b9c4c4..cd4d4e13d1b1 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1971,6 +1971,7 @@ impl ComponentExternData { fn new(data: wasmparser::ComponentExternName<'_>) -> Self { ComponentExternData { implements: data.implements.map(|s| s.to_string()), + external_id: data.external_id.map(|s| s.to_string()), } } } diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 56c2874fa8d6..b0622bfb350d 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -449,20 +449,29 @@ where } } -/// TODO +/// An import or an export of a component or a component instance. +/// +/// This records the type of the item that is being imported or exported along +/// with any metadata associated with the item. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ComponentExtern { - /// TODO + /// Metadata associated with this item's name, such as + /// `(implements "...")`. pub data: ComponentExternData, - /// TODO + /// The type of this item. pub ty: TypeDef, } -/// TODO +/// Metadata associated with the name of a component import or export. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ComponentExternData { - /// TODO + /// The `(implements "...")` annotation, if present: the name of the + /// interface that this item implements, used when matching this item + /// against imports by interface name rather than by import name. pub implements: Option, + /// The `(external-id "...")` annotation, if present: a free-form + /// host-defined identifier which is ignored by type checking. + pub external_id: Option, } /// Types of imports and exports in the component model. diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index a9822ea1afdc..0fadfb14d498 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -258,6 +258,7 @@ impl ComponentTypesBuilder { ty: self.convert_component_entity_type(types, ty.ty)?, data: ComponentExternData { implements: ty.implements.clone(), + external_id: ty.external_id.clone(), }, }) } diff --git a/crates/wasmtime/src/runtime/component/types.rs b/crates/wasmtime/src/runtime/component/types.rs index 4f94d17ad1cc..e299f4831946 100644 --- a/crates/wasmtime/src/runtime/component/types.rs +++ b/crates/wasmtime/src/runtime/component/types.rs @@ -1171,6 +1171,11 @@ pub struct ComponentExtern<'a> { pub ty: ComponentItem, /// The `(implements "...")` annotation, if present. pub implements: Option<&'a str>, + /// The `(external-id "...")` annotation, if present. + /// + /// This is a free-form host-defined identifier and is intended to indicate + /// how the host should satisfy an import (or interpret an export). + pub external_id: Option<&'a str>, } impl<'a> ComponentExtern<'a> { @@ -1181,6 +1186,7 @@ impl<'a> ComponentExtern<'a> { ) -> Self { Self { implements: env.data.implements.as_deref(), + external_id: env.data.external_id.as_deref(), ty: ComponentItem::from(engine, &env.ty, instance_ty), } } diff --git a/tests/all/component_model/aot.rs b/tests/all/component_model/aot.rs index aeae1a71d86b..6898ad98b1ba 100644 --- a/tests/all/component_model/aot.rs +++ b/tests/all/component_model/aot.rs @@ -248,6 +248,56 @@ fn implements_shows_up() -> Result<()> { Ok(()) } +#[test] +#[cfg_attr(miri, ignore)] +fn external_id_shows_up() -> Result<()> { + let mut config = Config::new(); + config.wasm_component_model_implements(true); + let engine = Engine::new(&config)?; + let component = Component::new( + &engine, + r#" + (component + (import "a" (implements "a1:b1/c1") (external-id "user-db-prod:region-a") (instance $a)) + (import "b" (external-id "user-db-prod:region-b") (instance $b + (export "f" (external-id "func-b-f") (func)) + )) + (import "c" (instance $c)) + (export "d" (external-id "queue-1") (instance $a)) + ) + "#, + )?; + + let assert_reflection = |component: &Component| { + let ty = component.component_type(); + let mut imports = ty.imports(&engine); + let (_, a) = imports.next().unwrap(); + assert_eq!(a.implements.as_deref(), Some("a1:b1/c1")); + assert_eq!(a.external_id.as_deref(), Some("user-db-prod:region-a")); + let (_, b) = imports.next().unwrap(); + assert_eq!(b.implements.as_deref(), None); + assert_eq!(b.external_id.as_deref(), Some("user-db-prod:region-b")); + let ComponentItem::ComponentInstance(instance) = &b.ty else { + panic!("expected an instance import"); + }; + let (name, f) = instance.exports(&engine).next().unwrap(); + assert_eq!(name, "f"); + assert_eq!(f.external_id.as_deref(), Some("func-b-f")); + let (_, c) = imports.next().unwrap(); + assert_eq!(c.external_id.as_deref(), None); + let mut exports = ty.exports(&engine); + let (_, d) = exports.next().unwrap(); + assert_eq!(d.external_id.as_deref(), Some("queue-1")); + }; + + assert_reflection(&component); + let bytes = component.serialize()?; + let component = unsafe { Component::deserialize(&engine, &bytes)? }; + assert_reflection(&component); + + Ok(()) +} + #[test] #[cfg_attr(miri, ignore)] fn issue_13540_resources_in_adapter_no_concurrency() -> Result<()> {