Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Microsoft.Windows.CsWin32/Generator.Com.cs
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,20 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null)
iface = iface.AddAttributeLists(AttributeList(GUID(DecodeGuidFromAttribute(guidAttribute.Value))));
}

// CS3016 ("Arrays as attribute arguments is not CLS-compliant") fires under
// [assembly: CLSCompliant(true)] on the CCW thunks we emit with
// [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]. CCW thunks are
// only emitted when canUseUnmanagedCallersOnlyAttribute is true (.NET 5+); on
// net472 / netstandard2.0 the generator produces no array-valued attribute arguments and
// [CLSCompliant(false)] is unnecessary. The attribute is also a no-op on public types
// (which the consumer's CLS surface owns), so we only emit it on internal struct
// wrappers that we know carry the array-valued attribute.
// See https://github.com/microsoft/CsWin32/issues/1703.
if (ccwThisParameter is not null && this.Visibility == SyntaxKind.InternalKeyword)
{
iface = iface.AddAttributeLists(AttributeList(CLSCompliantFalse()));
}

if (this.GetSupportedOSPlatformAttribute(typeDef.GetCustomAttributes()) is AttributeSyntax supportedOSPlatformAttribute)
{
iface = iface.AddAttributeLists(AttributeList(supportedOSPlatformAttribute));
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Windows.CsWin32/Generator.Invariants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ public partial class Generator
"CS0436", // conflicts with the imported type (InternalsVisibleTo between two projects that both use CsWin32)
"CS8981", // The type name only contains lower-cased ascii characters
"SYSLIB1092", // The return value in the managed definition will be converted to an 'out' parameter when calling the unmanaged COM method
"CS3021", // Type does not need a CLSCompliant attribute because the assembly does not have a CLSCompliant attribute (fires on generated COM struct wrappers that we mark [CLSCompliant(false)] to silence CS3016 — see https://github.com/microsoft/CsWin32/issues/1703)
"CS3019", // CLS compliance checking will not be performed on '...' because it is not visible from outside this assembly (same root cause: our internal COM struct wrappers are marked [CLSCompliant(false)] to silence CS3016)
};

private static readonly SyntaxTriviaList FileHeader = ParseLeadingTrivia(AutoGeneratedHeader).Add(
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ internal static AttributeSyntax GUID(Guid guid)
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(guid.ToString().ToUpperInvariant()))));
}

internal static AttributeSyntax CLSCompliantFalse()
{
return Attribute(IdentifierName("CLSCompliant")).AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
}

internal static AttributeSyntax InterfaceType(ComInterfaceType interfaceType)
{
return Attribute(IdentifierName("InterfaceType")).AddArgumentListArguments(
Expand Down
103 changes: 103 additions & 0 deletions test/Microsoft.Windows.CsWin32.Tests/COMTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,4 +552,107 @@ public void COMInterfaceStructReturn(bool allowMarshaling, string tfm)

// TODO: Check "GetResourceAllocationInfo"
}

/// <summary>
/// Regression test for <see href="https://github.com/microsoft/CsWin32/issues/1703">issue 1703</see>:
/// generated COM struct wrappers contain CCW thunks annotated with
/// <c>[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]</c>, which trips
/// <c>CS3016 "Arrays as attribute arguments is not CLS-compliant"</c> under
/// <c>[assembly: CLSCompliant(true)]</c>. The generator must mark such COM struct wrappers
/// <c>[CLSCompliant(false)]</c> so consumers do not have to hand-author partials per type.
/// </summary>
[Theory]
[CombinatorialData]
public void COMStructWrappers_AreCLSCompliantFalse_Issue1703(
[CombinatorialValues("net8.0", "net9.0", "net10.0")] string tfm)
{
this.compilation = this.starterCompilations[tfm];
this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm(tfm) ?? LanguageVersion.Latest);
this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });

// The exact NativeMethods.txt content from the issue repro.
Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
Assert.True(this.generator.TryGenerate("ITypeLib", CancellationToken.None));
Assert.True(this.generator.TryGenerate("IRecordInfo", CancellationToken.None));
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics();

// Every generated COM struct wrapper that carries CCW thunks (annotated with
// [UnmanagedCallersOnly(CallConvs = new[]{...})]) must itself bear [CLSCompliant(false)].
foreach (string structName in new[] { "ITypeInfo", "ITypeLib", "IRecordInfo" })
{
var type = Assert.IsType<StructDeclarationSyntax>(this.FindGeneratedType(structName).Single());
Assert.Contains(
type.AttributeLists.SelectMany(al => al.Attributes),
a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant"
&& a.ArgumentList?.Arguments.Count == 1
&& a.ArgumentList.Arguments[0].Expression is LiteralExpressionSyntax { Token.ValueText: "false" });
}

// End-to-end: a consuming assembly marked [assembly: CLSCompliant(true)] must compile
// entirely clean — no CS3016, no CS3019, no CS3021. The generator suppresses CS3019/CS3021
// in the generated-file pragma so consumers do not have to author per-file overrides.
this.compilation = this.AddCode("""
using System;

[assembly: CLSCompliant(true)]

internal static class Issue1703Consumer
{
public static void Touch()
{
// Mere presence of the generated types in this CLS-compliant assembly used to trip CS3016.
_ = typeof(Windows.Win32.System.Com.ITypeInfo);
_ = typeof(Windows.Win32.System.Com.ITypeLib);
_ = typeof(Windows.Win32.System.Ole.IRecordInfo);
}
}
""");
this.AssertNoDiagnostics(this.compilation, logAllGeneratedCode: false);
}

/// <summary>
/// Negative coverage for #1703: on downlevel TFMs (<c>net472</c>, <c>netstandard2.0</c>)
/// the generator does not emit <c>[UnmanagedCallersOnly]</c>-decorated CCW thunks, so there is
/// no array-valued attribute argument and no CS3016 to suppress. The generator must therefore
/// not emit <c>[CLSCompliant(false)]</c> in that case — the attribute would be unmotivated noise.
/// </summary>
[Theory]
[CombinatorialData]
public void COMStructWrappers_NoCLSCompliantFalse_OnDownlevelTFMs_Issue1703(
[CombinatorialValues("net472", "netstandard2.0")] string tfm)
{
this.compilation = this.starterCompilations[tfm];
this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });

Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
this.CollectGeneratedCode(this.generator);

var type = Assert.IsType<StructDeclarationSyntax>(this.FindGeneratedType("ITypeInfo").Single());
Assert.DoesNotContain(
type.AttributeLists.SelectMany(al => al.Attributes),
a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
}

/// <summary>
/// Negative coverage for #1703: when the generator is configured to emit public types
/// (<see cref="GeneratorOptions.Public"/> = <see langword="true"/>), the COM struct wrapper
/// is part of the consumer's CLS surface and they own its CLS-compliance contract — the
/// generator must not unilaterally stamp <c>[CLSCompliant(false)]</c> on it.
/// </summary>
[Fact]
public void COMStructWrappers_NoCLSCompliantFalse_WhenPublic_Issue1703()
{
this.compilation = this.starterCompilations["net10.0"];
this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm("net10.0") ?? LanguageVersion.Latest);
this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false, Public = true });

Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
this.CollectGeneratedCode(this.generator);

var type = Assert.IsType<StructDeclarationSyntax>(this.FindGeneratedType("ITypeInfo").Single());
Assert.DoesNotContain(
type.AttributeLists.SelectMany(al => al.Attributes),
a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
}
}
Loading