Skip to content
Open
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
35 changes: 35 additions & 0 deletions docfx/docs/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,41 @@ For an extender (e.g. `PInvokeHelpers`), the const lives on the extender's host
(`PInvokeHelpers.X`) and the forwarder lives on the receiver (`PInvoke.X`). You can always reach
the const via the host class.

### Constants typed as a typedef struct

Some constants are typed as a *typedef struct* rather than a primitive — for example `HRESULT`,
`NTSTATUS`, `HWND`, `BOOL`, or `HKEY`. Outside of composition, CsWin32 nests these directly onto the
struct (so `S_OK` reads as `HRESULT.S_OK`). Under `extensionReceiver`, when the struct is owned by a
*referenced* assembly, the extender cannot add members to it directly, so CsWin32 attaches the
constant to the struct with an `extension(<Struct>)` block instead. This keeps it reachable as
`<Struct>.<Name>` — the same shape the owning assembly uses for its own constants of that type:

```csharp
public static partial class AppPInvokes
{
// 1. The real field stays on the extender's host class.
public static readonly global::Windows.Win32.Foundation.HRESULT MY_HR = (global::Windows.Win32.Foundation.HRESULT)unchecked((int)0x80040001);

extension (global::Windows.Win32.Foundation.HRESULT)
{
// 2. A forwarder surfaces it as HRESULT.MY_HR, alongside the owner's HRESULT.S_OK etc.
public static global::Windows.Win32.Foundation.HRESULT MY_HR => global::Windows.Win32.AppPInvokes.MY_HR;
}
}
```

Note that this attaches to the struct, *not* the receiver — a struct-typed constant is never
forwarded through `PInvoke`. The behavior composes across assemblies and across winmds (an
extender may contribute constants onto a struct defined in a different metadata's namespace). Three
edge cases round it out:

- If the struct is generated in the *same* assembly, the constant is injected directly into the
struct as usual — no extension block is emitted, because the assembly owns the struct.
- If a lower layer already exposes a constant of that name on the struct (typically the owner, which
injected it directly), the higher layer does not re-emit it.
- If the struct cannot be found in any referenced assembly, the constant silently remains reachable
through the host class only.

## How duplicates are handled across layers

When an extender's `NativeMethods.txt` requests something the owner (or any other referenced
Expand Down
155 changes: 103 additions & 52 deletions src/Microsoft.Windows.CsWin32/Generator.Constant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,35 +141,56 @@ internal void RequestConstant(FieldDefinitionHandle fieldDefHandle)
{
FieldDefinition fieldDef = this.Reader.GetFieldDefinition(fieldDefHandle);

// Multi-assembly composition: if another assembly already exposes this constant as an extension
// property (or as a plain field/property on the receiver type), skip regeneration so layered
// CsWin32 outputs compose without duplicates. Constants are name-unique per type (no
// overloading), so we look for any member of the same name with zero parameters
// (field / property / parameterless accessor) by passing an empty parameter-types list.
if (this.options.ExtensionReceiver is not null)
{
string constantName = this.Reader.GetString(fieldDef.Name);
if (this.IsExtensionMemberAlreadyOnReceiver(constantName, Array.Empty<string>(), ReceiverMemberKind.Value))
{
return;
}
}

FieldDeclarationSyntax constantDeclaration = this.DeclareConstant(fieldDef);

// Decode the field type up front to learn whether this constant is typed as a typedef struct
// (e.g. HRESULT, NTSTATUS, HWND). Two pieces of information come out of this:
// * fieldType — a TypeDefinitionHandle in THIS metadata, set only when the struct is defined
// in this same winmd. It drives constant "nesting" (injecting the field directly into the
// struct declaration when that struct is generated locally).
// * extensionStructName — the fully-qualified metadata name of the typedef struct, captured
// even for cross-winmd references (e.g. a Windows.Wdk constant typed as the SDK's NTSTATUS).
// It drives the extensionReceiver feature's `extension(<struct>)` attachment.
TypeHandleInfo fieldTypeInfo = fieldDef.DecodeSignature<TypeHandleInfo, SignatureHandleProvider.IGenericContext?>(this.SignatureHandleProvider, null) with { IsConstantField = true };
TypeDefinitionHandle? fieldType = null;
string? extensionStructName = null;
if (fieldTypeInfo is HandleTypeHandleInfo handleInfo && this.IsTypeDefStruct(handleInfo) && handleInfo.Handle.Kind == HandleKind.TypeReference)
{
TypeReference tr = this.Reader.GetTypeReference((TypeReferenceHandle)handleInfo.Handle);
string fieldTypeName = this.Reader.GetString(tr.Name);
if (!TypeDefsThatDoNotNestTheirConstants.Contains(fieldTypeName) && this.TryGetTypeDefHandle(tr, out TypeDefinitionHandle candidate))
if (!TypeDefsThatDoNotNestTheirConstants.Contains(fieldTypeName))
{
string fieldTypeNamespace = this.Reader.GetString(tr.Namespace);
extensionStructName = fieldTypeNamespace.Length == 0 ? fieldTypeName : $"{fieldTypeNamespace}.{fieldTypeName}";
if (this.TryGetTypeDefHandle(tr, out TypeDefinitionHandle candidate))
{
fieldType = candidate;
}
}
}

// Multi-assembly composition: if another assembly already exposes this constant, skip regeneration
// so layered CsWin32 outputs compose without duplicates. Constants are name-unique per type (no
// overloading), so we look for any member of the same name with zero parameters (field / property /
// parameterless accessor). A struct-typed constant is deduped against the typedef struct it attaches
// to (which the owning assembly may have injected the constant directly into); every other constant
// is deduped against the configured receiver.
if (this.options.ExtensionReceiver is not null)
{
string constantName = this.Reader.GetString(fieldDef.Name);
if (extensionStructName is not null)
{
if (this.IsConstantAlreadyOnStructType(extensionStructName, constantName))
{
return;
}
}
else if (this.IsExtensionMemberAlreadyOnReceiver(constantName, Array.Empty<string>(), ReceiverMemberKind.Value))
{
fieldType = candidate;
return;
}
}

this.volatileCode.AddConstant(fieldDefHandle, constantDeclaration, fieldType);
FieldDeclarationSyntax constantDeclaration = this.DeclareConstant(fieldDef);
this.volatileCode.AddConstant(fieldDefHandle, constantDeclaration, fieldType, extensionStructName);
});
}

Expand Down Expand Up @@ -436,67 +457,97 @@ private FieldDeclarationSyntax DeclareConstant(FieldDefinition fieldDef)
private ClassDeclarationSyntax DeclareConstantDefiningClass()
{
#if ROSLYN5
// Skip the field-split when no receiver is configured OR when THIS generator's namespace can't
// resolve the configured receiver (multi-namespace pipeline edge case — see Generator.WrapAsExtensionMembers).
if (this.options.ExtensionReceiver is null || this.GetExtensionReceiverSymbol() is null)
if (this.options.ExtensionReceiver is null)
{
return ClassDeclaration(this.methodsAndConstantsClassName.Identifier, [.. this.committedCode.TopLevelFields])
.WithModifiers([TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.PartialKeyword)]);
return this.DeclarePlainConstantDefiningClass();
}

// When an extension receiver is configured, constants must remain on the host class because
// fields are not legal members of a C# 14 extension block. To still expose them through the
// receiver, we emit a forwarding static property inside the extension block that returns the
// underlying field. The backing field keeps its original visibility (typically <c>public</c>
// because const fields need to be reachable from C# constant contexts such as enum initializers,
// attribute arguments, and `fixed` array sizes — none of which can flow through a property).
// Consumers reach the constant either as `<HostClass>.X` (works in const contexts) or as
// `<Receiver>.X` via the extension property (works in any runtime context).
SyntaxToken publicVisibility = TokenWithSpace(this.Visibility);

var backingFields = new System.Collections.Generic.List<MemberDeclarationSyntax>(this.committedCode.TopLevelFields.Count());
var forwarderProperties = new System.Collections.Generic.List<MemberDeclarationSyntax>(this.committedCode.TopLevelFields.Count());

// Use a global-qualified host expression so the forwarder body is unambiguous even when the extension
// With extensionReceiver configured, constants cannot live inside a C# 14 extension block (fields are
// not legal extension members). Each constant's real field therefore stays on the host class, and a
// forwarding static property is emitted inside an extension block so the value is also reachable through
// the extension target:
// * A constant typed as a typedef struct (HRESULT, NTSTATUS, HWND, ...) attaches to THAT struct, so it
// reads as `<Struct>.<Name>` — matching how the struct's owning assembly surfaces its own constants.
// This composes across assemblies and across winmds (e.g. a Windows.Wdk layer contributing
// NTSTATUS-typed defines onto the SDK's NTSTATUS).
// * Every other constant forwards through the configured receiver as `<Receiver>.<Name>`.
// When neither target resolves (the struct isn't in a referenced assembly, or this generator can't see
// the receiver), the constant silently remains reachable through the host class only.
SyntaxToken memberVisibility = TokenWithSpace(this.Visibility);
bool receiverResolvable = this.GetExtensionReceiverSymbol() is not null;

// Use a global-qualified host expression so the forwarder body is unambiguous even when an extension
// block's own scope shadows the host class name.
ExpressionSyntax qualifiedHost = ParseName($"global::{this.Namespace}.{this.options.ClassName}");

foreach (FieldDeclarationSyntax originalField in this.committedCode.TopLevelFields)
var backingFields = new List<MemberDeclarationSyntax>();
var forwardersByTarget = new Dictionary<string, List<MemberDeclarationSyntax>>(StringComparer.Ordinal);
var targetOrder = new List<string>();

foreach ((FieldDeclarationSyntax originalField, string? extensionTypeName) in this.committedCode.TopLevelFieldsWithExtensionType)
{
// Keep the field as-is on the host class (original visibility preserved) so consumers can use
// `<HostClass>.<Name>` in const contexts. The forwarder property below adds the receiver-qualified
// discovery path for runtime contexts.
// The real field always stays on the host class (fields cannot live in an extension block), with its
// original visibility preserved so `<HostClass>.<Name>` keeps working in C# constant contexts.
backingFields.Add(originalField);

// Resolve the extension target: the typedef struct itself (when available as a referenced symbol),
// otherwise the configured receiver (for non-struct constants), otherwise none (silent fallback).
string? target = null;
if (extensionTypeName is not null)
{
if (this.FindTypeSymbolsIfAlreadyAvailable(extensionTypeName).OfType<INamedTypeSymbol>().Any())
{
target = $"global::{extensionTypeName}";
}
}
else if (receiverResolvable)
{
target = $"global::{this.Namespace}.{this.options.ExtensionReceiver}";
}

if (target is null)
{
continue;
}

if (!forwardersByTarget.TryGetValue(target, out List<MemberDeclarationSyntax>? forwarders))
{
forwardersByTarget.Add(target, forwarders = new List<MemberDeclarationSyntax>());
targetOrder.Add(target);
}

// Build the forwarder property: <visibility> static <Type> <Name> => <Host>.<Name>;
TypeSyntax fieldType = originalField.Declaration.Type;
foreach (VariableDeclaratorSyntax declarator in originalField.Declaration.Variables)
{
SyntaxToken nameIdent = declarator.Identifier;
SyntaxToken nameIdent = declarator.Identifier.WithoutTrivia();
ExpressionSyntax forwardExpr = MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
qualifiedHost,
IdentifierName(nameIdent.WithoutTrivia()));
PropertyDeclarationSyntax property = SyntaxFactory.PropertyDeclaration(fieldType.WithTrailingTrivia(TriviaList(Space)), nameIdent.WithoutTrivia())
.WithModifiers(SyntaxFactory.TokenList(publicVisibility, TokenWithSpace(SyntaxKind.StaticKeyword)))
IdentifierName(nameIdent));
PropertyDeclarationSyntax property = SyntaxFactory.PropertyDeclaration(fieldType.WithTrailingTrivia(TriviaList(Space)), nameIdent)
.WithModifiers(SyntaxFactory.TokenList(memberVisibility, TokenWithSpace(SyntaxKind.StaticKeyword)))
.WithExpressionBody(ArrowExpressionClause(forwardExpr))
.WithSemicolonToken(SemicolonWithLineFeed);
forwarderProperties.Add(property);
forwarders.Add(property);
}
}

var classMembers = new System.Collections.Generic.List<MemberDeclarationSyntax>(backingFields);
if (forwarderProperties.Count > 0)
var classMembers = new List<MemberDeclarationSyntax>(backingFields);
foreach (string target in targetOrder)
{
classMembers.Add(ExtensionBlock(ParseName($"global::{this.Namespace}.{this.options.ExtensionReceiver}"), SyntaxFactory.List(forwarderProperties)));
classMembers.Add(ExtensionBlock(ParseName(target), SyntaxFactory.List(forwardersByTarget[target])));
}

return ClassDeclaration(this.methodsAndConstantsClassName.Identifier, SyntaxFactory.List(classMembers))
.WithModifiers([TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.PartialKeyword)]);
#else
// The Roslyn 4 leg ignores ExtensionReceiver (rejected at validation time via PInvoke013).
return ClassDeclaration(this.methodsAndConstantsClassName.Identifier, [.. this.committedCode.TopLevelFields])
.WithModifiers([TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.PartialKeyword)]);
return this.DeclarePlainConstantDefiningClass();
#endif
}

private ClassDeclarationSyntax DeclarePlainConstantDefiningClass()
=> ClassDeclaration(this.methodsAndConstantsClassName.Identifier, [.. this.committedCode.TopLevelFields])
.WithModifiers([TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.PartialKeyword)]);
}
13 changes: 10 additions & 3 deletions src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private class GeneratedCode
/// </summary>
private readonly Dictionary<(TypeDefinitionHandle Type, bool HasUnmanagedName), MemberDeclarationSyntax> types = new();

private readonly Dictionary<FieldDefinitionHandle, (FieldDeclarationSyntax FieldDeclaration, TypeDefinitionHandle? FieldType)> fieldsToSyntax = new();
private readonly Dictionary<FieldDefinitionHandle, (FieldDeclarationSyntax FieldDeclaration, TypeDefinitionHandle? FieldType, string? ExtensionTypeName)> fieldsToSyntax = new();

private readonly List<ClassDeclarationSyntax> safeHandleTypes = new();

Expand Down Expand Up @@ -86,6 +86,13 @@ internal GeneratedCode(GeneratedCode parent)
where @field.FieldType is null || !this.types.ContainsKey((@field.FieldType.Value, false))
select @field.FieldDeclaration;

/// <summary>
/// Gets the top-level constant fields paired with the fully-qualified metadata name of the typedef struct they are typed as (or <see langword="null"/> when the constant is not typed as a typedef struct). Used by the <c>extensionReceiver</c> feature to attach struct-typed constants to their struct via a C# 14 extension block.
/// </summary>
internal IEnumerable<(FieldDeclarationSyntax Field, string? ExtensionTypeName)> TopLevelFieldsWithExtensionType => from @field in this.fieldsToSyntax.Values
where @field.FieldType is null || !this.types.ContainsKey((@field.FieldType.Value, false))
select (@field.FieldDeclaration, @field.ExtensionTypeName);

internal IEnumerable<IGrouping<string, MemberDeclarationSyntax>> MembersByModule
{
get
Expand Down Expand Up @@ -132,10 +139,10 @@ internal void AddMemberToModule(string moduleName, IEnumerable<MemberDeclaration
this.NeedsWinRTCustomMarshaler |= members.Any(m => RequiresWinRTCustomMarshaler(m));
}

internal void AddConstant(FieldDefinitionHandle fieldDefHandle, FieldDeclarationSyntax constantDeclaration, TypeDefinitionHandle? fieldType)
internal void AddConstant(FieldDefinitionHandle fieldDefHandle, FieldDeclarationSyntax constantDeclaration, TypeDefinitionHandle? fieldType, string? extensionTypeName)
{
this.ThrowIfNotGenerating();
this.fieldsToSyntax.Add(fieldDefHandle, (constantDeclaration, fieldType));
this.fieldsToSyntax.Add(fieldDefHandle, (constantDeclaration, fieldType, extensionTypeName));
}

internal void AddMacro(string macroName, MethodDeclarationSyntax macro)
Expand Down
Loading
Loading