Skip to content

Commit f7f169c

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 f4cbfaa commit f7f169c

File tree

12 files changed

+334
-116
lines changed

12 files changed

+334
-116
lines changed

Cargo.lock

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

Cargo.toml

+11-3
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 = "7", 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

+1
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

+47-5
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

+4
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
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

+16-1
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/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ 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

tests/runtime/wildcards.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use anyhow::Result;
2+
use wasmtime::Store;
3+
4+
wasmtime::component::bindgen!(in "tests/runtime/wildcards");
5+
6+
#[derive(Default)]
7+
struct Host;
8+
9+
impl imports::Host for Host {}
10+
11+
struct Match(u32);
12+
13+
impl imports::WildcardMatch<Host> for Match {
14+
fn call(&self, _host: &mut Host, _name: &str) -> Result<u32> {
15+
Ok(self.0)
16+
}
17+
}
18+
19+
#[test]
20+
fn run() -> Result<()> {
21+
eprintln!("yossa hello");
22+
crate::run_test(
23+
"wildcards",
24+
|linker| {
25+
eprintln!("yossa add to linker");
26+
Wildcards::add_to_linker(
27+
linker,
28+
WildcardMatches {
29+
imports: vec![("a", Match(42)), ("b", Match(43)), ("c", Match(44))],
30+
},
31+
|x| &mut x.0,
32+
)
33+
},
34+
|store, component, linker| Wildcards::instantiate(store, component, linker),
35+
run_test,
36+
)
37+
}
38+
39+
fn run_test(wildcards: Wildcards, store: &mut Store<crate::Wasi<Host>>) -> Result<()> {
40+
for (name, value) in [("x", 42), ("y", 43), ("z", 44)] {
41+
assert_eq!(
42+
value,
43+
wildcards
44+
.exports
45+
.get_wildcard_match(name)
46+
.unwrap()
47+
.call(&mut *store)?
48+
);
49+
}
50+
51+
Ok(())
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[wildcards]
2+
imports = ["a", "b", "c"]
3+
exports = ["x", "y", "z"]

tests/runtime/wildcards/wasm.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
wit_bindgen::generate!({
2+
world: "world",
3+
path: "../../tests/runtime/wildcards",
4+
substitutions_path: "../../tests/runtime/wildcards/substitutions.toml",
5+
});
6+
7+
struct Exports;
8+
9+
export_wildcards!(Exports);
10+
11+
impl exports::Exports for Exports {
12+
fn x() -> u32 {
13+
imports::a()
14+
}
15+
fn y() -> u32 {
16+
imports::b()
17+
}
18+
fn z() -> u32 {
19+
imports::c()
20+
}
21+
}

tests/runtime/wildcards/world.wit

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
interface foo {
2+
*: func() -> u32
3+
}
4+
5+
default world wildcards {
6+
import imports: self.foo
7+
export exports: self.foo
8+
}

0 commit comments

Comments
 (0)