From a706e75e0775489f59238f662779e3ca05a63864 Mon Sep 17 00:00:00 2001 From: Abhikr Date: Thu, 8 Oct 2026 08:28:31 +0530 Subject: [PATCH] Support TypeDefinition handles for struct-typed constants in nesting and extension blocks --- .../Generator.Constant.cs | 36 +++++++--- .../ExternalMetadata/NOTICE.md | 3 +- .../ExternalMetadata/TypeDefConstant.il | 24 +++++++ .../ExternalMetadata/TypeDefConstant.winmd | Bin 0 -> 2048 bytes .../MultiMetadataTests.cs | 65 +++++++++++++++++- 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.il create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.winmd diff --git a/src/Microsoft.Windows.CsWin32/Generator.Constant.cs b/src/Microsoft.Windows.CsWin32/Generator.Constant.cs index 687f3686..cbfeeedf 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Constant.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Constant.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.Windows.CsWin32; @@ -143,28 +143,46 @@ internal void RequestConstant(FieldDefinitionHandle fieldDefHandle) // 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 + // * 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 + // * 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) + if (fieldTypeInfo is HandleTypeHandleInfo handleInfo && this.IsTypeDefStruct(handleInfo)) { - TypeReference tr = this.Reader.GetTypeReference((TypeReferenceHandle)handleInfo.Handle); - string fieldTypeName = this.Reader.GetString(tr.Name); - if (!TypeDefsThatDoNotNestTheirConstants.Contains(fieldTypeName)) + string? fieldTypeName = null; + string? fieldTypeNamespace = null; + if (handleInfo.Handle.Kind == HandleKind.TypeReference) { - string fieldTypeNamespace = this.Reader.GetString(tr.Namespace); - extensionStructName = fieldTypeNamespace.Length == 0 ? fieldTypeName : $"{fieldTypeNamespace}.{fieldTypeName}"; + TypeReference tr = this.Reader.GetTypeReference((TypeReferenceHandle)handleInfo.Handle); + fieldTypeName = this.Reader.GetString(tr.Name); + fieldTypeNamespace = this.Reader.GetString(tr.Namespace); if (this.TryGetTypeDefHandle(tr, out TypeDefinitionHandle candidate)) { fieldType = candidate; } } + else if (handleInfo.Handle.Kind == HandleKind.TypeDefinition) + { + TypeDefinitionHandle tdh = (TypeDefinitionHandle)handleInfo.Handle; + TypeDefinition td = this.Reader.GetTypeDefinition(tdh); + fieldTypeName = this.Reader.GetString(td.Name); + fieldTypeNamespace = this.Reader.GetString(td.Namespace); + fieldType = tdh; + } + + if (fieldTypeName is not null && !TypeDefsThatDoNotNestTheirConstants.Contains(fieldTypeName)) + { + extensionStructName = string.IsNullOrEmpty(fieldTypeNamespace) ? fieldTypeName : $"{fieldTypeNamespace}.{fieldTypeName}"; + } + else + { + fieldType = null; + } } // Multi-assembly composition: if another assembly already exposes this constant, skip regeneration diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md index 4b2e1940..2ae27f2d 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md @@ -6,4 +6,5 @@ ServiceFabric.winmd | [youyuanwu/fabric-metadata](https://github.com/youyuanwu/f CustomIInspectable.winmd | Generated using WinMDGenerator toolchain. Project in the subdirectory [CustomIInspectable](../../CustomIInspectable) with build instructions in [readme.md](../../CustomIInspectable/readme.md). WnfWithoutStatusSuccess.winmd | Authored fixture for issue #1677 regression coverage. Its source is checked in as [WnfWithoutStatusSuccess.il](WnfWithoutStatusSuccess.il). We keep this one as IL because that is much easier to maintain than reproducing the full end-to-end WinmdGenerator workflow for a focused test asset. SelfContained.winmd | Authored fixture reproducing the OSClient single-`Generator` build configuration (a non-`Windows.Win32` metadata assembly with the Windows.Win32 projection provided by a reference), where special string typedefs (`PCWSTR`) must be referenced under `Windows.Win32.Foundation`. Its source is checked in as [SelfContained.il](SelfContained.il). Kept as IL for the same reason as WnfWithoutStatusSuccess.winmd. -FlatNamespace.winmd | Authored fixture for issue #1848 regression coverage, with native typedefs and associated enums at the common namespace root and in a nested namespace. Its source and regeneration command are checked in as [FlatNamespace.il](FlatNamespace.il). \ No newline at end of file +FlatNamespace.winmd | Authored fixture for issue #1848 regression coverage, with native typedefs and associated enums at the common namespace root and in a nested namespace. Its source and regeneration command are checked in as [FlatNamespace.il](FlatNamespace.il). +TypeDefConstant.winmd | Authored fixture verifying that struct-typed constants defined in the same metadata module using TypeDefinition handles (rather than TypeReference handles) correctly nest into the struct and attach to extension blocks. Its source is checked in as [TypeDefConstant.il](TypeDefConstant.il). Kept as IL for the same reason as WnfWithoutStatusSuccess.winmd. diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.il b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.il new file mode 100644 index 00000000..47d4d481 --- /dev/null +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.il @@ -0,0 +1,24 @@ +// Source for TypeDefConstant.winmd. +// Fixture verifying that constants typed as typedef structs defined in the same +// metadata module (encoded as TypeDefinition handles rather than TypeReference handles) +// correctly nest into the struct declaration and attach to extension blocks. + +.assembly extern netstandard { .publickeytoken = (CC 7B 13 FF CD 2D DD 51 ) .ver 2:1:0:0 } +.assembly extern Windows.Win32.winmd { .ver 0:0:0:0 } +.assembly TypeDefConstant { .ver 1:0:0:0 } +.module TypeDefConstant.winmd +.module extern somedll.dll +.corflags 0x00000001 + +.class public sequential ansi sealed beforefieldinit TypeDefConstant.Foundation.CUSTOM_HANDLE + extends [netstandard]System.ValueType +{ + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.NativeTypedefAttribute::.ctor() = ( 01 00 00 00 ) + .field public native int Value +} + +.class public abstract auto autochar sealed beforefieldinit TypeDefConstant.Apis + extends [netstandard]System.Object +{ + .field public static initonly valuetype TypeDefConstant.Foundation.CUSTOM_HANDLE CUSTOM_HANDLE_VALUE = int32(1) +} diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.winmd b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/TypeDefConstant.winmd new file mode 100644 index 0000000000000000000000000000000000000000..f64af2da51a3b1513cd5b07cd254d11609a6b5be GIT binary patch literal 2048 zcmeHH&1(};5dUrZLE4&N6|7>Ruf`r-swtwNB~4PT(zKPNRf^DVvajuy&2HJAG0 zBrcbMRnkleU+ykeC>-rti{f?5diSa_yxyIgGhDIY*t3pS6a~$)Y)|B+a7vaiERi0~ zilVJcH4p* zHQpOPd6tVc!6f;eICdMDqfHu#V_DBJtXY>cOxm0078}pPBb(kHVi(6RrAgl-%=adi zH0?ZTCUPkqooH-fu+KniUnmx3C;3d9C!URk9k+^w2* z`jKz{yw`-HcwUR2%h-C!l>NvqyPhm!LNiM;x4a;6)v$DX$yG_mPpKE|lBH{&VOwfO zdK#G;MhGwZ$-0~w@M!w^k|&`SJlnx&eqI(laH+JMN!ga`X_luh8CFq;YZs+%nkq3Y z>2XNc9DUoKmE59RupQILBQ>7Qjb^5X21e4ugTEE7HHd))!`&XQ9PHN`QxgNj6G!?sAM#a0sQGY1b9)#f zRvTqM+oFc+t?K`y=9@~{PTDjxnqk2|qLltPIHTMHk*KBa1xr-EEo1S$(Cq&n|MdiZ E03txV^Z)<= literal 0 HcmV?d00001 diff --git a/test/Microsoft.Windows.CsWin32.Tests/MultiMetadataTests.cs b/test/Microsoft.Windows.CsWin32.Tests/MultiMetadataTests.cs index 0689aefe..9838b0b9 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/MultiMetadataTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/MultiMetadataTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. public class MultiMetadataTests : GeneratorTestBase @@ -83,4 +83,67 @@ public void SingleGeneratorQualifiesSpecialTypeDefToWin32Namespace(string api) Assert.DoesNotContain("winmdroot.Foundation.PCWSTR", tree.ToString()); } } + + [Fact] + public void TypeDefConstant_NestsConstantIntoStruct() + { + string metadataPath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "TypeDefConstant.winmd"); + + this.generator = new Generator(metadataPath, null, [], DefaultTestGeneratorOptions, this.compilation, this.parseOptions); + Assert.True(this.generator.TryGenerate("CUSTOM_HANDLE_VALUE", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + + // The constant should be nested inside CUSTOM_HANDLE struct, not dumped as a top-level field on the host class. + StructDeclarationSyntax customHandle = Assert.Single(this.FindGeneratedType("CUSTOM_HANDLE").OfType()); + Assert.Contains(customHandle.Members.OfType(), f => f.Declaration.Variables.Any(v => v.Identifier.ValueText == "CUSTOM_HANDLE_VALUE")); + + // No top-level constant on the host class should be generated. + Assert.Empty(this.FindGeneratedType(DefaultTestGeneratorOptions.ClassName)); + } + + [Fact] + public void TypeDefConstant_WithExtensionReceiver_AttachesToStructExtensionBlock() + { + string metadataPath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "TypeDefConstant.winmd"); + + this.parseOptions = this.parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + // Provide the receiver and the struct externally so extensionReceiver attaches forwarders to the struct. + this.compilation = this.AddCode( + @"namespace TypeDefConstant { internal static class PInvoke { } } +namespace TypeDefConstant.Foundation +{ + internal readonly struct CUSTOM_HANDLE + { + internal readonly int Value; + internal CUSTOM_HANDLE(int value) => this.Value = value; + public static explicit operator CUSTOM_HANDLE(int value) => new CUSTOM_HANDLE(value); + } +}", + fileName: "Refs.cs"); + + GeneratorOptions options = new() + { + EmitSingleFile = true, + ClassName = "PInvokeExtensions", + ExtensionReceiver = "PInvoke", + }; + this.generator = new Generator(metadataPath, null, [], options, this.compilation, this.parseOptions); + + Assert.True(this.generator.TryGenerate("CUSTOM_HANDLE_VALUE", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + + ClassDeclarationSyntax hostClass = Assert.Single( + this.FindGeneratedType("PInvokeExtensions").OfType(), + c => c.Members.Count > 0); + + // Under extensionReceiver when the struct is external, CUSTOM_HANDLE_VALUE attaches to extension(CUSTOM_HANDLE), + // not to extension(PInvoke). + ExtensionBlockDeclarationSyntax structExtension = Assert.Single( + hostClass.Members.OfType(), + b => b.ParameterList!.Parameters.Single().Type!.ToString().Trim() == "global::TypeDefConstant.Foundation.CUSTOM_HANDLE"); + Assert.Contains(structExtension.Members.OfType(), p => p.Identifier.ValueText == "CUSTOM_HANDLE_VALUE"); + } }