diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index 78a1197ad..603222a77 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -127,6 +127,10 @@ impl Parse for Config { opts.additional_derive_ignore = list.into_iter().map(|i| i.value()).collect() } + Opt::AdditionalTypeAttributes(list) => opts.additional_type_attributes = list, + Opt::AdditionalMemberAttributes(list) => { + opts.additional_member_attributes = list + } Opt::With(with) => opts.with.extend(with), Opt::GenerateAll => { opts.generate_all = true; @@ -312,6 +316,8 @@ mod kw { syn::custom_keyword!(export_prefix); syn::custom_keyword!(additional_derives); syn::custom_keyword!(additional_derives_ignore); + syn::custom_keyword!(additional_type_attributes); + syn::custom_keyword!(additional_member_attributes); syn::custom_keyword!(with); syn::custom_keyword!(generate_all); syn::custom_keyword!(type_section_suffix); @@ -394,6 +400,8 @@ enum Opt { // Parse as paths so we can take the concrete types/macro names rather than raw strings AdditionalDerives(Vec), AdditionalDerivesIgnore(Vec), + AdditionalTypeAttributes(Vec<(String, String)>), + AdditionalMemberAttributes(Vec<(String, String)>), With(HashMap), GenerateAll, TypeSectionSuffix(syn::LitStr), @@ -522,6 +530,26 @@ impl Parse for Opt { syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; Ok(Opt::AdditionalDerivesIgnore(list.iter().cloned().collect())) + } else if l.peek(kw::additional_type_attributes) { + input.parse::()?; + input.parse::()?; + let contents; + braced!(contents in input); + let fields: Punctuated<_, Token![,]> = + contents.parse_terminated(attr_map_field_parse, Token![,])?; + Ok(Opt::AdditionalTypeAttributes( + fields.into_iter().flatten().collect(), + )) + } else if l.peek(kw::additional_member_attributes) { + input.parse::()?; + input.parse::()?; + let contents; + braced!(contents in input); + let fields: Punctuated<_, Token![,]> = + contents.parse_terminated(attr_map_field_parse, Token![,])?; + Ok(Opt::AdditionalMemberAttributes( + fields.into_iter().flatten().collect(), + )) } else if l.peek(kw::with) { input.parse::()?; input.parse::()?; @@ -601,6 +629,30 @@ impl Parse for Opt { } } +// Parse one `"selector": [#[attr] ...]` entry into a pair per attribute. +fn attr_map_field_parse(input: ParseStream<'_>) -> Result> { + let selector = input.parse::()?; + input.parse::()?; + let contents; + let bracket = syn::bracketed!(contents in input); + let attrs = contents.call(syn::Attribute::parse_outer)?; + if !contents.is_empty() { + return Err(contents.error("expected an outer attribute, `#[...]`")); + } + // An empty list would flatten away and escape the unused-selector check. + if attrs.is_empty() { + return Err(Error::new( + bracket.span.join(), + "attribute list must not be empty", + )); + } + let selector = selector.value(); + Ok(attrs + .into_iter() + .map(|a| (selector.clone(), a.to_token_stream().to_string())) + .collect()) +} + fn with_field_parse(input: ParseStream<'_>) -> Result<(String, WithOption)> { let interface = input.parse::()?.value(); input.parse::()?; diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 34aaea0df..3efde07ad 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -682,6 +682,23 @@ extern crate std; /// // By default this set is empty. /// additional_derives: [PartialEq, Eq, Hash, Clone], /// +/// // Extra attributes to emit on specific generated types (records, variants, +/// // and enums), rather than on all types like `additional_derives`. A type +/// // is selected by its fully qualified name, written as in `with`. +/// // +/// // By default this map is empty. +/// additional_type_attributes: { +/// "my:pkg/types/my-record": [#[derive(serde::Serialize)]], +/// }, +/// +/// // Like `additional_type_attributes`, but for generated record fields and +/// // enum/variant cases, selected by `.member-name`. +/// // +/// // By default this map is empty. +/// additional_member_attributes: { +/// "my:pkg/types/my-record.my-field": [#[serde(rename = "mf")]], +/// }, +/// /// // When generating bindings for interfaces that are not defined in the /// // same package as `world`, this option can be used to either generate /// // those bindings or point to already generated bindings. diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 148249be0..e92cc9160 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -6,7 +6,8 @@ use crate::{ }; use anyhow::Result; use heck::*; -use std::collections::{BTreeMap, BTreeSet}; +use indexmap::IndexSet; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::Write as _; use std::mem; use wit_bindgen_core::abi::{self, AbiVariant, LiftLower}; @@ -2086,6 +2087,46 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) result } + /// Matching attributes, deduped in configured order. Selectors that matched + /// are recorded in `used` so `finish` can report the ones that did not. + fn matching_attrs( + entries: &[(String, String)], + used: &mut HashSet, + matches: impl Fn(&str) -> bool, + ) -> Vec { + let mut attrs = IndexSet::new(); + for (sel, attr) in entries.iter().filter(|(sel, _)| matches(sel)) { + attrs.insert(attr.clone()); + used.insert(sel.clone()); + } + attrs.into_iter().collect() + } + + fn additional_type_attrs(&mut self, type_name: &str) -> Vec { + Self::matching_attrs( + &self.r#gen.opts.additional_type_attributes, + &mut self.r#gen.used_type_attr_selectors, + |sel| sel == type_name, + ) + } + + /// `member` is a record field or an enum/variant case, selected by + /// `.`. Member names contain no `.`, so the last one + /// separates the two halves. + fn additional_member_attrs(&mut self, type_name: &str, member: &str) -> Vec { + Self::matching_attrs( + &self.r#gen.opts.additional_member_attributes, + &mut self.r#gen.used_member_attr_selectors, + |sel| sel.rsplit_once('.') == Some((type_name, member)), + ) + } + + fn push_attrs(&mut self, attrs: &[String]) { + for attr in attrs { + uwriteln!(self.src, "{attr}"); + } + } + fn print_typedef_record(&mut self, id: TypeId, record: &Record, docs: &Docs) { let info = self.info(id); // We use a BTree set to make sure we don't have any duplicates and we have a stable order @@ -2096,6 +2137,14 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); + // Computed once, then emitted on every ownership mode below. + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); + let field_attrs: Vec> = record + .fields + .iter() + .map(|f| self.additional_member_attrs(&type_name, &f.name)) + .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); let mut derives = BTreeSet::new(); @@ -2118,11 +2167,13 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n") } + self.push_attrs(&injected_attrs); self.push_str(&format!("pub struct {name}")); self.print_generics(mode.lifetime); self.push_str(" {\n"); - for field in record.fields.iter() { + for (field, attrs) in record.fields.iter().zip(&field_attrs) { self.rustdoc(&field.docs); + self.push_attrs(attrs); self.push_str("pub "); self.push_str(&to_rust_ident(&field.name)); self.push_str(": "); @@ -2185,7 +2236,7 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) variant .cases .iter() - .map(|c| (c.name.to_upper_camel_case(), &c.docs, c.ty.as_ref())), + .map(|c| (c.name.clone(), &c.docs, c.ty.as_ref())), docs, ); } @@ -2207,6 +2258,13 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); + let case_attrs: Vec> = cases + .clone() + .into_iter() + .map(|(case_name, _, _)| self.additional_member_attrs(&type_name, &case_name)) + .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); let mut derives = BTreeSet::new(); @@ -2228,12 +2286,14 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n") } + self.push_attrs(&injected_attrs); self.push_str(&format!("pub enum {name}")); self.print_generics(mode.lifetime); self.push_str(" {\n"); - for (case_name, docs, payload) in cases.clone() { + for ((case_name, docs, payload), attrs) in cases.clone().into_iter().zip(&case_attrs) { self.rustdoc(docs); - self.push_str(&case_name); + self.push_attrs(attrs); + self.push_str(&case_name.to_upper_camel_case()); if let Some(ty) = payload { self.push_str("("); let mode = self.filter_mode(ty, mode); @@ -2250,7 +2310,7 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) cases .clone() .into_iter() - .map(|(name, _docs, ty)| (name, ty)), + .map(|(name, _docs, ty)| (name.to_upper_camel_case(), ty)), ); if info.error { @@ -2343,24 +2403,13 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } } - fn print_typedef_enum( - &mut self, - id: TypeId, - name: &str, - enum_: &Enum, - docs: &Docs, - attrs: &[String], - case_attr: Box String>, - ) where - Self: Sized, - { + fn print_typedef_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { let info = self.info(id); let name = to_upper_camel_case(name); self.rustdoc(docs); - for attr in attrs { - self.push_str(&format!("{attr}\n")); - } + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); self.push_str("#[repr("); self.int_repr(enum_.tag()); self.push_str(")]\n"); @@ -2382,10 +2431,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n"); + self.push_attrs(&injected_attrs); self.push_str(&format!("pub enum {name} {{\n")); for case in enum_.cases.iter() { self.rustdoc(&case.docs); - self.push_str(&case_attr(case)); + let case_attrs = self.additional_member_attrs(&type_name, &case.name); + self.push_attrs(&case_attrs); self.push_str(&case.name.to_upper_camel_case()); self.push_str(",\n"); } @@ -2967,7 +3018,7 @@ impl<'a> {camel}Borrow<'a>{{ } fn type_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { - self.print_typedef_enum(id, name, enum_, docs, &[], Box::new(|_| String::new())); + self.print_typedef_enum(id, name, enum_, docs); let name = to_upper_camel_case(name); let mut cases = String::new(); diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index e7f4a36ed..dfe8c2623 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -42,6 +42,9 @@ pub struct RustWasm { // Track which interfaces and types are generated. Remapped interfaces and types provided via `with` // are required to be used. generated_types: HashSet, + // Attribute selectors that matched something, so `finish` can reject the rest. + used_type_attr_selectors: HashSet, + used_member_attr_selectors: HashSet, world: Option, rt_module: IndexSet, @@ -139,6 +142,19 @@ fn parse_with(s: &str) -> Result<(String, WithOption), String> { Ok((k.to_string(), v)) } +// Split on the first `=` only, so the attribute may itself contain `=`. +#[cfg(feature = "clap")] +fn parse_attribute(s: &str) -> Result<(String, String), String> { + let (sel, attr) = s + .split_once('=') + .ok_or_else(|| format!("expected string of form `=`; got `{s}`"))?; + // An empty attribute would mark the selector used and escape the unused check. + if attr.trim().is_empty() { + return Err(format!("attribute must not be empty; got `{s}`")); + } + Ok((sel.to_string(), attr.to_string())) +} + #[derive(Default, Debug, Clone)] #[cfg_attr(feature = "clap", derive(clap::Parser))] #[cfg_attr( @@ -234,6 +250,34 @@ pub struct Opts { #[cfg_attr(feature = "clap", arg(long, value_name = "NAME"))] pub additional_derive_ignore: Vec, + /// Extra attributes to emit on specific generated types, rather than on all + /// types like `additional_derive_attributes`. + /// + /// Each entry pairs a selector with an attribute. A selector is a type's + /// fully qualified name, written as in `with`, so `my:pkg/types/point`, + /// carrying `@version` when the package is versioned. A selector matching + /// nothing is an error, as with `with`. + /// + /// Only records, variants, and enums are covered. Attributes are emitted + /// verbatim on every form of the type, including the borrowed form under + /// `ownership: Borrowing`, so an owned-only derive fails to compile there. + /// + /// In a CLI, this flag can be specified multiple times as + /// `selector=attribute`. + #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] + pub additional_type_attributes: Vec<(String, String)>, + + /// Extra attributes to emit on specific generated record fields and + /// enum/variant cases. + /// + /// As `additional_type_attributes`, except the selector is a type's fully + /// qualified name, a `.`, and the member name. + /// + /// In a CLI, this flag can be specified multiple times as + /// `selector=attribute`. + #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] + pub additional_member_attributes: Vec<(String, String)>, + /// Remapping of wit import interface and type names to Rust module names /// and types. /// @@ -1128,6 +1172,18 @@ impl WorldGenerator for RustWasm { self.opts.additional_derive_ignore ); } + for (selector, attr) in self.opts.additional_type_attributes.iter() { + uwriteln!( + self.src_preamble, + "// * additional type attribute {selector:?} = {attr:?}" + ); + } + for (selector, attr) in self.opts.additional_member_attributes.iter() { + uwriteln!( + self.src_preamble, + "// * additional member attribute {selector:?} = {attr:?}" + ); + } for (k, v) in self.opts.with.iter() { uwriteln!(self.src_preamble, "// * with {k:?} = {v}"); } @@ -1534,6 +1590,29 @@ impl WorldGenerator for RustWasm { bail!("unused remappings provided via `with`: {unused_keys:?}"); } + let mut unused_selectors = self + .opts + .additional_type_attributes + .iter() + .map(|(sel, _)| sel) + .filter(|sel| !self.used_type_attr_selectors.contains(*sel)) + .chain( + self.opts + .additional_member_attributes + .iter() + .map(|(sel, _)| sel) + .filter(|sel| !self.used_member_attr_selectors.contains(*sel)), + ) + .collect::>(); + unused_selectors.sort(); + unused_selectors.dedup(); + if !unused_selectors.is_empty() { + bail!( + "unused selectors provided via `additional_type_attributes` / \ + `additional_member_attributes`: {unused_selectors:?}" + ); + } + // Error about unused async configuration to help catch configuration // errors. self.opts.async_.ensure_all_used()?; diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index acc068a96..e8046ceaf 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -301,3 +301,105 @@ mod merge_structurally_equal_types { merge_structurally_equal_types: true }); } + +mod targeted_attributes { + wit_bindgen::generate!({ + inline: r#" + package test:attrs; + + interface iface { + record point { x: u32, y: u32 } + enum color { red, green, blue } + variant shape { circle(u32), square(point), a-b } + + // used as both return and param, so `duplicate_if_necessary` generates + // an owned `String` form and a borrowed `&str` form + record label { text: string } + + paint: func(c: color) -> color; + draw: func(s: shape) -> shape; + round: func(p: point) -> point; + make-label: func() -> label; + use-label: func(l: label) -> u32; + } + + world test { + import iface; + } + "#, + generate_all, + ownership: Borrowing { duplicate_if_necessary: true }, + additional_type_attributes: { + // two entries for one selector, both apply + "test:attrs/iface/point": [#[derive(PartialEq, Eq)] #[derive(Hash)]], + "test:attrs/iface/color": [#[doc = "a primary color"] #[derive(Hash)]], + "test:attrs/iface/shape": [#[derive(PartialEq)]], + "test:attrs/iface/label": [#[derive(PartialEq, Eq)]], + }, + additional_member_attributes: { + "test:attrs/iface/point.x": [#[allow(dead_code)]], + "test:attrs/iface/color.red": [#[doc = "the primary color"]], + "test:attrs/iface/shape.circle": [#[doc = "a round shape"]], + // `a-b` upper-camels to `AB`, so this matches only via the raw wit name + "test:attrs/iface/shape.a-b": [#[allow(dead_code)]], + "test:attrs/iface/label.text": [#[allow(dead_code)]], + }, + }); + + #[test] + fn injected_derives_are_usable() { + use test::attrs::iface::{Color, LabelParam, LabelResult, Point, Shape}; + + let a = Point { x: 1, y: 2 }; + assert_eq!(a, a.clone()); + let mut set = std::collections::HashSet::new(); + set.insert(a); + assert!(set.contains(&Point { x: 1, y: 2 })); + + let mut colors = std::collections::HashSet::new(); + colors.insert(Color::Red); + assert!(colors.contains(&Color::Red)); + assert!(!colors.contains(&Color::Blue)); + + assert_eq!(Shape::Circle(3), Shape::Circle(3).clone()); + assert_ne!(Shape::Circle(3), Shape::Circle(4)); + + // both the owned `String` form and the borrowed `&str` form got the derives + assert_eq!( + LabelResult { text: "hi".into() }, + LabelResult { text: "hi".into() } + ); + assert_eq!(LabelParam { text: "hi" }, LabelParam { text: "hi" }); + assert_ne!(LabelParam { text: "hi" }, LabelParam { text: "bye" }); + } +} + +// Guards where `@version` lands in a qualified selector: after the interface, +// before the type. +mod versioned_selectors { + wit_bindgen::generate!({ + inline: r#" + package test:hier@1.2.3; + interface types { + record alpha { x: u32 } + get-alpha: func() -> alpha; + } + world w { import types; } + "#, + generate_all, + additional_type_attributes: { + "test:hier/types@1.2.3/alpha": [#[derive(PartialEq, Eq)] #[derive(PartialOrd, Ord)]], + }, + additional_member_attributes: { + "test:hier/types@1.2.3/alpha.x": [#[allow(dead_code)]], + }, + }); + + #[test] + fn versioned_selectors_resolve() { + use test::hier::types::Alpha; + + assert_eq!(Alpha { x: 1 }, Alpha { x: 1 }); + assert!(Alpha { x: 1 } < Alpha { x: 2 }); + } +}