From 464aeabb0b002ff14f01e814173718d5d223aa70 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 25 Sep 2024 19:46:02 -0700 Subject: [PATCH 1/8] Use `ExportMap` for naming component exports Use the map's metadata to determine what the core wasm name is for each export instead of recalculating it in the encoder which would duplicate work done in validation. --- crates/wit-component/src/encoding.rs | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 19dd760583..1917e3db25 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -615,6 +615,37 @@ impl<'a> EncodingState<'a> { CustomModule::Main => &self.info.encoder.main_module_exports, CustomModule::Adapter(name) => &self.info.encoder.adapters[name].required_exports, }; + if exports.is_empty() { + return Ok(()); + } + + let mut interface_func_core_names = IndexMap::new(); + let mut world_func_core_names = IndexMap::new(); + for (core_name, export) in self.info.exports_for(module).iter() { + match export { + Export::WorldFunc(name) => { + let prev = world_func_core_names.insert(name, core_name); + assert!(prev.is_none()); + } + Export::InterfaceFunc(id, name) => { + let prev = interface_func_core_names + .entry(id) + .or_insert(IndexMap::new()) + .insert(name.as_str(), core_name); + assert!(prev.is_none()); + } + Export::WorldFuncPostReturn(..) + | Export::InterfaceFuncPostReturn(..) + | Export::ResourceDtor(..) + | Export::Memory + | Export::GeneralPurposeRealloc + | Export::GeneralPurposeExportRealloc + | Export::GeneralPurposeImportRealloc + | Export::Initialize + | Export::ReallocForAdapter => continue, + } + } + let world = &resolve.worlds[self.info.encoder.metadata.world]; for export_name in exports { let export_string = resolve.name_world_key(export_name); @@ -623,13 +654,14 @@ impl<'a> EncodingState<'a> { let ty = self .root_import_type_encoder(None) .encode_func_type(resolve, func)?; - let core_name = func.core_export_name(None); + let core_name = world_func_core_names[&func.name]; let idx = self.encode_lift(module, &core_name, None, func, ty)?; self.component .export(&export_string, ComponentExportKind::Func, idx, None); } WorldItem::Interface { id, .. } => { - self.encode_interface_export(&export_string, module, *id)?; + let core_names = interface_func_core_names.get(id); + self.encode_interface_export(&export_string, module, *id, core_names)?; } WorldItem::Type(_) => unreachable!(), } @@ -643,6 +675,7 @@ impl<'a> EncodingState<'a> { export_name: &str, module: CustomModule<'_>, export: InterfaceId, + interface_func_core_names: Option<&IndexMap<&str, &str>>, ) -> Result<()> { log::trace!("encode interface export `{export_name}`"); let resolve = &self.info.encoder.metadata.resolve; @@ -656,7 +689,7 @@ impl<'a> EncodingState<'a> { let mut imports = Vec::new(); let mut root = self.root_export_type_encoder(Some(export)); for (_, func) in &resolve.interfaces[export].functions { - let core_name = func.core_export_name(Some(export_name)); + let core_name = interface_func_core_names.unwrap()[func.name.as_str()]; let ty = root.encode_func_type(resolve, func)?; let func_index = root .state From 51a2f8be16ed98aed9e07b8cc11d4b415b3df51a Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 25 Sep 2024 20:46:48 -0700 Subject: [PATCH 2/8] Decouple import/export encodings from core names This commit decouples the string encodings listed for imports/exports from their core wasm names to instead being registered with WIT-level constructs instead. Previously the parsing phase of a module would register a string encoding for core wasm import/export names but this subverted the logic of validation where detection of how exactly an import lines up with WIT-level items is determined. The goal of this commit is to decouple this relation. Worlds are encoding into custom sections with a known string encoding for all imports/exports of that world. This can possibly differ for different parts of an application to theoretically enable one interface to be imported with UTF-8 and another with UTF-16. This means that encodings are tracked per-import/export rather than per-world. Previously this process would assume that there is a single name for an import's/export's encoding but with new detection and names coming down the line this is no longer going to be the case. For example with the new names in WebAssembly/component-model#378 there are new names to be supported meaning that there's not one single name to register encodings with. To help bridge this gap the abstraction here is changed to where metadata for a module records string encodings on a WIT level, for example per WIT import/export, instead of per core wasm import/export. Then during encoding of a component the WIT level constructs are matched up instead of the core names to determine the string encoding in the lift/lower operation. The end goal is that the connection between core wasm names and WIT names continues to be decoupled where validation is the only location concerned about this. --- crates/wit-component/src/encoding.rs | 60 +++---- crates/wit-component/src/encoding/world.rs | 4 +- crates/wit-component/src/metadata.rs | 172 +++++++++++++-------- crates/wit-component/src/validation.rs | 43 +++--- 4 files changed, 162 insertions(+), 117 deletions(-) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 1917e3db25..b5f0b7d938 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -655,13 +655,19 @@ impl<'a> EncodingState<'a> { .root_import_type_encoder(None) .encode_func_type(resolve, func)?; let core_name = world_func_core_names[&func.name]; - let idx = self.encode_lift(module, &core_name, None, func, ty)?; + let idx = self.encode_lift(module, &core_name, export_name, func, ty)?; self.component .export(&export_string, ComponentExportKind::Func, idx, None); } WorldItem::Interface { id, .. } => { let core_names = interface_func_core_names.get(id); - self.encode_interface_export(&export_string, module, *id, core_names)?; + self.encode_interface_export( + &export_string, + module, + export_name, + *id, + core_names, + )?; } WorldItem::Type(_) => unreachable!(), } @@ -674,6 +680,7 @@ impl<'a> EncodingState<'a> { &mut self, export_name: &str, module: CustomModule<'_>, + key: &WorldKey, export: InterfaceId, interface_func_core_names: Option<&IndexMap<&str, &str>>, ) -> Result<()> { @@ -691,9 +698,7 @@ impl<'a> EncodingState<'a> { for (_, func) in &resolve.interfaces[export].functions { let core_name = interface_func_core_names.unwrap()[func.name.as_str()]; let ty = root.encode_func_type(resolve, func)?; - let func_index = root - .state - .encode_lift(module, &core_name, Some(export), func, ty)?; + let func_index = root.state.encode_lift(module, &core_name, key, func, ty)?; imports.push(( import_func_name(func), ComponentExportKind::Func, @@ -986,7 +991,7 @@ impl<'a> EncodingState<'a> { &mut self, module: CustomModule<'_>, core_name: &str, - interface: Option, + key: &WorldKey, func: &Function, ty: u32, ) -> Result { @@ -997,16 +1002,19 @@ impl<'a> EncodingState<'a> { let options = RequiredOptions::for_export(resolve, func); - let encoding = metadata.export_encodings[core_name]; + let encoding = metadata + .export_encodings + .get(resolve, key, &func.name) + .unwrap(); let exports = self.info.exports_for(module); let realloc_index = exports - .export_realloc_for(interface, func) + .export_realloc_for(key, func) .map(|name| self.core_alias_export(instance_index, name, ExportKind::Func)); let mut options = options .into_iter(encoding, self.memory_index, realloc_index)? .collect::>(); - if let Some(post_return) = exports.post_return(interface, func) { + if let Some(post_return) = exports.post_return(key, func) { let post_return = self.core_alias_export(instance_index, post_return, ExportKind::Func); options.push(CanonicalOption::PostReturn(post_return)); } @@ -1379,7 +1387,7 @@ impl<'a> EncodingState<'a> { log::trace!("attempting to materialize import of `{module}::{field}` for {for_module:?}"); let resolve = &self.info.encoder.metadata.resolve; let name_tmp; - let (key, name) = match import { + let (key, name, interface_key) = match import { // Main module dependencies on an adapter in use are done with an // indirection here, so load the shim function and use that. Import::AdapterExport(_) => { @@ -1446,18 +1454,17 @@ impl<'a> EncodingState<'a> { // through to the code below. This is where these are connected to a // WIT `ImportedInterface` one way or another with the name that was // detected during validation. - Import::ImportedResourceDrop(key, id) => { + Import::ImportedResourceDrop(key, iface, id) => { let ty = &resolve.types[*id]; let name = ty.name.as_ref().unwrap(); name_tmp = format!("{RESOURCE_DROP}{name}"); - (key.as_ref(), &name_tmp) + (key, &name_tmp, iface.map(|_| resolve.name_world_key(key))) } - Import::WorldFunc(name) => (None, name), - Import::InterfaceFunc(key, _, name) => (Some(key), name), + Import::WorldFunc(key, name) => (key, name, None), + Import::InterfaceFunc(key, _, name) => (key, name, Some(resolve.name_world_key(key))), }; - let interface = key.map(|key| resolve.name_world_key(key)); - let import = &self.info.import_map[&interface]; + let import = &self.info.import_map[&interface_key]; let (index, _, lowering) = import.lowerings.get_full(name).unwrap(); let metadata = self.info.module_metadata_for(for_module); @@ -1480,12 +1487,12 @@ impl<'a> EncodingState<'a> { // created, so the specific export is loaded here and used as an // import. Lowering::Indirect { .. } => { - let encoding = metadata.import_encodings[&(module.to_string(), field.to_string())]; + let encoding = metadata.import_encodings.get(resolve, key, name).unwrap(); self.core_alias_export( self.shim_instance_index .expect("shim should be instantiated"), &shims.shims[&ShimKind::IndirectLowering { - interface: interface.clone(), + interface: interface_key, index, realloc: for_module, encoding, @@ -1696,7 +1703,7 @@ impl<'a> Shims<'a> { let resolve = &world.encoder.metadata.resolve; for (module, field, import) in module_imports.imports() { - let (key, name) = match import { + let (key, name, interface_key) = match import { // These imports don't require shims, they can be satisfied // as-needed when required. Import::ImportedResourceDrop(..) @@ -1746,11 +1753,12 @@ impl<'a> Shims<'a> { // WIT-level functions may require an indirection, so yield some // metadata out of this `match` to the loop below to figure that // out. - Import::InterfaceFunc(key, _, name) => (Some(key), name), - Import::WorldFunc(name) => (None, name), + Import::InterfaceFunc(key, _, name) => { + (key, name, Some(resolve.name_world_key(key))) + } + Import::WorldFunc(key, name) => (key, name, None), }; - let key = key.map(|key| resolve.name_world_key(key)); - let interface = &world.import_map[&key]; + let interface = &world.import_map[&interface_key]; let (index, _, lowering) = interface.lowerings.get_full(name).unwrap(); let shim_name = self.shims.len().to_string(); match lowering { @@ -1760,9 +1768,9 @@ impl<'a> Shims<'a> { log::debug!( "shim {shim_name} is import `{module}::{field}` lowering {index} `{name}`", ); - let encoding = *metadata + let encoding = metadata .import_encodings - .get(&(module.to_string(), field.to_string())) + .get(resolve, key, name) .ok_or_else(|| { anyhow::anyhow!( "missing component metadata for import of \ @@ -1774,7 +1782,7 @@ impl<'a> Shims<'a> { debug_name: format!("indirect-{module}-{field}"), options: *options, kind: ShimKind::IndirectLowering { - interface: key, + interface: interface_key, index, realloc: for_module, encoding, diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index d969e39798..18943abd39 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -250,7 +250,7 @@ impl<'a> ComponentWorld<'a> { .chain(self.info.imports.imports()) { match import { - Import::WorldFunc(name) => { + Import::WorldFunc(_, name) => { required .interface_funcs .entry(None) @@ -264,7 +264,7 @@ impl<'a> ComponentWorld<'a> { .or_default() .insert(name); } - Import::ImportedResourceDrop(_, id) => { + Import::ImportedResourceDrop(_, _, id) => { required.resource_drops.insert(*id); } _ => {} diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs index 18b2608def..7eaf5a68d4 100644 --- a/crates/wit-component/src/metadata.rs +++ b/crates/wit-component/src/metadata.rs @@ -41,7 +41,6 @@ //! The dual of `encode` is the `decode_custom_section` fucntion which decodes //! the three arguments originally passed to `encode`. -use crate::validation::BARE_FUNC_MODULE_NAME; use crate::{DecodedWasm, StringEncoding}; use anyhow::{bail, Context, Result}; use indexmap::{IndexMap, IndexSet}; @@ -112,11 +111,105 @@ impl Default for Bindgen { pub struct ModuleMetadata { /// Per-function options imported into the core wasm module, currently only /// related to string encoding. - pub import_encodings: IndexMap<(String, String), StringEncoding>, + pub import_encodings: EncodingMap, /// Per-function options exported from the core wasm module, currently only /// related to string encoding. - pub export_encodings: IndexMap, + pub export_encodings: EncodingMap, +} + +/// Internal map that keeps track of encodings for various world imports and +/// exports. +/// +/// Stored in [`ModuleMetadata`]. +#[derive(Default)] +pub struct EncodingMap { + /// A map of an "identifying string" for world items to what string + /// encoding the import or export is using. + /// + /// The keys of this map are created by `EncodingMap::key` and are + /// specifically chosen to be able to be looked up during both insertion and + /// fetching. Note that in particular this map does not use `*Id` types such + /// as `InterfaceId` from `wit_parser`. This is due to the fact that during + /// world merging new interfaces are created for named imports (e.g. `import + /// x: interface { ... }`) as inline interfaces are copied from one world to + /// another. Additionally during world merging different interfaces at the + /// same version may be deduplicated. + /// + /// For these reasons a string-based key is chosen to avoid juggling IDs + /// through the world merging process. Additionally versions are chopped off + /// for now to help with a problem such as: + /// + /// * The main module imports a:b/c@0.1.0 + /// * An adapter imports a:b/c@0.1.1 + /// * The final world uses a:b/c@0.1.1, but the main module has no + /// encoding listed for that exact item. + /// + /// By chopping off versions this is able to get everything registered + /// correctly even in the fact of merging interfaces and worlds. + encodings: IndexMap, +} + +impl EncodingMap { + fn insert_all( + &mut self, + resolve: &Resolve, + set: &IndexMap, + encoding: StringEncoding, + ) { + for (name, item) in set { + match item { + WorldItem::Function(func) => { + let key = self.key(resolve, name, &func.name); + self.encodings.insert(key, encoding); + } + WorldItem::Interface { id, .. } => { + for (func, _) in resolve.interfaces[*id].functions.iter() { + let key = self.key(resolve, name, func); + self.encodings.insert(key, encoding); + } + } + WorldItem::Type(_) => {} + } + } + } + + /// Looks up the encoding of the function `func` which is scoped under `key` + /// in the world in question. + pub fn get(&self, resolve: &Resolve, key: &WorldKey, func: &str) -> Option { + let key = self.key(resolve, key, func); + self.encodings.get(&key).copied() + } + + fn key(&self, resolve: &Resolve, key: &WorldKey, func: &str) -> String { + format!( + "{}/{func}", + match key { + WorldKey::Name(name) => name.to_string(), + WorldKey::Interface(id) => { + let iface = &resolve.interfaces[*id]; + let pkg = &resolve.packages[iface.package.unwrap()]; + format!( + "{}:{}/{}", + pkg.name.namespace, + pkg.name.name, + iface.name.as_ref().unwrap() + ) + } + } + ) + } + + fn merge(&mut self, other: EncodingMap) -> Result<()> { + for (key, encoding) in other.encodings { + if let Some(prev) = self.encodings.insert(key.clone(), encoding) { + if prev != encoding { + bail!("conflicting string encodings specified for `{key}`"); + } + } + } + Ok(()) + } } /// This function will parse the core `wasm` binary given as input and return a @@ -313,38 +406,18 @@ impl Bindgen { producers, } = other; - let world = self + let remap = self .resolve .merge(resolve) - .context("failed to merge WIT package sets together")? - .map_world(world, None)?; + .context("failed to merge WIT package sets together")?; + let world = remap.map_world(world, None)?; let exports = self.resolve.worlds[world].exports.keys().cloned().collect(); self.resolve .merge_worlds(world, self.world) .context("failed to merge worlds from two documents")?; - for (name, encoding) in export_encodings { - let prev = self - .metadata - .export_encodings - .insert(name.clone(), encoding); - if let Some(prev) = prev { - if prev != encoding { - bail!("conflicting string encodings specified for export `{name}`"); - } - } - } - for ((module, name), encoding) in import_encodings { - let prev = self - .metadata - .import_encodings - .insert((module.clone(), name.clone()), encoding); - if let Some(prev) = prev { - if prev != encoding { - bail!("conflicting string encodings specified for import `{module}::{name}`"); - } - } - } + self.metadata.import_encodings.merge(import_encodings)?; + self.metadata.export_encodings.merge(export_encodings)?; if let Some(producers) = producers { if let Some(mine) = &mut self.producers { mine.merge(&producers); @@ -364,45 +437,10 @@ impl ModuleMetadata { let mut ret = ModuleMetadata::default(); let world = &resolve.worlds[world]; - for (name, item) in world.imports.iter() { - let name = resolve.name_world_key(name); - match item { - WorldItem::Function(_) => { - let prev = ret - .import_encodings - .insert((BARE_FUNC_MODULE_NAME.to_string(), name.clone()), encoding); - assert!(prev.is_none()); - } - WorldItem::Interface { id, .. } => { - for (func, _) in resolve.interfaces[*id].functions.iter() { - let prev = ret - .import_encodings - .insert((name.clone(), func.clone()), encoding); - assert!(prev.is_none()); - } - } - WorldItem::Type(_) => {} - } - } - - for (name, item) in world.exports.iter() { - let name = resolve.name_world_key(name); - match item { - WorldItem::Function(func) => { - let name = func.core_export_name(None).into_owned(); - let prev = ret.export_encodings.insert(name.clone(), encoding); - assert!(prev.is_none()); - } - WorldItem::Interface { id, .. } => { - for (_, func) in resolve.interfaces[*id].functions.iter() { - let name = func.core_export_name(Some(&name)).into_owned(); - let prev = ret.export_encodings.insert(name, encoding); - assert!(prev.is_none()); - } - } - WorldItem::Type(_) => {} - } - } + ret.export_encodings + .insert_all(resolve, &world.exports, encoding); + ret.import_encodings + .insert_all(resolve, &world.imports, encoding); ret } diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index 3bb7dfdddf..489a1ab120 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -140,7 +140,7 @@ pub enum ImportInstance { pub enum Import { /// A top-level world function, with the name provided here, is imported /// into the module. - WorldFunc(String), + WorldFunc(WorldKey, String), /// An interface's function is imported into the module. /// @@ -154,7 +154,7 @@ pub enum Import { /// The key provided indicates whether it's for the top-level types of the /// world (`None`) or an interface (`Some` with the name of the interface). /// The `TypeId` is what resource is being dropped. - ImportedResourceDrop(Option, TypeId), + ImportedResourceDrop(WorldKey, Option, TypeId), /// A `canon resource.drop` intrinsic for an exported item is being /// imported. @@ -210,7 +210,7 @@ impl ImportMap { ImportInstance::Names(names) => names.get(func), _ => None, }); - matches!(item, Some(Import::WorldFunc(_))) + matches!(item, Some(Import::WorldFunc(_, _))) } /// Returns whether the interface function specified is imported. @@ -224,7 +224,7 @@ impl ImportMap { /// Returns whether the specified resource's drop method is needed to import. pub fn uses_imported_resource_drop(&self, resource: TypeId) -> bool { self.imports().any(|(_, _, import)| match import { - Import::ImportedResourceDrop(_, id) => resource == *id, + Import::ImportedResourceDrop(_, _, id) => resource == *id, _ => false, }) } @@ -337,12 +337,12 @@ impl ImportMap { let key = WorldKey::Name(name.to_string()); if let Some(WorldItem::Function(func)) = world.imports.get(&key) { validate_func(resolve, ty, func, AbiVariant::GuestImport)?; - return Ok(Import::WorldFunc(func.name.clone())); + return Ok(Import::WorldFunc(key, func.name.clone())); } let get_resource = resource_test_for_world(resolve, world_id); if let Some(id) = valid_resource_drop(name, ty, get_resource)? { - return Ok(Import::ImportedResourceDrop(None, id)); + return Ok(Import::ImportedResourceDrop(key, None, id)); } match world.imports.get(&key) { @@ -397,7 +397,7 @@ impl ImportMap { })?; Ok(Import::InterfaceFunc(key, id, f.name.clone())) } else if let Some(ty) = valid_resource_drop(import.name, ty, get_resource)? { - Ok(Import::ImportedResourceDrop(Some(key), ty)) + Ok(Import::ImportedResourceDrop(key, Some(id), ty)) } else { bail!( "import interface `{}` is missing function \ @@ -576,13 +576,13 @@ pub enum Export { WorldFunc(String), /// A post-return for a top-level function of a world. - WorldFuncPostReturn(String), + WorldFuncPostReturn(WorldKey), /// An export of a function in an interface. InterfaceFunc(InterfaceId, String), /// A post-return for the above function. - InterfaceFuncPostReturn(InterfaceId, String), + InterfaceFuncPostReturn(WorldKey, String), /// A destructor for an exported resource. ResourceDtor(TypeId), @@ -676,11 +676,14 @@ impl ExportMap { format!("failed to validate export for `{key}`") })?; match id { - Some(id) => { - return Ok(Some(Export::InterfaceFuncPostReturn(id, f.name.clone()))); + Some(_id) => { + return Ok(Some(Export::InterfaceFuncPostReturn( + key.clone(), + f.name.clone(), + ))); } None => { - return Ok(Some(Export::WorldFuncPostReturn(f.name.clone()))); + return Ok(Some(Export::WorldFuncPostReturn(key.clone()))); } } } @@ -765,25 +768,21 @@ impl ExportMap { /// Returns the name of the post-return export, if any, for the `interface` /// and `func` combo. - pub fn post_return(&self, interface: Option, func: &Function) -> Option<&str> { - self.find(|m| match (m, interface) { - (Export::WorldFuncPostReturn(f), None) => func.name == *f, - (Export::InterfaceFuncPostReturn(i, f), Some(id)) => *i == id && func.name == *f, + pub fn post_return(&self, key: &WorldKey, func: &Function) -> Option<&str> { + self.find(|m| match m { + Export::WorldFuncPostReturn(k) => k == key, + Export::InterfaceFuncPostReturn(k, f) => k == key && func.name == *f, _ => false, }) } /// Returns the realloc that the exported function `interface` and `func` /// are using. - pub fn export_realloc_for( - &self, - interface: Option, - func: &Function, - ) -> Option<&str> { + pub fn export_realloc_for(&self, key: &WorldKey, func: &Function) -> Option<&str> { // TODO: This realloc detection should probably be improved with // some sort of scheme to have per-function reallocs like // `cabi_realloc_{name}` or something like that. - let _ = (interface, func); + let _ = (key, func); if let Some(name) = self.find(|m| matches!(m, Export::GeneralPurposeExportRealloc)) { return Some(name); From 9094e79c9a915e08e643f010e50179b49e2d3d4a Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sat, 28 Sep 2024 05:42:33 -0700 Subject: [PATCH 3/8] Remove core wasm name guess in adapter GC This commit removes the need for the GC pass on the adapter module to guess what core wasm export names are needed for WIT. Previously it was assumed that certain exports would have exact core wasm names but that's going to change soon so this refactoring is empowering these future changes. The GC pass for adapters is restructured to run validation over the non-GC'd adapter first. This validation pass will identify WIT export functions and such and then this information is used to determine the set of live exports. These live exports are then used to perform a GC pass, and then afterwards the validation pass is run a second time to recalculate information with possibly-removed imports. --- crates/wit-component/src/encoding/world.rs | 106 +++++++++------------ crates/wit-component/src/gc.rs | 24 +---- 2 files changed, 47 insertions(+), 83 deletions(-) diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index 18943abd39..e6e763a71a 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -6,9 +6,8 @@ use anyhow::{Context, Result}; use indexmap::{IndexMap, IndexSet}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; -use wasmparser::FuncType; use wit_parser::{ - abi::{AbiVariant, WasmSignature, WasmType}, + abi::{AbiVariant, WasmSignature}, Function, InterfaceId, LiveTypes, Resolve, TypeDefKind, TypeId, TypeOwner, WorldId, WorldItem, WorldKey, }; @@ -130,12 +129,51 @@ impl<'a> ComponentWorld<'a> { let wasm = if library_info.is_some() { Cow::Borrowed(wasm as &[u8]) } else { - let required = self.required_adapter_exports( + // Without `library_info` this means that this is an adapter. + // The goal of the adapter is to provide a suite of symbols that + // can be imported, but not all symbols may be imported. Here + // the module is trimmed down to only what's needed by the + // original main module. + // + // The main module requires `required_by_import` above, but + // adapters may themselves also export WIT items. To handle this + // the sequence of operations here are: + // + // 1. First the adapter is validated as-is. This ensures that + // everything looks good before GC. + // 2. The metadata from step (1) is used to determine the set of + // WIT-level exports that are needed. This includes things + // like realloc functions and such. + // 3. The set of WIT-level functions from (2) is unioned with + // `required_by_import` to create the set of required exports + // of the adapter. + // 4. This set of exports is used to delete some exports of the + // adapter and then perform a GC pass. + // + // Finally at the end of all of this the + // `validate_adapter_module` method is called for a second time + // on the minimized adapter. This is done because deleting + // imports may have deleted some imports which means that the + // final component may not need to import as many interfaces. + let info = validate_adapter_module( + &wasm, resolve, world, - required_exports, &required_by_import, - ); + required_exports, + library_info.as_ref(), + adapters, + ) + .with_context(|| { + format!("failed to validate the imports of the adapter module `{name}`") + })?; + let mut required = IndexSet::new(); + for (name, _ty) in required_by_import.iter() { + required.insert(name.to_string()); + } + for (name, _export) in info.exports.iter() { + required.insert(name.to_string()); + } Cow::Owned( crate::gc::run( @@ -174,64 +212,6 @@ impl<'a> ComponentWorld<'a> { Ok(()) } - /// Returns the set of functions required to be exported from an adapter, - /// either because they're exported from the adapter's world or because - /// they're required as an import to the main module. - fn required_adapter_exports<'r>( - &self, - resolve: &'r Resolve, - world: WorldId, - required_exports: &IndexSet, - required_by_import: &IndexMap, - ) -> IndexMap)> { - use wasmparser::ValType; - - let mut required = IndexMap::new(); - for (name, ty) in required_by_import { - required.insert(name.to_string(), (ty.clone(), None)); - } - let mut add_func = |func: &'r Function, name: Option<&str>| { - let name = func.core_export_name(name); - let ty = resolve.wasm_signature(AbiVariant::GuestExport, func); - let prev = required.insert( - name.into_owned(), - ( - wasmparser::FuncType::new( - ty.params.iter().map(to_valty), - ty.results.iter().map(to_valty), - ), - Some(func), - ), - ); - assert!(prev.is_none()); - }; - for name in required_exports { - match &resolve.worlds[world].exports[name] { - WorldItem::Function(func) => add_func(func, None), - WorldItem::Interface { id, .. } => { - let name = resolve.name_world_key(name); - for (_, func) in resolve.interfaces[*id].functions.iter() { - add_func(func, Some(&name)); - } - } - WorldItem::Type(_) => {} - } - } - return required; - - fn to_valty(ty: &WasmType) -> ValType { - match ty { - WasmType::I32 => ValType::I32, - WasmType::I64 => ValType::I64, - WasmType::F32 => ValType::F32, - WasmType::F64 => ValType::F64, - WasmType::Pointer => ValType::I32, - WasmType::PointerOrI64 => ValType::I64, - WasmType::Length => ValType::I32, - } - } - } - /// Fills out the `import_map` field of `self` by determining the live /// functions from all imports. This additionally classifies imported /// functions into direct or indirect lowerings for managing shims. diff --git a/crates/wit-component/src/gc.rs b/crates/wit-component/src/gc.rs index a0a345afca..ab22187dcf 100644 --- a/crates/wit-component/src/gc.rs +++ b/crates/wit-component/src/gc.rs @@ -19,9 +19,9 @@ const PAGE_SIZE: i32 = 64 * 1024; /// /// This internally performs a "gc" pass after removing exports to ensure that /// the resulting module imports the minimal set of functions necessary. -pub fn run( +pub fn run( wasm: &[u8], - required: &IndexMap, + required: &IndexSet, main_module_realloc: Option<&str>, ) -> Result> { assert!(!required.is_empty()); @@ -31,21 +31,14 @@ pub fn run( // Make sure that all required names are present in the module, and then // remove all names that are not required. - for (name, _ty) in required { + for name in required { if !module.exports.contains_key(name.as_str()) { bail!("adapter module does not have export `{name}`") } } let mut not_required = IndexSet::new(); for name in module.exports.keys().copied() { - // If we need `name` then we also need cabi_post_`name`: - let name = if let Some(suffix) = name.strip_prefix("cabi_post_") { - suffix - } else { - name - }; - - if !required.contains_key(name) && !always_keep(name) { + if !required.contains(name) { not_required.insert(name); } } @@ -57,15 +50,6 @@ pub fn run( module.encode(main_module_realloc) } -fn always_keep(name: &str) -> bool { - match name { - // Explicitly keep `cabi_realloc` if it's there in case an interface - // needs it for a lowering. - "cabi_realloc" | "cabi_import_realloc" | "cabi_export_realloc" => true, - _ => false, - } -} - /// This function generates a Wasm function body which implements `cabi_realloc` in terms of `memory.grow`. It /// only accepts new, page-sized allocations. fn realloc_via_memory_grow() -> wasm_encoder::Function { From 22eaa4096c00598c445854f37baf02bdc42fcbcf Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 25 Sep 2024 19:29:15 -0700 Subject: [PATCH 4/8] Support the new name mangling scheme for components This commit adds support for WebAssembly/component-model#378 to `wit-component`. Notably a new set of alternative names are registered and recognized during the module-to-component translation process. Support for the previous set of names are all preserved and will continue to be supported for some time. The new names are, for now, recognized in parallel to the old names. This involved some refactoring to the validation part of `wit-component` and further encapsulation of various names to one small location instead of a shared location for everywhere else to use as well. --- crates/wit-component/src/dummy.rs | 2 +- crates/wit-component/src/encoding.rs | 5 +- crates/wit-component/src/encoding/world.rs | 4 +- crates/wit-component/src/validation.rs | 759 ++++++++++++------ .../tests/components/cm32-names/component.wat | 366 +++++++++ .../components/cm32-names/component.wit.print | 25 + .../tests/components/cm32-names/module.wat | 32 + .../tests/components/cm32-names/module.wit | 30 + .../error.txt | 2 +- .../error-export-sig-mismatch/error.txt | 2 +- crates/wit-parser/src/lib.rs | 55 +- crates/wit-parser/src/resolve.rs | 30 + 12 files changed, 1053 insertions(+), 259 deletions(-) create mode 100644 crates/wit-component/tests/components/cm32-names/component.wat create mode 100644 crates/wit-component/tests/components/cm32-names/component.wit.print create mode 100644 crates/wit-component/tests/components/cm32-names/module.wat create mode 100644 crates/wit-component/tests/components/cm32-names/module.wit diff --git a/crates/wit-component/src/dummy.rs b/crates/wit-component/src/dummy.rs index 6a3cbb457d..50c217a8e9 100644 --- a/crates/wit-component/src/dummy.rs +++ b/crates/wit-component/src/dummy.rs @@ -67,7 +67,7 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec { WorldItem::Interface { id: export, .. } => { let name = resolve.name_world_key(name); for (_, func) in resolve.interfaces[*export].functions.iter() { - let name = func.core_export_name(Some(&name)); + let name = func.legacy_core_export_name(Some(&name)); push_func(&mut wat, &name, resolve, func); } diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index b5f0b7d938..5843a7e338 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -72,7 +72,7 @@ //! component model. use crate::metadata::{self, Bindgen, ModuleMetadata}; -use crate::validation::{Export, ExportMap, Import, ImportInstance, ImportMap, RESOURCE_DROP}; +use crate::validation::{Export, ExportMap, Import, ImportInstance, ImportMap}; use crate::StringEncoding; use anyhow::{anyhow, bail, Context, Result}; use indexmap::{IndexMap, IndexSet}; @@ -647,6 +647,7 @@ impl<'a> EncodingState<'a> { } let world = &resolve.worlds[self.info.encoder.metadata.world]; + for export_name in exports { let export_string = resolve.name_world_key(export_name); match &world.exports[export_name] { @@ -1457,7 +1458,7 @@ impl<'a> EncodingState<'a> { Import::ImportedResourceDrop(key, iface, id) => { let ty = &resolve.types[*id]; let name = ty.name.as_ref().unwrap(); - name_tmp = format!("{RESOURCE_DROP}{name}"); + name_tmp = format!("{name}_drop"); (key, &name_tmp, iface.map(|_| resolve.name_world_key(key))) } Import::WorldFunc(key, name) => (key, name, None), diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index e6e763a71a..70f1a89415 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -1,6 +1,6 @@ use super::{Adapter, ComponentEncoder, LibraryInfo, RequiredOptions}; use crate::validation::{ - validate_adapter_module, validate_module, Import, ImportMap, ValidatedModule, RESOURCE_DROP, + validate_adapter_module, validate_module, Import, ImportMap, ValidatedModule, }; use anyhow::{Context, Result}; use indexmap::{IndexMap, IndexSet}; @@ -461,7 +461,7 @@ impl ImportedInterface { let name = ty.name.as_deref().expect("resources must be named"); if required.resource_drops.contains(&id) { - let name = format!("{RESOURCE_DROP}{name}"); + let name = format!("{name}_drop"); let prev = self.lowerings.insert(name, Lowering::ResourceDrop(id)); assert!(prev.is_none()); } diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index 489a1ab120..63a1224752 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -33,21 +33,6 @@ fn wasm_sig_to_func_type(signature: WasmSignature) -> FuncType { ) } -pub const MAIN_MODULE_IMPORT_NAME: &str = "__main_module__"; - -/// The module name used when a top-level function in a world is imported into a -/// core wasm module. Note that this is not a valid WIT identifier to avoid -/// clashes with valid WIT interfaces. This is also not empty because LLVM -/// interprets an empty module import string as "not specified" which means it -/// turns into `env`. -pub const BARE_FUNC_MODULE_NAME: &str = "$root"; - -pub const RESOURCE_DROP: &str = "[resource-drop]"; -pub const RESOURCE_REP: &str = "[resource-rep]"; -pub const RESOURCE_NEW: &str = "[resource-new]"; - -pub const POST_RETURN_PREFIX: &str = "cabi_post_"; - /// Metadata about a validated module and what was found internally. /// /// This structure houses information about `imports` and `exports` to the @@ -203,14 +188,10 @@ pub enum Import { impl ImportMap { /// Returns whether the top-level world function `func` is imported. pub fn uses_toplevel_func(&self, func: &str) -> bool { - let item = self - .names - .get(BARE_FUNC_MODULE_NAME) - .and_then(|map| match map { - ImportInstance::Names(names) => names.get(func), - _ => None, - }); - matches!(item, Some(Import::WorldFunc(_, _))) + self.imports().any(|(_, _, item)| match item { + Import::WorldFunc(_, name) => func == name, + _ => false, + }) } /// Returns whether the interface function specified is imported. @@ -302,8 +283,6 @@ impl ImportMap { adapters: &IndexSet<&str>, types: TypesRef<'_>, ) -> Result { - let world = &resolve.worlds[world_id]; - // Special-case the main module's memory imported into adapters which // currently with `wasm-ld` is not easily configurable. if import.module == "env" && import.name == "memory" { @@ -311,7 +290,7 @@ impl ImportMap { } // Special-case imports from the main module into adapters. - if import.module == MAIN_MODULE_IMPORT_NAME { + if import.module == "__main_module__" { return Ok(Import::MainModuleExport { name: import.name.to_string(), kind: match import.ty { @@ -330,10 +309,33 @@ impl ImportMap { }; let ty = types[types.core_type_at(ty_index).unwrap_sub()].unwrap_func(); - // Handle top-level function imports if they're going through the "bare - // name" representing the world root. - if import.module == BARE_FUNC_MODULE_NAME { - let name = import.name; + // Handle main module imports that match known adapters and set it up as + // an import of an adapter export. + if adapters.contains(import.module) { + return Ok(Import::AdapterExport(ty.clone())); + } + + let (module, names) = match import.module.strip_prefix("cm32p2") { + Some(suffix) => (suffix, STANDARD), + None => (import.module, LEGACY), + }; + self.classify_component_model_import(module, import.name, resolve, world_id, ty, names) + } + + /// Attempts to classify the import `{module}::{name}` with the rules + /// specified in WebAssembly/component-model#378 + fn classify_component_model_import( + &self, + module: &str, + name: &str, + resolve: &Resolve, + world_id: WorldId, + ty: &FuncType, + names: &dyn NameMangling, + ) -> Result { + let world = &resolve.worlds[world_id]; + + if module == names.import_root() { let key = WorldKey::Name(name.to_string()); if let Some(WorldItem::Function(func)) = world.imports.get(&key) { validate_func(resolve, ty, func, AbiVariant::GuestImport)?; @@ -341,8 +343,12 @@ impl ImportMap { } let get_resource = resource_test_for_world(resolve, world_id); - if let Some(id) = valid_resource_drop(name, ty, get_resource)? { - return Ok(Import::ImportedResourceDrop(key, None, id)); + if let Some(resource) = names.resource_drop_name(name) { + if let Some(id) = get_resource(resource) { + let expected = FuncType::new([ValType::I32], []); + validate_func_sig(name, &expected, ty)?; + return Ok(Import::ImportedResourceDrop(key, None, id)); + } } match world.imports.get(&key) { @@ -351,143 +357,59 @@ impl ImportMap { } } - // Handle main module imports that match known adapters and set it up as - // an import of an adapter export. - if adapters.contains(import.module) { - return Ok(Import::AdapterExport(ty.clone())); - } + let interface = match module.strip_prefix(names.import_non_root_prefix()) { + Some(name) => name, + None => bail!("unknown or invalid component model import syntax"), + }; - // Handle imports which are used to manipulate state for exported - // resources. - if let Some(suffix) = import.module.strip_prefix("[export]") { - let (key, id) = self.module_to_interface(suffix, resolve, &world.exports)?; - let get_resource = resource_test_for_interface(resolve, id); + if let Some(interface) = interface.strip_prefix(names.import_exported_intrinsic_prefix()) { + let (key, id) = names.module_to_interface(interface, resolve, &world.exports)?; - return if let Some(ty) = valid_resource_drop(import.name, ty, &get_resource)? { - Ok(Import::ExportedResourceDrop(key, ty)) - } else if let Some(id) = import - .name - .strip_prefix(RESOURCE_NEW) - .and_then(&get_resource) - { - let expected = FuncType::new([ValType::I32], [ValType::I32]); - validate_func_sig(import.name, &expected, ty)?; - Ok(Import::ExportedResourceNew(key, id)) - } else if let Some(id) = import - .name - .strip_prefix(RESOURCE_REP) - .and_then(&get_resource) - { - let expected = FuncType::new([ValType::I32], [ValType::I32]); - validate_func_sig(import.name, &expected, ty)?; - Ok(Import::ExportedResourceRep(key, id)) - } else { - bail!("unknown function `{}`", import.name) - }; + let get_resource = resource_test_for_interface(resolve, id); + if let Some(name) = names.resource_drop_name(name) { + if let Some(id) = get_resource(name) { + let expected = FuncType::new([ValType::I32], []); + validate_func_sig(name, &expected, ty)?; + return Ok(Import::ExportedResourceDrop(key, id)); + } + } + if let Some(name) = names.resource_new_name(name) { + if let Some(id) = get_resource(name) { + let expected = FuncType::new([ValType::I32], [ValType::I32]); + validate_func_sig(name, &expected, ty)?; + return Ok(Import::ExportedResourceNew(key, id)); + } + } + if let Some(name) = names.resource_rep_name(name) { + if let Some(id) = get_resource(name) { + let expected = FuncType::new([ValType::I32], [ValType::I32]); + validate_func_sig(name, &expected, ty)?; + return Ok(Import::ExportedResourceRep(key, id)); + } + } + bail!("unknown function `{name}`") } - // And finally handle imports of functions from interfaces here. - let (key, id) = self.module_to_interface(import.module, resolve, &world.imports)?; + let (key, id) = names.module_to_interface(interface, resolve, &world.imports)?; let interface = &resolve.interfaces[id]; let get_resource = resource_test_for_interface(resolve, id); - if let Some(f) = interface.functions.get(import.name) { + if let Some(f) = interface.functions.get(name) { validate_func(resolve, ty, f, AbiVariant::GuestImport).with_context(|| { let name = resolve.name_world_key(&key); format!("failed to validate import interface `{name}`") })?; - Ok(Import::InterfaceFunc(key, id, f.name.clone())) - } else if let Some(ty) = valid_resource_drop(import.name, ty, get_resource)? { - Ok(Import::ImportedResourceDrop(key, Some(id), ty)) - } else { - bail!( - "import interface `{}` is missing function \ - `{}` that is required by the module", - import.module, - import.name, - ) - } - } - - fn module_to_interface( - &self, - module: &str, - resolve: &Resolve, - items: &IndexMap, - ) -> Result<(WorldKey, InterfaceId)> { - // First see if this is a bare name - let bare_name = WorldKey::Name(module.to_string()); - if let Some(WorldItem::Interface { id, .. }) = items.get(&bare_name) { - return Ok((bare_name, *id)); - } - - // ... and if this isn't a bare name then it's time to do some parsing - // related to interfaces, versions, and such. First up the `module` name - // is parsed as a normal component name from `wasmparser` to see if it's - // of the "interface kind". If it's not then that means the above match - // should have been a hit but it wasn't, so an error is returned. - let kebab_name = ComponentName::new(module, 0); - let name = match kebab_name.as_ref().map(|k| k.kind()) { - Ok(ComponentNameKind::Interface(name)) => name, - _ => bail!("module requires an import interface named `{module}`"), - }; - - // Prioritize an exact match based on versions, so try that first. - let pkgname = PackageName { - namespace: name.namespace().to_string(), - name: name.package().to_string(), - version: name.version(), - }; - if let Some(pkg) = resolve.package_names.get(&pkgname) { - if let Some(id) = resolve.packages[*pkg] - .interfaces - .get(name.interface().as_str()) - { - let key = WorldKey::Interface(*id); - if items.contains_key(&key) { - return Ok((key, *id)); - } - } - } - - // If an exact match wasn't found then instead search for the first - // match based on versions. This means that a core wasm import for - // "1.2.3" might end up matching an interface at "1.2.4", for example. - // (or "1.2.2", depending on what's available). - for (key, _) in items { - let id = match key { - WorldKey::Interface(id) => *id, - WorldKey::Name(_) => continue, - }; - // Make sure the interface names match - let interface = &resolve.interfaces[id]; - if interface.name.as_ref().unwrap() != name.interface().as_str() { - continue; - } - - // Make sure the package name (without version) matches - let pkg = &resolve.packages[interface.package.unwrap()]; - if pkg.name.namespace != pkgname.namespace || pkg.name.name != pkgname.name { - continue; - } - - let module_version = match name.version() { - Some(version) => version, - None => continue, - }; - let pkg_version = match &pkg.name.version { - Some(version) => version, - None => continue, - }; - - // Test if the two semver versions are compatible - let module_compat = PackageName::version_compat_track(&module_version); - let pkg_compat = PackageName::version_compat_track(pkg_version); - if module_compat == pkg_compat { - return Ok((key.clone(), id)); + return Ok(Import::InterfaceFunc(key, id, f.name.clone())); + } else if let Some(resource) = names.resource_drop_name(name) { + if let Some(resource) = get_resource(resource) { + let expected = FuncType::new([ValType::I32], []); + validate_func_sig(name, &expected, ty)?; + return Ok(Import::ImportedResourceDrop(key, Some(id), resource)); } } - - bail!("module requires an import interface named `{module}`") + bail!( + "import interface `{module}` is missing function \ + `{name}` that is required by the module", + ) } fn classify_import_with_library( @@ -570,6 +492,7 @@ pub struct ExportMap { /// All possible (known) exports from a core wasm module that are recognized and /// handled during the componentization process. +#[derive(Debug)] pub enum Export { /// An export of a top-level function of a world, where the world function /// is named here. @@ -616,6 +539,7 @@ impl ExportMap { types: TypesRef<'_>, ) -> Result<()> { if let Some(item) = self.classify(export, resolve, world, exports, types)? { + log::debug!("classifying export `{}` as {item:?}", export.name); let prev = self.names.insert(export.name.to_string(), item); assert!(prev.is_none()); } @@ -631,16 +555,16 @@ impl ExportMap { types: TypesRef<'_>, ) -> Result> { match export.kind { - ExternalKind::Func => {} + ExternalKind::Func => { + let ty = types[types.core_function_at(export.index)].unwrap_func(); + self.raw_exports.insert(export.name.to_string(), ty.clone()); + } ExternalKind::Memory => return Ok(Some(Export::Memory)), _ => return Ok(None), } - let ty = types[types.core_function_at(export.index)].unwrap_func(); - self.raw_exports.insert(export.name.to_string(), ty.clone()); - // Handle a few special-cased names first. - if export.name == "cabi_realloc" || export.name == "canonical_abi_realloc" { + if export.name == "canonical_abi_realloc" { return Ok(Some(Export::GeneralPurposeRealloc)); } else if export.name == "cabi_import_realloc" { return Ok(Some(Export::GeneralPurposeImportRealloc)); @@ -648,12 +572,57 @@ impl ExportMap { return Ok(Some(Export::GeneralPurposeExportRealloc)); } else if export.name == "cabi_realloc_adapter" { return Ok(Some(Export::ReallocForAdapter)); - } else if export.name == "_initialize" { + } + + let (name, names) = match export.name.strip_prefix("cm32p2") { + Some(name) => (name, STANDARD), + None => (export.name, LEGACY), + }; + if let Some(export) = self + .classify_component_export(names, name, &export, resolve, world, exports, types) + .with_context(|| format!("failed to classify export `{}`", export.name))? + { + return Ok(Some(export)); + } + log::debug!("unknown export `{}`", export.name); + Ok(None) + } + + fn classify_component_export( + &mut self, + names: &dyn NameMangling, + name: &str, + export: &wasmparser::Export<'_>, + resolve: &Resolve, + world: WorldId, + exports: &IndexSet, + types: TypesRef<'_>, + ) -> Result> { + match export.kind { + ExternalKind::Func => {} + ExternalKind::Memory => { + if name == names.export_memory() { + return Ok(Some(Export::Memory)); + } + return Ok(None); + } + _ => return Ok(None), + } + let ty = types[types.core_function_at(export.index)].unwrap_func(); + + // Handle a few special-cased names first. + if name == names.export_realloc() { + let expected = FuncType::new([ValType::I32; 4], [ValType::I32]); + validate_func_sig(name, &expected, ty)?; + return Ok(Some(Export::GeneralPurposeRealloc)); + } else if name == names.export_initialize() { + let expected = FuncType::new([], []); + validate_func_sig(name, &expected, ty)?; return Ok(Some(Export::Initialize)); } // Try to match this to a known WIT export that `exports` allows. - if let Some((key, id, f)) = self.match_wit_export(export.name, resolve, world, exports) { + if let Some((key, id, f)) = names.match_wit_export(name, resolve, world, exports) { validate_func(resolve, ty, f, AbiVariant::GuestExport).with_context(|| { let key = resolve.name_world_key(key); format!("failed to validate export for `{key}`") @@ -669,8 +638,8 @@ impl ExportMap { } // See if this is a post-return for any known WIT export. - if let Some(suffix) = export.name.strip_prefix(POST_RETURN_PREFIX) { - if let Some((key, id, f)) = self.match_wit_export(suffix, resolve, world, exports) { + if let Some(remaining) = names.strip_post_return(name) { + if let Some((key, id, f)) = names.match_wit_export(remaining, resolve, world, exports) { validate_post_return(resolve, ty, f).with_context(|| { let key = resolve.name_world_key(key); format!("failed to validate export for `{key}`") @@ -690,82 +659,15 @@ impl ExportMap { } // And, finally, see if it matches a known destructor. - if let Some(dtor) = self.match_wit_resource_dtor(export.name, resolve, world, exports) { + if let Some(dtor) = names.match_wit_resource_dtor(name, resolve, world, exports) { let expected = FuncType::new([ValType::I32], []); validate_func_sig(export.name, &expected, ty)?; return Ok(Some(Export::ResourceDtor(dtor))); } - log::debug!("unknown export `{}`", export.name); Ok(None) } - fn match_wit_export<'a>( - &self, - export_name: &str, - resolve: &'a Resolve, - world: WorldId, - exports: &'a IndexSet, - ) -> Option<(&'a WorldKey, Option, &'a Function)> { - let world = &resolve.worlds[world]; - for name in exports { - match &world.exports[name] { - WorldItem::Function(f) => { - if f.core_export_name(None) == export_name { - return Some((name, None, f)); - } - } - WorldItem::Interface { id, .. } => { - let string = resolve.name_world_key(name); - for (_, func) in resolve.interfaces[*id].functions.iter() { - if func.core_export_name(Some(&string)) == export_name { - return Some((name, Some(*id), func)); - } - } - } - - WorldItem::Type(_) => unreachable!(), - } - } - - None - } - - fn match_wit_resource_dtor<'a>( - &self, - export_name: &str, - resolve: &'a Resolve, - world: WorldId, - exports: &'a IndexSet, - ) -> Option { - let world = &resolve.worlds[world]; - for name in exports { - let id = match &world.exports[name] { - WorldItem::Interface { id, .. } => *id, - WorldItem::Function(_) => continue, - WorldItem::Type(_) => unreachable!(), - }; - let name = resolve.name_world_key(name); - let resource = match export_name - .strip_prefix(&name) - .and_then(|s| s.strip_prefix("#[dtor]")) - .and_then(|r| resolve.interfaces[id].types.get(r)) - { - Some(id) => *id, - None => continue, - }; - - match resolve.types[resource].kind { - TypeDefKind::Resource => {} - _ => continue, - } - - return Some(resource); - } - - None - } - /// Returns the name of the post-return export, if any, for the `interface` /// and `func` combo. pub fn post_return(&self, key: &WorldKey, func: &Function) -> Option<&str> { @@ -906,6 +808,380 @@ impl ExportMap { } } +/// Trait dispatch and definition for parsing and interpreting "mangled names" +/// which show up in imports and exports of the component model. +/// +/// This trait is used to implement classification of imports and exports in the +/// component model. The methods on `ImportMap` and `ExportMap` will use this to +/// determine what an import is and how it's lifted/lowered in the world being +/// bound. +/// +/// This trait has a bit of history behind it as well. Before +/// WebAssembly/component-model#378 there was no standard naming scheme for core +/// wasm imports or exports when componenitizing. This meant that +/// `wit-component` implemented a particular scheme which mostly worked but was +/// mostly along the lines of "this at least works" rather than "someone sat +/// down and designed this". Since then, however, an standard naming scheme has +/// now been specified which was indeed designed. +/// +/// This trait serves as the bridge between these two. The historical naming +/// scheme is still supported for now through the `Legacy` implementation below +/// and will be for some time. The transition plan at this time is to support +/// the new scheme, eventually get it supported in bindings generators, and once +/// that's all propagated remove support for the legacy scheme. +trait NameMangling { + fn import_root(&self) -> &str; + fn import_non_root_prefix(&self) -> &str; + fn import_exported_intrinsic_prefix(&self) -> &str; + fn export_memory(&self) -> &str; + fn export_initialize(&self) -> &str; + fn export_realloc(&self) -> &str; + fn resource_drop_name<'a>(&self, s: &'a str) -> Option<&'a str>; + fn resource_new_name<'a>(&self, s: &'a str) -> Option<&'a str>; + fn resource_rep_name<'a>(&self, s: &'a str) -> Option<&'a str>; + fn module_to_interface( + &self, + module: &str, + resolve: &Resolve, + items: &IndexMap, + ) -> Result<(WorldKey, InterfaceId)>; + fn strip_post_return<'a>(&self, s: &'a str) -> Option<&'a str>; + fn match_wit_export<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option<(&'a WorldKey, Option, &'a Function)>; + fn match_wit_resource_dtor<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option; +} + +/// Definition of the "standard" naming scheme which currently starts with +/// "cm32p2". Note that wasm64 is not supported at this time. +struct Standard; + +const STANDARD: &'static dyn NameMangling = &Standard; + +impl NameMangling for Standard { + fn import_root(&self) -> &str { + "" + } + fn import_non_root_prefix(&self) -> &str { + "|" + } + fn import_exported_intrinsic_prefix(&self) -> &str { + "_ex_" + } + fn export_memory(&self) -> &str { + "_memory" + } + fn export_initialize(&self) -> &str { + "_initialize" + } + fn export_realloc(&self) -> &str { + "_realloc" + } + fn resource_drop_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_suffix("_drop") + } + fn resource_new_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_suffix("_new") + } + fn resource_rep_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_suffix("_rep") + } + fn module_to_interface( + &self, + interface: &str, + resolve: &Resolve, + items: &IndexMap, + ) -> Result<(WorldKey, InterfaceId)> { + for (key, item) in items.iter() { + let id = match key { + // Bare keys are matched exactly against `interface` + WorldKey::Name(name) => match item { + WorldItem::Interface { id, .. } if name == interface => *id, + _ => continue, + }, + // ID-identified keys are matched with their "canonical name" + WorldKey::Interface(id) => { + if resolve.canonicalized_id_of(*id).as_deref() != Some(interface) { + continue; + } + *id + } + }; + return Ok((key.clone(), id)); + } + bail!("failed to find world item corresponding to interface `{interface}`") + } + fn strip_post_return<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_suffix("_post") + } + fn match_wit_export<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option<(&'a WorldKey, Option, &'a Function)> { + if let Some(world_export_name) = export_name.strip_prefix("||") { + let key = exports.get(&WorldKey::Name(world_export_name.to_string()))?; + match &resolve.worlds[world].exports[key] { + WorldItem::Function(f) => return Some((key, None, f)), + _ => return None, + } + } + + let (key, id, func_name) = + self.match_wit_interface(export_name, resolve, world, exports)?; + let func = resolve.interfaces[id].functions.get(func_name)?; + Some((key, Some(id), func)) + } + + fn match_wit_resource_dtor<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option { + let (_key, id, name) = + self.match_wit_interface(export_name.strip_suffix("_dtor")?, resolve, world, exports)?; + let ty = *resolve.interfaces[id].types.get(name)?; + match resolve.types[ty].kind { + TypeDefKind::Resource => Some(ty), + _ => None, + } + } +} + +impl Standard { + fn match_wit_interface<'a, 'b>( + &self, + export_name: &'b str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option<(&'a WorldKey, InterfaceId, &'b str)> { + let world = &resolve.worlds[world]; + let export_name = export_name.strip_prefix("|")?; + + for export in exports { + let id = match &world.exports[export] { + WorldItem::Interface { id, .. } => *id, + WorldItem::Function(_) => continue, + WorldItem::Type(_) => unreachable!(), + }; + let remaining = match export { + WorldKey::Name(name) => export_name.strip_prefix(name), + WorldKey::Interface(_) => { + let prefix = resolve.canonicalized_id_of(id).unwrap(); + export_name.strip_prefix(&prefix) + } + }; + let item_name = match remaining.and_then(|s| s.strip_prefix("|")) { + Some(name) => name, + None => continue, + }; + return Some((export, id, item_name)); + } + + None + } +} + +/// Definition of wit-component's "legacy" naming scheme which predates +/// WebAssembly/component-model#378. +struct Legacy; + +const LEGACY: &'static dyn NameMangling = &Legacy; + +impl NameMangling for Legacy { + fn import_root(&self) -> &str { + "$root" + } + fn import_non_root_prefix(&self) -> &str { + "" + } + fn import_exported_intrinsic_prefix(&self) -> &str { + "[export]" + } + fn export_memory(&self) -> &str { + "memory" + } + fn export_initialize(&self) -> &str { + "_initialize" + } + fn export_realloc(&self) -> &str { + "cabi_realloc" + } + fn resource_drop_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_prefix("[resource-drop]") + } + fn resource_new_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_prefix("[resource-new]") + } + fn resource_rep_name<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_prefix("[resource-rep]") + } + fn module_to_interface( + &self, + module: &str, + resolve: &Resolve, + items: &IndexMap, + ) -> Result<(WorldKey, InterfaceId)> { + // First see if this is a bare name + let bare_name = WorldKey::Name(module.to_string()); + if let Some(WorldItem::Interface { id, .. }) = items.get(&bare_name) { + return Ok((bare_name, *id)); + } + + // ... and if this isn't a bare name then it's time to do some parsing + // related to interfaces, versions, and such. First up the `module` name + // is parsed as a normal component name from `wasmparser` to see if it's + // of the "interface kind". If it's not then that means the above match + // should have been a hit but it wasn't, so an error is returned. + let kebab_name = ComponentName::new(module, 0); + let name = match kebab_name.as_ref().map(|k| k.kind()) { + Ok(ComponentNameKind::Interface(name)) => name, + _ => bail!("module requires an import interface named `{module}`"), + }; + + // Prioritize an exact match based on versions, so try that first. + let pkgname = PackageName { + namespace: name.namespace().to_string(), + name: name.package().to_string(), + version: name.version(), + }; + if let Some(pkg) = resolve.package_names.get(&pkgname) { + if let Some(id) = resolve.packages[*pkg] + .interfaces + .get(name.interface().as_str()) + { + let key = WorldKey::Interface(*id); + if items.contains_key(&key) { + return Ok((key, *id)); + } + } + } + + // If an exact match wasn't found then instead search for the first + // match based on versions. This means that a core wasm import for + // "1.2.3" might end up matching an interface at "1.2.4", for example. + // (or "1.2.2", depending on what's available). + for (key, _) in items { + let id = match key { + WorldKey::Interface(id) => *id, + WorldKey::Name(_) => continue, + }; + // Make sure the interface names match + let interface = &resolve.interfaces[id]; + if interface.name.as_ref().unwrap() != name.interface().as_str() { + continue; + } + + // Make sure the package name (without version) matches + let pkg = &resolve.packages[interface.package.unwrap()]; + if pkg.name.namespace != pkgname.namespace || pkg.name.name != pkgname.name { + continue; + } + + let module_version = match name.version() { + Some(version) => version, + None => continue, + }; + let pkg_version = match &pkg.name.version { + Some(version) => version, + None => continue, + }; + + // Test if the two semver versions are compatible + let module_compat = PackageName::version_compat_track(&module_version); + let pkg_compat = PackageName::version_compat_track(pkg_version); + if module_compat == pkg_compat { + return Ok((key.clone(), id)); + } + } + + bail!("module requires an import interface named `{module}`") + } + fn strip_post_return<'a>(&self, s: &'a str) -> Option<&'a str> { + s.strip_prefix("cabi_post_") + } + fn match_wit_export<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option<(&'a WorldKey, Option, &'a Function)> { + let world = &resolve.worlds[world]; + for name in exports { + match &world.exports[name] { + WorldItem::Function(f) => { + if f.legacy_core_export_name(None) == export_name { + return Some((name, None, f)); + } + } + WorldItem::Interface { id, .. } => { + let string = resolve.name_world_key(name); + for (_, func) in resolve.interfaces[*id].functions.iter() { + if func.legacy_core_export_name(Some(&string)) == export_name { + return Some((name, Some(*id), func)); + } + } + } + + WorldItem::Type(_) => unreachable!(), + } + } + + None + } + + fn match_wit_resource_dtor<'a>( + &self, + export_name: &str, + resolve: &'a Resolve, + world: WorldId, + exports: &'a IndexSet, + ) -> Option { + let world = &resolve.worlds[world]; + for name in exports { + let id = match &world.exports[name] { + WorldItem::Interface { id, .. } => *id, + WorldItem::Function(_) => continue, + WorldItem::Type(_) => unreachable!(), + }; + let name = resolve.name_world_key(name); + let resource = match export_name + .strip_prefix(&name) + .and_then(|s| s.strip_prefix("#[dtor]")) + .and_then(|r| resolve.interfaces[id].types.get(r)) + { + Some(id) => *id, + None => continue, + }; + + match resolve.types[resource].kind { + TypeDefKind::Resource => {} + _ => continue, + } + + return Some(resource); + } + + None + } +} + /// This function validates the following: /// /// * The `bytes` represent a valid core WebAssembly module. @@ -972,21 +1248,6 @@ pub fn validate_adapter_module( Ok(ret) } -fn valid_resource_drop( - func_name: &str, - ty: &FuncType, - get_resource: impl Fn(&str) -> Option, -) -> Result> { - if let Some(resource_name) = func_name.strip_prefix(RESOURCE_DROP) { - if let Some(id) = get_resource(resource_name) { - let expected = FuncType::new([ValType::I32], []); - validate_func_sig(func_name, &expected, ty)?; - return Ok(Some(id)); - } - } - Ok(None) -} - fn resource_test_for_interface<'a>( resolve: &'a Resolve, id: InterfaceId, diff --git a/crates/wit-component/tests/components/cm32-names/component.wat b/crates/wit-component/tests/components/cm32-names/component.wat new file mode 100644 index 0000000000..7b513ab5b7 --- /dev/null +++ b/crates/wit-component/tests/components/cm32-names/component.wat @@ -0,0 +1,366 @@ +(component + (type (;0;) + (instance + (export (;0;) "r" (type (sub resource))) + (type (;1;) (own 0)) + (type (;2;) (func (param "s" string) (result 1))) + (export (;0;) "[constructor]r" (func (type 2))) + (type (;3;) (borrow 0)) + (type (;4;) (func (param "self" 3) (result string))) + (export (;1;) "[method]r.m" (func (type 4))) + (type (;5;) (func (param "in" 1) (result 1))) + (export (;2;) "frob" (func (type 5))) + ) + ) + (import "ns:pkg/i@0.2.1" (instance (;0;) (type 0))) + (type (;1;) + (instance + (export (;0;) "r" (type (sub resource))) + (type (;1;) (own 0)) + (type (;2;) (func (param "s" string) (result 1))) + (export (;0;) "[constructor]r" (func (type 2))) + (type (;3;) (borrow 0)) + (type (;4;) (func (param "self" 3) (result string))) + (export (;1;) "[method]r.m" (func (type 4))) + (type (;5;) (func (param "in" 1) (result 1))) + (export (;2;) "frob" (func (type 5))) + ) + ) + (import "j" (instance (;1;) (type 1))) + (type (;2;) (func (result string))) + (import "f" (func (;0;) (type 2))) + (core module (;0;) + (type (;0;) (func (param i32))) + (type (;1;) (func (param i32 i32) (result i32))) + (type (;2;) (func (param i32 i32))) + (type (;3;) (func (param i32) (result i32))) + (type (;4;) (func (result i32))) + (type (;5;) (func (param i32 i32 i32 i32) (result i32))) + (type (;6;) (func)) + (import "cm32p2" "f" (func (;0;) (type 0))) + (import "cm32p2|ns:pkg/i@0.2" "[constructor]r" (func (;1;) (type 1))) + (import "cm32p2|ns:pkg/i@0.2" "[method]r.m" (func (;2;) (type 2))) + (import "cm32p2|ns:pkg/i@0.2" "frob" (func (;3;) (type 3))) + (import "cm32p2|ns:pkg/i@0.2" "r_drop" (func (;4;) (type 0))) + (import "cm32p2|j" "[constructor]r" (func (;5;) (type 1))) + (import "cm32p2|j" "[method]r.m" (func (;6;) (type 2))) + (import "cm32p2|j" "frob" (func (;7;) (type 3))) + (import "cm32p2|j" "r_drop" (func (;8;) (type 0))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_drop" (func (;9;) (type 0))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_new" (func (;10;) (type 3))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_rep" (func (;11;) (type 3))) + (import "cm32p2|_ex_j" "r_drop" (func (;12;) (type 0))) + (import "cm32p2|_ex_j" "r_new" (func (;13;) (type 3))) + (import "cm32p2|_ex_j" "r_rep" (func (;14;) (type 3))) + (memory (;0;) 0) + (export "cm32p2_memory" (memory 0)) + (export "cm32p2||g" (func 15)) + (export "cm32p2||g_post" (func 16)) + (export "cm32p2|ns:pkg/i@0.2|[constructor]r" (func 17)) + (export "cm32p2|ns:pkg/i@0.2|[method]r.m" (func 18)) + (export "cm32p2|ns:pkg/i@0.2|frob" (func 19)) + (export "cm32p2|ns:pkg/i@0.2|r_dtor" (func 20)) + (export "cm32p2|j|[constructor]r" (func 21)) + (export "cm32p2|j|[method]r.m" (func 22)) + (export "cm32p2|j|frob" (func 23)) + (export "cm32p2|j|r_dtor" (func 24)) + (export "cm32p2_realloc" (func 25)) + (export "cm32p2_initialize" (func 26)) + (func (;15;) (type 4) (result i32) + unreachable + ) + (func (;16;) (type 0) (param i32) + unreachable + ) + (func (;17;) (type 1) (param i32 i32) (result i32) + unreachable + ) + (func (;18;) (type 3) (param i32) (result i32) + unreachable + ) + (func (;19;) (type 3) (param i32) (result i32) + unreachable + ) + (func (;20;) (type 0) (param i32) + unreachable + ) + (func (;21;) (type 1) (param i32 i32) (result i32) + unreachable + ) + (func (;22;) (type 3) (param i32) (result i32) + unreachable + ) + (func (;23;) (type 3) (param i32) (result i32) + unreachable + ) + (func (;24;) (type 0) (param i32) + unreachable + ) + (func (;25;) (type 5) (param i32 i32 i32 i32) (result i32) + unreachable + ) + (func (;26;) (type 6)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + (processed-by "my-fake-bindgen" "123.45") + ) + ) + (core module (;1;) + (type (;0;) (func (param i32))) + (type (;1;) (func (param i32 i32) (result i32))) + (type (;2;) (func (param i32 i32))) + (type (;3;) (func (param i32))) + (table (;0;) 7 7 funcref) + (export "0" (func $indirect-cm32p2-f)) + (export "1" (func $"indirect-cm32p2|ns:pkg/i@0.2-[constructor]r")) + (export "2" (func $"indirect-cm32p2|ns:pkg/i@0.2-[method]r.m")) + (export "3" (func $"indirect-cm32p2|j-[constructor]r")) + (export "4" (func $"indirect-cm32p2|j-[method]r.m")) + (export "5" (func $dtor-r)) + (export "6" (func $"#func6 dtor-r")) + (export "$imports" (table 0)) + (func $indirect-cm32p2-f (;0;) (type 0) (param i32) + local.get 0 + i32.const 0 + call_indirect (type 0) + ) + (func $"indirect-cm32p2|ns:pkg/i@0.2-[constructor]r" (;1;) (type 1) (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.const 1 + call_indirect (type 1) + ) + (func $"indirect-cm32p2|ns:pkg/i@0.2-[method]r.m" (;2;) (type 2) (param i32 i32) + local.get 0 + local.get 1 + i32.const 2 + call_indirect (type 2) + ) + (func $"indirect-cm32p2|j-[constructor]r" (;3;) (type 1) (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.const 3 + call_indirect (type 1) + ) + (func $"indirect-cm32p2|j-[method]r.m" (;4;) (type 2) (param i32 i32) + local.get 0 + local.get 1 + i32.const 4 + call_indirect (type 2) + ) + (func $dtor-r (;5;) (type 3) (param i32) + local.get 0 + i32.const 5 + call_indirect (type 3) + ) + (func $"#func6 dtor-r" (@name "dtor-r") (;6;) (type 3) (param i32) + local.get 0 + i32.const 6 + call_indirect (type 3) + ) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core module (;2;) + (type (;0;) (func (param i32))) + (type (;1;) (func (param i32 i32) (result i32))) + (type (;2;) (func (param i32 i32))) + (type (;3;) (func (param i32))) + (import "" "0" (func (;0;) (type 0))) + (import "" "1" (func (;1;) (type 1))) + (import "" "2" (func (;2;) (type 2))) + (import "" "3" (func (;3;) (type 1))) + (import "" "4" (func (;4;) (type 2))) + (import "" "5" (func (;5;) (type 3))) + (import "" "6" (func (;6;) (type 3))) + (import "" "$imports" (table (;0;) 7 7 funcref)) + (elem (;0;) (i32.const 0) func 0 1 2 3 4 5 6) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core instance (;0;) (instantiate 1)) + (alias core export 0 "5" (core func (;0;))) + (type (;3;) (resource (rep i32) (dtor (func 0)))) + (alias core export 0 "6" (core func (;1;))) + (type (;4;) (resource (rep i32) (dtor (func 1)))) + (alias core export 0 "0" (core func (;2;))) + (core instance (;1;) + (export "f" (func 2)) + ) + (alias core export 0 "1" (core func (;3;))) + (alias core export 0 "2" (core func (;4;))) + (alias export 0 "frob" (func (;1;))) + (core func (;5;) (canon lower (func 1))) + (alias export 0 "r" (type (;5;))) + (core func (;6;) (canon resource.drop 5)) + (core instance (;2;) + (export "[constructor]r" (func 3)) + (export "[method]r.m" (func 4)) + (export "frob" (func 5)) + (export "r_drop" (func 6)) + ) + (alias core export 0 "3" (core func (;7;))) + (alias core export 0 "4" (core func (;8;))) + (alias export 1 "frob" (func (;2;))) + (core func (;9;) (canon lower (func 2))) + (alias export 1 "r" (type (;6;))) + (core func (;10;) (canon resource.drop 6)) + (core instance (;3;) + (export "[constructor]r" (func 7)) + (export "[method]r.m" (func 8)) + (export "frob" (func 9)) + (export "r_drop" (func 10)) + ) + (core func (;11;) (canon resource.drop 3)) + (core func (;12;) (canon resource.new 3)) + (core func (;13;) (canon resource.rep 3)) + (core instance (;4;) + (export "r_drop" (func 11)) + (export "r_new" (func 12)) + (export "r_rep" (func 13)) + ) + (core func (;14;) (canon resource.drop 4)) + (core func (;15;) (canon resource.new 4)) + (core func (;16;) (canon resource.rep 4)) + (core instance (;5;) + (export "r_drop" (func 14)) + (export "r_new" (func 15)) + (export "r_rep" (func 16)) + ) + (core instance (;6;) (instantiate 0 + (with "cm32p2" (instance 1)) + (with "cm32p2|ns:pkg/i@0.2" (instance 2)) + (with "cm32p2|j" (instance 3)) + (with "cm32p2|_ex_ns:pkg/i@0.2" (instance 4)) + (with "cm32p2|_ex_j" (instance 5)) + ) + ) + (alias core export 6 "cm32p2_memory" (core memory (;0;))) + (alias core export 0 "$imports" (core table (;0;))) + (alias core export 6 "cm32p2_realloc" (core func (;17;))) + (core func (;18;) (canon lower (func 0) (memory 0) (realloc 17) string-encoding=utf8)) + (alias export 0 "[constructor]r" (func (;3;))) + (core func (;19;) (canon lower (func 3) (memory 0) string-encoding=utf8)) + (alias export 0 "[method]r.m" (func (;4;))) + (core func (;20;) (canon lower (func 4) (memory 0) (realloc 17) string-encoding=utf8)) + (alias export 1 "[constructor]r" (func (;5;))) + (core func (;21;) (canon lower (func 5) (memory 0) string-encoding=utf8)) + (alias export 1 "[method]r.m" (func (;6;))) + (core func (;22;) (canon lower (func 6) (memory 0) (realloc 17) string-encoding=utf8)) + (alias core export 6 "cm32p2|ns:pkg/i@0.2|r_dtor" (core func (;23;))) + (alias core export 6 "cm32p2|j|r_dtor" (core func (;24;))) + (core instance (;7;) + (export "$imports" (table 0)) + (export "0" (func 18)) + (export "1" (func 19)) + (export "2" (func 20)) + (export "3" (func 21)) + (export "4" (func 22)) + (export "5" (func 23)) + (export "6" (func 24)) + ) + (core instance (;8;) (instantiate 2 + (with "" (instance 7)) + ) + ) + (alias core export 6 "cm32p2_initialize" (core func (;25;))) + (core module (;3;) + (type (;0;) (func)) + (import "" "" (func (;0;) (type 0))) + (start 0) + ) + (core instance (;9;) + (export "" (func 25)) + ) + (core instance (;10;) (instantiate 3 + (with "" (instance 9)) + ) + ) + (alias core export 6 "cm32p2||g" (core func (;26;))) + (alias core export 6 "cm32p2||g_post" (core func (;27;))) + (func (;7;) (type 2) (canon lift (core func 26) (memory 0) string-encoding=utf8 (post-return 27))) + (export (;8;) "g" (func 7)) + (type (;7;) (own 3)) + (type (;8;) (func (param "s" string) (result 7))) + (alias core export 6 "cm32p2|ns:pkg/i@0.2|[constructor]r" (core func (;28;))) + (func (;9;) (type 8) (canon lift (core func 28) (memory 0) (realloc 17) string-encoding=utf8)) + (type (;9;) (borrow 3)) + (type (;10;) (func (param "self" 9) (result string))) + (alias core export 6 "cm32p2|ns:pkg/i@0.2|[method]r.m" (core func (;29;))) + (func (;10;) (type 10) (canon lift (core func 29) (memory 0) string-encoding=utf8)) + (type (;11;) (func (param "in" 7) (result 7))) + (alias core export 6 "cm32p2|ns:pkg/i@0.2|frob" (core func (;30;))) + (func (;11;) (type 11) (canon lift (core func 30))) + (component (;0;) + (import "import-type-r" (type (;0;) (sub resource))) + (type (;1;) (own 0)) + (type (;2;) (func (param "s" string) (result 1))) + (import "import-constructor-r" (func (;0;) (type 2))) + (type (;3;) (borrow 0)) + (type (;4;) (func (param "self" 3) (result string))) + (import "import-method-r-m" (func (;1;) (type 4))) + (type (;5;) (func (param "in" 1) (result 1))) + (import "import-func-frob" (func (;2;) (type 5))) + (export (;6;) "r" (type 0)) + (type (;7;) (own 6)) + (type (;8;) (func (param "s" string) (result 7))) + (export (;3;) "[constructor]r" (func 0) (func (type 8))) + (type (;9;) (borrow 6)) + (type (;10;) (func (param "self" 9) (result string))) + (export (;4;) "[method]r.m" (func 1) (func (type 10))) + (type (;11;) (func (param "in" 7) (result 7))) + (export (;5;) "frob" (func 2) (func (type 11))) + ) + (instance (;2;) (instantiate 0 + (with "import-constructor-r" (func 9)) + (with "import-method-r-m" (func 10)) + (with "import-func-frob" (func 11)) + (with "import-type-r" (type 3)) + ) + ) + (export (;3;) "ns:pkg/i@0.2.1" (instance 2)) + (type (;12;) (own 4)) + (type (;13;) (func (param "s" string) (result 12))) + (alias core export 6 "cm32p2|j|[constructor]r" (core func (;31;))) + (func (;12;) (type 13) (canon lift (core func 31) (memory 0) (realloc 17) string-encoding=utf8)) + (type (;14;) (borrow 4)) + (type (;15;) (func (param "self" 14) (result string))) + (alias core export 6 "cm32p2|j|[method]r.m" (core func (;32;))) + (func (;13;) (type 15) (canon lift (core func 32) (memory 0) string-encoding=utf8)) + (type (;16;) (func (param "in" 12) (result 12))) + (alias core export 6 "cm32p2|j|frob" (core func (;33;))) + (func (;14;) (type 16) (canon lift (core func 33))) + (component (;1;) + (import "import-type-r" (type (;0;) (sub resource))) + (type (;1;) (own 0)) + (type (;2;) (func (param "s" string) (result 1))) + (import "import-constructor-r" (func (;0;) (type 2))) + (type (;3;) (borrow 0)) + (type (;4;) (func (param "self" 3) (result string))) + (import "import-method-r-m" (func (;1;) (type 4))) + (type (;5;) (func (param "in" 1) (result 1))) + (import "import-func-frob" (func (;2;) (type 5))) + (export (;6;) "r" (type 0)) + (type (;7;) (own 6)) + (type (;8;) (func (param "s" string) (result 7))) + (export (;3;) "[constructor]r" (func 0) (func (type 8))) + (type (;9;) (borrow 6)) + (type (;10;) (func (param "self" 9) (result string))) + (export (;4;) "[method]r.m" (func 1) (func (type 10))) + (type (;11;) (func (param "in" 7) (result 7))) + (export (;5;) "frob" (func 2) (func (type 11))) + ) + (instance (;4;) (instantiate 1 + (with "import-constructor-r" (func 12)) + (with "import-method-r-m" (func 13)) + (with "import-func-frob" (func 14)) + (with "import-type-r" (type 4)) + ) + ) + (export (;5;) "j" (instance 4)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/components/cm32-names/component.wit.print b/crates/wit-component/tests/components/cm32-names/component.wit.print new file mode 100644 index 0000000000..a01bff8de2 --- /dev/null +++ b/crates/wit-component/tests/components/cm32-names/component.wit.print @@ -0,0 +1,25 @@ +package root:component; + +world root { + import ns:pkg/i@0.2.1; + import j: interface { + resource r { + constructor(s: string); + m: func() -> string; + } + + frob: func(in: r) -> r; + } + import f: func() -> string; + + export g: func() -> string; + export ns:pkg/i@0.2.1; + export j: interface { + resource r { + constructor(s: string); + m: func() -> string; + } + + frob: func(in: r) -> r; + } +} diff --git a/crates/wit-component/tests/components/cm32-names/module.wat b/crates/wit-component/tests/components/cm32-names/module.wat new file mode 100644 index 0000000000..6b1b47b5d4 --- /dev/null +++ b/crates/wit-component/tests/components/cm32-names/module.wat @@ -0,0 +1,32 @@ +(module + (import "cm32p2" "f" (func (param i32))) + (import "cm32p2|ns:pkg/i@0.2" "[constructor]r" (func (param i32 i32) (result i32))) + (import "cm32p2|ns:pkg/i@0.2" "[method]r.m" (func (param i32 i32))) + (import "cm32p2|ns:pkg/i@0.2" "frob" (func (param i32) (result i32))) + (import "cm32p2|ns:pkg/i@0.2" "r_drop" (func (param i32))) + (import "cm32p2|j" "[constructor]r" (func (param i32 i32) (result i32))) + (import "cm32p2|j" "[method]r.m" (func (param i32 i32))) + (import "cm32p2|j" "frob" (func (param i32) (result i32))) + (import "cm32p2|j" "r_drop" (func (param i32))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_drop" (func (param i32))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_new" (func (param i32) (result i32))) + (import "cm32p2|_ex_ns:pkg/i@0.2" "r_rep" (func (param i32) (result i32))) + (import "cm32p2|_ex_j" "r_drop" (func (param i32))) + (import "cm32p2|_ex_j" "r_new" (func (param i32) (result i32))) + (import "cm32p2|_ex_j" "r_rep" (func (param i32) (result i32))) + + (memory (export "cm32p2_memory") 0) + + (func (export "cm32p2||g") (result i32) unreachable) + (func (export "cm32p2||g_post") (param i32) unreachable) + (func (export "cm32p2|ns:pkg/i@0.2|[constructor]r") (param i32 i32) (result i32) unreachable) + (func (export "cm32p2|ns:pkg/i@0.2|[method]r.m") (param i32) (result i32) unreachable) + (func (export "cm32p2|ns:pkg/i@0.2|frob") (param i32) (result i32) unreachable) + (func (export "cm32p2|ns:pkg/i@0.2|r_dtor") (param i32) unreachable) + (func (export "cm32p2|j|[constructor]r") (param i32 i32) (result i32) unreachable) + (func (export "cm32p2|j|[method]r.m") (param i32) (result i32) unreachable) + (func (export "cm32p2|j|frob") (param i32) (result i32) unreachable) + (func (export "cm32p2|j|r_dtor") (param i32) unreachable) + (func (export "cm32p2_realloc") (param i32 i32 i32 i32) (result i32) unreachable) + (func (export "cm32p2_initialize")) +) diff --git a/crates/wit-component/tests/components/cm32-names/module.wit b/crates/wit-component/tests/components/cm32-names/module.wit new file mode 100644 index 0000000000..32d8820e8f --- /dev/null +++ b/crates/wit-component/tests/components/cm32-names/module.wit @@ -0,0 +1,30 @@ +package ns:pkg@0.2.1; + +interface i { + resource r { + constructor(s: string); + m: func() -> string; + } + frob: func(in: r) -> r; +} + +world module { + import f: func() -> string; + import i; + import j: interface { + resource r { + constructor(s: string); + m: func() -> string; + } + frob: func(in: r) -> r; + } + export g: func() -> string; + export i; + export j: interface { + resource r { + constructor(s: string); + m: func() -> string; + } + frob: func(in: r) -> r; + } +} diff --git a/crates/wit-component/tests/components/error-default-export-sig-mismatch/error.txt b/crates/wit-component/tests/components/error-default-export-sig-mismatch/error.txt index e223246a10..d8e5eafd76 100644 --- a/crates/wit-component/tests/components/error-default-export-sig-mismatch/error.txt +++ b/crates/wit-component/tests/components/error-default-export-sig-mismatch/error.txt @@ -1 +1 @@ -failed to decode world from module: module was not valid: failed to validate export for `a`: type mismatch for function `a`: expected `[I32, I32] -> [I32]` but found `[] -> []` \ No newline at end of file +failed to decode world from module: module was not valid: failed to classify export `a`: failed to validate export for `a`: type mismatch for function `a`: expected `[I32, I32] -> [I32]` but found `[] -> []` \ No newline at end of file diff --git a/crates/wit-component/tests/components/error-export-sig-mismatch/error.txt b/crates/wit-component/tests/components/error-export-sig-mismatch/error.txt index 548e42b291..da7e08eaf9 100644 --- a/crates/wit-component/tests/components/error-export-sig-mismatch/error.txt +++ b/crates/wit-component/tests/components/error-export-sig-mismatch/error.txt @@ -1 +1 @@ -failed to decode world from module: module was not valid: failed to validate export for `foo`: type mismatch for function `a`: expected `[I32, I32] -> [I32]` but found `[] -> []` \ No newline at end of file +failed to decode world from module: module was not valid: failed to classify export `foo#a`: failed to validate export for `foo`: type mismatch for function `a`: expected `[I32, I32] -> [I32]` but found `[] -> []` \ No newline at end of file diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index e85cb2101a..638dd6e96b 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -219,6 +219,23 @@ impl PackageName { } version } + + /// Returns the string corresponding to + /// [`PackageName::version_compat_track`]. This is done to match the + /// component model's expected naming scheme of imports and exports. + pub fn version_compat_track_string(version: &Version) -> String { + let version = Self::version_compat_track(version); + if !version.pre.is_empty() { + return version.to_string(); + } + if version.major != 0 { + return format!("{}", version.major); + } + if version.minor != 0 { + return format!("{}.{}", version.major, version.minor); + } + version.to_string() + } } impl fmt::Display for PackageName { @@ -850,6 +867,20 @@ impl FunctionKind { } } +/// Possible forms of name mangling that are supported by this crate. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Mangling { + /// The "standard" component model mangling format for 32-bit linear + /// memories. This is specified in WebAssembly/component-model#378 + Standard32, + + /// The "legacy" name mangling supported in versions 218-and-prior for this + /// crate. This is the original support for how components were created from + /// core wasm modules and this does not correspond to any standard. This is + /// preserved for now while tools transition to the new scheme. + Legacy, +} + impl Function { pub fn item_name(&self) -> &str { match &self.kind { @@ -873,10 +904,28 @@ impl Function { } /// Gets the core export name for this function. - pub fn core_export_name<'a>(&'a self, interface: Option<&str>) -> Cow<'a, str> { + pub fn standard32_core_export_name<'a>(&'a self, interface: Option<&str>) -> Cow<'a, str> { + self.core_export_name(interface, Mangling::Standard32) + } + + pub fn legacy_core_export_name<'a>(&'a self, interface: Option<&str>) -> Cow<'a, str> { + self.core_export_name(interface, Mangling::Legacy) + } + /// Gets the core export name for this function. + pub fn core_export_name<'a>( + &'a self, + interface: Option<&str>, + mangling: Mangling, + ) -> Cow<'a, str> { match interface { - Some(interface) => Cow::Owned(format!("{interface}#{}", self.name)), - None => Cow::Borrowed(&self.name), + Some(interface) => match mangling { + Mangling::Standard32 => Cow::Owned(format!("cm32p2|{interface}|{}", self.name)), + Mangling::Legacy => Cow::Owned(format!("{interface}#{}", self.name)), + }, + None => match mangling { + Mangling::Standard32 => Cow::Owned(format!("cm32p2||{}", self.name)), + Mangling::Legacy => Cow::Borrowed(&self.name), + }, } } } diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs index 5122d854a1..d5a62739c3 100644 --- a/crates/wit-parser/src/resolve.rs +++ b/crates/wit-parser/src/resolve.rs @@ -1012,6 +1012,15 @@ package {name} is defined in two different locations:\n\ Some(self.id_of_name(interface.package.unwrap(), interface.name.as_ref()?)) } + /// Returns the "canonicalized interface name" of `interface`. + /// + /// Returns `None` for unnamed interfaces. See `BuildTargets.md` in the + /// upstream component model repository for more information about this. + pub fn canonicalized_id_of(&self, interface: InterfaceId) -> Option { + let interface = &self.interfaces[interface]; + Some(self.canonicalized_id_of_name(interface.package.unwrap(), interface.name.as_ref()?)) + } + /// Convert a world to an "importized" version where the world is updated /// in-place to reflect what it would look like to be imported. /// @@ -1090,6 +1099,27 @@ package {name} is defined in two different locations:\n\ base } + /// Returns the "canonicalized interface name" of the specified `name` + /// within the `pkg`. + /// + /// See `BuildTargets.md` in the upstream component model repository for + /// more information about this. + pub fn canonicalized_id_of_name(&self, pkg: PackageId, name: &str) -> String { + let package = &self.packages[pkg]; + let mut base = String::new(); + base.push_str(&package.name.namespace); + base.push_str(":"); + base.push_str(&package.name.name); + base.push_str("/"); + base.push_str(name); + if let Some(version) = &package.name.version { + base.push_str("@"); + let string = PackageName::version_compat_track_string(version); + base.push_str(&string); + } + base + } + /// Attempts to locate a world given the "default" set of `packages` and the /// optional string specifier `world`. /// From 3368973bc963e21d594819e6f47794bba779abd6 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sat, 28 Sep 2024 07:52:37 -0700 Subject: [PATCH 5/8] Update `embed --dummy` with new ABI names This commit updates the `wasm-tools component embed` subcommand, specifically the `--dummy` flag. This flag now uses the new "standard32" names for the core module that is generated. Additionally a new `--dummy-names $FOO` option has been added to enable generating the old names as well as the new names. Utilities have also been added to `Resolve` for bindings generators to avoid hardcoding ABI names and instead use the add categories of imports/exports to name items. --- crates/wit-component/src/dummy.rs | 148 ++++++++--- crates/wit-component/src/encoding.rs | 3 +- crates/wit-component/src/semver_check.rs | 4 +- crates/wit-parser/src/lib.rs | 21 +- crates/wit-parser/src/resolve.rs | 237 +++++++++++++++++- fuzz/src/roundtrip_wit.rs | 9 +- src/bin/wasm-tools/component.rs | 28 ++- tests/cli/embed-dummy.wit | 13 + tests/cli/embed-dummy.wit.legacy.stdout | 21 ++ tests/cli/embed-dummy.wit.standard32.stdout | 21 ++ tests/cli/semver-check-add-export.wit.stderr | 2 +- .../cli/semver-check-remove-import.wit.stderr | 2 +- 12 files changed, 453 insertions(+), 56 deletions(-) create mode 100644 tests/cli/embed-dummy.wit create mode 100644 tests/cli/embed-dummy.wit.legacy.stdout create mode 100644 tests/cli/embed-dummy.wit.standard32.stdout diff --git a/crates/wit-component/src/dummy.rs b/crates/wit-component/src/dummy.rs index 50c217a8e9..78980e73d1 100644 --- a/crates/wit-component/src/dummy.rs +++ b/crates/wit-component/src/dummy.rs @@ -1,8 +1,11 @@ use wit_parser::abi::{AbiVariant, WasmType}; -use wit_parser::{Function, Resolve, TypeDefKind, TypeId, WorldId, WorldItem}; +use wit_parser::{ + Function, Mangling, Resolve, ResourceIntrinsic, TypeDefKind, TypeId, WasmExport, WasmImport, + WorldId, WorldItem, WorldKey, +}; /// Generate a dummy implementation core Wasm module for a given WIT document -pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec { +pub fn dummy_module(resolve: &Resolve, world: WorldId, mangling: Mangling) -> Vec { let world = &resolve.worlds[world]; let mut wat = String::new(); wat.push_str("(module\n"); @@ -11,27 +14,41 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec { WorldItem::Function(func) => { let sig = resolve.wasm_signature(AbiVariant::GuestImport, func); - wat.push_str(&format!("(import \"$root\" \"{}\" (func", func.name)); + let (module, name) = resolve.wasm_import_name( + mangling, + WasmImport::Func { + interface: None, + func, + }, + ); + + wat.push_str(&format!("(import {module:?} {name:?} (func")); push_tys(&mut wat, "param", &sig.params); push_tys(&mut wat, "result", &sig.results); wat.push_str("))\n"); } WorldItem::Interface { id: import, .. } => { - let name = resolve.name_world_key(name); for (_, func) in resolve.interfaces[*import].functions.iter() { let sig = resolve.wasm_signature(AbiVariant::GuestImport, func); - wat.push_str(&format!("(import \"{name}\" \"{}\" (func", func.name)); + let (module, name) = resolve.wasm_import_name( + mangling, + WasmImport::Func { + interface: Some(name), + func, + }, + ); + wat.push_str(&format!("(import {module:?} {name:?} (func")); push_tys(&mut wat, "param", &sig.params); push_tys(&mut wat, "result", &sig.results); wat.push_str("))\n"); } for (_, ty) in resolve.interfaces[*import].types.iter() { - push_resource_func_imports(&mut wat, resolve, &name, *ty); + push_resource_func_imports(&mut wat, resolve, Some(name), *ty, mangling); } } WorldItem::Type(id) => { - push_resource_func_imports(&mut wat, resolve, "$root", *id); + push_resource_func_imports(&mut wat, resolve, None, *id, mangling); } } } @@ -42,77 +59,136 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec { WorldItem::Interface { id, .. } => *id, _ => continue, }; - let module = format!("[export]{}", resolve.name_world_key(name)); - for (name, ty) in resolve.interfaces[export].types.iter() { - let ty = &resolve.types[*ty]; + for resource in resolve.interfaces[export].types.values().copied() { + let ty = &resolve.types[resource]; match ty.kind { TypeDefKind::Resource => {} _ => continue, } - wat.push_str(&format!( - "\ -(import \"{module}\" \"[resource-drop]{name}\" (func (param i32))) -(import \"{module}\" \"[resource-new]{name}\" (func (param i32) (result i32))) -(import \"{module}\" \"[resource-rep]{name}\" (func (param i32) (result i32))) - " - )); + let intrinsics = [ + (ResourceIntrinsic::ExportedDrop, "(func (param i32))"), + ( + ResourceIntrinsic::ExportedNew, + "(func (param i32) (result i32))", + ), + ( + ResourceIntrinsic::ExportedRep, + "(func (param i32) (result i32))", + ), + ]; + for (intrinsic, sig) in intrinsics { + let (module, name) = resolve.wasm_import_name( + mangling, + WasmImport::ResourceIntrinsic { + interface: Some(name), + resource, + intrinsic, + }, + ); + wat.push_str(&format!("(import {module:?} {name:?} {sig})\n")); + } } } for (name, export) in world.exports.iter() { match export { WorldItem::Function(func) => { - push_func(&mut wat, &func.name, resolve, func); + push_func_export(&mut wat, resolve, None, func, mangling); } WorldItem::Interface { id: export, .. } => { - let name = resolve.name_world_key(name); for (_, func) in resolve.interfaces[*export].functions.iter() { - let name = func.legacy_core_export_name(Some(&name)); - push_func(&mut wat, &name, resolve, func); + push_func_export(&mut wat, resolve, Some(name), func, mangling); } // Feign destructors for any resource that this interface // exports - for (resource_name, ty) in resolve.interfaces[*export].types.iter() { - let ty = &resolve.types[*ty]; + for resource in resolve.interfaces[*export].types.values().copied() { + let ty = &resolve.types[resource]; match ty.kind { TypeDefKind::Resource => {} _ => continue, } - wat.push_str(&format!( - "(func (export \"{name}#[dtor]{resource_name}\") (param i32))" - )); + let name = resolve.wasm_export_name( + mangling, + WasmExport::ResourceDtor { + interface: name, + resource, + }, + ); + wat.push_str(&format!("(func (export {name:?}) (param i32))")); } } WorldItem::Type(_) => {} } } - wat.push_str("(memory (export \"memory\") 0)\n"); - wat.push_str( - "(func (export \"cabi_realloc\") (param i32 i32 i32 i32) (result i32) unreachable)\n", - ); + let memory = resolve.wasm_export_name(mangling, WasmExport::Memory); + wat.push_str(&format!("(memory (export {memory:?}) 0)\n")); + let realloc = resolve.wasm_export_name(mangling, WasmExport::Realloc); + wat.push_str(&format!( + "(func (export {realloc:?}) (param i32 i32 i32 i32) (result i32) unreachable)\n" + )); + let initialize = resolve.wasm_export_name(mangling, WasmExport::Initialize); + wat.push_str(&format!("(func (export {initialize:?}))")); wat.push_str(")\n"); return wat::parse_str(&wat).unwrap(); - fn push_resource_func_imports(wat: &mut String, resolve: &Resolve, module: &str, ty: TypeId) { - let ty = &resolve.types[ty]; + fn push_resource_func_imports( + wat: &mut String, + resolve: &Resolve, + interface: Option<&WorldKey>, + resource: TypeId, + mangling: Mangling, + ) { + let ty = &resolve.types[resource]; match ty.kind { TypeDefKind::Resource => {} _ => return, } - let name = ty.name.as_ref().unwrap(); - wat.push_str(&format!("(import \"{module}\" \"[resource-drop]{name}\"")); - wat.push_str(" (func (param i32)))\n"); + let (module, name) = resolve.wasm_import_name( + mangling, + WasmImport::ResourceIntrinsic { + interface, + resource, + intrinsic: ResourceIntrinsic::ImportedDrop, + }, + ); + wat.push_str(&format!("(import {module:?} {name:?} (func (param i32)))")); } - fn push_func(wat: &mut String, name: &str, resolve: &Resolve, func: &Function) { + fn push_func_export( + wat: &mut String, + resolve: &Resolve, + interface: Option<&WorldKey>, + func: &Function, + mangling: Mangling, + ) { let sig = resolve.wasm_signature(AbiVariant::GuestExport, func); + let name = resolve.wasm_export_name( + mangling, + WasmExport::Func { + interface, + func, + post_return: false, + }, + ); wat.push_str(&format!("(func (export \"{name}\")")); push_tys(wat, "param", &sig.params); push_tys(wat, "result", &sig.results); wat.push_str(" unreachable)\n"); + + let name = resolve.wasm_export_name( + mangling, + WasmExport::Func { + interface, + func, + post_return: true, + }, + ); + wat.push_str(&format!("(func (export \"{name}\")")); + push_tys(wat, "param", &sig.results); + wat.push_str(")\n"); } fn push_tys(dst: &mut String, desc: &str, params: &[WasmType]) { diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 5843a7e338..faf0c0723f 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -2158,6 +2158,7 @@ impl ComponentWorld<'_> { mod test { use super::*; use crate::{dummy_module, embed_component_metadata}; + use wit_parser::Mangling; #[test] fn it_renames_imports() { @@ -2183,7 +2184,7 @@ world test { .unwrap(); let world = resolve.select_world(pkg, None).unwrap(); - let mut module = dummy_module(&resolve, world); + let mut module = dummy_module(&resolve, world, Mangling::Standard32); embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8).unwrap(); diff --git a/crates/wit-component/src/semver_check.rs b/crates/wit-component/src/semver_check.rs index d6f4c6c145..1c4a9f4f5e 100644 --- a/crates/wit-component/src/semver_check.rs +++ b/crates/wit-component/src/semver_check.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::{bail, Context, Result}; use wasm_encoder::{ComponentBuilder, ComponentExportKind, ComponentTypeRef}; use wasmparser::Validator; -use wit_parser::{Resolve, WorldId}; +use wit_parser::{Mangling, Resolve, WorldId}; /// Tests whether `new` is a semver-compatible upgrade from the world `prev`. /// @@ -63,7 +63,7 @@ pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result let mut root_component = ComponentBuilder::default(); // (1) above - create a dummy component which has the shape of `prev`. - let mut prev_as_module = dummy_module(&resolve, prev); + let mut prev_as_module = dummy_module(&resolve, prev, Mangling::Standard32); embed_component_metadata(&mut prev_as_module, &resolve, prev, StringEncoding::UTF8) .context("failed to embed component metadata")?; let prev_as_component = ComponentEncoder::default() diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index 638dd6e96b..906f6a7a13 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use id_arena::{Arena, Id}; use indexmap::IndexMap; use semver::Version; @@ -21,7 +21,7 @@ pub use ast::{parse_use_path, ParsedUsePath}; mod sizealign; pub use sizealign::*; mod resolve; -pub use resolve::{InvalidTransitiveDependency, Package, PackageId, Remap, Resolve}; +pub use resolve::*; mod live; pub use live::{LiveTypes, TypeIdVisitor}; @@ -881,6 +881,23 @@ pub enum Mangling { Legacy, } +impl std::str::FromStr for Mangling { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "legacy" => Ok(Mangling::Legacy), + "standard32" => Ok(Mangling::Standard32), + _ => { + bail!( + "unknown name mangling `{s}`, \ + supported values are `legacy` or `standard32`" + ) + } + } + } +} + impl Function { pub fn item_name(&self) -> &str { match &self.kind { diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs index d5a62739c3..02efb93867 100644 --- a/crates/wit-parser/src/resolve.rs +++ b/crates/wit-parser/src/resolve.rs @@ -18,9 +18,9 @@ use crate::ast::{parse_use_path, ParsedUsePath}; use crate::serde_::{serialize_arena, serialize_id_map}; use crate::{ AstItem, Docs, Error, Function, FunctionKind, Handle, IncludeName, Interface, InterfaceId, - InterfaceSpan, PackageName, Results, SourceMap, Stability, Type, TypeDef, TypeDefKind, TypeId, - TypeIdVisitor, TypeOwner, UnresolvedPackage, UnresolvedPackageGroup, World, WorldId, WorldItem, - WorldKey, WorldSpan, + InterfaceSpan, Mangling, PackageName, Results, SourceMap, Stability, Type, TypeDef, + TypeDefKind, TypeId, TypeIdVisitor, TypeOwner, UnresolvedPackage, UnresolvedPackageGroup, + World, WorldId, WorldItem, WorldKey, WorldSpan, }; mod clone; @@ -1308,6 +1308,17 @@ package {name} is defined in two different locations:\n\ } } + /// Same as [`Resolve::name_world_key`] except that `WorldKey::Interfaces` + /// uses [`Resolve::canonicalized_id_of`]. + pub fn name_canonicalized_world_key(&self, key: &WorldKey) -> String { + match key { + WorldKey::Name(s) => s.to_string(), + WorldKey::Interface(i) => self + .canonicalized_id_of(*i) + .expect("unexpected anonymous interface"), + } + } + /// Returns the interface that `id` uses a type from, if it uses a type from /// a different interface than `id` is defined within. /// @@ -2203,6 +2214,226 @@ package {name} is defined in two different locations:\n\ let replacement_id = self.interfaces[*replace_with].types[name]; self.types[ty].kind = TypeDefKind::Type(Type::Id(replacement_id)); } + + /// Returns the core wasm module/field names for the specified `import`. + /// + /// This function will return the core wasm module/field that can be used to + /// use `import` with the name `mangling` scheme specified as well. This can + /// be useful for bindings generators, for example, and these names are + /// recognized by `wit-component` and `wasm-tools component new`. + pub fn wasm_import_name(&self, mangling: Mangling, import: WasmImport<'_>) -> (String, String) { + match mangling { + Mangling::Standard32 => match import { + WasmImport::Func { interface, func } => { + let module = match interface { + Some(key) => format!("cm32p2|{}", self.name_canonicalized_world_key(key)), + None => format!("cm32p2"), + }; + (module, func.name.clone()) + } + WasmImport::ResourceIntrinsic { + interface, + resource, + intrinsic, + } => { + let name = self.types[resource].name.as_ref().unwrap(); + let (prefix, name) = match intrinsic { + ResourceIntrinsic::ImportedDrop => ("", format!("{name}_drop")), + ResourceIntrinsic::ExportedDrop => ("_ex_", format!("{name}_drop")), + ResourceIntrinsic::ExportedNew => ("_ex_", format!("{name}_new")), + ResourceIntrinsic::ExportedRep => ("_ex_", format!("{name}_rep")), + }; + let module = match interface { + Some(key) => { + format!("cm32p2|{prefix}{}", self.name_canonicalized_world_key(key)) + } + None => { + assert_eq!(prefix, ""); + format!("cm32p2") + } + }; + (module, name) + } + }, + Mangling::Legacy => match import { + WasmImport::Func { interface, func } => { + let module = match interface { + Some(key) => self.name_world_key(key), + None => format!("$root"), + }; + (module, func.name.clone()) + } + WasmImport::ResourceIntrinsic { + interface, + resource, + intrinsic, + } => { + let name = self.types[resource].name.as_ref().unwrap(); + let (prefix, name) = match intrinsic { + ResourceIntrinsic::ImportedDrop => ("", format!("[resource-drop]{name}")), + ResourceIntrinsic::ExportedDrop => { + ("[export]", format!("[resource-drop]{name}")) + } + ResourceIntrinsic::ExportedNew => { + ("[export]", format!("[resource-new]{name}")) + } + ResourceIntrinsic::ExportedRep => { + ("[export]", format!("[resource-rep]{name}")) + } + }; + let module = match interface { + Some(key) => format!("{prefix}{}", self.name_world_key(key)), + None => { + assert_eq!(prefix, ""); + format!("$root") + } + }; + (module, name) + } + }, + } + } + + /// Returns the core wasm export name for the specified `import`. + /// + /// This is the same as [`Resovle::wasm_import_name`], except for exports. + pub fn wasm_export_name(&self, mangling: Mangling, import: WasmExport<'_>) -> String { + match mangling { + Mangling::Standard32 => match import { + WasmExport::Func { + interface, + func, + post_return, + } => { + let mut name = String::from("cm32p2|"); + if let Some(interface) = interface { + let s = self.name_canonicalized_world_key(interface); + name.push_str(&s); + } + name.push_str("|"); + name.push_str(&func.name); + if post_return { + name.push_str("_post"); + } + name + } + WasmExport::ResourceDtor { + interface, + resource, + } => { + let name = self.types[resource].name.as_ref().unwrap(); + let interface = self.name_canonicalized_world_key(interface); + format!("cm32p2|{interface}|{name}_dtor") + } + WasmExport::Memory => "cm32p2_memory".to_string(), + WasmExport::Initialize => "cm32p2_initialize".to_string(), + WasmExport::Realloc => "cm32p2_realloc".to_string(), + }, + Mangling::Legacy => match import { + WasmExport::Func { + interface, + func, + post_return, + } => { + let mut name = String::new(); + if post_return { + name.push_str("cabi_post_"); + } + if let Some(interface) = interface { + let s = self.name_world_key(interface); + name.push_str(&s); + name.push_str("#"); + } + name.push_str(&func.name); + name + } + WasmExport::ResourceDtor { + interface, + resource, + } => { + let name = self.types[resource].name.as_ref().unwrap(); + let interface = self.name_world_key(interface); + format!("{interface}#[dtor]{name}") + } + WasmExport::Memory => "memory".to_string(), + WasmExport::Initialize => "_initialize".to_string(), + WasmExport::Realloc => "cabi_realloc".to_string(), + }, + } + } +} + +/// Possible imports that can be passed to [`Resolve::wasm_import_name`]. +#[derive(Debug)] +pub enum WasmImport<'a> { + /// A WIT function is being imported. Optionally from an interface. + Func { + /// The name of the interface that the function is being imported from. + /// + /// If the function is imported directly from the world then this is + /// `Noen`. + interface: Option<&'a WorldKey>, + + /// The function being imported. + func: &'a Function, + }, + + /// A resource-related intrinsic is being imported. + ResourceIntrinsic { + /// The optional interface to import from, same as `WasmImport::Func`. + interface: Option<&'a WorldKey>, + + /// The resource that's being operated on. + resource: TypeId, + + /// The intrinsic that's being imported. + intrinsic: ResourceIntrinsic, + }, +} + +/// Intrinsic definitions to go with [`WasmImport::ResourceIntrinsic`] which +/// also goes with [`Resolve::wasm_import_name`]. +#[derive(Debug)] +pub enum ResourceIntrinsic { + ImportedDrop, + ExportedDrop, + ExportedNew, + ExportedRep, +} + +/// Different kinds of exports that can be passed to +/// [`Resolve::wasm_export_name`] to export from core wasm modules. +#[derive(Debug)] +pub enum WasmExport<'a> { + /// A WIT function is being exported, optionally from an interface. + Func { + /// An optional interface which owns `func`. Use `None` for top-level + /// world function. + interface: Option<&'a WorldKey>, + + /// The function being exported. + func: &'a Function, + + /// Whether or not this is a post-return function or not. + post_return: bool, + }, + + /// A destructor for a resource exported from this module. + ResourceDtor { + /// The interface that owns the resource. + interface: &'a WorldKey, + /// The resource itself that the destructor is for. + resource: TypeId, + }, + + /// Linear memory, the one that the canonical ABI uses. + Memory, + + /// An initialization function (not the core wasm `start`). + Initialize, + + /// The general-purpose realloc hook. + Realloc, } /// Structure returned by [`Resolve::merge`] which contains mappings from diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index ed6961608e..f94d52dd2c 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -1,7 +1,7 @@ use arbitrary::{Result, Unstructured}; use std::path::Path; use wit_component::*; -use wit_parser::{PackageId, Resolve}; +use wit_parser::{Mangling, PackageId, Resolve}; pub fn run(u: &mut Unstructured<'_>) -> Result<()> { let wasm = u.arbitrary().and_then(|config| { @@ -36,7 +36,12 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { let mut decoded_bindgens = Vec::new(); for (id, world) in resolve.worlds.iter().take(20) { log::debug!("embedding world {} as in a dummy module", world.name); - let mut dummy = wit_component::dummy_module(&resolve, id); + let mangling = match u.int_in_range(0..=1)? { + 0 => Mangling::Legacy, + 1 => Mangling::Standard32, + _ => unreachable!(), + }; + let mut dummy = wit_component::dummy_module(&resolve, id, mangling); wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8) .unwrap(); write_file("dummy.wasm", &dummy); diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 9c95f14dc5..de0d290ef2 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -15,7 +15,7 @@ use wat::Detect; use wit_component::{ embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter, }; -use wit_parser::{PackageId, Resolve}; +use wit_parser::{Mangling, PackageId, Resolve}; /// WebAssembly wit-based component tooling. #[derive(Parser)] @@ -278,9 +278,20 @@ pub struct EmbedOpts { /// imports/exports and the right signatures for the component model. This /// can be useful to, perhaps, inspect a template module and what it looks /// like to work with an interface in the component model. - #[clap(long)] + /// + /// This option is equivalent to `--dummy-names standard32` + #[clap(long, conflicts_with = "dummy_names")] dummy: bool, + /// Same as `--dummy`, but the style of core wasm names is specified. + /// + /// This flag is the same as `--dummy` where if specified a core wasm module + /// is not read but is instead generated. The value of the option here is + /// the name mangling scheme to use for core wasm names generated. Current + /// options are `legacy|standard32`. + #[clap(long, conflicts_with = "dummy")] + dummy_names: Option, + /// Print the output in the WebAssembly text format instead of binary. #[clap(long, short = 't')] wat: bool, @@ -293,14 +304,15 @@ impl EmbedOpts { /// Executes the application. fn run(self) -> Result<()> { - let wasm = if self.dummy { - None - } else { - Some(self.io.parse_input_wasm()?) - }; let (resolve, pkg_id) = self.resolve.load()?; let world = resolve.select_world(pkg_id, self.world.as_deref())?; - let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world)); + let mut wasm = if self.dummy { + wit_component::dummy_module(&resolve, world, Mangling::Standard32) + } else if let Some(mangling) = self.dummy_names { + wit_component::dummy_module(&resolve, world, mangling) + } else { + self.io.parse_input_wasm()? + }; embed_component_metadata( &mut wasm, diff --git a/tests/cli/embed-dummy.wit b/tests/cli/embed-dummy.wit new file mode 100644 index 0000000000..ff256eff07 --- /dev/null +++ b/tests/cli/embed-dummy.wit @@ -0,0 +1,13 @@ +// RUN[legacy]: component embed -t --dummy-names legacy % | strip -t +// RUN[standard32]: component embed -t --dummy-names standard32 % | strip -t + +package a:b; + +interface x { + y: func() -> string; +} + +world w { + import x; + export x; +} diff --git a/tests/cli/embed-dummy.wit.legacy.stdout b/tests/cli/embed-dummy.wit.legacy.stdout new file mode 100644 index 0000000000..43af3e09eb --- /dev/null +++ b/tests/cli/embed-dummy.wit.legacy.stdout @@ -0,0 +1,21 @@ +(module + (type (;0;) (func (param i32))) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32 i32 i32 i32) (result i32))) + (type (;3;) (func)) + (import "a:b/x" "y" (func (;0;) (type 0))) + (memory (;0;) 0) + (export "a:b/x#y" (func 1)) + (export "cabi_post_a:b/x#y" (func 2)) + (export "memory" (memory 0)) + (export "cabi_realloc" (func 3)) + (export "_initialize" (func 4)) + (func (;1;) (type 1) (result i32) + unreachable + ) + (func (;2;) (type 0) (param i32)) + (func (;3;) (type 2) (param i32 i32 i32 i32) (result i32) + unreachable + ) + (func (;4;) (type 3)) +) diff --git a/tests/cli/embed-dummy.wit.standard32.stdout b/tests/cli/embed-dummy.wit.standard32.stdout new file mode 100644 index 0000000000..7b874d823a --- /dev/null +++ b/tests/cli/embed-dummy.wit.standard32.stdout @@ -0,0 +1,21 @@ +(module + (type (;0;) (func (param i32))) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32 i32 i32 i32) (result i32))) + (type (;3;) (func)) + (import "cm32p2|a:b/x" "y" (func (;0;) (type 0))) + (memory (;0;) 0) + (export "cm32p2|a:b/x|y" (func 1)) + (export "cm32p2|a:b/x|y_post" (func 2)) + (export "cm32p2_memory" (memory 0)) + (export "cm32p2_realloc" (func 3)) + (export "cm32p2_initialize" (func 4)) + (func (;1;) (type 1) (result i32) + unreachable + ) + (func (;2;) (type 0) (param i32)) + (func (;3;) (type 2) (param i32 i32 i32 i32) (result i32) + unreachable + ) + (func (;4;) (type 3)) +) diff --git a/tests/cli/semver-check-add-export.wit.stderr b/tests/cli/semver-check-add-export.wit.stderr index e7391d9617..76f4a07ff0 100644 --- a/tests/cli/semver-check-add-export.wit.stderr +++ b/tests/cli/semver-check-add-export.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing export named `a` (at offset 0xf0) + missing export named `a` (at offset 0x15a) diff --git a/tests/cli/semver-check-remove-import.wit.stderr b/tests/cli/semver-check-remove-import.wit.stderr index 339e755284..8e588913e1 100644 --- a/tests/cli/semver-check-remove-import.wit.stderr +++ b/tests/cli/semver-check-remove-import.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing import named `a` (at offset 0x119) + missing import named `a` (at offset 0x182) From ed037a55ed46388f1c962dfb82d4368f393dc188 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sat, 28 Sep 2024 08:19:45 -0700 Subject: [PATCH 6/8] Add a flag to require the new mangling scheme This commit adds a new `--reject-legacy-names` flag to the `wasm-tools component new` subcommand which can be used to disable support for the legacy naming scheme. This is intended to help with testing out the new naming scheme for tools and to help evaluate in the future if it's theoretically possible to remove support for the old naming scheme. --- crates/wit-component/src/encoding.rs | 22 ++++- crates/wit-component/src/encoding/world.rs | 25 +---- crates/wit-component/src/validation.rs | 95 +++++++------------ src/bin/wasm-tools/component.rs | 15 ++- tests/cli/reject-legacy-names.wit | 8 ++ tests/cli/reject-legacy-names.wit.stderr | 7 ++ tests/cli/semver-check-add-export.wit.stderr | 2 +- .../cli/semver-check-remove-import.wit.stderr | 2 +- 8 files changed, 89 insertions(+), 87 deletions(-) create mode 100644 tests/cli/reject-legacy-names.wit create mode 100644 tests/cli/reject-legacy-names.wit.stderr diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index faf0c0723f..51a45cffb1 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -1878,7 +1878,7 @@ pub struct LibraryInfo { } /// Represents an adapter or library to be instantiated as part of the component -struct Adapter { +pub(super) struct Adapter { /// The wasm of the module itself, with `component-type` sections stripped wasm: Vec, @@ -1900,13 +1900,14 @@ struct Adapter { #[derive(Default)] pub struct ComponentEncoder { module: Vec, - metadata: Bindgen, + pub(super) metadata: Bindgen, validate: bool, - main_module_exports: IndexSet, - adapters: IndexMap, + pub(super) main_module_exports: IndexSet, + pub(super) adapters: IndexMap, import_name_map: HashMap, realloc_via_memory_grow: bool, merge_imports_based_on_semver: Option, + pub(super) reject_legacy_names: bool, } impl ComponentEncoder { @@ -1959,6 +1960,19 @@ impl ComponentEncoder { self } + /// Sets whether to reject the historical mangling/name scheme for core wasm + /// imports/exports as they map to the component model. + /// + /// The `wit-component` crate supported a different set of names prior to + /// WebAssembly/component-model#378 and this can be used to disable this + /// support. + /// + /// This is disabled by default. + pub fn reject_legacy_names(mut self, reject: bool) -> Self { + self.reject_legacy_names = reject; + self + } + /// Specifies a new adapter which is used to translate from a historical /// wasm ABI to the canonical ABI and the `interface` provided. /// diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index 70f1a89415..acf126a174 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -63,18 +63,7 @@ pub enum Lowering { impl<'a> ComponentWorld<'a> { pub fn new(encoder: &'a ComponentEncoder) -> Result { - let adapters = encoder - .adapters - .keys() - .map(|s| s.as_str()) - .collect::>(); - let info = validate_module( - &encoder.module, - &encoder.metadata, - &encoder.main_module_exports, - &adapters, - ) - .context("module was not valid")?; + let info = validate_module(encoder, &encoder.module).context("module was not valid")?; let mut ret = ComponentWorld { encoder, @@ -85,7 +74,7 @@ impl<'a> ComponentWorld<'a> { exports_used: HashMap::new(), }; - ret.process_adapters(&adapters)?; + ret.process_adapters()?; ret.process_imports()?; ret.process_exports_used(); ret.process_live_type_imports(); @@ -97,7 +86,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, adapters: &IndexSet<&str>) -> Result<()> { + fn process_adapters(&mut self) -> Result<()> { let resolve = &self.encoder.metadata.resolve; let world = self.encoder.metadata.world; for ( @@ -156,13 +145,11 @@ impl<'a> ComponentWorld<'a> { // imports may have deleted some imports which means that the // final component may not need to import as many interfaces. let info = validate_adapter_module( + self.encoder, &wasm, - resolve, - world, &required_by_import, required_exports, library_info.as_ref(), - adapters, ) .with_context(|| { format!("failed to validate the imports of the adapter module `{name}`") @@ -189,13 +176,11 @@ impl<'a> ComponentWorld<'a> { ) }; let info = validate_adapter_module( + self.encoder, &wasm, - resolve, - world, &required_by_import, required_exports, library_info.as_ref(), - adapters, ) .with_context(|| { format!("failed to validate the imports of the minimized adapter module `{name}`") diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index 63a1224752..4b4d7876fa 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -1,5 +1,5 @@ use crate::encoding::{Instance, Item, LibraryInfo, MainOrAdapter}; -use crate::metadata::Bindgen; +use crate::ComponentEncoder; use anyhow::{bail, Context, Result}; use indexmap::{map::Entry, IndexMap, IndexSet}; use std::mem; @@ -50,11 +50,9 @@ pub struct ValidatedModule { impl ValidatedModule { fn new( + encoder: &ComponentEncoder, bytes: &[u8], - resolve: &Resolve, - world: WorldId, exports: &IndexSet, - adapters: &IndexSet<&str>, info: Option<&LibraryInfo>, ) -> Result { let mut validator = Validator::new(); @@ -75,21 +73,20 @@ impl ValidatedModule { Payload::ImportSection(s) => { for import in s { let import = import?; - ret.imports - .add(import, resolve, world, adapters, info, types)?; + ret.imports.add(import, encoder, info, types)?; } } Payload::ExportSection(s) => { for export in s { let export = export?; - ret.exports.add(export, resolve, world, &exports, types)?; + ret.exports.add(export, encoder, &exports, types)?; } } _ => continue, } } - ret.exports.validate(resolve, world, exports)?; + ret.exports.validate(encoder, exports)?; Ok(ret) } @@ -255,32 +252,26 @@ impl ImportMap { fn add( &mut self, import: wasmparser::Import<'_>, - resolve: &Resolve, - world: WorldId, - adapters: &IndexSet<&str>, + encoder: &ComponentEncoder, library_info: Option<&LibraryInfo>, types: TypesRef<'_>, ) -> Result<()> { if self.classify_import_with_library(import, library_info)? { return Ok(()); } - let item = self - .classify(import, resolve, world, adapters, types) - .with_context(|| { - format!( - "failed to resolve import `{}::{}`", - import.module, import.name, - ) - })?; + let item = self.classify(import, encoder, types).with_context(|| { + format!( + "failed to resolve import `{}::{}`", + import.module, import.name, + ) + })?; self.insert_import(import, item) } fn classify( &self, import: wasmparser::Import<'_>, - resolve: &Resolve, - world_id: WorldId, - adapters: &IndexSet<&str>, + encoder: &ComponentEncoder, types: TypesRef<'_>, ) -> Result { // Special-case the main module's memory imported into adapters which @@ -311,15 +302,16 @@ impl ImportMap { // Handle main module imports that match known adapters and set it up as // an import of an adapter export. - if adapters.contains(import.module) { + if encoder.adapters.contains_key(import.module) { return Ok(Import::AdapterExport(ty.clone())); } let (module, names) = match import.module.strip_prefix("cm32p2") { Some(suffix) => (suffix, STANDARD), + None if encoder.reject_legacy_names => (import.module, STANDARD), None => (import.module, LEGACY), }; - self.classify_component_model_import(module, import.name, resolve, world_id, ty, names) + self.classify_component_model_import(module, import.name, encoder, ty, names) } /// Attempts to classify the import `{module}::{name}` with the rules @@ -328,11 +320,12 @@ impl ImportMap { &self, module: &str, name: &str, - resolve: &Resolve, - world_id: WorldId, + encoder: &ComponentEncoder, ty: &FuncType, names: &dyn NameMangling, ) -> Result { + let resolve = &encoder.metadata.resolve; + let world_id = encoder.metadata.world; let world = &resolve.worlds[world_id]; if module == names.import_root() { @@ -408,7 +401,7 @@ impl ImportMap { } bail!( "import interface `{module}` is missing function \ - `{name}` that is required by the module", + `{name}` that is required by the module", ) } @@ -533,12 +526,11 @@ impl ExportMap { fn add( &mut self, export: wasmparser::Export<'_>, - resolve: &Resolve, - world: WorldId, + encoder: &ComponentEncoder, exports: &IndexSet, types: TypesRef<'_>, ) -> Result<()> { - if let Some(item) = self.classify(export, resolve, world, exports, types)? { + if let Some(item) = self.classify(export, encoder, exports, types)? { log::debug!("classifying export `{}` as {item:?}", export.name); let prev = self.names.insert(export.name.to_string(), item); assert!(prev.is_none()); @@ -549,8 +541,7 @@ impl ExportMap { fn classify( &mut self, export: wasmparser::Export<'_>, - resolve: &Resolve, - world: WorldId, + encoder: &ComponentEncoder, exports: &IndexSet, types: TypesRef<'_>, ) -> Result> { @@ -559,7 +550,6 @@ impl ExportMap { let ty = types[types.core_function_at(export.index)].unwrap_func(); self.raw_exports.insert(export.name.to_string(), ty.clone()); } - ExternalKind::Memory => return Ok(Some(Export::Memory)), _ => return Ok(None), } @@ -576,10 +566,11 @@ impl ExportMap { let (name, names) = match export.name.strip_prefix("cm32p2") { Some(name) => (name, STANDARD), + None if encoder.reject_legacy_names => return Ok(None), None => (export.name, LEGACY), }; if let Some(export) = self - .classify_component_export(names, name, &export, resolve, world, exports, types) + .classify_component_export(names, name, &export, encoder, exports, types) .with_context(|| format!("failed to classify export `{}`", export.name))? { return Ok(Some(export)); @@ -593,11 +584,12 @@ impl ExportMap { names: &dyn NameMangling, name: &str, export: &wasmparser::Export<'_>, - resolve: &Resolve, - world: WorldId, + encoder: &ComponentEncoder, exports: &IndexSet, types: TypesRef<'_>, ) -> Result> { + let resolve = &encoder.metadata.resolve; + let world = encoder.metadata.world; match export.kind { ExternalKind::Func => {} ExternalKind::Memory => { @@ -748,12 +740,9 @@ impl ExportMap { self.names.iter().map(|(n, e)| (n.as_str(), e)) } - fn validate( - &self, - resolve: &Resolve, - world: WorldId, - exports: &IndexSet, - ) -> Result<()> { + fn validate(&self, encoder: &ComponentEncoder, exports: &IndexSet) -> Result<()> { + let resolve = &encoder.metadata.resolve; + let world = encoder.metadata.world; // Multi-memory isn't supported because otherwise we don't know what // memory to put things in. if self @@ -1193,20 +1182,8 @@ impl NameMangling for Legacy { /// The `ValidatedModule` return value contains the metadata which describes the /// input module on success. This is then further used to generate a component /// for this module. -pub fn validate_module( - bytes: &[u8], - metadata: &Bindgen, - exports: &IndexSet, - adapters: &IndexSet<&str>, -) -> Result { - ValidatedModule::new( - bytes, - &metadata.resolve, - metadata.world, - exports, - adapters, - None, - ) +pub fn validate_module(encoder: &ComponentEncoder, bytes: &[u8]) -> Result { + ValidatedModule::new(encoder, bytes, &encoder.main_module_exports, None) } /// This function will validate the `bytes` provided as a wasm adapter module. @@ -1227,15 +1204,13 @@ pub fn validate_module( /// allowing the module to import tables and globals, as well as import /// functions at the world level, not just at the interface level. pub fn validate_adapter_module( + encoder: &ComponentEncoder, bytes: &[u8], - resolve: &Resolve, - world: WorldId, required_by_import: &IndexMap, exports: &IndexSet, library_info: Option<&LibraryInfo>, - adapters: &IndexSet<&str>, ) -> Result { - let ret = ValidatedModule::new(bytes, resolve, world, exports, adapters, library_info)?; + let ret = ValidatedModule::new(encoder, bytes, exports, library_info)?; for (name, required_ty) in required_by_import { let actual = match ret.exports.raw_exports.get(name) { diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index de0d290ef2..9c38da4aac 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -148,6 +148,17 @@ pub struct NewOpts { /// This is enabled by default. #[clap(long, value_name = "MERGE")] merge_imports_based_on_semver: Option, + + /// Reject usage of the "legacy" naming scheme of `wit-component` and + /// require the new naming scheme to be used. + /// + /// This flag can be used to ignore core module imports/exports that don't + /// conform to WebAssembly/component-model#378. This turns off + /// compatibility `wit-component`'s historical naming scheme. This is + /// intended to be used to test if a tool is compatible with a hypothetical + /// removal of the old scheme in the future. + #[clap(long)] + reject_legacy_names: bool, } impl NewOpts { @@ -158,7 +169,9 @@ impl NewOpts { /// Executes the application. fn run(self) -> Result<()> { let wasm = self.io.parse_input_wasm()?; - let mut encoder = ComponentEncoder::default().validate(!self.skip_validation); + let mut encoder = ComponentEncoder::default() + .validate(!self.skip_validation) + .reject_legacy_names(self.reject_legacy_names); if let Some(merge) = self.merge_imports_based_on_semver { encoder = encoder.merge_imports_based_on_semver(merge); diff --git a/tests/cli/reject-legacy-names.wit b/tests/cli/reject-legacy-names.wit new file mode 100644 index 0000000000..2cc55e5183 --- /dev/null +++ b/tests/cli/reject-legacy-names.wit @@ -0,0 +1,8 @@ +// FAIL: component embed % --dummy-names legacy | component new --reject-legacy-names + +package a:b; + +world foo { + export f: func(); + import y: func(); +} diff --git a/tests/cli/reject-legacy-names.wit.stderr b/tests/cli/reject-legacy-names.wit.stderr new file mode 100644 index 0000000000..f4b37ce1f3 --- /dev/null +++ b/tests/cli/reject-legacy-names.wit.stderr @@ -0,0 +1,7 @@ +error: failed to encode a component from module + +Caused by: + 0: failed to decode world from module + 1: module was not valid + 2: failed to resolve import `$root::y` + 3: unknown or invalid component model import syntax diff --git a/tests/cli/semver-check-add-export.wit.stderr b/tests/cli/semver-check-add-export.wit.stderr index 76f4a07ff0..120a23cc26 100644 --- a/tests/cli/semver-check-add-export.wit.stderr +++ b/tests/cli/semver-check-add-export.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing export named `a` (at offset 0x15a) + missing export named `a` (at offset 0x148) diff --git a/tests/cli/semver-check-remove-import.wit.stderr b/tests/cli/semver-check-remove-import.wit.stderr index 8e588913e1..34ac5e2a50 100644 --- a/tests/cli/semver-check-remove-import.wit.stderr +++ b/tests/cli/semver-check-remove-import.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing import named `a` (at offset 0x182) + missing import named `a` (at offset 0x170) From d614725d82f50ba21aaf826acff22807e4801f39 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sat, 28 Sep 2024 08:33:15 -0700 Subject: [PATCH 7/8] Fix tests --- crates/wit-component/src/validation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index 4b4d7876fa..9e170b99fa 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -550,7 +550,7 @@ impl ExportMap { let ty = types[types.core_function_at(export.index)].unwrap_func(); self.raw_exports.insert(export.name.to_string(), ty.clone()); } - _ => return Ok(None), + _ => {} } // Handle a few special-cased names first. From da3e9730810c2e8782eb30db9a450aaa5fce881b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 30 Sep 2024 08:11:49 -0700 Subject: [PATCH 8/8] Update some test expectations --- tests/cli/semver-check-add-export.wit.stderr | 2 +- tests/cli/semver-check-remove-import.wit.stderr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli/semver-check-add-export.wit.stderr b/tests/cli/semver-check-add-export.wit.stderr index 120a23cc26..76f4a07ff0 100644 --- a/tests/cli/semver-check-add-export.wit.stderr +++ b/tests/cli/semver-check-add-export.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing export named `a` (at offset 0x148) + missing export named `a` (at offset 0x15a) diff --git a/tests/cli/semver-check-remove-import.wit.stderr b/tests/cli/semver-check-remove-import.wit.stderr index 34ac5e2a50..8e588913e1 100644 --- a/tests/cli/semver-check-remove-import.wit.stderr +++ b/tests/cli/semver-check-remove-import.wit.stderr @@ -2,4 +2,4 @@ error: new world is not semver-compatible with the previous world Caused by: 0: type mismatch for import `new` - missing import named `a` (at offset 0x170) + missing import named `a` (at offset 0x182)