diff --git a/docfx/docs/composition.md b/docfx/docs/composition.md index fb576f27..634467a9 100644 --- a/docfx/docs/composition.md +++ b/docfx/docs/composition.md @@ -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()` block instead. This keeps it reachable as +`.` — 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 diff --git a/src/Microsoft.Windows.CsWin32/Generator.Constant.cs b/src/Microsoft.Windows.CsWin32/Generator.Constant.cs index be0cda84..687f3686 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Constant.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Constant.cs @@ -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(), 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()` attachment. TypeHandleInfo fieldTypeInfo = fieldDef.DecodeSignature(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(), ReceiverMemberKind.Value)) { - fieldType = candidate; + return; } } - this.volatileCode.AddConstant(fieldDefHandle, constantDeclaration, fieldType); + FieldDeclarationSyntax constantDeclaration = this.DeclareConstant(fieldDef); + this.volatileCode.AddConstant(fieldDefHandle, constantDeclaration, fieldType, extensionStructName); }); } @@ -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 public - // 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 `.X` (works in const contexts) or as - // `.X` via the extension property (works in any runtime context). - SyntaxToken publicVisibility = TokenWithSpace(this.Visibility); - - var backingFields = new System.Collections.Generic.List(this.committedCode.TopLevelFields.Count()); - var forwarderProperties = new System.Collections.Generic.List(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 `.` — 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 `.`. + // 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(); + var forwardersByTarget = new Dictionary>(StringComparer.Ordinal); + var targetOrder = new List(); + + 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 - // `.` 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 `.` 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().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? forwarders)) + { + forwardersByTarget.Add(target, forwarders = new List()); + targetOrder.Add(target); + } + // Build the forwarder property: static => .; 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(backingFields); - if (forwarderProperties.Count > 0) + var classMembers = new List(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)]); } diff --git a/src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs b/src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs index 87d7aa31..6aedffaf 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs @@ -16,7 +16,7 @@ private class GeneratedCode /// private readonly Dictionary<(TypeDefinitionHandle Type, bool HasUnmanagedName), MemberDeclarationSyntax> types = new(); - private readonly Dictionary fieldsToSyntax = new(); + private readonly Dictionary fieldsToSyntax = new(); private readonly List safeHandleTypes = new(); @@ -86,6 +86,13 @@ internal GeneratedCode(GeneratedCode parent) where @field.FieldType is null || !this.types.ContainsKey((@field.FieldType.Value, false)) select @field.FieldDeclaration; + /// + /// Gets the top-level constant fields paired with the fully-qualified metadata name of the typedef struct they are typed as (or when the constant is not typed as a typedef struct). Used by the extensionReceiver feature to attach struct-typed constants to their struct via a C# 14 extension block. + /// + 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> MembersByModule { get @@ -132,10 +139,10 @@ internal void AddMemberToModule(string moduleName, IEnumerable 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) diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index a0b3919c..450a1043 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -1634,6 +1634,45 @@ internal bool IsExtensionMemberAlreadyOnReceiver(string memberName, IReadOnlyLis #endif } + /// + /// Tests whether the typedef struct named by already exposes an accessible value member (a field or a parameterless property) named in this or a referenced assembly. Supports multi-assembly composition: when a lower layer already surfaced a struct-typed constant (typically by injecting it directly into the generated struct), a higher layer must not re-emit it as an extension(<struct>) member and cause a duplicate. Always returns on the Roslyn 4 leg (the feature is gated to Roslyn 5). + /// + /// The fully-qualified metadata name of the typedef struct (e.g. Windows.Win32.Foundation.NTSTATUS). + /// The simple constant name to look for. + /// if the struct already exposes a matching accessible value member; otherwise . + internal bool IsConstantAlreadyOnStructType(string structFullMetadataName, string memberName) + { +#if ROSLYN5 + if (this.compilation is null) + { + return false; + } + + foreach (ISymbol symbol in this.FindTypeSymbolsIfAlreadyAvailable(structFullMetadataName)) + { + if (symbol is not INamedTypeSymbol structType) + { + continue; + } + + foreach (ISymbol member in structType.GetMembers(memberName)) + { + bool isValueMember = member is IFieldSymbol || (member is IPropertySymbol property && property.Parameters.IsEmpty); + if (isValueMember && this.compilation.IsSymbolAccessibleWithin(member, this.compilation.Assembly)) + { + return true; + } + } + } + + return false; +#else + _ = structFullMetadataName; + _ = memberName; + return false; +#endif + } + #if ROSLYN5 #pragma warning disable SA1201 // Keep dedup helpers (display format + signature/name normalization) co-located. private static readonly SymbolDisplayFormat ParameterTypeDisplayFormat = new SymbolDisplayFormat( diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExtensionReceiverTests.cs b/test/Microsoft.Windows.CsWin32.Tests/ExtensionReceiverTests.cs index b97af363..e6245e9f 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/ExtensionReceiverTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/ExtensionReceiverTests.cs @@ -607,6 +607,156 @@ internal static class CoreLayer Assert.NotEmpty(hostMethods); } + /// + /// A constant typed as a typedef struct (e.g. HWND_BOTTOM typed as HWND) attaches to that + /// struct via an extension(<struct>) block — so it reads as HWND.HWND_BOTTOM — rather + /// than through the receiver. This is the composition case: the struct is owned by a referenced assembly. + /// + [Fact] + public void Option_StructTypedConstant_AttachesToStructExtensionNotReceiver() + { + // Both the receiver and the typedef struct live in a referenced assembly. They are simulated here as + // non-partial stubs so FindTypeSymbolsIfAlreadyAvailable resolves them and CsWin32 does not regenerate them. + this.compilation = this.AddCode( + @"namespace Windows.Win32 { internal static class PInvoke { } } +namespace Windows.Win32.Foundation { internal readonly struct HWND { } }", + fileName: "Refs.cs"); + this.generator = this.CreateGenerator(new GeneratorOptions + { + EmitSingleFile = true, + ClassName = HostClassName, + ExtensionReceiver = ReceiverName, + }); + Assert.True(this.generator.TryGenerate("HWND_BOTTOM", TestContext.Current.CancellationToken)); + Assert.True(this.generator.TryGenerate("WM_NULL", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + + ClassDeclarationSyntax host = SingleHostClass(this.FindGeneratedType(HostClassName)); + + // The real field for the struct-typed constant stays on the host class. + Assert.Contains( + host.Members.OfType(), + f => f.Declaration.Variables.Any(v => v.Identifier.ValueText == "HWND_BOTTOM")); + + // HWND_BOTTOM is surfaced through extension(HWND) — the struct itself — not the receiver. + ExtensionBlockDeclarationSyntax hwndExtension = Assert.Single( + host.Members.OfType(), + b => ReceiverTypeName(b) == "global::Windows.Win32.Foundation.HWND"); + Assert.Contains(hwndExtension.Members.OfType(), p => p.Identifier.ValueText == "HWND_BOTTOM"); + + // The plain constant WM_NULL still forwards through the receiver, and the receiver block must NOT carry the struct-typed constant. + ExtensionBlockDeclarationSyntax receiverExtension = Assert.Single( + host.Members.OfType(), + b => ReceiverTypeName(b) == $"global::Windows.Win32.{ReceiverName}"); + Assert.Contains(receiverExtension.Members.OfType(), p => p.Identifier.ValueText == "WM_NULL"); + Assert.DoesNotContain(receiverExtension.Members.OfType(), p => p.Identifier.ValueText == "HWND_BOTTOM"); + } + + /// + /// Multi-assembly composition: when the typedef struct already exposes the constant (as the owning assembly + /// does after injecting it directly into the struct), a higher layer must not re-emit it — neither the + /// backing field nor the extension forwarder. + /// + [Fact] + public void Dedup_StructConstantAlreadyOnStruct_NotRegenerated() + { + this.compilation = this.AddCode( + @"namespace Windows.Win32 { internal static class PInvoke { } } +namespace Windows.Win32.Foundation +{ + internal readonly struct HWND + { + public static readonly HWND HWND_BOTTOM; + } +}", + fileName: "Refs.cs"); + this.generator = this.CreateGenerator(new GeneratorOptions + { + EmitSingleFile = true, + ClassName = HostClassName, + ExtensionReceiver = ReceiverName, + }); + Assert.True(this.generator.TryGenerate("HWND_BOTTOM", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + + // Neither a backing field nor a forwarder property for HWND_BOTTOM should appear under the host class. + List hostClasses = this.FindGeneratedType(HostClassName).OfType().ToList(); + Assert.DoesNotContain( + hostClasses.SelectMany(c => c.DescendantNodes().OfType()), + f => f.Declaration.Variables.Any(v => v.Identifier.ValueText == "HWND_BOTTOM")); + Assert.DoesNotContain( + hostClasses.SelectMany(c => c.DescendantNodes().OfType()), + p => p.Identifier.ValueText == "HWND_BOTTOM"); + } + + /// + /// When the typedef struct is generated locally (this assembly owns it), the struct-typed constant is + /// injected directly into the struct declaration exactly as without the feature — no extension(<struct>) + /// block is created, since we can add the member to the struct we own. + /// + [Fact] + public void Option_StructTypedConstant_StructGeneratedLocally_IsInjectedNotExtended() + { + // Only the receiver is stubbed; HWND is NOT, so CsWin32 generates it locally and injects the constant. + this.compilation = this.AddCode( + $"namespace Windows.Win32 {{ internal static class {ReceiverName} {{ }} }}", + fileName: "ReceiverStub.cs"); + this.generator = this.CreateGenerator(new GeneratorOptions + { + EmitSingleFile = true, + ClassName = HostClassName, + ExtensionReceiver = ReceiverName, + }); + Assert.True(this.generator.TryGenerate("HWND_BOTTOM", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + + // HWND is generated locally with HWND_BOTTOM injected as a member (unchanged nesting behavior). + StructDeclarationSyntax hwnd = Assert.Single(this.FindGeneratedType("HWND").OfType()); + Assert.Contains( + hwnd.Members.OfType(), + f => f.Declaration.Variables.Any(v => v.Identifier.ValueText == "HWND_BOTTOM")); + + // No extension block targets HWND: we own the struct, so we inject directly instead. + IEnumerable allExtensions = this.compilation.SyntaxTrees + .SelectMany(t => t.GetRoot().DescendantNodes().OfType()); + Assert.DoesNotContain(allExtensions, b => ReceiverTypeName(b).EndsWith(".HWND", StringComparison.Ordinal)); + } + + /// + /// End-to-end: a struct-typed constant attached through an extension(<struct>) block produces + /// source that compiles cleanly under C# 14, with the struct provided by a (stubbed) referenced assembly. + /// + [Fact] + public void Option_StructTypedConstant_GeneratedCodeCompiles() + { + this.compilation = this.starterCompilations["net10.0"]; + this.parseOptions = this.parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + // Simulate the owning assembly: the receiver plus a usable HWND struct the extender attaches a constant to. + this.compilation = this.AddCode( + @"namespace Windows.Win32 { internal static class PInvoke { } } +namespace Windows.Win32.Foundation +{ + internal readonly struct HWND + { + public HWND(nint value) { } + + public static explicit operator HWND(nint value) => new HWND(value); + } +}", + fileName: "Refs.cs"); + + this.generator = this.CreateGenerator(new GeneratorOptions + { + EmitSingleFile = true, + ClassName = HostClassName, + ExtensionReceiver = ReceiverName, + }); + Assert.True(this.generator.TryGenerate("HWND_BOTTOM", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + private static ClassDeclarationSyntax SingleHostClass(IEnumerable types) { // A single emitted host class with members (the one that actually has the wrap).