Skip to content

Commit 8b2cbfd

Browse files
jevansaksCopilot
andcommitted
Qualify PCWSTR correctly in ReadOnlySpan<string> array marshaling
The friendly overload that projects a counted array of const strings (`[Const, NativeArrayInfo] PCWSTR*`) as `ReadOnlySpan<string>` marshals via `GCHandle`/`ArrayPool<PCWSTR>`. The temporary `ArrayPool<PCWSTR>` used the `PCWSTRTypeSyntax` helper, which was hard-coded to the `winmdroot` alias. For the Win32 SDK itself `winmdroot` is `Windows.Win32`, so this was correct. But when projecting other metadata (whose `winmdroot` aliases a different assembly, e.g. `global::OSClient`), `winmdroot.Foundation.PCWSTR` resolves to a non-existent `OSClient.Foundation.PCWSTR` and fails to compile -- even though the COM signature itself correctly references `global::Windows.Win32.Foundation.PCWSTR`. Qualify the ArrayPool element type via `Win32NamespacePrefix` (as HRESULT and other well-known types already do), so it resolves to `global::Windows.Win32.Foundation.PCWSTR` outside the Win32 SDK and remains `winmdroot.Foundation.PCWSTR` within it. Extends the StringArrayInterop fixture with a counted-array interface and adds MultiMetadataTests.StringArrayFriendlyOverloadQualifiesPcwstrCorrectly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 773b3bf commit 8b2cbfd

4 files changed

Lines changed: 68 additions & 18 deletions

File tree

src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ namespace Microsoft.Windows.CsWin32;
55

66
public partial class Generator
77
{
8-
private static readonly TypeSyntax PCWSTRTypeSyntax = QualifiedName(QualifiedName(IdentifierName(GlobalWinmdRootNamespaceAlias), IdentifierName("Foundation")), IdentifierName("PCWSTR"));
9-
108
private enum FriendlyOverloadOf
119
{
1210
ExternMethod,
1311
StructMethod,
1412
InterfaceMethod,
1513
}
1614

15+
/// <summary>
16+
/// Gets the syntax for the <c>PCWSTR</c> type, qualified to its true location. This resolves to
17+
/// <c>winmdroot.Foundation.PCWSTR</c> when generating the Win32 SDK itself, and to
18+
/// <c>global::Windows.Win32.Foundation.PCWSTR</c> when projecting other metadata (whose <c>winmdroot</c>
19+
/// alias points at a different assembly), matching how the parameter type itself is qualified.
20+
/// </summary>
21+
private TypeSyntax PCWSTRTypeSyntax => QualifiedName(QualifiedName(this.Win32NamespacePrefix, IdentifierName("Foundation")), IdentifierName("PCWSTR"));
22+
1723
private static ParameterSyntax StripAttributes(ParameterSyntax parameter) => parameter.WithAttributeLists(default);
1824

1925
/// <summary>
@@ -815,7 +821,7 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
815821
SyntaxKind.SimpleMemberAccessExpression,
816822
MemberAccessExpression(
817823
SyntaxKind.SimpleMemberAccessExpression,
818-
ParseTypeName($"global::System.Buffers.ArrayPool<{PCWSTRTypeSyntax.ToString()}>"),
824+
ParseTypeName($"global::System.Buffers.ArrayPool<{this.PCWSTRTypeSyntax}>"),
819825
IdentifierName("Shared")),
820826
IdentifierName("Rent")),
821827
[Argument(GetSpanLength(origName, false))])))
@@ -891,7 +897,7 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
891897
InvocationExpression(
892898
MemberAccessExpression(
893899
SyntaxKind.SimpleMemberAccessExpression,
894-
ParseTypeName($"global::System.Buffers.ArrayPool<{PCWSTRTypeSyntax.ToString()}> "),
900+
ParseTypeName($"global::System.Buffers.ArrayPool<{this.PCWSTRTypeSyntax}> "),
895901
IdentifierName("Shared.Return")),
896902
[Argument(pcwstrLocal)]));
897903

test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/StringArrayInterop.il

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
// Source for StringArrayInterop.winmd.
22
//
3-
// This fixture reproduces the friendly-overload bug hit by real UDK-generated metadata: a COM interface
4-
// method that takes a pointer to a const string pointer (an "_In_reads_opt_(count) LPCWSTR*" array of
5-
// strings) whose array-count SAL was NOT captured as a NativeArrayInfoAttribute. In metadata this is just
6-
// a [Const] PWSTR* parameter (which CsWin32 projects as PCWSTR*).
7-
//
8-
// Because PCWSTR is itself a pointer wrapper, such a parameter is a pointer-to-pointer and must NOT be
9-
// projected by the friendly-overload generator as a single "in PCWSTR" value. (When the count *is*
10-
// captured as NativeArrayInfo the parameter is correctly projected as a ReadOnlySpan; this fixture covers
11-
// the case where it is absent.)
3+
// This fixture reproduces two friendly-overload bugs hit by real UDK-generated metadata, where a COM
4+
// interface method takes a pointer to a const string pointer (an "_In_reads_opt_(count) LPCWSTR*" array of
5+
// strings). In metadata this is a [Const] PWSTR* parameter, which CsWin32 projects as PCWSTR*.
126
//
137
// The common namespace is "StringArrayInterop" (not "Windows.Win32") so the generator treats this like a
14-
// customer's own metadata assembly, and PWSTR/PCWSTR come from the referenced Windows.Win32 metadata.
8+
// customer's own metadata assembly. PWSTR/PCWSTR come from the referenced Windows.Win32 metadata, so the
9+
// generated PCWSTR type lives at global::Windows.Win32.Foundation.PCWSTR rather than under the local
10+
// "winmdroot" (= global::StringArrayInterop) alias.
11+
//
12+
// * IStringArrayConsumer.AcceptStrings has NO NativeArrayInfo, so CsWin32 cannot tell it is an array. It
13+
// must be left as a raw PCWSTR* pointer, NOT projected as a single "in PCWSTR" value (PCWSTR is itself a
14+
// pointer wrapper).
15+
// * ICountedStringArrayConsumer.AcceptCountedStrings HAS NativeArrayInfo, so it is projected as
16+
// ReadOnlySpan<string>. The generated GCHandle/ArrayPool<PCWSTR> marshaling must qualify PCWSTR as
17+
// global::Windows.Win32.Foundation.PCWSTR, not the local "winmdroot.Foundation.PCWSTR" (which would be
18+
// StringArrayInterop.Foundation.PCWSTR and fail to compile).
19+
//
20+
// The trailing [out] parameter forces a friendly overload to be emitted in both cases.
1521

1622
.assembly extern netstandard
1723
{
@@ -36,10 +42,7 @@
3642
.subsystem 0x0003
3743
.corflags 0x00000001
3844

39-
// A COM interface whose method accepts an array of strings via [Const] PWSTR* (projected as PCWSTR*),
40-
// like IShareEngine::StopSharing's "_In_reads_opt_(cSids) LPCWSTR* ppszUserSids". The trailing [out]
41-
// parameter forces a friendly overload to be emitted so the projection of the string-array parameter can
42-
// be asserted.
45+
// An array of strings WITHOUT count metadata: must stay a PCWSTR* pointer (not "in PCWSTR").
4346
.class interface public abstract auto ansi StringArrayInterop.Apis.IStringArrayConsumer
4447
{
4548
.custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.GuidAttribute::.ctor(uint32, uint16, uint16, uint8, uint8, uint8, uint8, uint8, uint8, uint8, uint8) = (
@@ -54,3 +57,21 @@
5457
.custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.ConstAttribute::.ctor() = ( 01 00 00 00 )
5558
}
5659
}
60+
61+
// An array of strings WITH count metadata (NativeArrayInfo -> CountParamIndex = 1): projected as
62+
// ReadOnlySpan<string>, exercising the ArrayPool<PCWSTR> marshaling that must qualify PCWSTR correctly.
63+
.class interface public abstract auto ansi StringArrayInterop.Apis.ICountedStringArrayConsumer
64+
{
65+
.custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.GuidAttribute::.ctor(uint32, uint16, uint16, uint8, uint8, uint8, uint8, uint8, uint8, uint8, uint8) = (
66+
01 00 79 56 34 12 34 12 34 12 12 34 56 78 9A BC DE F0 00 00 )
67+
68+
.method public hidebysig newslot abstract virtual instance int32
69+
AcceptCountedStrings([in] valuetype [Windows.Win32.winmd]Windows.Win32.Foundation.PWSTR* strings,
70+
uint32 count,
71+
[out] uint32* accepted) cil managed
72+
{
73+
.param [1]
74+
.custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.ConstAttribute::.ctor() = ( 01 00 00 00 )
75+
.custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.NativeArrayInfoAttribute::.ctor() = ( 01 00 01 00 53 06 0F 43 6F 75 6E 74 50 61 72 61 6D 49 6E 64 65 78 01 00 )
76+
}
77+
}
Binary file not shown.

test/Microsoft.Windows.CsWin32.Tests/MultiMetadataTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,27 @@ public void PointerToStringPointerParameterIsNotProjectedAsInParameter()
6565
Assert.True(stringsParam.Type is PointerTypeSyntax, $"'strings' should remain a pointer but was: {stringsParam.Type}");
6666
}
6767
}
68+
69+
/// <summary>
70+
/// Asserts that the <c>ReadOnlySpan&lt;string&gt;</c> friendly overload generated for a counted array of
71+
/// strings (<c>[Const, NativeArrayInfo] PCWSTR*</c>) qualifies the <c>PCWSTR</c> type used by its
72+
/// <c>ArrayPool&lt;PCWSTR&gt;</c> marshaling to <c>global::Windows.Win32.Foundation.PCWSTR</c> rather than
73+
/// the local <c>winmdroot</c> alias (which, for non-Win32 metadata, points at a different assembly and
74+
/// therefore fails to compile).
75+
/// </summary>
76+
[Fact]
77+
public void StringArrayFriendlyOverloadQualifiesPcwstrCorrectly()
78+
{
79+
this.generator = this.CreateSuperGenerator([StringArrayInteropMetadataPath, .. DefaultMetadataPaths], DefaultTestGeneratorOptions);
80+
81+
// GenerateApi asserts the generated code compiles; before the fix the ArrayPool<PCWSTR> temporaries
82+
// referenced StringArrayInterop.Foundation.PCWSTR (via the winmdroot alias), which does not exist.
83+
this.GenerateApi("ICountedStringArrayConsumer");
84+
85+
MethodDeclarationSyntax friendly = Assert.Single(
86+
this.FindGeneratedMethod("AcceptCountedStrings"),
87+
m => m.ParameterList.Parameters.Any(p => p.Type is GenericNameSyntax { Identifier.ValueText: "ReadOnlySpan" }));
88+
string body = friendly.ToFullString();
89+
Assert.Contains("ArrayPool<global::Windows.Win32.Foundation.PCWSTR>", body);
90+
}
6891
}

0 commit comments

Comments
 (0)