diff --git a/src/Microsoft.Windows.CsWin32/Generator.Struct.cs b/src/Microsoft.Windows.CsWin32/Generator.Struct.cs
index 80e83d1f..74712676 100644
--- a/src/Microsoft.Windows.CsWin32/Generator.Struct.cs
+++ b/src/Microsoft.Windows.CsWin32/Generator.Struct.cs
@@ -38,6 +38,25 @@ private static bool IsAnonymousFieldName(string name)
return true;
}
+ ///
+ /// Computes the C# type of a bitfield sub-property given the backing field's primitive type and the bit length.
+ /// This mirrors the property-type selection used when bitfields are emitted on their declaring struct in
+ /// , so a forwarded accessor's type matches the nested property it forwards to.
+ ///
+ private static TypeSyntax GetBitfieldProjectedType(PrimitiveTypeCode backingTypeCode, byte propLength)
+ {
+ bool signed = backingTypeCode is PrimitiveTypeCode.SByte or PrimitiveTypeCode.Int16 or PrimitiveTypeCode.Int32 or PrimitiveTypeCode.Int64 or PrimitiveTypeCode.IntPtr;
+ return propLength switch
+ {
+ 1 => PredefinedType(Token(SyntaxKind.BoolKeyword)),
+ <= 8 => PredefinedType(Token(signed ? SyntaxKind.SByteKeyword : SyntaxKind.ByteKeyword)),
+ <= 16 => PredefinedType(Token(signed ? SyntaxKind.ShortKeyword : SyntaxKind.UShortKeyword)),
+ <= 32 => PredefinedType(Token(signed ? SyntaxKind.IntKeyword : SyntaxKind.UIntKeyword)),
+ <= 64 => PredefinedType(Token(signed ? SyntaxKind.LongKeyword : SyntaxKind.ULongKeyword)),
+ _ => throw new NotSupportedException(),
+ };
+ }
+
private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle, Context context)
{
TypeDefinition typeDef = this.Reader.GetTypeDefinition(typeDefHandle);
@@ -761,6 +780,39 @@ private void GatherFlattenedAnonymousAccessors(
continue;
}
+ // Forward computed bitfield sub-properties (e.g. PSAPI_WORKING_SET_EX_BLOCK's Valid/ShareCount/...) as value
+ // get/set properties on the outer struct. They are generated on the nested struct from NativeBitfieldAttribute
+ // decorations on a backing field; Phase 1 only surfaces the raw backing field as a ref, so without this the
+ // friendly bitfield members would only be reachable through the Anonymous holder chain. This runs in addition to
+ // (not instead of) surfacing the backing field below, mirroring the nested struct, which exposes both.
+ bool fieldObsolete = ancestorObsolete || this.HasObsoleteAttribute(fieldAttributes);
+ foreach (CustomAttribute bitfieldAttribute in MetadataUtilities.FindAttributes(this.Reader, fieldAttributes, InteropDecorationNamespace, NativeBitfieldAttribute))
+ {
+ if (fieldDef.DecodeSignature(this.SignatureHandleProvider, null) is not PrimitiveTypeHandleInfo bitfieldBackingType)
+ {
+ continue;
+ }
+
+ CustomAttributeValue decodedBitfield = bitfieldAttribute.DecodeValue(CustomAttributeTypeProvider.Instance);
+ string bitfieldPropName = (string)decodedBitfield.FixedArguments[0].Value!;
+ byte bitfieldPropLength = (byte)(long)decodedBitfield.FixedArguments[2].Value!;
+
+ // A zero-length bitfield produces no property on the nested struct, so there is nothing to forward.
+ if (bitfieldPropLength == 0)
+ {
+ continue;
+ }
+
+ // Skip collisions with an existing member or another flattened accessor.
+ if (reservedNames.Contains(bitfieldPropName) || !surfacedNames.Add(bitfieldPropName))
+ {
+ continue;
+ }
+
+ TypeSyntax bitfieldPropType = GetBitfieldProjectedType(bitfieldBackingType.PrimitiveTypeCode, bitfieldPropLength);
+ accessors.Add(this.CreateFlattenedBitfieldAccessor(bitfieldPropName, bitfieldPropType, accessPrefix, crefTypePrefix, fieldObsolete));
+ }
+
// Skip fields whose generated representation is not a plain, ref-returnable field of the decoded type.
if (this.FindAssociatedEnum(fieldAttributes) is not null)
{
@@ -829,8 +881,7 @@ private void GatherFlattenedAnonymousAccessors(
// Mark the accessor obsolete when the leaf field (or any holder along its access path) is obsolete,
// so its body may legally reference the obsolete member without triggering CS0612.
- bool isObsolete = ancestorObsolete || this.HasObsoleteAttribute(fieldAttributes);
- accessors.Add(this.CreateFlattenedAnonymousAccessor(fieldName, fieldType, accessPrefix, crefTypePrefix, isObsolete));
+ accessors.Add(this.CreateFlattenedAnonymousAccessor(fieldName, fieldType, accessPrefix, crefTypePrefix, fieldObsolete));
this.DeclareUnscopedRefAttributeIfNecessary();
}
}
@@ -872,6 +923,46 @@ private PropertyDeclarationSyntax CreateFlattenedAnonymousAccessor(string fieldN
return accessor.WithLeadingTrivia(inheritDoc);
}
+ ///
+ /// Creates a value get/set property that forwards to a computed bitfield sub-property reached through anonymous
+ /// holders (e.g. value.Valid instead of value.Anonymous.Anonymous.Valid), with documentation inherited
+ /// from the underlying bitfield property. Unlike the scalar accessor this is a by-value property, not a ref:
+ /// the bitfield property has no addressable backing storage of its own (it reads and writes bits of a shared field).
+ ///
+ private PropertyDeclarationSyntax CreateFlattenedBitfieldAccessor(string propName, TypeSyntax propertyType, ExpressionSyntax accessPrefix, NameSyntax crefTypePrefix, bool isObsolete)
+ {
+ // { readonly get => .; set => . = value; }
+ ExpressionSyntax target = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, accessPrefix, IdentifierName(SafeIdentifier(propName)));
+ AccessorDeclarationSyntax getter = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
+ .AddModifiers(TokenWithSpace(SyntaxKind.ReadOnlyKeyword))
+ .WithExpressionBody(ArrowExpressionClause(target))
+ .WithSemicolonToken(SemicolonWithLineFeed);
+ AccessorDeclarationSyntax setter = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
+ .WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, target, IdentifierName("value"))))
+ .WithSemicolonToken(SemicolonWithLineFeed);
+
+ PropertyDeclarationSyntax accessor = PropertyDeclaration(propertyType.WithTrailingTrivia(Space), SafeIdentifier(propName))
+ .AddModifiers(TokenWithSpace(this.Visibility))
+ .WithAccessorList(AccessorList(getter, setter));
+
+ if (isObsolete)
+ {
+ accessor = accessor.AddAttributeLists(AttributeList(ObsoleteAttributeSyntax));
+ }
+
+ // ///
+ CrefSyntax cref = SyntaxFactory.NameMemberCref(QualifiedName(crefTypePrefix, SafeIdentifierName(propName)));
+ SyntaxTrivia inheritDoc = Trivia(DocumentationCommentTrivia(
+ SyntaxKind.SingleLineDocumentationCommentTrivia,
+ [
+ XmlText("/// "),
+ XmlEmptyElement("inheritdoc", [XmlCrefAttribute(cref)]),
+ XmlText(XmlTextNewLine("\n", continueXmlDocumentationComment: false)),
+ ]));
+
+ return accessor.WithLeadingTrivia(inheritDoc);
+ }
+
///
/// Resolves the nested type definition referenced by an anonymous holder field, if the field's type is in fact
/// a type nested within .
diff --git a/test/GenerationSandbox.Tests/BasicTests.cs b/test/GenerationSandbox.Tests/BasicTests.cs
index 6dec0ac1..c671b769 100644
--- a/test/GenerationSandbox.Tests/BasicTests.cs
+++ b/test/GenerationSandbox.Tests/BasicTests.cs
@@ -542,6 +542,105 @@ public unsafe void FlattenedAnonymousFieldSupportsPointers()
Assert.Equal(0xABCDEF01u, si.Anonymous.dwOemId);
}
+ [Fact]
+ public void FlattenedBitfieldPropertiesAliasNestedBitfields()
+ {
+ // PSAPI_WORKING_SET_EX_BLOCK's anonymous union nests an anonymous struct of bitfields packed into one field.
+ // The flattened forwarding properties read and write the same underlying bits as the nested path.
+ Windows.Win32.System.ProcessStatus.PSAPI_WORKING_SET_EX_BLOCK block = default;
+
+ // Writing a forwarded multi-bit property writes through to the nested struct's bitfield.
+ block.ShareCount = 5;
+ Assert.Equal((byte)5, block.Anonymous.Anonymous.ShareCount);
+
+ // Reading a forwarded property reflects a write made through the nested path.
+ block.Anonymous.Anonymous.Win32Protection = 11;
+ Assert.Equal((ushort)11, block.Win32Protection);
+
+ // A 1-bit (bool) bitfield round-trips through the forwarding property.
+ block.Valid = true;
+ Assert.True(block.Anonymous.Anonymous.Valid);
+
+ // Independent bitfields packed into the same backing field do not disturb each other.
+ Assert.Equal((byte)5, block.ShareCount);
+ Assert.Equal((ushort)11, block.Win32Protection);
+ }
+
+ [Fact]
+ public void FlattenedBitfieldPropertiesPackWithoutOverlap()
+ {
+ // PSAPI_WORKING_SET_EX_BLOCK packs nine bitfields into one nuint:
+ // Valid:bit0, ShareCount:bits1-3, Win32Protection:bits4-14, Shared:bit15,
+ // Node:bits16-21, Locked:bit22, LargePage:bit23, Reserved:bits24-30, Bad:bit31.
+ // Set every sub-field (through the flattened outer properties) to a distinct in-range value chosen so each
+ // field's bits form a recognizable pattern, then assert (a) every value round-trips and (b) the raw backing
+ // field equals exactly the bitwise-OR of the shifted values — which can only hold if no two fields share a bit.
+ Windows.Win32.System.ProcessStatus.PSAPI_WORKING_SET_EX_BLOCK block = default;
+ block.Valid = true; // bit 0
+ block.ShareCount = 5; // bits 1-3 (0b101, max 7)
+ block.Win32Protection = 0x6AB; // bits 4-14 (max 2047)
+ block.Shared = false; // bit 15
+ block.Node = 0x2A; // bits 16-21 (max 63)
+ block.Locked = true; // bit 22
+ block.LargePage = false; // bit 23
+ block.Reserved = 0x55; // bits 24-30 (max 127)
+ block.Bad = true; // bit 31
+
+ // (a) Every value round-trips through the flattened property (no cross-talk between adjacent fields).
+ Assert.True(block.Valid);
+ Assert.Equal((byte)5, block.ShareCount);
+ Assert.Equal((ushort)0x6AB, block.Win32Protection);
+ Assert.False(block.Shared);
+ Assert.Equal((byte)0x2A, block.Node);
+ Assert.True(block.Locked);
+ Assert.False(block.LargePage);
+ Assert.Equal((byte)0x55, block.Reserved);
+ Assert.True(block.Bad);
+
+ // The nested path observes identical values (the flattened properties truly alias the same storage).
+ Assert.Equal(block.ShareCount, block.Anonymous.Anonymous.ShareCount);
+ Assert.Equal(block.Win32Protection, block.Anonymous.Anonymous.Win32Protection);
+ Assert.Equal(block.Node, block.Anonymous.Anonymous.Node);
+ Assert.Equal(block.Reserved, block.Anonymous.Anonymous.Reserved);
+
+ // (b) The raw backing field holds exactly those bits and no others.
+ nuint expected =
+ ((nuint)1 << 0) | // Valid
+ ((nuint)5 << 1) | // ShareCount
+ ((nuint)0x6AB << 4) | // Win32Protection
+ ((nuint)0x2A << 16) | // Node
+ ((nuint)1 << 22) | // Locked
+ ((nuint)0x55 << 24) | // Reserved
+ ((nuint)1 << 31); // Bad
+ Assert.Equal(expected, block._bitfield);
+ }
+
+ [Fact]
+ public void FlattenedBitfieldPropertyWritesAreIsolatedFromNeighbors()
+ {
+ // Mutating one flattened bitfield must never disturb its immediate neighbors. ShareCount (bits 1-3) sits
+ // between Valid (bit 0) and Win32Protection (bits 4-14); drive its full range and confirm the neighbors hold.
+ Windows.Win32.System.ProcessStatus.PSAPI_WORKING_SET_EX_BLOCK block = default;
+ block.Valid = true;
+ block.Win32Protection = 0x7FF; // all 11 bits set
+
+ for (byte value = 0; value <= 7; value++)
+ {
+ block.ShareCount = value;
+ Assert.Equal(value, block.ShareCount);
+
+ // Neighbors on both sides are untouched no matter what ShareCount is set to.
+ Assert.True(block.Valid);
+ Assert.Equal((ushort)0x7FF, block.Win32Protection);
+ }
+
+ // Clearing the high neighbor likewise leaves ShareCount and Valid intact.
+ block.Win32Protection = 0;
+ Assert.True(block.Valid);
+ Assert.Equal((byte)7, block.ShareCount);
+ Assert.Equal((ushort)0, block.Win32Protection);
+ }
+
[Fact]
public void FieldWithAssociatedEnum()
{
diff --git a/test/GenerationSandbox.Tests/NativeMethods.txt b/test/GenerationSandbox.Tests/NativeMethods.txt
index a872dab7..d540f0bc 100644
--- a/test/GenerationSandbox.Tests/NativeMethods.txt
+++ b/test/GenerationSandbox.Tests/NativeMethods.txt
@@ -59,6 +59,7 @@ PAGESET
PathParseIconLocation
POINTS
PROCESS_BASIC_INFORMATION
+PSAPI_WORKING_SET_EX_BLOCK
PZZSTR
PZZWSTR
RECT
diff --git a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs
index 763f6866..1dac4bc5 100644
--- a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs
+++ b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs
@@ -568,9 +568,87 @@ public void FlattenNestedAnonymousTypes_DoesNotFlattenIntoNestedStructContaining
AssertFlattenedAccessor(FindProperty(decimalDecl, "Lo64"), "this.Anonymous2.Lo64");
}
+ [Theory, PairwiseData]
+ public void FlattenNestedAnonymousTypes_ForwardsBitfieldProperties(bool allowMarshaling)
+ {
+ // PSAPI_WORKING_SET_EX_BLOCK is a union whose anonymous _Anonymous_e__Struct (reached via Anonymous.Anonymous)
+ // carries bitfields packed into a single nuint backing field. Phase 1 surfaces only the raw _bitfield as a ref;
+ // Phase 2 also forwards the computed bitfield sub-properties (Valid, ShareCount, Win32Protection, ...) as value
+ // get/set properties on the outer struct so they can be read and written directly.
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = allowMarshaling, FlattenNestedAnonymousTypes = true });
+ this.GenerateApi("PSAPI_WORKING_SET_EX_BLOCK");
+ var structDecl = (StructDeclarationSyntax)Assert.Single(this.FindGeneratedType("PSAPI_WORKING_SET_EX_BLOCK"));
+
+ // A 1-bit field is forwarded as a bool; a multi-bit field as the smallest unsigned integer that fits.
+ AssertForwardedBitfieldAccessor(FindProperty(structDecl, "Valid"), "this.Anonymous.Anonymous.Valid");
+ Assert.Equal(SyntaxKind.BoolKeyword, ((PredefinedTypeSyntax)FindProperty(structDecl, "Valid").Type).Keyword.Kind());
+ AssertForwardedBitfieldAccessor(FindProperty(structDecl, "ShareCount"), "this.Anonymous.Anonymous.ShareCount");
+ Assert.Equal(SyntaxKind.ByteKeyword, ((PredefinedTypeSyntax)FindProperty(structDecl, "ShareCount").Type).Keyword.Kind());
+ AssertForwardedBitfieldAccessor(FindProperty(structDecl, "Win32Protection"), "this.Anonymous.Anonymous.Win32Protection");
+ Assert.Equal(SyntaxKind.UShortKeyword, ((PredefinedTypeSyntax)FindProperty(structDecl, "Win32Protection").Type).Keyword.Kind());
+
+ // The raw backing field is still surfaced as a ref (Phase 1 behavior is unchanged).
+ AssertFlattenedAccessor(FindProperty(structDecl, "_bitfield"), "this.Anonymous.Anonymous._bitfield");
+ }
+
+ [Fact]
+ public void FlattenNestedAnonymousTypes_DoesNotForwardBitfieldsThroughNamedHolder()
+ {
+ // PSAPI_WORKING_SET_EX_BLOCK's union also has a *named* member 'Invalid' (_Invalid_e__Struct) whose bitfields
+ // include Reserved0/Reserved1. Named holders are surfaced as a single ref to the whole value, never flattened,
+ // so those bitfields must NOT appear on the outer struct.
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { FlattenNestedAnonymousTypes = true });
+ this.GenerateApi("PSAPI_WORKING_SET_EX_BLOCK");
+ var structDecl = (StructDeclarationSyntax)Assert.Single(this.FindGeneratedType("PSAPI_WORKING_SET_EX_BLOCK"));
+
+ // 'Invalid' is surfaced as a whole-value ref...
+ AssertFlattenedAccessor(FindProperty(structDecl, "Invalid"), "this.Anonymous.Invalid");
+
+ // ...but its bitfields (unique names Reserved0/Reserved1) are not hoisted onto the outer struct.
+ Assert.DoesNotContain(
+ structDecl.Members.OfType(),
+ p => p.Identifier.ValueText is "Reserved0" or "Reserved1");
+ }
+
+ [Fact]
+ public void FlattenNestedAnonymousTypes_BitfieldForwardingRequiresCSharp11()
+ {
+ // The whole flattening feature (including bitfield forwarding) is gated on C# 11 (for [UnscopedRef]).
+ this.parseOptions = this.parseOptions.WithLanguageVersion(LanguageVersion.CSharp10);
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { FlattenNestedAnonymousTypes = true });
+ this.GenerateApi("PSAPI_WORKING_SET_EX_BLOCK");
+ var structDecl = (StructDeclarationSyntax)Assert.Single(this.FindGeneratedType("PSAPI_WORKING_SET_EX_BLOCK"));
+ Assert.DoesNotContain(structDecl.Members.OfType(), p => p.Identifier.ValueText == "ShareCount");
+ }
+
private static PropertyDeclarationSyntax FindProperty(StructDeclarationSyntax structDecl, string name) =>
Assert.Single(structDecl.Members.OfType(), p => p.Identifier.ValueText == name);
+ private static void AssertForwardedBitfieldAccessor(PropertyDeclarationSyntax property, string expectedTarget)
+ {
+ // It is a by-value property (not ref-returning) and is not annotated [UnscopedRef].
+ Assert.IsNotType(property.Type);
+ Assert.DoesNotContain(property.AttributeLists.SelectMany(al => al.Attributes), a => a.Name.ToString() == "UnscopedRef");
+
+ SyntaxList accessors = Assert.IsType(property.AccessorList).Accessors;
+
+ // readonly get => ;
+ AccessorDeclarationSyntax getter = Assert.Single(accessors, a => a.IsKind(SyntaxKind.GetAccessorDeclaration));
+ Assert.Contains(getter.Modifiers, m => m.IsKind(SyntaxKind.ReadOnlyKeyword));
+ Assert.Equal(expectedTarget, Assert.IsType(getter.ExpressionBody).Expression.ToString());
+
+ // set => = value;
+ AccessorDeclarationSyntax setter = Assert.Single(accessors, a => a.IsKind(SyntaxKind.SetAccessorDeclaration));
+ var assignment = Assert.IsType(Assert.IsType(setter.ExpressionBody).Expression);
+ Assert.Equal(expectedTarget, assignment.Left.ToString());
+ Assert.Equal("value", assignment.Right.ToString());
+
+ // It inherits documentation via .
+ Assert.Contains(
+ property.GetLeadingTrivia().Select(t => t.GetStructure()).OfType().SelectMany(d => d.Content.OfType()),
+ e => e.Name.ToString() == "inheritdoc" && e.Attributes.OfType().Any());
+ }
+
private static void AssertFlattenedAccessor(PropertyDeclarationSyntax property, string expectedRefTarget)
{
// It returns by ref.