Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 93 additions & 2 deletions src/Microsoft.Windows.CsWin32/Generator.Struct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ private static bool IsAnonymousFieldName(string name)
return true;
}

/// <summary>
/// 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
/// <see cref="DeclareStruct"/>, so a forwarded accessor's type matches the nested property it forwards to.
/// </summary>
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);
Expand Down Expand Up @@ -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<TypeSyntax> 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)
{
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -872,6 +923,46 @@ private PropertyDeclarationSyntax CreateFlattenedAnonymousAccessor(string fieldN
return accessor.WithLeadingTrivia(inheritDoc);
}

/// <summary>
/// Creates a value <c>get/set</c> property that forwards to a computed bitfield sub-property reached through anonymous
/// holders (e.g. <c>value.Valid</c> instead of <c>value.Anonymous.Anonymous.Valid</c>), with documentation inherited
/// from the underlying bitfield property. Unlike the scalar accessor this is a by-value property, not a <c>ref</c>:
/// the bitfield property has no addressable backing storage of its own (it reads and writes bits of a shared field).
/// </summary>
private PropertyDeclarationSyntax CreateFlattenedBitfieldAccessor(string propName, TypeSyntax propertyType, ExpressionSyntax accessPrefix, NameSyntax crefTypePrefix, bool isObsolete)
{
// <propertyType> <propName> { readonly get => <accessPrefix>.<propName>; set => <accessPrefix>.<propName> = 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));
}

// /// <inheritdoc cref="NestedType.PropName"/>
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);
}

/// <summary>
/// Resolves the nested type definition referenced by an anonymous holder field, if the field's type is in fact
/// a type nested within <paramref name="containingType"/>.
Expand Down
99 changes: 99 additions & 0 deletions test/GenerationSandbox.Tests/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
1 change: 1 addition & 0 deletions test/GenerationSandbox.Tests/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ PAGESET
PathParseIconLocation
POINTS
PROCESS_BASIC_INFORMATION
PSAPI_WORKING_SET_EX_BLOCK
PZZSTR
PZZWSTR
RECT
Expand Down
78 changes: 78 additions & 0 deletions test/Microsoft.Windows.CsWin32.Tests/StructTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropertyDeclarationSyntax>(),
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<PropertyDeclarationSyntax>(), p => p.Identifier.ValueText == "ShareCount");
}

private static PropertyDeclarationSyntax FindProperty(StructDeclarationSyntax structDecl, string name) =>
Assert.Single(structDecl.Members.OfType<PropertyDeclarationSyntax>(), 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<RefTypeSyntax>(property.Type);
Assert.DoesNotContain(property.AttributeLists.SelectMany(al => al.Attributes), a => a.Name.ToString() == "UnscopedRef");

SyntaxList<AccessorDeclarationSyntax> accessors = Assert.IsType<AccessorListSyntax>(property.AccessorList).Accessors;

// readonly get => <target>;
AccessorDeclarationSyntax getter = Assert.Single(accessors, a => a.IsKind(SyntaxKind.GetAccessorDeclaration));
Assert.Contains(getter.Modifiers, m => m.IsKind(SyntaxKind.ReadOnlyKeyword));
Assert.Equal(expectedTarget, Assert.IsType<ArrowExpressionClauseSyntax>(getter.ExpressionBody).Expression.ToString());

// set => <target> = value;
AccessorDeclarationSyntax setter = Assert.Single(accessors, a => a.IsKind(SyntaxKind.SetAccessorDeclaration));
var assignment = Assert.IsType<AssignmentExpressionSyntax>(Assert.IsType<ArrowExpressionClauseSyntax>(setter.ExpressionBody).Expression);
Assert.Equal(expectedTarget, assignment.Left.ToString());
Assert.Equal("value", assignment.Right.ToString());

// It inherits documentation via <inheritdoc cref="..."/>.
Assert.Contains(
property.GetLeadingTrivia().Select(t => t.GetStructure()).OfType<DocumentationCommentTriviaSyntax>().SelectMany(d => d.Content.OfType<XmlEmptyElementSyntax>()),
e => e.Name.ToString() == "inheritdoc" && e.Attributes.OfType<XmlCrefAttributeSyntax>().Any());
}

private static void AssertFlattenedAccessor(PropertyDeclarationSyntax property, string expectedRefTarget)
{
// It returns by ref.
Expand Down
Loading