Skip to content

Commit 619226e

Browse files
authored
allow implementing world imports via adapter (#1459)
Previously, it was not possible to supply an adapter which implements e.g. `wasi:cli/environment@0.2.0` such that `wit-component` would replace the main (or library) module's import with the adapter's version. This commit makes that possible by prefering the adapter version over the import whenever there's a match (i.e. an adapter with a name matching the WIT interface name and version). This enables a form of lightweight virtualization in cases where we want to replace imported functions with e.g. trapping stubs or a few lines of WAT -- without adding component composition to the mix. This approach also works with e.g. `wasmtime-py`, which as of this writing does not support composed components. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
1 parent a562387 commit 619226e

23 files changed

Lines changed: 735 additions & 34 deletions

crates/wit-component/src/encoding/world.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ impl<'a> ComponentWorld<'a> {
8787
exports_used: HashMap::new(),
8888
};
8989

90-
ret.process_adapters()?;
90+
ret.process_adapters(&adapters)?;
9191
ret.process_imports()?;
9292
ret.process_exports_used();
9393
ret.process_live_type_imports();
@@ -99,7 +99,7 @@ impl<'a> ComponentWorld<'a> {
9999
/// adapters and figure out what functions are required from the
100100
/// adapter itself, either because the functions are imported by the
101101
/// main module or they're part of the adapter's exports.
102-
fn process_adapters(&mut self) -> Result<()> {
102+
fn process_adapters(&mut self, adapters: &IndexSet<&str>) -> Result<()> {
103103
let resolve = &self.encoder.metadata.resolve;
104104
let world = self.encoder.metadata.world;
105105
for (
@@ -157,6 +157,7 @@ impl<'a> ComponentWorld<'a> {
157157
required_by_import,
158158
required_exports,
159159
library_info.is_some(),
160+
adapters,
160161
)
161162
.context("failed to validate the imports of the minimized adapter module")?;
162163
self.adapters.insert(

crates/wit-component/src/validation.rs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -229,25 +229,31 @@ pub fn validate_module<'a>(
229229
continue;
230230
}
231231

232-
match world.imports.get(&world_key(&metadata.resolve, name)) {
233-
Some(WorldItem::Interface(interface)) => {
234-
let required =
235-
validate_imported_interface(&metadata.resolve, *interface, name, funcs, &types)
236-
.with_context(|| format!("failed to validate import interface `{name}`"))?;
237-
let prev = ret.required_imports.insert(name, required);
238-
assert!(prev.is_none());
232+
if adapters.contains(name) {
233+
let map = ret.adapters_required.entry(name).or_default();
234+
for (func, ty) in funcs {
235+
let ty = types[types.core_type_at(*ty).unwrap_sub()].unwrap_func();
236+
map.insert(func, ty.clone());
239237
}
240-
None if adapters.contains(name) => {
241-
let map = ret.adapters_required.entry(name).or_default();
242-
for (func, ty) in funcs {
243-
let ty = types[types.core_type_at(*ty).unwrap_sub()].unwrap_func();
244-
map.insert(func, ty.clone());
238+
} else {
239+
match world.imports.get(&world_key(&metadata.resolve, name)) {
240+
Some(WorldItem::Interface(interface)) => {
241+
let required = validate_imported_interface(
242+
&metadata.resolve,
243+
*interface,
244+
name,
245+
funcs,
246+
&types,
247+
)
248+
.with_context(|| format!("failed to validate import interface `{name}`"))?;
249+
let prev = ret.required_imports.insert(name, required);
250+
assert!(prev.is_none());
245251
}
252+
Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
253+
bail!("import `{}` is not an interface", name)
254+
}
255+
None => bail!("module requires an import interface named `{name}`"),
246256
}
247-
Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
248-
bail!("import `{}` is not an interface", name)
249-
}
250-
None => bail!("module requires an import interface named `{name}`"),
251257
}
252258
}
253259

@@ -389,6 +395,7 @@ pub fn validate_adapter_module<'a>(
389395
required_by_import: Option<&IndexMap<&str, FuncType>>,
390396
exports: &IndexSet<WorldKey>,
391397
is_library: bool,
398+
adapters: &IndexSet<&str>,
392399
) -> Result<ValidatedAdapter<'a>> {
393400
let mut validator = Validator::new();
394401
let mut import_funcs = IndexMap::new();
@@ -517,20 +524,24 @@ pub fn validate_adapter_module<'a>(
517524
continue;
518525
}
519526

520-
match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
521-
Some(WorldItem::Interface(interface)) => {
522-
let required =
523-
validate_imported_interface(resolve, *interface, name, funcs, &types)
524-
.with_context(|| format!("failed to validate import interface `{name}`"))?;
525-
let prev = ret.required_imports.insert(name.to_string(), required);
526-
assert!(prev.is_none());
527-
}
528-
None | Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
529-
if !is_library {
530-
bail!(
531-
"adapter module requires an import interface named `{}`",
532-
name
533-
)
527+
if !(is_library && adapters.contains(name)) {
528+
match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
529+
Some(WorldItem::Interface(interface)) => {
530+
let required =
531+
validate_imported_interface(resolve, *interface, name, funcs, &types)
532+
.with_context(|| {
533+
format!("failed to validate import interface `{name}`")
534+
})?;
535+
let prev = ret.required_imports.insert(name.to_string(), required);
536+
assert!(prev.is_none());
537+
}
538+
None | Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
539+
if !is_library {
540+
bail!(
541+
"adapter module requires an import interface named `{}`",
542+
name
543+
)
544+
}
534545
}
535546
}
536547
}

crates/wit-component/tests/components.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ use wit_parser::{PackageId, Resolve, UnresolvedPackage};
2222
/// interfaces of the `module.wat` or `lib-$name.wat` and
2323
/// `dlopen-lib-$name.wat` files. Must have a `default world`
2424
/// * [optional] `adapt-$name.wat` - optional adapter for the module name
25-
/// `$name`, can be specified for multiple `$name`s
25+
/// `$name`, can be specified for multiple `$name`s. Alternatively, if $name
26+
/// doesn't work as part of a filename (e.g. contains forward slashes), it may
27+
/// be specified on the first line of the file with the prefix `;; module name:
28+
/// `, e.g. `;; module name: wasi:cli/environment@0.2.0`.
2629
/// * [optional] `adapt-$name.wit` - required for each `*.wat` adapter to
2730
/// describe imports/exports of the adapter.
2831
/// * [optional] `stub-missing-functions` - if linking libraries and this file
@@ -184,7 +187,15 @@ fn read_name_and_module(
184187
) -> Result<(String, Vec<u8>)> {
185188
let wasm = read_core_module(path, resolve, pkg)?;
186189
let stem = path.file_stem().unwrap().to_str().unwrap();
187-
let name = stem.trim_start_matches(prefix).to_owned();
190+
let name = if let Some(name) = fs::read_to_string(path)?
191+
.lines()
192+
.next()
193+
.and_then(|line| line.strip_prefix(";; module name: "))
194+
{
195+
name.to_owned()
196+
} else {
197+
stem.trim_start_matches(prefix).to_owned()
198+
};
188199
Ok((name, wasm))
189200
}
190201

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
;; module name: wasi:cli/environment@0.2.0
2+
(module
3+
(type (func (param i32)))
4+
(func $get-environment (type 0)
5+
unreachable
6+
)
7+
(export "get-environment" (func $get-environment))
8+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
world adapt-wasip2 { }
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
(component
2+
(core module (;0;)
3+
(type (;0;) (func))
4+
(type (;1;) (func (param i32)))
5+
(type (;2;) (func (param i32 i32 i32 i32) (result i32)))
6+
(import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (;0;) (type 1)))
7+
(func $start (;1;) (type 0))
8+
(func $realloc (;2;) (type 2) (param i32 i32 i32 i32) (result i32)
9+
unreachable
10+
)
11+
(memory (;0;) 1)
12+
(export "cabi_realloc" (func $realloc))
13+
(export "memory" (memory 0))
14+
(start $start)
15+
(@producers
16+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
17+
(processed-by "my-fake-bindgen" "123.45")
18+
)
19+
)
20+
(core module (;1;)
21+
(type (;0;) (func (param i32)))
22+
(func $get-environment (;0;) (type 0) (param i32)
23+
unreachable
24+
)
25+
(export "get-environment" (func $get-environment))
26+
)
27+
(core module (;2;)
28+
(type (;0;) (func (param i32)))
29+
(func $adapt-wasi:cli/environment@0.2.0-get-environment (;0;) (type 0) (param i32)
30+
local.get 0
31+
i32.const 0
32+
call_indirect (type 0)
33+
)
34+
(table (;0;) 1 1 funcref)
35+
(export "0" (func $adapt-wasi:cli/environment@0.2.0-get-environment))
36+
(export "$imports" (table 0))
37+
(@producers
38+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
39+
)
40+
)
41+
(core module (;3;)
42+
(type (;0;) (func (param i32)))
43+
(import "" "0" (func (;0;) (type 0)))
44+
(import "" "$imports" (table (;0;) 1 1 funcref))
45+
(elem (;0;) (i32.const 0) func 0)
46+
(@producers
47+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
48+
)
49+
)
50+
(core instance (;0;) (instantiate 2))
51+
(alias core export 0 "0" (core func (;0;)))
52+
(core instance (;1;)
53+
(export "get-environment" (func 0))
54+
)
55+
(core instance (;2;) (instantiate 0
56+
(with "wasi:cli/environment@0.2.0" (instance 1))
57+
)
58+
)
59+
(alias core export 2 "memory" (core memory (;0;)))
60+
(alias core export 2 "cabi_realloc" (core func (;1;)))
61+
(core instance (;3;) (instantiate 1))
62+
(alias core export 0 "$imports" (core table (;0;)))
63+
(alias core export 3 "get-environment" (core func (;2;)))
64+
(core instance (;4;)
65+
(export "$imports" (table 0))
66+
(export "0" (func 2))
67+
)
68+
(core instance (;5;) (instantiate 3
69+
(with "" (instance 4))
70+
)
71+
)
72+
(@producers
73+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
74+
)
75+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package root:component;
2+
3+
world root {
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package wasi:cli@0.2.0;
2+
3+
interface environment {
4+
get-environment: func() -> list<tuple<string, string>>;
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(module
2+
(type (func))
3+
(type (func (param i32)))
4+
(type (func (param i32 i32 i32 i32) (result i32)))
5+
(import "wasi:cli/environment@0.2.0" "get-environment" (func $get-environment (type 1)))
6+
(func $start (type 0))
7+
(func $realloc (type 2) unreachable)
8+
(export "cabi_realloc" (func $realloc))
9+
(memory (export "memory") 1)
10+
(start $start)
11+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package test:test;
2+
3+
world module {
4+
import wasi:cli/environment@0.2.0;
5+
}

0 commit comments

Comments
 (0)