Skip to content

Commit 7be8acc

Browse files
Encode component types in canonical ABI module custom sections (#331)
* Remove support for handles and resources This commit removes all support for the `resource` and `Handle` types from the AST of `wit-parser` and all related support in all code generators. The motivation for this commit is that `wit-bindgen` is on the cusp of actually being able to work with components: producing a component from guest output and consuming components in host generators. More detail about this is in #314. With components as an intermediate format, however, there is no way to encode resources since they are not part of the component model proposal yet. All is not lost for handles and resources, though. The official design for handles and resources is being worked on upstream in the component model repository itself at this time and once added all of this support will be re-added to `wit-bindgen`. In the meantime though I personally think that the best way forward is to remove the interim support for a few reasons: * Primarily it unblocks progress at this time towards fully integrating components and the `wit-bindgen` generators. The requirement to run existing tests that use handles would mean that no host generator could actually switch to components and/or modes for today's core-wasm-lookalike would need to be preserved. * Otherwise though the semantics of the current handles are basically invented out of thin air by myself and were never really formally specified, debated, or designed deliberately. I grafted `witx`-style handles into `wit-component` and added features as necessary over time, but it seems highly unlikely that the handles designed as part of the component model will be the ones that `wit-bindgen` currently supports. This inevitably means that a new system would need new code anyway and would likely result in removal regardless. As usual git always has the history of handles and this all may come back in one shape or another if only slightly tweaked. I'm confident in our history spelunking abilities, though, so I don't feel that keeping support in the repository is necessary for this purpose. * Remove resources from the demo * smuggle wit information in custom sections * move transcoder to the crate, and make it available in the cli * gen-guest-rust can emit custom component-type section * custom section takes a pub static, not a const * ComponentEncoder needs to own its Interfaces so that I can use the Interfaces decoded from the module's custom section * make ComponentEncoder always transcode component-type info from custom sections * flavorful tests: types and functions are actually the same namespace theyre not in wit, but thats a bug we need to fix, because they are in component types * test-helpers: build rust guests with wasm32-unknown-unknown and assert they encode as components except for "invalid" and "handles" which are not gonna work * refactor * gen-guest-rust: now infallible Co-authored-by: Alex Crichton <[email protected]>
1 parent 25596c7 commit 7be8acc

File tree

15 files changed

+294
-103
lines changed

15 files changed

+294
-103
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gen-guest-rust/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ doctest = false
1111
[dependencies]
1212
wit-bindgen-core = { workspace = true }
1313
wit-bindgen-gen-rust-lib = { workspace = true }
14+
wit-component = { workspace = true }
1415
heck = { workspace = true }
1516
clap = { workspace = true, optional = true }
1617

crates/gen-guest-rust/src/lib.rs

+22
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,28 @@ impl Generator for RustWasm {
468468
));
469469
}
470470

471+
let component_type = wit_component::InterfaceEncoder::new(iface)
472+
.encode()
473+
.expect(&format!(
474+
"encoding interface {} as a component type",
475+
iface.name
476+
));
477+
let direction = match dir {
478+
Direction::Import => "import",
479+
Direction::Export => "export",
480+
};
481+
let iface_name = &iface.name;
482+
483+
self.src.push_str("#[cfg(target_arch = \"wasm32\")]\n");
484+
self.src.push_str(&format!(
485+
"#[link_section = \"component-type:{direction}:{iface_name}\"]\n"
486+
));
487+
self.src.push_str(&format!(
488+
"pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; {}] = ",
489+
component_type.len()
490+
));
491+
self.src.push_str(&format!("{:?};\n", component_type));
492+
471493
// For standalone generation, close the export! macro
472494
if self.opts.standalone && dir == Direction::Export {
473495
self.src.push_str("});\n");

crates/test-helpers/build.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::fs;
33
use std::path::PathBuf;
44
use std::process::Command;
55
use wit_bindgen_core::{wit_parser::Interface, Direction, Generator};
6+
use wit_component::ComponentEncoder;
67

78
fn main() {
89
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
@@ -13,14 +14,20 @@ fn main() {
1314
let mut cmd = Command::new("cargo");
1415
cmd.arg("build")
1516
.current_dir("../test-rust-wasm")
16-
.arg("--target=wasm32-wasi")
17+
// TODO: this should go back to wasm32-wasi once we have an adapter
18+
// for snapshot 1 to a component
19+
.arg("--target=wasm32-unknown-unknown")
1720
.env("CARGO_TARGET_DIR", &out_dir)
1821
.env("CARGO_PROFILE_DEV_DEBUG", "1")
1922
.env("RUSTFLAGS", "-Clink-args=--export-table")
2023
.env_remove("CARGO_ENCODED_RUSTFLAGS");
2124
let status = cmd.status().unwrap();
2225
assert!(status.success());
23-
for file in out_dir.join("wasm32-wasi/debug").read_dir().unwrap() {
26+
for file in out_dir
27+
.join("wasm32-unknown-unknown/debug")
28+
.read_dir()
29+
.unwrap()
30+
{
2431
let file = file.unwrap().path();
2532
if file.extension().and_then(|s| s.to_str()) != Some("wasm") {
2633
continue;
@@ -31,6 +38,22 @@ fn main() {
3138
file.to_str().unwrap().to_string(),
3239
));
3340

41+
// The "invalid" test doesn't actually use the rust-guest macro
42+
// and doesn't put the custom sections in, so component translation
43+
// will fail.
44+
if file.file_stem().unwrap().to_str().unwrap() != "invalid" {
45+
// Validate that the module can be translated to a component, using
46+
// the component-type custom sections. We don't yet consume this component
47+
// anywhere.
48+
let module = fs::read(&file).expect("failed to read wasm file");
49+
ComponentEncoder::default()
50+
.module(module.as_slice())
51+
.expect("pull custom sections from module")
52+
.validate(true)
53+
.encode()
54+
.expect("module can be translated to a component");
55+
}
56+
3457
let dep_file = file.with_extension("d");
3558
let deps = fs::read_to_string(&dep_file).expect("failed to read dep file");
3659
for dep in deps

crates/wit-component/src/cli.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl WitComponentApp {
139139
.with_context(|| format!("failed to parse module `{}`", self.module.display()))?;
140140

141141
let mut encoder = ComponentEncoder::default()
142-
.module(&module)
142+
.module(&module)?
143143
.imports(&self.imports)
144144
.exports(&self.exports)
145145
.validate(!self.skip_validation);

crates/wit-component/src/encoding.rs

+50-12
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
//! component model.
5454
5555
use crate::{
56+
decode_interface_component,
5657
validation::{
5758
expected_export_name, validate_adapter_module, validate_module, ValidatedAdapter,
5859
ValidatedModule,
@@ -2097,19 +2098,53 @@ impl<'a> ImportEncoder<'a> {
20972098
pub struct ComponentEncoder<'a> {
20982099
module: &'a [u8],
20992100
encoding: StringEncoding,
2100-
interface: Option<&'a Interface>,
2101-
imports: &'a [Interface],
2102-
exports: &'a [Interface],
2101+
interface: Option<Interface>,
2102+
imports: Vec<Interface>,
2103+
exports: Vec<Interface>,
21032104
validate: bool,
21042105
types_only: bool,
21052106
adapters: IndexMap<&'a str, (&'a [u8], &'a Interface)>,
21062107
}
21072108

21082109
impl<'a> ComponentEncoder<'a> {
21092110
/// Set the core module to encode as a component.
2110-
pub fn module(mut self, module: &'a [u8]) -> Self {
2111+
/// This method will also parse any component type information stored in custom sections
2112+
/// inside the module, and add them as the interface, imports, and exports.
2113+
pub fn module(mut self, module: &'a [u8]) -> Result<Self> {
2114+
for payload in wasmparser::Parser::new(0).parse_all(&module) {
2115+
match payload.context("decoding item in module")? {
2116+
wasmparser::Payload::CustomSection(cs) => {
2117+
if let Some(export) = cs.name().strip_prefix("component-type:export:") {
2118+
let mut i = decode_interface_component(cs.data()).with_context(|| {
2119+
format!("decoding component-type in export section {}", export)
2120+
})?;
2121+
i.name = export.to_owned();
2122+
self.interface = Some(i);
2123+
} else if let Some(import) = cs.name().strip_prefix("component-type:import:") {
2124+
let mut i = decode_interface_component(cs.data()).with_context(|| {
2125+
format!("decoding component-type in import section {}", import)
2126+
})?;
2127+
i.name = import.to_owned();
2128+
self.imports.push(i);
2129+
} else if let Some(export_instance) =
2130+
cs.name().strip_prefix("component-type:export-instance:")
2131+
{
2132+
let mut i = decode_interface_component(cs.data()).with_context(|| {
2133+
format!(
2134+
"decoding component-type in export-instance section {}",
2135+
export_instance
2136+
)
2137+
})?;
2138+
i.name = export_instance.to_owned();
2139+
self.exports.push(i);
2140+
}
2141+
}
2142+
_ => {}
2143+
}
2144+
}
2145+
21112146
self.module = module;
2112-
self
2147+
Ok(self)
21132148
}
21142149

21152150
/// Set the string encoding expected by the core module.
@@ -2126,19 +2161,23 @@ impl<'a> ComponentEncoder<'a> {
21262161

21272162
/// Set the default interface exported by the component.
21282163
pub fn interface(mut self, interface: &'a Interface) -> Self {
2129-
self.interface = Some(interface);
2164+
self.interface = Some(interface.clone());
21302165
self
21312166
}
21322167

21332168
/// Set the interfaces the component imports.
21342169
pub fn imports(mut self, imports: &'a [Interface]) -> Self {
2135-
self.imports = imports;
2170+
for i in imports {
2171+
self.imports.push(i.clone())
2172+
}
21362173
self
21372174
}
21382175

21392176
/// Set the interfaces the component exports.
21402177
pub fn exports(mut self, exports: &'a [Interface]) -> Self {
2141-
self.exports = exports;
2178+
for e in exports {
2179+
self.exports.push(e.clone())
2180+
}
21422181
self
21432182
}
21442183

@@ -2171,8 +2210,8 @@ impl<'a> ComponentEncoder<'a> {
21712210
validate_module(
21722211
self.module,
21732212
&self.interface,
2174-
self.imports,
2175-
self.exports,
2213+
&self.imports,
2214+
&self.exports,
21762215
&adapters,
21772216
)?
21782217
} else {
@@ -2182,15 +2221,14 @@ impl<'a> ComponentEncoder<'a> {
21822221
let exports = self
21832222
.interface
21842223
.iter()
2185-
.copied()
21862224
.map(|i| (i, true))
21872225
.chain(self.exports.iter().map(|i| (i, false)));
21882226

21892227
let mut state = EncodingState::default();
21902228
let mut types = TypeEncoder::default();
21912229
let mut imports = ImportEncoder::default();
21922230
types.encode_func_types(exports.clone(), false)?;
2193-
types.encode_instance_imports(self.imports, &info, &mut imports)?;
2231+
types.encode_instance_imports(&self.imports, &info, &mut imports)?;
21942232

21952233
if self.types_only {
21962234
if !self.module.is_empty() {

crates/wit-component/src/validation.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub struct ValidatedModule<'a> {
8686
/// for this module.
8787
pub fn validate_module<'a>(
8888
bytes: &'a [u8],
89-
interface: &Option<&Interface>,
89+
interface: &Option<Interface>,
9090
imports: &[Interface],
9191
exports: &[Interface],
9292
adapters: &IndexSet<&str>,

crates/wit-component/tests/components.rs

+109-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{bail, Context, Result};
22
use pretty_assertions::assert_eq;
33
use std::{fs, path::Path};
4-
use wit_component::ComponentEncoder;
4+
use wit_component::{ComponentEncoder, InterfaceEncoder};
55
use wit_parser::Interface;
66

77
fn read_interface(path: &Path) -> Result<Interface> {
@@ -78,7 +78,7 @@ fn read_adapters(dir: &Path) -> Result<Vec<(String, Vec<u8>, Interface)>> {
7878
/// Run the test with the environment variable `BLESS` set to update
7979
/// either `component.wat` or `error.txt` depending on the outcome of the encoding.
8080
#[test]
81-
fn component_encoding() -> Result<()> {
81+
fn component_encoding_via_flags() -> Result<()> {
8282
drop(env_logger::try_init());
8383

8484
for entry in fs::read_dir("tests/components")? {
@@ -105,7 +105,7 @@ fn component_encoding() -> Result<()> {
105105
let adapters = read_adapters(&path)?;
106106

107107
let mut encoder = ComponentEncoder::default()
108-
.module(&module)
108+
.module(&module)?
109109
.imports(&imports)
110110
.exports(&exports)
111111
.validate(true);
@@ -119,6 +119,112 @@ fn component_encoding() -> Result<()> {
119119
}
120120

121121
let r = encoder.encode();
122+
123+
let (output, baseline_path) = if error_path.is_file() {
124+
match r {
125+
Ok(_) => bail!("encoding should fail for test case `{}`", test_case),
126+
Err(e) => (e.to_string(), &error_path),
127+
}
128+
} else {
129+
(
130+
wasmprinter::print_bytes(
131+
&r.with_context(|| format!("failed to encode for test case `{}`", test_case))?,
132+
)
133+
.with_context(|| {
134+
format!(
135+
"failed to print component bytes for test case `{}`",
136+
test_case
137+
)
138+
})?,
139+
&component_path,
140+
)
141+
};
142+
143+
if std::env::var_os("BLESS").is_some() {
144+
fs::write(&baseline_path, output)?;
145+
} else {
146+
assert_eq!(
147+
fs::read_to_string(&baseline_path)?.replace("\r\n", "\n"),
148+
output,
149+
"failed baseline comparison for test case `{}` ({})",
150+
test_case,
151+
baseline_path.display(),
152+
);
153+
}
154+
}
155+
156+
Ok(())
157+
}
158+
159+
/// Tests the encoding of components.
160+
///
161+
/// This test looks in the `components/` directory for test cases. It parses
162+
/// the inputs to the test out of that directly exactly like
163+
/// `component_encoding_via_flags` does in this same file.
164+
///
165+
/// Rather than pass the default interface, imports, and exports directly to
166+
/// the `ComponentEncoder`, this test encodes those Interfaces as component
167+
/// types in custom sections of the wasm Module.
168+
///
169+
/// This simulates the flow that toolchains which don't yet know how to
170+
/// emit a Component will emit a canonical ABI Module containing these custom sections,
171+
/// and those will then be translated by wit-component to a Component without
172+
/// needing the wit files passed in as well.
173+
#[test]
174+
fn component_encoding_via_custom_sections() -> Result<()> {
175+
use wasm_encoder::{Encode, Section};
176+
177+
for entry in fs::read_dir("tests/components")? {
178+
let path = entry?.path();
179+
if !path.is_dir() {
180+
continue;
181+
}
182+
183+
let test_case = path.file_stem().unwrap().to_str().unwrap();
184+
185+
let module_path = path.join("module.wat");
186+
let interface_path = path.join("default.wit");
187+
let component_path = path.join("component.wat");
188+
let error_path = path.join("error.txt");
189+
190+
let mut module = wat::parse_file(&module_path)
191+
.with_context(|| format!("expected file `{}`", module_path.display()))?;
192+
193+
fn encode_interface(i: &Interface, module: &mut Vec<u8>, kind: &str) -> Result<()> {
194+
let name = &format!("component-type:{}:{}", kind, i.name);
195+
let contents = InterfaceEncoder::new(&i).validate(true).encode()?;
196+
let section = wasm_encoder::CustomSection {
197+
name,
198+
data: &contents,
199+
};
200+
module.push(section.id());
201+
section.encode(module);
202+
Ok(())
203+
}
204+
205+
// Encode the interface, exports, and imports into the module, instead of
206+
// passing them to the ComponentEncoder explicitly.
207+
if interface_path.is_file() {
208+
let i = read_interface(&interface_path)?;
209+
encode_interface(&i, &mut module, "export")?;
210+
}
211+
for i in read_interfaces(&path, "import-*.wit")? {
212+
encode_interface(&i, &mut module, "import")?;
213+
}
214+
for i in read_interfaces(&path, "export-*.wit")? {
215+
encode_interface(&i, &mut module, "export-instance")?;
216+
}
217+
//
218+
let adapters = read_adapters(&path)?;
219+
220+
let mut encoder = ComponentEncoder::default().module(&module)?.validate(true);
221+
222+
for (name, wasm, interface) in adapters.iter() {
223+
encoder = encoder.adapter(name, wasm, interface);
224+
}
225+
226+
let r = encoder.encode();
227+
122228
let (output, baseline_path) = if error_path.is_file() {
123229
match r {
124230
Ok(_) => bail!("encoding should fail for test case `{}`", test_case),

0 commit comments

Comments
 (0)