Skip to content

Commit 3626fda

Browse files
fibonacci1729dicej
authored andcommitted
feat: preliminary support for WIT templates
This is a minimum viable implementation of WIT templates per WebAssembly/component-model#172. It supports interfaces with at most one wildcard function, which may be expanded (i.e. monomorphized) with a set of substitutions provided by the application developer when generating guest bindings. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
1 parent 80d9ffa commit 3626fda

File tree

21 files changed

+377
-184
lines changed

21 files changed

+377
-184
lines changed

Cargo.lock

Lines changed: 200 additions & 165 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ indexmap = "1.9.1"
3232
wasm-encoder = "0.25.0"
3333
wasm-metadata = "0.3.1"
3434
wat = "1.0.61"
35-
wit-parser = "0.6.3"
36-
wit-component = "0.7.2"
35+
wit-parser = "0.6.4"
36+
wit-component = "0.7.3"
3737

3838
wit-bindgen-core = { path = 'crates/core', version = '0.4.0' }
3939
wit-bindgen-c = { path = 'crates/c', version = '0.4.0' }
@@ -60,6 +60,7 @@ wit-bindgen-go = { workspace = true, features = ['clap'], optional = true }
6060
wat = { workspace = true }
6161
wit-component = { workspace = true }
6262
wasm-encoder = { workspace = true }
63+
toml = "0.7.2"
6364

6465
[features]
6566
default = ['c', 'rust', 'markdown', 'teavm-java', 'go']
@@ -71,6 +72,13 @@ go = ['dep:wit-bindgen-go']
7172

7273
[dev-dependencies]
7374
heck = { workspace = true }
74-
wasmtime = { version = "6", features = ['component-model'] }
75+
wasmtime = { version = "8", features = ['component-model'] }
7576
test-artifacts = { path = 'crates/test-rust-wasm/artifacts' }
7677
wit-parser = { workspace = true }
78+
79+
[patch.crates-io]
80+
wit-component = { git = "https://github.com/dicej/wasm-tools", branch = "wit-templates" }
81+
wit-parser = { git = "https://github.com/dicej/wasm-tools", branch = "wit-templates" }
82+
wasm-metadata = { git = "https://github.com/dicej/wasm-tools", branch = "wit-templates" }
83+
wasmtime = { git = "https://github.com/dicej/wasmtime", branch = "wit-templates-minimal" }
84+
wasmtime-wit-bindgen = { git = "https://github.com/dicej/wasmtime", branch = "wit-templates-minimal" }

crates/rust-macro/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ wit-bindgen-core = { workspace = true }
2222
wit-bindgen-rust = { workspace = true }
2323
wit-component = { workspace = true }
2424
anyhow = { workspace = true }
25+
toml = "0.7.2"

crates/rust-macro/src/lib.rs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use proc_macro2::{Span, TokenStream};
2+
use std::fs;
23
use std::path::{Path, PathBuf};
34
use syn::parse::{Error, Parse, ParseStream, Result};
45
use syn::punctuated::Punctuated;
56
use syn::{token, Token};
6-
use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackage, WorldId};
7+
use wit_bindgen_core::wit_parser::{self, PackageId, Resolve, UnresolvedPackage, WorldId};
78
use wit_bindgen_rust::Opts;
89

910
#[proc_macro]
@@ -32,6 +33,7 @@ impl Parse for Config {
3233
let mut opts = Opts::default();
3334
let mut world = None;
3435
let mut source = None;
36+
let mut substitutions = None;
3537

3638
if input.peek(token::Brace) {
3739
let content;
@@ -57,6 +59,24 @@ impl Parse for Config {
5759
}
5860
source = Some(Source::Inline(s.value()));
5961
}
62+
Opt::SubstitutionsPath(s) => {
63+
if substitutions.is_some() {
64+
return Err(Error::new(
65+
s.span(),
66+
"cannot specify second substitutions",
67+
));
68+
}
69+
substitutions = Some(Source::Path(s.value()));
70+
}
71+
Opt::SubstitutionsInline(s) => {
72+
if substitutions.is_some() {
73+
return Err(Error::new(
74+
s.span(),
75+
"cannot specify second substitutions",
76+
));
77+
}
78+
substitutions = Some(Source::Inline(s.value()));
79+
}
6080
Opt::UseStdFeature => opts.std_feature = true,
6181
Opt::RawStrings => opts.raw_strings = true,
6282
Opt::MacroExport => opts.macro_export = true,
@@ -71,8 +91,8 @@ impl Parse for Config {
7191
source = Some(Source::Path(input.parse::<syn::LitStr>()?.value()));
7292
}
7393
}
74-
let (resolve, pkg, files) =
75-
parse_source(&source).map_err(|err| Error::new(call_site, format!("{err:?}")))?;
94+
let (resolve, pkg, files) = parse_source(&source, &substitutions)
95+
.map_err(|err| Error::new(call_site, format!("{err:?}")))?;
7696
let world = resolve
7797
.select_world(pkg, world.as_deref())
7898
.map_err(|e| Error::new(call_site, format!("{e:?}")))?;
@@ -85,7 +105,10 @@ impl Parse for Config {
85105
}
86106
}
87107

88-
fn parse_source(source: &Option<Source>) -> anyhow::Result<(Resolve, PackageId, Vec<PathBuf>)> {
108+
fn parse_source(
109+
source: &Option<Source>,
110+
substitutions: &Option<Source>,
111+
) -> anyhow::Result<(Resolve, PackageId, Vec<PathBuf>)> {
89112
let mut resolve = Resolve::default();
90113
let mut files = Vec::new();
91114
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
@@ -108,7 +131,14 @@ fn parse_source(source: &Option<Source>) -> anyhow::Result<(Resolve, PackageId,
108131
Some(Source::Path(s)) => parse(&root.join(&s))?,
109132
None => parse(&root.join("wit"))?,
110133
};
111-
134+
match substitutions {
135+
Some(Source::Inline(s)) => wit_parser::expand(&mut resolve, toml::from_str(s)?)?,
136+
Some(Source::Path(s)) => wit_parser::expand(
137+
&mut resolve,
138+
toml::from_str(&fs::read_to_string(&root.join(&s))?)?,
139+
)?,
140+
None => (),
141+
}
112142
Ok((resolve, pkg, files))
113143
}
114144

@@ -146,12 +176,16 @@ mod kw {
146176
syn::custom_keyword!(world);
147177
syn::custom_keyword!(path);
148178
syn::custom_keyword!(inline);
179+
syn::custom_keyword!(substitutions_path);
180+
syn::custom_keyword!(substitutions_inline);
149181
}
150182

151183
enum Opt {
152184
World(syn::LitStr),
153185
Path(syn::LitStr),
154186
Inline(syn::LitStr),
187+
SubstitutionsPath(syn::LitStr),
188+
SubstitutionsInline(syn::LitStr),
155189
UseStdFeature,
156190
RawStrings,
157191
MacroExport,
@@ -171,6 +205,14 @@ impl Parse for Opt {
171205
input.parse::<kw::inline>()?;
172206
input.parse::<Token![:]>()?;
173207
Ok(Opt::Inline(input.parse()?))
208+
} else if l.peek(kw::substitutions_path) {
209+
input.parse::<kw::substitutions_path>()?;
210+
input.parse::<Token![:]>()?;
211+
Ok(Opt::SubstitutionsPath(input.parse()?))
212+
} else if l.peek(kw::substitutions_inline) {
213+
input.parse::<kw::substitutions_inline>()?;
214+
input.parse::<Token![:]>()?;
215+
Ok(Opt::SubstitutionsInline(input.parse()?))
174216
} else if l.peek(kw::world) {
175217
input.parse::<kw::world>()?;
176218
input.parse::<Token![:]>()?;

crates/test-rust-wasm/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ test = false
4747
[[bin]]
4848
name = "results"
4949
test = false
50+
51+
[[bin]]
52+
name = "wildcards"
53+
test = false
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include!("../../../../tests/runtime/wildcards/wasm.rs");
2+
3+
fn main() {}

src/bin/wit-bindgen.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{bail, Context, Result};
22
use clap::Parser;
33
use std::path::PathBuf;
4-
use std::str;
4+
use std::{fs, str};
55
use wit_bindgen_core::{wit_parser, Files, WorldGenerator};
66
use wit_parser::{Resolve, UnresolvedPackage};
77

@@ -78,6 +78,10 @@ struct Common {
7878
/// they're up-to-date with the source files.
7979
#[clap(long)]
8080
check: bool,
81+
82+
/// Path to template substitutions for expansion.
83+
#[clap(long)]
84+
expand: Option<PathBuf>,
8185
}
8286

8387
fn main() -> Result<()> {
@@ -143,6 +147,15 @@ fn gen_world(
143147
opts: &Common,
144148
files: &mut Files,
145149
) -> Result<()> {
150+
let substitutions = match &opts.expand {
151+
Some(path) => {
152+
let input =
153+
fs::read_to_string(path).context("failed to read substitutions from file")?;
154+
toml::from_str(&input).context("failed to parse substitutions from TOML")?
155+
}
156+
None => Default::default(),
157+
};
158+
146159
let mut resolve = Resolve::default();
147160
let pkg = if opts.wit.is_dir() {
148161
resolve.push_dir(&opts.wit)?.0
@@ -152,6 +165,8 @@ fn gen_world(
152165
&Default::default(),
153166
)?
154167
};
168+
169+
wit_parser::expand(&mut resolve, substitutions)?;
155170
let world = resolve.select_world(pkg, opts.world.as_deref())?;
156171
generator.generate(&resolve, world, files);
157172
Ok(())

tests/runtime/flavorful.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub struct MyImports {
99
errored: bool,
1010
}
1111

12-
impl imports::Imports for MyImports {
12+
impl imports::Host for MyImports {
1313
fn f_list_in_record1(&mut self, ty: imports::ListInRecord1Result) -> Result<()> {
1414
assert_eq!(ty.a, "list_in_record1");
1515
Ok(())

tests/runtime/lists.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use imports::*;
88
#[derive(Default)]
99
pub struct MyImports;
1010

11-
impl Imports for MyImports {
11+
impl Host for MyImports {
1212
fn empty_list_param(&mut self, a: Vec<u8>) -> Result<()> {
1313
assert_eq!(a, []);
1414
Ok(())

tests/runtime/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ mod smoke;
1818
mod strings;
1919
mod unions;
2020
mod variants;
21+
mod wildcards;
2122

2223
wasmtime::component::bindgen!("testwasi" in "crates/wasi_snapshot_preview1/wit");
2324

2425
#[derive(Default)]
2526
struct Wasi<T>(T);
2627

27-
impl<T> testwasi::Testwasi for Wasi<T> {
28+
impl<T> testwasi::Host for Wasi<T> {
2829
fn log(&mut self, bytes: Vec<u8>) -> Result<()> {
2930
std::io::stdout().write_all(&bytes)?;
3031
Ok(())

0 commit comments

Comments
 (0)