chenyan2002 opened PR #14462 from chenyan2002:namemap to bytecodealliance:main:
Fix https://github.com/bytecodealliance/wasmtime/issues/14346
Breaking changes:
- A
Linkercan no longer hold two versions on the same semver track. Before, defining botha:b/c@1.0.0anda:b/c@1.0.1succeeded and imports picks the highest. Now the second definition is an error, or replaces the first withallow_shadowing(true).- A component that exports several versions on one track now exposes only the highest in
component_type().exports(). Lookups of any version on the track resolve to it.- Two root exports with the same full name are now an error, e.g.,
a:b/c@1with suffix.0.1alongsidea:b/c@1.0.1is a defined twice error. Two imports with the same full name are allowed. Each is typechecked against the same host definition, and the component type lists the first one.Concretely, we changed the following:
- Linker
- Holds one definition per semver track. Imports of any version on that track resolve to it.
- Instances are shared across a track:
linker.instance("a:b/c@0.2.1")reopens an existinga:b/c@0.2.0(ora:b/c@0.2) instance.instance/into_instancevalidate the name, rejecting invalid versions.- Removed the unused
pathfieldNameMapis keyed by canonical name
- Entries are keyed by
canonical_name(name)- Lookups are plain string equality on the canonical name. This removes
alternate_lookups.insertwithout shadowing rejects a second name on an existing track. With shadowing, the new definition replaces the old one.- Names are validated on insert. An interface version much be either a full semver or a canonical version. For example, inserting
a:b/c@1.2is rejected.- New
get_or_insert_withlets the linker reopen an instance from any version on its semver track.- New
insert_highestis used for guest exports (dfg.rs). A component may export several versions on one semver track, and the highest full version is kept.As a side effect, this fixed a bug about
define_unknown_imports_as_traps. Before, this flag was not semver aware, so a guest importinga:b/c@0.1.0witha:b/c@0.1.1on the host will get traps for the imports, instead of linking to0.1.1.
chenyan2002 requested cfallin for a review on PR #14462.
chenyan2002 requested wasmtime-core-reviewers for a review on PR #14462.
chenyan2002 requested alexcrichton for a review on PR #14462.
chenyan2002 requested wasmtime-default-reviewers for a review on PR #14462.
chenyan2002 updated PR #14462.
chenyan2002 edited PR #14462:
Fix https://github.com/bytecodealliance/wasmtime/issues/14346
Breaking changes:
- A
Linkercan no longer hold two versions on the same semver track. Before, defining botha:b/c@1.0.0anda:b/c@1.0.1succeeded and imports picks the highest. Now the second definition is an error, or replaces the first withallow_shadowing(true).- A component that exports several versions on one track now exposes only the highest in
component_type().exports(). Lookups of any version on the track resolve to it.- Two root exports with the same full name are now an error, e.g.,
a:b/c@1with suffix.0.1alongsidea:b/c@1.0.1is a defined twice error. Two imports with the same full name are allowed. Each is typechecked against the same host definition, and the component type lists the first one.Linkerdefinitions now reject names that aren't valid component import/export names. Before, any string was accepted, even though an invalid name can never match an import.Concretely, we changed the following:
- Linker
- Holds one definition per semver track. Imports of any version on that track resolve to it.
- Instances are shared across a track:
linker.instance("a:b/c@0.2.1")reopens an existinga:b/c@0.2.0(ora:b/c@0.2) instance.instance/into_instancevalidate the name, rejecting invalid versions.- Removed the unused
pathfieldNameMapis keyed by canonical name
- Entries are keyed by
canonical_name(name)- Lookups are plain string equality on the canonical name. This removes
alternate_lookups.insertwithout shadowing rejects a second name on an existing track. With shadowing, the new definition replaces the old one.- Names are validated on insert. An interface version much be either a full semver or a canonical version. For example, inserting
a:b/c@1.2is rejected.- New
get_or_insert_withlets the linker reopen an instance from any version on its semver track.- New
insert_highestis used for guest exports (dfg.rs). A component may export several versions on one semver track, and the highest full version is kept.As a side effect, this fixed a bug about
define_unknown_imports_as_traps. Before, this flag was not semver aware, so a guest importinga:b/c@0.1.0witha:b/c@0.1.1on the host will get traps for the imports, instead of linking to0.1.1.
chenyan2002 updated PR #14462.
github-actions[bot] added the label wasmtime:api on PR #14462.
:memo: alexcrichton submitted PR review:
Thanks for this! At a high level this feels like a good approach to take, but I think that there might be some details to work out still. My main hesitation here has to do with how Wasmtime reinterepts loaded components. Rejecting duplicate-exports-named-different-ways seems fine to me, but as-implemented this has some interesting behavior on some components:
- If a component imports
@0.2 (suffix 0.1)and@0.2.1that currently can't be represented separately so the type-reflection is basically broken. Those two unique imports can have different types at the component level, but at the host level that can't be seen/configured. This means that an API like defining unknown imports as traps will break. Could imports get the same treatment as exports where the same-import-named-multiple-ways is a load-time error?- It looks like deduplication along version tracks only happens at the root, not nested layers. For example an exported instance which itself exports fields with versions I think has those versions not-unified, but at the top layer it's unified.
An example of the first issue is:
<details>
#[test] #[cfg_attr(miri, ignore)] fn component_type_covers_imports_with_same_full_name() -> Result<()> { let mut config = Config::new(); config.wasm_component_model_canonical_names(true); let engine = Engine::new(&config)?; // Two distinct imports with the same full name, `a:b/c@0.2.1`, but // different types. let component = r#" (component (import "a:b/c@0.2" (versionsuffix ".1") (instance (export "f" (func)) )) (import "a:b/c@0.2.1" (instance (export "g" (func)) )) ) "#; let component = Component::new(&engine, component)?; // Define everything that `component_type` says is imported, which should // be enough to instantiate the component. let mut linker = Linker::<()>::new(&engine); for (name, import) in component.component_type().imports(&engine) { let types::ComponentItem::ComponentInstance(ty) = import.ty else { panic!("expected an instance"); }; let mut instance = linker.instance(name)?; for (export, _) in ty.exports(&engine) { instance.func_wrap(export, |_, (): ()| Ok(()))?; } } let mut store = Store::new(&engine, ()); linker.instantiate(&mut store, &component)?; Ok(()) }</details>
and an example of the second issue is:
<details>
#[test] #[cfg_attr(miri, ignore)] fn nested_export_keeps_highest_with_versionsuffix() -> Result<()> { let mut config = Config::new(); config.wasm_component_model_canonical_names(true); let engine = Engine::new(&config)?; let component = r#" (component (core module $m1) (core module $m2 (import "" "" (func))) (instance $i1 (export "m" (core module $m1))) (instance $i2 (export "m" (core module $m2))) (instance $o (export "a:b/n@0.2.1" (instance $i1)) (export "a:b/n@0.2" (versionsuffix ".3") (instance $i2)) ) (export "o" (instance $o)) ) "#; fn assert_m2(module: &Module) { assert_eq!(module.imports().len(), 1); } let component = Component::new(&engine, component)?; // Only the highest full version on a semver track is exported, which for // `a:b/n@0.2` is `a:b/n@0.2.3` due to its `versionsuffix`. let ty = component.component_type(); let (_, o) = ty .exports(&engine) .find(|(name, _)| *name == "o") .unwrap(); let types::ComponentItem::ComponentInstance(o) = o.ty else { panic!("expected an instance"); }; let names = o .exports(&engine) .map(|(name, _)| name.to_string()) .collect::<Vec<_>>(); assert_eq!(names, ["a:b/n@0.2.3"]); let mut store = Store::new(&engine, ()); let instance = Linker::new(&engine).instantiate(&mut store, &component)?; let o = component.get_export_index(None, "o").unwrap(); for name in ["a:b/n@0.2.1", "a:b/n@0.2.3", "a:b/n@0.2"] { println!("test {name}"); let i = component.get_export_index(Some(&o), name).unwrap(); let m = component.get_export_index(Some(&i), "m").unwrap(); assert_m2(&instance.get_module(&mut store, &m).unwrap()); } Ok(()) }</details>
Overall though I like the idea of moving closer to a world where we might actually be able to only accept canonical names by default. I mostly just want to make sure that everything's lined up to at least not break anyone as-is while we transition over.
:speech_balloon: alexcrichton created PR review comment:
Could this have a small comment for what it's rejecting? This is rejecting
[method]a.bfor an instance name, right?
alexcrichton commented on PR #14462:
Oh another thing I want to mention -- I think it'd be good to try to get as much feedback about this before landing this as this has a nontrivial chance of breaking downstream consumers. One example I think would be to update https://github.com/spinframework/spin to use this PR (on a branch or maybe in CI over there) to ensure that all tests still pass. I'm not sure if that'd be a complete test suite, but I know Spin often exposes various colorful ways of satisfying component imports and such. It might also be good to run
wac's test suite, and perhapswit-bindgen's, althoughwit-bindgen's I doubt would stress anything here.
chenyan2002 edited PR #14462:
Fix https://github.com/bytecodealliance/wasmtime/issues/14346
Breaking changes:
- A
Linkercan no longer hold two versions on the same semver track. Before, defining botha:b/c@1.0.0anda:b/c@1.0.1succeeded and imports picks the highest. Now the second definition is an error, or replaces the first withallow_shadowing(true).- A component that exports several versions on one track now exposes only the highest full version, both at the root and within exported instances in
component_type(). Lookups of any version on the track, e.g.,get_export_index, resolve to it. Imports with several versions on one track are all kept incomponent_type(), and each is typechecked against the same host definition.- Two items with the same full name in one scope are an error, e.g.,
a:b/c@1with suffix.0.1alongsidea:b/c@1.0.1. This applies to root imports, root exports, the exports of an instance type, and the imports and exports of a component type.Linkerdefinitions now reject names that aren't valid component import/export names. Before, any string was accepted, even though an invalid name can never match an import.Concretely, we changed the following:
- Linker
- Holds one definition per semver track. Imports of any version on that track resolve to it.
- Instances are shared across a track:
linker.instance("a:b/c@0.2.1")reopens an existinga:b/c@0.2.0(ora:b/c@0.2) instance.instance/into_instancevalidate the name, rejecting invalid versions.- Removed the unused
pathfieldNameMapis keyed by canonical name
- Entries are keyed by
canonical_name(name)- Lookups are plain string equality on the canonical name. This removes
alternate_lookups.insertwithout shadowing rejects a second name on an existing track. With shadowing, the new definition replaces the old one.- Names are validated on insert. An interface version much be either a full semver or a canonical version. For example, inserting
a:b/c@1.2is rejected.- New
get_or_insert_withlets the linker reopen an instance from any version on its semver track.- New
insert_highestis used for guest exports (dfg.rs). A component may export several versions on one semver track, and the highest full version is kept.- environ/component and type reflection
ComponentExternDatarecords each item's full name (its literal name plus versionsuffix fromwasmparser). Items are still keyed by literal name.type_builder.rsrejects duplicate full names when converting instance and component types, which covers every nested scope: nested imports, nested exports, and internal instances.inline.rsrejects duplicate full names for root imports and root exports.dfg.rskeeps only the highest full version on each semver track for nested exports and rebuilds the exported instance's type to match.component_type()reports the full names at every level, andget_import/get_exportmatch on them.As a side effect, this fixed a bug about
define_unknown_imports_as_traps. Before, this flag was not semver aware, so a guest importinga:b/c@0.1.0witha:b/c@0.1.1on the host will get traps for the imports, instead of linking to0.1.1.
chenyan2002 updated PR #14462.
:memo: chenyan2002 submitted PR review.
:speech_balloon: chenyan2002 created PR review comment:
Yes, added.
Last updated: Oct 11 2026 at 04:10 UTC