From f4d27473546a706bad9f8f8c59865544f0f75514 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 8 Oct 2026 13:37:58 -0700 Subject: [PATCH] Fix custom marshaller namespaces for root metadata types Avoid empty namespace segments in native typedef and associated enum marshallers. Add build-task generation and compilation coverage for root and nested metadata namespaces on .NET 8 through .NET 10. Fixes #1848 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ad80cd5b-a9a2-4cee-aa3d-030ab5462c4d --- .../Generator.CustomMarshaller.cs | 4 +- .../CsWin32GeneratorTests.cs | 58 ++++++++++++++ .../ExternalMetadata/FlatNamespace.il | 71 ++++++++++++++++++ .../ExternalMetadata/FlatNamespace.winmd | Bin 0 -> 2560 bytes .../ExternalMetadata/NOTICE.md | 3 +- .../GeneratorTestBase.cs | 1 + 6 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.il create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.winmd diff --git a/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs b/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs index c90e8e32..3e5ac809 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs @@ -23,7 +23,7 @@ internal string RequestCustomEnumMarshaller(string qualifiedEnumTypeName, Unmana } // Custom marshallers should go in a InteropServices sub-namespace. - shortNamespace += ".InteropServices"; + shortNamespace = shortNamespace.Length > 0 ? $"{shortNamespace}.InteropServices" : "InteropServices"; string customTypeMarshallerName = $"{enumTypeName}To{unmanagedType}Marshaller"; @@ -115,7 +115,7 @@ internal string RequestCustomTypeDefMarshaller(string fullyQualifiedTypeName, Ty } // Custom marshallers should go in a InteropServices sub-namespace. - shortNamespace += ".InteropServices"; + shortNamespace = shortNamespace.Length > 0 ? $"{shortNamespace}.InteropServices" : "InteropServices"; string customTypeMarshallerName = $"{typeDefName}Marshaller"; diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index 2eef5c5a..63ec3d8d 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -691,6 +691,64 @@ public async Task CrossWinMD_NTSTATUSSafeHandleConstant() Assert.Contains(this.FindGeneratedType("RtlUnsubscribeWnfStateChangeNotificationSafeHandle"), static type => type is ClassDeclarationSyntax); } + /// + /// Verifies native typedef marshaller namespaces through build-task generation and compilation. + /// + /// Whether the typedef is in a child of the common namespace. + /// The target framework of the generated projection. + [Theory, CombinatorialData] + public async Task NativeTypedefMarshallerNamespace( + bool nestedNamespace, + [CombinatorialValues("net8.0", "net9.0", "net10.0")] string tfm) + { + this.compilation = this.starterCompilations[tfm]; + this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm(tfm)!.Value); + this.tfm = tfm; + this.win32winmdPaths = [FlatNamespaceMetadataPath]; + string typeName = nestedNamespace ? "NESTED_HANDLE" : "TEST_HANDLE"; + string typeNamespace = nestedNamespace ? "FlatNamespace.Nested" : "FlatNamespace"; + string marshallerName = $"{typeNamespace}.InteropServices.{typeName}Marshaller"; + this.nativeMethods.Add(nestedNamespace ? "UseNestedHandle" : "UseRootHandle"); + + await this.InvokeGeneratorAndCompile($"{nameof(this.NativeTypedefMarshallerNamespace)}_{nestedNamespace}_{tfm}"); + + StructDeclarationSyntax typedef = Assert.Single(this.FindGeneratedType(typeName).OfType()); + AttributeSyntax attribute = Assert.Single(FindAttribute(typedef.AttributeLists, "NativeMarshalling")); + TypeOfExpressionSyntax marshallerType = Assert.IsType(Assert.Single(attribute.ArgumentList!.Arguments).Expression); + Assert.Equal($"global::{marshallerName}", marshallerType.Type.ToString()); + Assert.NotNull(this.compilation.GetTypeByMetadataName(marshallerName)); + } + + /// + /// Verifies associated enum marshaller namespaces through build-task generation and compilation. + /// + /// Whether the enum is in a child of the common namespace. + /// The target framework of the generated projection. + [Theory, CombinatorialData] + public async Task AssociatedEnumMarshallerNamespace( + bool nestedNamespace, + [CombinatorialValues("net8.0", "net9.0", "net10.0")] string tfm) + { + this.compilation = this.starterCompilations[tfm]; + this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm(tfm)!.Value); + this.tfm = tfm; + this.win32winmdPaths = [FlatNamespaceMetadataPath]; + string typeName = nestedNamespace ? "NESTED_ENUM" : "TEST_ENUM"; + string typeNamespace = nestedNamespace ? "FlatNamespace.Nested" : "FlatNamespace"; + string marshallerName = $"{typeNamespace}.InteropServices.{typeName}ToU4Marshaller"; + string apiName = nestedNamespace ? "UseNestedEnum" : "UseRootEnum"; + this.nativeMethods.Add(apiName); + + await this.InvokeGeneratorAndCompile($"{nameof(this.AssociatedEnumMarshallerNamespace)}_{nestedNamespace}_{tfm}"); + + MethodDeclarationSyntax method = Assert.Single(this.FindGeneratedMethod(apiName), m => FindAttribute(m.AttributeLists, "LibraryImport").Any()); + ParameterSyntax parameter = Assert.Single(method.ParameterList.Parameters); + AttributeSyntax attribute = Assert.Single(FindAttribute(parameter.AttributeLists, "global::System.Runtime.InteropServices.Marshalling.MarshalUsing")); + TypeOfExpressionSyntax marshallerType = Assert.IsType(Assert.Single(attribute.ArgumentList!.Arguments).Expression); + Assert.Equal($"global::{marshallerName}", marshallerType.Type.ToString()); + Assert.NotNull(this.compilation.GetTypeByMetadataName(marshallerName)); + } + [Fact] public async Task TestComVariantReturnValue() { diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.il b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.il new file mode 100644 index 00000000..611fd81d --- /dev/null +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.il @@ -0,0 +1,71 @@ +// Source for FlatNamespace.winmd. +// Root and nested native typedefs and associated enums exercise custom marshaller namespaces. +// Regenerate with: ilasm FlatNamespace.il /dll /output=FlatNamespace.winmd + +.assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 4:0:0:0 } +.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 FlatNamespace { .ver 1:0:0:0 } +.module FlatNamespace.winmd +.module extern example.dll +.corflags 0x00000001 + +.class public sequential ansi sealed beforefieldinit FlatNamespace.TEST_HANDLE + extends [netstandard]System.ValueType +{ + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.NativeTypedefAttribute::.ctor() = ( 01 00 00 00 ) + .field public void* Value +} + +.class public auto ansi sealed FlatNamespace.TEST_ENUM + extends [netstandard]System.Enum +{ + .field public specialname rtspecialname int32 value__ + .field public static literal valuetype FlatNamespace.TEST_ENUM TestValue = int32(1) +} + +.class public abstract auto autochar sealed beforefieldinit FlatNamespace.Apis + extends [netstandard]System.Object +{ + .method public hidebysig static pinvokeimpl("example.dll" nomangle winapi) + void UseRootHandle([in] valuetype FlatNamespace.TEST_HANDLE handle) cil managed + { + } + + .method public hidebysig static pinvokeimpl("example.dll" nomangle winapi) + void UseRootEnum([in] uint32 enumValue) cil managed + { + .param [1] + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute::.ctor(string) = ( 01 00 09 54 45 53 54 5F 45 4E 55 4D 00 00 ) + } +} + +.class public sequential ansi sealed beforefieldinit FlatNamespace.Nested.NESTED_HANDLE + extends [netstandard]System.ValueType +{ + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.NativeTypedefAttribute::.ctor() = ( 01 00 00 00 ) + .field public void* Value +} + +.class public auto ansi sealed FlatNamespace.Nested.NESTED_ENUM + extends [netstandard]System.Enum +{ + .field public specialname rtspecialname int32 value__ + .field public static literal valuetype FlatNamespace.Nested.NESTED_ENUM TestValue = int32(1) +} + +.class public abstract auto autochar sealed beforefieldinit FlatNamespace.Nested.Apis + extends [netstandard]System.Object +{ + .method public hidebysig static pinvokeimpl("example.dll" nomangle winapi) + void UseNestedHandle([in] valuetype FlatNamespace.Nested.NESTED_HANDLE handle) cil managed + { + } + + .method public hidebysig static pinvokeimpl("example.dll" nomangle winapi) + void UseNestedEnum([in] uint32 enumValue) cil managed + { + .param [1] + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute::.ctor(string) = ( 01 00 0B 4E 45 53 54 45 44 5F 45 4E 55 4D 00 00 ) + } +} diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.winmd b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.winmd new file mode 100644 index 0000000000000000000000000000000000000000..ae47958ea40d3e6655bdeee747abb670aca4ec6c GIT binary patch literal 2560 zcmeHI-)kII6#nl1%I4RaHAN~~Iw1`x*r7I+AZXd7yJ-yBZR&2)QVN^do!fTH?94JV zo3;uif>)Rewn31p$V zydZkJ)!a5unJ3KI6SJ?ru9BtUP(Nhy>D$0%_Ohk?YUsDztAS$t#FtUEH@#NHuk?9) zm_EDqR+07*v90ie<27iNFon2q1c!zd_+w@k&qcMXu@3Q3uMpeO54n1L3akEN%e0i+ zy{a?FV+1+8i5Iy068#M7I&y^(CDCkDm&0NL_wayyDI~b?3(+`F5qM8Li{FUyJSSp4 z#2%R`es797DfY-r6GhBDGKYvVI4Y))$;^lutZy9`Fa0O7sH*_BjXCiOb4Rjmtca7k zKZUoAhFE5{c+KaUszac}!v!vC9s7CI7<-vI&FPQfMe4)UXD~`##3bhMKJ_X-)sc_c zK@xK+he-4LRdxssZHM90Dv2wLYIs1SCLYTO?r2xdDwaun9=ikftJr^|?AcvMo<{Xn z5Xv^zEvGAMw>lCRTCVNg2u#}9ljfq=b!{tbd9GQOp+(n1g?LkwZFyxrT;8rXG9{CO=j|A{8B_d z%QsOB=lE6hJ0>ZASg#(M5E$+Ct{YQ_{&-9ut<~?!H~M(ci+|7hvHBU$FFH=yYPr!J c$>_cmV}Axlp-+7pJ)`%(<+Z1=`Tu+V3%ra76aWAK literal 0 HcmV?d00001 diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md index 50d9a5a5..4b2e1940 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md @@ -5,4 +5,5 @@ Metadata | Source ServiceFabric.winmd | [youyuanwu/fabric-metadata](https://github.com/youyuanwu/fabric-metadata/raw/a1bcca6ad6f6a772c9e5ff4bdba80ae5e5f24cfc/.windows/winmd/ServiceFabric.winmd) 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. \ No newline at end of file +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 diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index e77d2763..452f02fb 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -15,6 +15,7 @@ public abstract class GeneratorTestBase : IDisposable, IAsyncLifetime protected static readonly string ServiceFabricMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "ServiceFabric.winmd"); protected static readonly string CustomIInspectableMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "CustomIInspectable.winmd"); protected static readonly string WnfWithoutStatusSuccessMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "WnfWithoutStatusSuccess.winmd"); + protected static readonly string FlatNamespaceMetadataPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "ExternalMetadata", "FlatNamespace.winmd"); protected static readonly string ApiDocsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "apidocs.msgpack"); protected readonly ITestOutputHelper logger;