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 00000000..ae47958e
Binary files /dev/null and b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/FlatNamespace.winmd differ
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;