Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/gen-guest-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ doctest = false
[dependencies]
wit-bindgen-core = { workspace = true }
wit-bindgen-gen-rust-lib = { workspace = true }
wit-component = { workspace = true }
heck = { workspace = true }
clap = { workspace = true, optional = true }

Expand Down
22 changes: 22 additions & 0 deletions crates/gen-guest-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,28 @@ impl Generator for RustWasm {
));
}

let component_type = wit_component::InterfaceEncoder::new(iface)
.encode()
.expect(&format!(
"encoding interface {} as a component type",
iface.name
));
let direction = match dir {
Direction::Import => "import",
Direction::Export => "export",
};
let iface_name = &iface.name;

self.src.push_str("#[cfg(target_arch = \"wasm32\")]\n");
self.src.push_str(&format!(
"#[link_section = \"component-type:{direction}:{iface_name}\"]\n"
));
self.src.push_str(&format!(
"pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; {}] = ",
component_type.len()
));
self.src.push_str(&format!("{:?};\n", component_type));

// For standalone generation, close the export! macro
if self.opts.standalone && dir == Direction::Export {
self.src.push_str("});\n");
Expand Down
27 changes: 25 additions & 2 deletions crates/test-helpers/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fs;
use std::path::PathBuf;
use std::process::Command;
use wit_bindgen_core::{wit_parser::Interface, Direction, Generator};
use wit_component::ComponentEncoder;

fn main() {
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
Expand All @@ -13,14 +14,20 @@ fn main() {
let mut cmd = Command::new("cargo");
cmd.arg("build")
.current_dir("../test-rust-wasm")
.arg("--target=wasm32-wasi")
// TODO: this should go back to wasm32-wasi once we have an adapter
// for snapshot 1 to a component
.arg("--target=wasm32-unknown-unknown")
.env("CARGO_TARGET_DIR", &out_dir)
.env("CARGO_PROFILE_DEV_DEBUG", "1")
.env("RUSTFLAGS", "-Clink-args=--export-table")
.env_remove("CARGO_ENCODED_RUSTFLAGS");
let status = cmd.status().unwrap();
assert!(status.success());
for file in out_dir.join("wasm32-wasi/debug").read_dir().unwrap() {
for file in out_dir
.join("wasm32-unknown-unknown/debug")
.read_dir()
.unwrap()
{
let file = file.unwrap().path();
if file.extension().and_then(|s| s.to_str()) != Some("wasm") {
continue;
Expand All @@ -31,6 +38,22 @@ fn main() {
file.to_str().unwrap().to_string(),
));

// The "invalid" test doesn't actually use the rust-guest macro
// and doesn't put the custom sections in, so component translation
// will fail.
if file.file_stem().unwrap().to_str().unwrap() != "invalid" {
// Validate that the module can be translated to a component, using
// the component-type custom sections. We don't yet consume this component
// anywhere.
let module = fs::read(&file).expect("failed to read wasm file");
ComponentEncoder::default()
.module(module.as_slice())
.expect("pull custom sections from module")
.validate(true)
.encode()
.expect("module can be translated to a component");
}

let dep_file = file.with_extension("d");
let deps = fs::read_to_string(&dep_file).expect("failed to read dep file");
for dep in deps
Expand Down
2 changes: 1 addition & 1 deletion crates/wit-component/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl WitComponentApp {
.with_context(|| format!("failed to parse module `{}`", self.module.display()))?;

let mut encoder = ComponentEncoder::default()
.module(&module)
.module(&module)?
.imports(&self.imports)
.exports(&self.exports)
.validate(!self.skip_validation);
Expand Down
62 changes: 50 additions & 12 deletions crates/wit-component/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
//! component model.

use crate::{
decode_interface_component,
validation::{
expected_export_name, validate_adapter_module, validate_module, ValidatedAdapter,
ValidatedModule,
Expand Down Expand Up @@ -2097,19 +2098,53 @@ impl<'a> ImportEncoder<'a> {
pub struct ComponentEncoder<'a> {
module: &'a [u8],
encoding: StringEncoding,
interface: Option<&'a Interface>,
imports: &'a [Interface],
exports: &'a [Interface],
interface: Option<Interface>,
imports: Vec<Interface>,
exports: Vec<Interface>,
validate: bool,
types_only: bool,
adapters: IndexMap<&'a str, (&'a [u8], &'a Interface)>,
}

impl<'a> ComponentEncoder<'a> {
/// Set the core module to encode as a component.
pub fn module(mut self, module: &'a [u8]) -> Self {
/// This method will also parse any component type information stored in custom sections
/// inside the module, and add them as the interface, imports, and exports.
pub fn module(mut self, module: &'a [u8]) -> Result<Self> {
for payload in wasmparser::Parser::new(0).parse_all(&module) {
match payload.context("decoding item in module")? {
wasmparser::Payload::CustomSection(cs) => {
if let Some(export) = cs.name().strip_prefix("component-type:export:") {
let mut i = decode_interface_component(cs.data()).with_context(|| {
format!("decoding component-type in export section {}", export)
})?;
i.name = export.to_owned();
self.interface = Some(i);
} else if let Some(import) = cs.name().strip_prefix("component-type:import:") {
let mut i = decode_interface_component(cs.data()).with_context(|| {
format!("decoding component-type in import section {}", import)
})?;
i.name = import.to_owned();
self.imports.push(i);
} else if let Some(export_instance) =
cs.name().strip_prefix("component-type:export-instance:")
{
let mut i = decode_interface_component(cs.data()).with_context(|| {
format!(
"decoding component-type in export-instance section {}",
export_instance
)
})?;
i.name = export_instance.to_owned();
self.exports.push(i);
}
}
_ => {}
}
}

self.module = module;
self
Ok(self)
}

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

/// Set the default interface exported by the component.
pub fn interface(mut self, interface: &'a Interface) -> Self {
self.interface = Some(interface);
self.interface = Some(interface.clone());
self
}

/// Set the interfaces the component imports.
pub fn imports(mut self, imports: &'a [Interface]) -> Self {
self.imports = imports;
for i in imports {
self.imports.push(i.clone())
}
self
}

/// Set the interfaces the component exports.
pub fn exports(mut self, exports: &'a [Interface]) -> Self {
self.exports = exports;
for e in exports {
self.exports.push(e.clone())
}
self
}

Expand Down Expand Up @@ -2171,8 +2210,8 @@ impl<'a> ComponentEncoder<'a> {
validate_module(
self.module,
&self.interface,
self.imports,
self.exports,
&self.imports,
&self.exports,
&adapters,
)?
} else {
Expand All @@ -2182,15 +2221,14 @@ impl<'a> ComponentEncoder<'a> {
let exports = self
.interface
.iter()
.copied()
.map(|i| (i, true))
.chain(self.exports.iter().map(|i| (i, false)));

let mut state = EncodingState::default();
let mut types = TypeEncoder::default();
let mut imports = ImportEncoder::default();
types.encode_func_types(exports.clone(), false)?;
types.encode_instance_imports(self.imports, &info, &mut imports)?;
types.encode_instance_imports(&self.imports, &info, &mut imports)?;

if self.types_only {
if !self.module.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion crates/wit-component/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub struct ValidatedModule<'a> {
/// for this module.
pub fn validate_module<'a>(
bytes: &'a [u8],
interface: &Option<&Interface>,
interface: &Option<Interface>,
imports: &[Interface],
exports: &[Interface],
adapters: &IndexSet<&str>,
Expand Down
112 changes: 109 additions & 3 deletions crates/wit-component/tests/components.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{bail, Context, Result};
use pretty_assertions::assert_eq;
use std::{fs, path::Path};
use wit_component::ComponentEncoder;
use wit_component::{ComponentEncoder, InterfaceEncoder};
use wit_parser::Interface;

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

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

let mut encoder = ComponentEncoder::default()
.module(&module)
.module(&module)?
.imports(&imports)
.exports(&exports)
.validate(true);
Expand All @@ -119,6 +119,112 @@ fn component_encoding() -> Result<()> {
}

let r = encoder.encode();

let (output, baseline_path) = if error_path.is_file() {
match r {
Ok(_) => bail!("encoding should fail for test case `{}`", test_case),
Err(e) => (e.to_string(), &error_path),
}
} else {
(
wasmprinter::print_bytes(
&r.with_context(|| format!("failed to encode for test case `{}`", test_case))?,
)
.with_context(|| {
format!(
"failed to print component bytes for test case `{}`",
test_case
)
})?,
&component_path,
)
};

if std::env::var_os("BLESS").is_some() {
fs::write(&baseline_path, output)?;
} else {
assert_eq!(
fs::read_to_string(&baseline_path)?.replace("\r\n", "\n"),
output,
"failed baseline comparison for test case `{}` ({})",
test_case,
baseline_path.display(),
);
}
}

Ok(())
}

/// Tests the encoding of components.
///
/// This test looks in the `components/` directory for test cases. It parses
/// the inputs to the test out of that directly exactly like
/// `component_encoding_via_flags` does in this same file.
///
/// Rather than pass the default interface, imports, and exports directly to
/// the `ComponentEncoder`, this test encodes those Interfaces as component
/// types in custom sections of the wasm Module.
///
/// This simulates the flow that toolchains which don't yet know how to
/// emit a Component will emit a canonical ABI Module containing these custom sections,
/// and those will then be translated by wit-component to a Component without
/// needing the wit files passed in as well.
#[test]
fn component_encoding_via_custom_sections() -> Result<()> {
use wasm_encoder::{Encode, Section};

for entry in fs::read_dir("tests/components")? {
let path = entry?.path();
if !path.is_dir() {
continue;
}

let test_case = path.file_stem().unwrap().to_str().unwrap();

let module_path = path.join("module.wat");
let interface_path = path.join("default.wit");
let component_path = path.join("component.wat");
let error_path = path.join("error.txt");

let mut module = wat::parse_file(&module_path)
.with_context(|| format!("expected file `{}`", module_path.display()))?;

fn encode_interface(i: &Interface, module: &mut Vec<u8>, kind: &str) -> Result<()> {
let name = &format!("component-type:{}:{}", kind, i.name);
let contents = InterfaceEncoder::new(&i).validate(true).encode()?;
let section = wasm_encoder::CustomSection {
name,
data: &contents,
};
module.push(section.id());
section.encode(module);
Ok(())
}

// Encode the interface, exports, and imports into the module, instead of
// passing them to the ComponentEncoder explicitly.
if interface_path.is_file() {
let i = read_interface(&interface_path)?;
encode_interface(&i, &mut module, "export")?;
}
for i in read_interfaces(&path, "import-*.wit")? {
encode_interface(&i, &mut module, "import")?;
}
for i in read_interfaces(&path, "export-*.wit")? {
encode_interface(&i, &mut module, "export-instance")?;
}
//
let adapters = read_adapters(&path)?;

let mut encoder = ComponentEncoder::default().module(&module)?.validate(true);

for (name, wasm, interface) in adapters.iter() {
encoder = encoder.adapter(name, wasm, interface);
}

let r = encoder.encode();

let (output, baseline_path) = if error_path.is_file() {
match r {
Ok(_) => bail!("encoding should fail for test case `{}`", test_case),
Expand Down
Loading