diff --git a/crates/libs/rdl/rdl.md b/crates/libs/rdl/rdl.md index e732a0d89a..f8419dceae 100644 --- a/crates/libs/rdl/rdl.md +++ b/crates/libs/rdl/rdl.md @@ -250,6 +250,51 @@ union SprocketHandle { --- +#### Nested Types + +Some Windows metadata types contain anonymous inner structs or unions (nested types). The `#[nested(OuterTypeName)]` pseudo-attribute marks a struct or union as a nested type within the named enclosing type, enabling faithful roundtripping of nested types from Windows metadata. + +**Rules:** +- The outer type must be declared **before** the nested type in the file (in alphabetical name order, as the RDL writer produces). +- `#[nested]` can be combined with `#[packed(N)]` and other attributes. +- Multiple levels of nesting are supported by chaining: `A` contains `A_0`, which contains `A_0_0`, and so on. + +**Syntax:** + +```rust +struct OuterName { + AnonymousField: InnerName, +} +#[nested(OuterName)] +union InnerName { + FieldA: u32, + FieldB: u16, +} +``` + +**Example:** + +```rust +#[win32] +mod Contoso { + mod Sprockets { + struct SprocketBuffer { + size: u32, + Anonymous: SprocketBuffer_0, + } + #[nested(SprocketBuffer)] + union SprocketBuffer_0 { + data: u32, + raw: [u8; 4], + } + } +} +``` + +The `#[nested]` attribute is a pseudo-attribute: it is not stored as a custom metadata attribute. Instead it causes the RDL reader to set the `NestedPublic` flag on the inner TypeDef and emit a `NestedClass` metadata record linking the inner type to the outer type. + +--- + #### Interfaces Interfaces define contracts for method implementations. diff --git a/crates/libs/rdl/src/reader/mod.rs b/crates/libs/rdl/src/reader/mod.rs index 218e0f9c47..8b71f57890 100644 --- a/crates/libs/rdl/src/reader/mod.rs +++ b/crates/libs/rdl/src/reader/mod.rs @@ -34,6 +34,7 @@ use r#const::*; use r#enum::*; use r#fn::*; use r#struct::*; +use std::collections::HashMap; use union::*; use windows_metadata as metadata; @@ -113,6 +114,8 @@ impl Reader { let mut output = metadata::writer::File::new(assembly_name); output.set_reference(reference); + let mut typedef_ids: HashMap = HashMap::new(); + for (namespace, members) in &index.namespaces { for variants in members.types.values() { for (file, item) in variants { @@ -124,6 +127,7 @@ impl Reader { namespace, name: &name, generics: vec![], + typedef_ids: &mut typedef_ids, }; match item { Item::Attribute(ty) => encoder.encode_attribute(ty), @@ -166,6 +170,7 @@ impl Reader { namespace, name, generics: vec![], + typedef_ids: &mut typedef_ids, } .encode_fn(ty)?; } @@ -183,6 +188,7 @@ impl Reader { namespace, name, generics: vec![], + typedef_ids: &mut typedef_ids, } .encode_const(ty)?; } @@ -385,6 +391,7 @@ struct Encoder<'a> { namespace: &'a str, name: &'a str, generics: Vec, + typedef_ids: &'a mut HashMap, } impl Encoder<'_> { @@ -421,6 +428,25 @@ impl Encoder<'_> { Ok(None) } + /// Parse an optional `#[nested(TypeName)]` attribute from `attrs`. Returns + /// `Some(ident)` with the outer type's name if the attribute is present and + /// well-formed, `None` if absent, or an error if the attribute is malformed. + fn read_nested(&self, attrs: &[syn::Attribute]) -> Result, Error> { + for attr in attrs { + if !attr.path().is_ident("nested") { + continue; + } + + let Ok(ident) = attr.parse_args::() else { + return self.err(attr, "`nested` attribute requires a type name"); + }; + + return Ok(Some(ident)); + } + + Ok(None) + } + fn encode_type(&self, ty: &syn::Type) -> Result { match ty { syn::Type::Path(ty) => self.encode_type_path(ty), diff --git a/crates/libs/rdl/src/reader/struct.rs b/crates/libs/rdl/src/reader/struct.rs index 87963ad82e..8f113feffa 100644 --- a/crates/libs/rdl/src/reader/struct.rs +++ b/crates/libs/rdl/src/reader/struct.rs @@ -35,17 +35,41 @@ impl syn::parse::Parse for Struct { impl Encoder<'_> { pub fn encode_struct(&mut self, item: &Struct) -> Result<(), Error> { - let type_def = - self.encode_struct_or_union(&item.name.to_string(), item.winrt, false, &item.fields)?; + let nested_in = self.read_nested(&item.attrs)?; + let type_def = self.encode_struct_or_union( + &item.name.to_string(), + item.winrt, + false, + &item.fields, + nested_in.as_ref().map(|id| id.to_string()).as_deref(), + )?; if let Some(packing_size) = self.read_packed(&item.attrs)? { self.output.ClassLayout(type_def, packing_size, 0); } + if let Some(outer_ident) = &nested_in { + let outer_name = outer_ident.to_string(); + match self.typedef_ids.get(&outer_name).copied() { + Some(outer_id) => self.output.NestedClass(type_def, outer_id), + None => { + return self.err( + outer_ident, + &format!( + "`nested` outer type `{outer_name}` not found; \ + ensure the outer type appears before the nested type" + ), + ); + } + } + } + + self.typedef_ids.insert(item.name.to_string(), type_def); + self.encode_attrs( metadata::writer::HasAttribute::TypeDef(type_def), &item.attrs, - &["packed"], + &["packed", "nested"], ) } @@ -55,6 +79,7 @@ impl Encoder<'_> { winrt: bool, is_union: bool, fields: &[Field], + nested_in: Option<&str>, ) -> Result { let value_type = self.output.TypeRef("System", "ValueType"); @@ -64,17 +89,30 @@ impl Encoder<'_> { metadata::TypeAttributes::SequentialLayout }; + let visibility_flag = if nested_in.is_some() { + metadata::TypeAttributes::NestedPublic + } else { + metadata::TypeAttributes::Public + }; + let flags = layout_flag | metadata::TypeAttributes::Sealed - | metadata::TypeAttributes::Public + | visibility_flag | if winrt { metadata::TypeAttributes::WindowsRuntime } else { metadata::TypeAttributes::default() }; + // Nested types are stored with an empty namespace in ECMA-335 metadata. + let namespace = if nested_in.is_some() { + "" + } else { + self.namespace + }; + let type_def = self.output.TypeDef( - self.namespace, + namespace, item_name, metadata::writer::TypeDefOrRef::TypeRef(value_type), flags, diff --git a/crates/libs/rdl/src/reader/union.rs b/crates/libs/rdl/src/reader/union.rs index f702b600e4..124c2a6b3b 100644 --- a/crates/libs/rdl/src/reader/union.rs +++ b/crates/libs/rdl/src/reader/union.rs @@ -31,17 +31,41 @@ impl syn::parse::Parse for Union { impl Encoder<'_> { pub fn encode_union(&mut self, item: &Union) -> Result<(), Error> { - let type_def = - self.encode_struct_or_union(&item.name.to_string(), false, true, &item.fields)?; + let nested_in = self.read_nested(&item.attrs)?; + let type_def = self.encode_struct_or_union( + &item.name.to_string(), + false, + true, + &item.fields, + nested_in.as_ref().map(|id| id.to_string()).as_deref(), + )?; if let Some(packing_size) = self.read_packed(&item.attrs)? { self.output.ClassLayout(type_def, packing_size, 0); } + if let Some(outer_ident) = &nested_in { + let outer_name = outer_ident.to_string(); + match self.typedef_ids.get(&outer_name).copied() { + Some(outer_id) => self.output.NestedClass(type_def, outer_id), + None => { + return self.err( + outer_ident, + &format!( + "`nested` outer type `{outer_name}` not found; \ + ensure the outer type appears before the nested type" + ), + ); + } + } + } + + self.typedef_ids.insert(item.name.to_string(), type_def); + self.encode_attrs( metadata::writer::HasAttribute::TypeDef(type_def), &item.attrs, - &["packed"], + &["packed", "nested"], ) } } diff --git a/crates/libs/rdl/src/writer/struct.rs b/crates/libs/rdl/src/writer/struct.rs index fa2ab84e42..34f89be2e9 100644 --- a/crates/libs/rdl/src/writer/struct.rs +++ b/crates/libs/rdl/src/writer/struct.rs @@ -132,6 +132,7 @@ fn collect_nested( // so we don't emit it twice when the nested type already has one). let arch_attr = write_arch_attr(effective_arches); let packed_attr = write_packed_attr_value(effective_packing); + let outer_ident = write_ident(outer_flat_name); let custom_attrs = write_custom_attributes_except( nested.attributes(), namespace, @@ -141,7 +142,7 @@ fn collect_nested( output.push(( flat_name, - quote! { #packed_attr #arch_attr #(#custom_attrs)* #keyword #name_ident { #(#fields)* } }, + quote! { #packed_attr #arch_attr #[nested(#outer_ident)] #(#custom_attrs)* #keyword #name_ident { #(#fields)* } }, )); } diff --git a/crates/tests/libs/rdl/tests/nested-arches.rdl b/crates/tests/libs/rdl/tests/nested-arches.rdl index 4b4166addb..aae3a28159 100644 --- a/crates/tests/libs/rdl/tests/nested-arches.rdl +++ b/crates/tests/libs/rdl/tests/nested-arches.rdl @@ -19,22 +19,26 @@ mod Windows { Anonymous: SLIST_HEADER_0, } #[Windows::Win32::Foundation::Metadata::SupportedArchitecture(Arm64)] + #[nested(SLIST_HEADER)] struct SLIST_HEADER_0 { Alignment: u64, Region: u64, } #[Windows::Win32::Foundation::Metadata::SupportedArchitecture(X64)] + #[nested(SLIST_HEADER)] struct SLIST_HEADER_0 { Alignment: u64, Region: u64, } #[Windows::Win32::Foundation::Metadata::SupportedArchitecture(X86)] + #[nested(SLIST_HEADER)] struct SLIST_HEADER_0 { Next: SINGLE_LIST_ENTRY, Depth: u16, CpuId: u16, } #[Windows::Win32::Foundation::Metadata::SupportedArchitecture(Arm64)] + #[nested(SLIST_HEADER)] struct SLIST_HEADER_1 { #[Windows::Win32::Foundation::Metadata::NativeBitfield("Depth", 0, 16)] #[Windows::Win32::Foundation::Metadata::NativeBitfield("Sequence", 16, 48)] @@ -44,6 +48,7 @@ mod Windows { _bitfield2: u64, } #[Windows::Win32::Foundation::Metadata::SupportedArchitecture(X64)] + #[nested(SLIST_HEADER)] struct SLIST_HEADER_1 { #[Windows::Win32::Foundation::Metadata::NativeBitfield("Depth", 0, 16)] #[Windows::Win32::Foundation::Metadata::NativeBitfield("Sequence", 16, 48)] diff --git a/crates/tests/libs/rdl/tests/nested-packing.rdl b/crates/tests/libs/rdl/tests/nested-packing.rdl index 30ae46374d..852266a377 100644 --- a/crates/tests/libs/rdl/tests/nested-packing.rdl +++ b/crates/tests/libs/rdl/tests/nested-packing.rdl @@ -10,6 +10,7 @@ mod Windows { Anonymous: BTH_INFO_RSP_0, } #[packed(1)] + #[nested(BTH_INFO_RSP)] union BTH_INFO_RSP_0 { connectionlessMTU: u16, data: [u8; 44], diff --git a/crates/tests/libs/rdl/tests/panic.rs b/crates/tests/libs/rdl/tests/panic.rs index e27ef0c1ae..89cf0ad6eb 100644 --- a/crates/tests/libs/rdl/tests/panic.rs +++ b/crates/tests/libs/rdl/tests/panic.rs @@ -872,3 +872,42 @@ mod Test { "#, ) } + +#[test] +#[should_panic(expected = "error: `nested` attribute requires a type name\n --> .rdl:4:5")] +fn nested_no_args_errors() { + should_panic( + r#" +#[win32] +mod Test { + #[nested] + union Foo { + a: u32, + } +} + "#, + ) +} + +#[test] +#[should_panic( + expected = "error: `nested` outer type `OuterStruct` not found; ensure the outer type appears before the nested type\n --> .rdl:4:14" +)] +fn nested_outer_not_found_errors() { + // "AnonymousUnion" sorts before "OuterStruct", so the inner type is encoded + // before the outer — this should produce a clear error. + should_panic( + r#" +#[win32] +mod Test { + #[nested(OuterStruct)] + union AnonymousUnion { + a: u32, + } + struct OuterStruct { + Anonymous: AnonymousUnion, + } +} + "#, + ) +} diff --git a/crates/tests/libs/rdl/tests/roundtrip/nested-flat.rdl b/crates/tests/libs/rdl/tests/roundtrip/nested-flat.rdl new file mode 100644 index 0000000000..7b5fea77f8 --- /dev/null +++ b/crates/tests/libs/rdl/tests/roundtrip/nested-flat.rdl @@ -0,0 +1,17 @@ +#[win32] +mod Test { + struct OUTER { + Anonymous: OUTER_0, + value: u32, + } + #[nested(OUTER)] + union OUTER_0 { + a: OUTER_0_0, + b: u16, + } + #[nested(OUTER_0)] + struct OUTER_0_0 { + x: u32, + y: u32, + } +}