Skip to content

Flatten anonymous nested struct/union fields (#408)#1721

Merged
jevansaks merged 6 commits into
microsoft:mainfrom
JeremyKuhne:feature/408-flatten-anonymous-structs
Jun 12, 2026
Merged

Flatten anonymous nested struct/union fields (#408)#1721
jevansaks merged 6 commits into
microsoft:mainfrom
JeremyKuhne:feature/408-flatten-anonymous-structs

Conversation

@JeremyKuhne

@JeremyKuhne JeremyKuhne commented Jun 11, 2026

Copy link
Copy Markdown
Member

Addresses #408.

What

Adds an opt-out generator option, FlattenNestedAnonymousTypes, that surfaces fields nested inside anonymous structs/unions as [UnscopedRef] ref-returning properties on the declaring struct. This lets callers write value.field instead of value.Anonymous.Anonymous.field, with full read / write / pointer support — the shape I suggested in the issue and that dotnet/winforms' VARIANT uses.

SYSTEM_INFO si = default;
si.wProcessorArchitecture = PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_AMD64; // was: si.Anonymous.Anonymous.wProcessorArchitecture

Generated shape:

/// <inheritdoc cref="_Anonymous_e__Union._Anonymous_e__Struct.wProcessorArchitecture"/>
[UnscopedRef]
internal ref PROCESSOR_ARCHITECTURE wProcessorArchitecture => ref this.Anonymous.Anonymous.wProcessorArchitecture;

Key points to review

  • Opt-out, default true.
  • Gated on C# 11+ (UnscopedRefAttribute). On older language versions no accessors are emitted and the Anonymous holder remains the only path. Works on .NET Framework via the existing polyfill.
  • Only anonymous holders are flattened. Detection requires both a holder field named Anonymous/Anonymous1/… and that its type is a nested type of the struct. Named nested unions (e.g. KEY_EVENT_RECORD.uChar) are deliberately left alone.
  • Recursive, full-path accessors built from the outer struct (e.g. this.Anonymous.Anonymous.field), so every nesting level stays usable. Collisions and already-reserved names are skipped.
  • Docs via <inheritdoc>. Accessors inherit from the underlying field. Because nested anonymous leaf fields were previously undocumented, this also propagates the declaring type's API-doc field summaries down to those leaf fields so the inherited docs resolve. This required a small refactor of Generator.ApiDocs.csnote: that file's large diff is relocation, not behavior change (EmitDoc/EmitLine promoted to private static methods; ApplyFieldDocs extracted). Existing doc behavior is unchanged (full suite green).

Two bugs caught & fixed during development

A broad stress-test (enabling the option across the whole GenerationSandbox.Tests surface) surfaced two real issues, each now covered by a dedicated regression test in both marshaling modes:

  1. CS8151 — explicit-layout unions force their fields to be generated without marshaling; the accessor must decode the leaf with that same context or the ref return type mismatches. (Guard: ELEMDESC.)
  2. CS1574 — in marshaling mode, managed nested types get the _unmanaged suffix, so the inheritdoc cref must use the mangled name. (Guard: PROPVARIANT, 3 levels deep.)

Testing

  • Full Microsoft.Windows.CsWin32.Tests suite: 781 passed, 0 failed (net8.0).
  • 15 new flatten unit cases: ref shape, [UnscopedRef], numbered/multiple holders (DECIMAL), unsafe pointer leaves (VARDESC), public visibility, named-union exclusion, C#10 no-op, no-op for non-anonymous structs (LUID), skipped reinterpreted array leaves (BLUETOOTH_ADDRESS), doc inheritance, plus the two regression guards.
  • 3 runtime functional tests in GenerationSandbox.Tests (SYSTEM_INFO read/write aliasing + pointer use); whole sandbox (139 tests) compiles & passes across net472/net8/net9/net10.
  • Generator builds with 0 warnings.

Notes for reviewers

  • Follow-up (Phase 2, not in this PR): forward computed members (bitfield / associated-enum properties) as value get/set accessors for fuller coverage.

Add an opt-in FlattenNestedAnonymousTypes generator option that surfaces fields nested within anonymous structs and unions as [UnscopedRef] ref-returning properties on the declaring struct, so callers can write value.field instead of value.Anonymous.Anonymous.field. The accessors support read, write, and pointer use, and inherit documentation from the underlying field via <inheritdoc/>.

Gated on C# 11+ (UnscopedRefAttribute). Named nested types are left alone; only fields reached exclusively through Anonymous holders are flattened. Leaf fields in nested anonymous types now receive documentation propagated from the declaring type's API docs so the inherited docs resolve.
Cover numbered/multiple anonymous holders (DECIMAL), unsafe pointer leaves (VARDESC), public visibility, and regression guards for the explicit-layout field-type context (ELEMDESC/CS8151) and the deeply-nested managed-union inheritdoc cref suffix (PROPVARIANT/CS1574).
…osoft#408)

Verify the option is a no-op for a struct without anonymous members (LUID), and that a fixed-length array leaf inside an anonymous union (BLUETOOTH_ADDRESS.rgBytes) is not surfaced as a ref accessor while its scalar sibling (ullLong) is.
Flattening must hoist only anonymous union/struct grouping fields, never the members of a nested struct *value* reached through them.

- INPUT: union of struct values (mi/ki/hi) are surfaced as whole refs; their inner fields (dx/dy/...) are not flattened onto INPUT.

- PROPVARIANT: the DECIMAL value (decVal) is surfaced as a single ref; DECIMAL's own union members (Lo64/scale/...) are flattened only onto DECIMAL, not transitively onto PROPVARIANT.
…it exposed

Flip GeneratorOptions.FlattenNestedAnonymousTypes (and settings.schema.json) default to true and update the option/schema docs accordingly.

Turning the option on across the full metadata surface revealed two latent bugs in the flattened-accessor generator, both now fixed:

- CS8151: a nested *managed* struct value reached through an anonymous union (e.g. MSP_EVENT_INFO) was given a fully-qualified ref-return type that applied the _unmanaged suffix to every ancestor, naming the unmanaged twin instead of the type actually reachable through this.Anonymous. The leaf type is now referenced relative to the declaring struct's container chain.

- CS0612: a flattened accessor whose body references an [Obsolete] leaf field (e.g. PEER_GROUP_EVENT_DATA) is now itself marked [Obsolete], matching how the existing field/property generation handles obsolete members.

Adds explicit regression tests for both cases (option set explicitly so they survive a future default change).
@JeremyKuhne JeremyKuhne marked this pull request as ready for review June 11, 2026 21:25
The CLI/source-generator generation path (CsWin32Generator.Tests, exercised by the Heavy FullGen CI jobs) defaults UseComSourceGenerators=true. In that mode an anonymous holder field on a managed struct is typed as the *unmanaged* twin of its nested union (e.g. MSP_EVENT_INFO_unmanaged._Anonymous_e__Union_unmanaged), and each twin declares its own nested leaf types.

The previous fix reconstructed the leaf ref-return type from the managed-twin container chain, which did not match the type actually reached through this.Anonymous in source-generator mode, reintroducing CS8151 (MSP_EVENT_INFO, VDS_ASYNC_OUTPUT, SSVARIANT, etc.).

Type each flattened accessor relative to the holder field's *actual* declared type (captured from the already-generated field declaration) so the ref-return type always selects the correct managed/unmanaged twin regardless of generation mode. Adds a source-generator regression test covering these structs.
@jevansaks jevansaks merged commit 39111b6 into microsoft:main Jun 12, 2026
14 checks passed
jevansaks pushed a commit that referenced this pull request Jun 13, 2026
* Flatten anonymous bitfield sub-properties (#408 phase 2)

Phase 1 (#1721) surfaces fields nested in anonymous structs/unions as [UnscopedRef] ref accessors, but only the raw bitfield backing field was reachable that way. This forwards the computed bitfield sub-properties (e.g. PSAPI_WORKING_SET_EX_BLOCK's Valid/ShareCount/Win32Protection) as value get/set properties on the declaring struct, so they can be read and written directly as value.Field instead of value.Anonymous.Anonymous.Field.

Each forwarded property delegates to the nested property (readonly get => this.Anonymous...Prop; set => ... = value) and inherits its docs via <inheritdoc>. The projected type is computed from the bitfield width (1 bit => bool, else smallest fitting integer of the backing field's signedness), matching the nested property exactly, so no narrowing occurs even when the backing field is wider (e.g. nuint backing, byte/ushort sub-properties). The raw backing field continues to be surfaced as a ref (phase 1 behavior is unchanged; this is additive).

Only fields reached through anonymous holders are forwarded; bitfields under a named holder are not. Gated on C# 11 like the rest of the feature.

Adds syntax-shape unit tests (PSAPI_WORKING_SET_EX_BLOCK, including the named-holder negative case and the C# 10 gate) and functional tests proving the forwarded properties alias the same storage, pack without bit overlap (exact backing-field bit pattern asserted), and isolate neighbors. Validated across net8.0, net472, and net10.0.

* Fix SA1025 in bitfield test comment alignment

The aligned trailing comments in FlattenedBitfieldPropertiesPackWithoutOverlap used multiple consecutive spaces, which fails the CI -warnAsError build (SA1025). Collapse to a single space before each comment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants