Skip to content

Commit 9e19a7d

Browse files
authored
Allow adapters to import multiple interfaces (#398)
This commit lifts the restriction that an adapter module as part of `wit-component` can only import a single interface. That restriction was a temporary artifact of the initial implementation due to the fact that at the time there was no convenient way to communicate about multiple imported interfaces. With the `ComponentInterfaces` encoding of a "world" now, though, the `wit-component` CLI is updated to require that the incoming adapter module has a `component-type*` custom section describing the world that it's importing.
1 parent 5ab5480 commit 9e19a7d

9 files changed

Lines changed: 203 additions & 122 deletions

File tree

crates/wit-component/src/cli.rs

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
//! The WebAssembly component tool command line interface.
22
3-
use crate::extract::{extract_module_interfaces, ModuleInterfaces};
4-
use crate::{
5-
decode_component_interfaces, ComponentEncoder, ComponentInterfaces, InterfacePrinter,
6-
StringEncoding,
7-
};
3+
use crate::{decode_component_interfaces, ComponentEncoder, InterfacePrinter, StringEncoding};
84
use anyhow::{anyhow, bail, Context, Result};
95
use clap::Parser;
106
use std::path::{Path, PathBuf};
@@ -48,38 +44,10 @@ fn parse_interface(name: Option<String>, path: &Path) -> Result<Interface> {
4844
Ok(interface)
4945
}
5046

51-
fn parse_adapter(s: &str) -> Result<(String, Vec<u8>, Interface)> {
52-
let mut parts = s.splitn(2, ':');
53-
let maybe_named_module = parts.next().unwrap();
54-
let (name, path) = parse_optionally_name_file(maybe_named_module);
47+
fn parse_adapter(s: &str) -> Result<(String, Vec<u8>)> {
48+
let (name, path) = parse_optionally_name_file(s);
5549
let wasm = wat::parse_file(path)?;
56-
57-
match parts.next() {
58-
Some(maybe_named_interface) => {
59-
let interface = parse_named_interface(maybe_named_interface)?;
60-
Ok((name.to_string(), wasm, interface))
61-
}
62-
None => {
63-
let ModuleInterfaces {
64-
wasm,
65-
interfaces:
66-
ComponentInterfaces {
67-
imports,
68-
exports,
69-
default,
70-
},
71-
} = extract_module_interfaces(&wasm)?;
72-
if !exports.is_empty() || default.is_some() {
73-
bail!("adapter modules cannot have an exported interface");
74-
}
75-
let import = match imports.len() {
76-
0 => Interface::default(),
77-
1 => imports.into_iter().next().unwrap().1,
78-
_ => bail!("adapter modules can only import one interface at this time"),
79-
};
80-
Ok((name.to_string(), wasm, import))
81-
}
82-
}
50+
Ok((name.to_string(), wasm))
8351
}
8452

8553
/// WebAssembly component encoder.
@@ -107,8 +75,8 @@ pub struct WitComponentApp {
10775
/// The second part of this argument, optionally specified, is the interface
10876
/// that this adapter module imports. If not specified then the interface
10977
/// imported is inferred from the adapter module itself.
110-
#[clap(long = "adapt", value_name = "[NAME=]MODULE[:[NAME=]INTERFACE]", value_parser = parse_adapter)]
111-
pub adapters: Vec<(String, Vec<u8>, Interface)>,
78+
#[clap(long = "adapt", value_name = "[NAME=]MODULE", value_parser = parse_adapter)]
79+
pub adapters: Vec<(String, Vec<u8>)>,
11280

11381
/// The path of the output WebAssembly component.
11482
#[clap(long, short = 'o', value_name = "OUTPUT")]
@@ -157,8 +125,8 @@ impl WitComponentApp {
157125
.exports(self.exports)?
158126
.validate(!self.skip_validation);
159127

160-
for (name, wasm, interface) in self.adapters.iter() {
161-
encoder = encoder.adapter(name, wasm, interface);
128+
for (name, wasm) in self.adapters.iter() {
129+
encoder = encoder.adapter(name, wasm)?;
162130
}
163131

164132
if let Some(interface) = self.interface {

crates/wit-component/src/encoding.rs

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ impl<'a> EncodingState<'a> {
13861386
// interface imported into the shim module itself.
13871387
for (adapter, funcs) in info.adapters_required.iter() {
13881388
let info = &imports.adapters[adapter];
1389-
if let Some(name) = info.required_import {
1389+
for (name, _) in info.required_imports.iter() {
13901390
let import = &imports.map[name];
13911391
ret.append_indirect(
13921392
name,
@@ -1654,7 +1654,7 @@ impl<'a> EncodingState<'a> {
16541654
// different from the imported interface. That's true enough for now
16551655
// since it's `env::memory`.
16561656
if let Some((module, name)) = &info.needs_memory {
1657-
if let Some(import_name) = info.required_import {
1657+
for (import_name, _) in info.required_imports.iter() {
16581658
assert!(module != import_name);
16591659
}
16601660
assert!(module != name);
@@ -1666,7 +1666,7 @@ impl<'a> EncodingState<'a> {
16661666
)]);
16671667
args.push((module.as_str(), ModuleArg::Instance(instance)));
16681668
}
1669-
if let Some(import_name) = info.required_import {
1669+
for (import_name, _) in info.required_imports.iter() {
16701670
let instance = self.import_instance_to_lowered_core_instance(
16711671
CustomModule::Adapter(name),
16721672
import_name,
@@ -2094,7 +2094,13 @@ pub struct ComponentEncoder {
20942094
exports: IndexMap<String, Interface>,
20952095
validate: bool,
20962096
types_only: bool,
2097-
adapters: IndexMap<String, (Vec<u8>, Interface)>,
2097+
2098+
// This is a map from the name of the adapter to a pair of:
2099+
//
2100+
// * the wasm of the adapter itself, with `component-type` sections stripped
2101+
// * the map of imported interfaces into the adapter, keyed by name of the
2102+
// interface
2103+
adapters: IndexMap<String, (Vec<u8>, IndexMap<String, Interface>)>,
20982104
}
20992105

21002106
impl ComponentEncoder {
@@ -2181,10 +2187,21 @@ impl ComponentEncoder {
21812187
/// wasm module specified by `bytes` imports. The `bytes` will then import
21822188
/// `interface` and export functions to get imported from the module `name`
21832189
/// in the core wasm that's being wrapped.
2184-
pub fn adapter(mut self, name: &str, bytes: &[u8], interface: &Interface) -> Self {
2185-
self.adapters
2186-
.insert(name.to_string(), (bytes.to_vec(), interface.clone()));
2187-
self
2190+
pub fn adapter(mut self, name: &str, bytes: &[u8]) -> Result<Self> {
2191+
let ModuleInterfaces {
2192+
wasm,
2193+
interfaces:
2194+
ComponentInterfaces {
2195+
imports,
2196+
exports,
2197+
default,
2198+
},
2199+
} = extract_module_interfaces(bytes)?;
2200+
if !exports.is_empty() || default.is_some() {
2201+
bail!("adapters cannot have any exported interfaces");
2202+
}
2203+
self.adapters.insert(name.to_string(), (wasm, imports));
2204+
Ok(self)
21882205
}
21892206

21902207
/// This is a convenience method for [`ComponentEncoder::adapter`] for
@@ -2194,31 +2211,13 @@ impl ComponentEncoder {
21942211
/// embedded information about its imported interfaces. Additionally the
21952212
/// name of the adapter is inferred from the file name itself as the file
21962213
/// stem.
2197-
pub fn adapter_file(mut self, path: &Path) -> Result<Self> {
2214+
pub fn adapter_file(self, path: &Path) -> Result<Self> {
21982215
let name = path
21992216
.file_stem()
22002217
.and_then(|s| s.to_str())
22012218
.ok_or_else(|| anyhow!("input filename was not valid utf-8"))?;
22022219
let wasm = wat::parse_file(path)?;
2203-
let ModuleInterfaces {
2204-
wasm,
2205-
interfaces:
2206-
ComponentInterfaces {
2207-
imports,
2208-
exports,
2209-
default,
2210-
},
2211-
} = extract_module_interfaces(&wasm)?;
2212-
if !exports.is_empty() || default.is_some() {
2213-
bail!("adapter modules cannot have an exported interface");
2214-
}
2215-
let import = match imports.len() {
2216-
0 => Interface::default(),
2217-
1 => imports.into_iter().next().unwrap().1,
2218-
_ => bail!("adapter modules can only import one interface at this time"),
2219-
};
2220-
self.adapters.insert(name.to_string(), (wasm, import));
2221-
Ok(self)
2220+
self.adapter(name, &wasm)
22222221
}
22232222

22242223
/// Indicates whether this encoder is only encoding types and does not
@@ -2308,17 +2307,16 @@ impl ComponentEncoder {
23082307
// provided to this encoder, gc it to an appropriate size, and then
23092308
// register its metadata in our data structures.
23102309
for (name, required) in info.adapters_required.iter() {
2311-
let (wasm, interface) = &self.adapters[*name];
2310+
let (wasm, adapter_imports) = &self.adapters[*name];
23122311
let wasm = crate::gc::run(wasm, required)
23132312
.context("failed to reduce input adapter module to its minimal size")?;
2314-
let info = validate_adapter_module(&wasm, interface, required)
2313+
let info = validate_adapter_module(&wasm, adapter_imports, required)
23152314
.context("failed to validate the imports of the minimized adapter module")?;
23162315
state.encode_core_adapter_module(name, &wasm);
2317-
types.encode_instance_import(
2318-
interface,
2319-
Some(&info.required_funcs),
2320-
&mut imports,
2321-
)?;
2316+
for (name, required) in info.required_imports.iter() {
2317+
let interface = &adapter_imports[*name];
2318+
types.encode_instance_import(interface, Some(required), &mut imports)?;
2319+
}
23222320
imports.adapters.insert(name, info);
23232321
}
23242322

crates/wit-component/src/validation.rs

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,10 @@ pub fn validate_module<'a>(
189189
/// This is created by the `validate_adapter_module` function.
190190
#[derive(Default, Debug)]
191191
pub struct ValidatedAdapter<'a> {
192-
/// If specified then this is the name of the required interface imported
193-
/// into the adapter module.
194-
///
195-
/// At this time only one interface import is supported. If this is `None`
196-
/// then the adapter module didn't import any component model functions to
197-
/// implement the required functionality.
198-
pub required_import: Option<&'a str>,
199-
200-
/// This is the set of required functions imported from `required_import`,
201-
/// if `required_import` is specified.
202-
pub required_funcs: IndexSet<&'a str>,
192+
/// If specified this is the list of required imports from the original set
193+
/// of possible imports along with the set of functions required from each
194+
/// imported interface.
195+
pub required_imports: IndexMap<&'a str, IndexSet<&'a str>>,
203196

204197
/// This is the module and field name of the memory import, if one is
205198
/// specified.
@@ -227,7 +220,7 @@ pub struct ValidatedAdapter<'a> {
227220
/// didn't accidentally break the wasm module.
228221
pub fn validate_adapter_module<'a>(
229222
bytes: &[u8],
230-
interface: &'a Interface,
223+
imports: &'a IndexMap<String, Interface>,
231224
required: &IndexMap<&str, FuncType>,
232225
) -> Result<ValidatedAdapter<'a>> {
233226
let mut validator = Validator::new();
@@ -308,21 +301,14 @@ pub fn validate_adapter_module<'a>(
308301
}
309302

310303
let types = types.unwrap();
311-
let mut import_funcs = import_funcs.iter();
312-
if let Some((name, funcs)) = import_funcs.next() {
313-
if *name != interface.name {
314-
bail!(
315-
"adapter module imports from `{name}` which does not match \
316-
its interface `{}`",
317-
interface.name
318-
);
319-
}
320-
ret.required_funcs = validate_imported_interface(interface, name, funcs, &types)?;
321-
ret.required_import = Some(interface.name.as_str());
322-
323-
if let Some((name, _)) = import_funcs.next() {
324-
bail!("adapter module cannot import from a second interface `{name}`")
325-
}
304+
for (name, funcs) in import_funcs {
305+
let interface = imports
306+
.get(name)
307+
.ok_or_else(|| anyhow!("adapter module imports unknown module `{name}`"))?;
308+
let required_funcs = validate_imported_interface(interface, name, &funcs, &types)?;
309+
assert_eq!(interface.name, name);
310+
ret.required_imports
311+
.insert(interface.name.as_str(), required_funcs);
326312
}
327313

328314
for (name, ty) in required {

crates/wit-component/tests/components.rs

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use anyhow::{bail, Context, Result};
22
use pretty_assertions::assert_eq;
33
use std::{fs, path::Path};
4+
use wasm_encoder::{Encode, Section};
45
use wit_component::ComponentEncoder;
56
use wit_parser::Interface;
67

@@ -27,28 +28,34 @@ fn read_interfaces(dir: &Path, pattern: &str) -> Result<Vec<Interface>> {
2728
.collect::<Result<_>>()
2829
}
2930

30-
fn read_adapters(dir: &Path) -> Result<Vec<(String, Vec<u8>, Interface)>> {
31+
fn read_adapters(dir: &Path) -> Result<Vec<(String, Vec<u8>, Vec<Interface>)>> {
3132
glob::glob(dir.join("adapt-*.wat").to_str().unwrap())?
3233
.map(|p| {
3334
let p = p?;
3435
let adapter =
3536
wat::parse_file(&p).with_context(|| format!("expected file `{}`", p.display()))?;
3637
let stem = p.file_stem().unwrap().to_str().unwrap();
3738
let glob = format!("{stem}-import-*.wit");
38-
let wit = match glob::glob(dir.join(&glob).to_str().unwrap())?.next() {
39-
Some(path) => path?,
40-
None => bail!("failed to find `{glob}` match"),
41-
};
42-
let mut i = read_interface(&wit)?;
43-
i.name = wit
44-
.file_stem()
45-
.unwrap()
46-
.to_str()
47-
.unwrap()
48-
.trim_start_matches(stem)
49-
.trim_start_matches("-import-")
50-
.to_string();
51-
Ok((stem.trim_start_matches("adapt-").to_string(), adapter, i))
39+
let imports = glob::glob(dir.join(&glob).to_str().unwrap())?
40+
.map(|path| {
41+
let path = path?;
42+
let mut i = read_interface(&path)?;
43+
i.name = path
44+
.file_stem()
45+
.unwrap()
46+
.to_str()
47+
.unwrap()
48+
.trim_start_matches(stem)
49+
.trim_start_matches("-import-")
50+
.to_string();
51+
Ok(i)
52+
})
53+
.collect::<Result<Vec<_>>>()?;
54+
Ok((
55+
stem.trim_start_matches("adapt-").to_string(),
56+
adapter,
57+
imports,
58+
))
5259
})
5360
.collect::<Result<_>>()
5461
}
@@ -136,8 +143,6 @@ fn component_encoding_via_flags() -> Result<()> {
136143
/// needing the wit files passed in as well.
137144
#[test]
138145
fn component_encoding_via_custom_sections() -> Result<()> {
139-
use wasm_encoder::{Encode, Section};
140-
141146
for entry in fs::read_dir("tests/components")? {
142147
let path = entry?.path();
143148
if !path.is_dir() {
@@ -184,9 +189,22 @@ fn component_encoding_via_custom_sections() -> Result<()> {
184189
}
185190

186191
fn add_adapters(mut encoder: ComponentEncoder, path: &Path) -> Result<ComponentEncoder> {
187-
let adapters = read_adapters(path)?;
188-
for (name, wasm, interface) in adapters.iter() {
189-
encoder = encoder.adapter(name, wasm, interface);
192+
for (name, mut wasm, imports) in read_adapters(path)? {
193+
// Create a `component-type` custom section by slurping up `imports` as
194+
// a "world" and encoding it.
195+
let mut types_encoder = ComponentEncoder::default().types_only(true).validate(true);
196+
types_encoder = types_encoder.imports(imports)?;
197+
let contents = types_encoder.encode()?;
198+
let section = wasm_encoder::CustomSection {
199+
name: "component-type",
200+
data: &contents,
201+
};
202+
wasm.push(section.id());
203+
section.encode(&mut wasm);
204+
205+
// Then register our new wasm blob which has the necessary custom
206+
// section.
207+
encoder = encoder.adapter(&name, &wasm)?;
190208
}
191209
Ok(encoder)
192210
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo: func()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bar: func()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
;; this is a polyfill module that translates from wasi-preview1 to a different
2+
;; interface
3+
4+
(module
5+
(import "other1" "foo" (func $foo))
6+
(import "other2" "bar" (func $bar))
7+
8+
(func (export "foo") call $foo)
9+
(func (export "bar") call $bar)
10+
)

0 commit comments

Comments
 (0)