Skip to content

CsWin32-generated COM structs trip CS3016 under [assembly: CLSCompliant(true)] #1703

@JeremyKuhne

Description

@JeremyKuhne

Summary

When the consuming assembly is marked [assembly: CLSCompliant(true)], every CsWin32-generated COM struct that decorates a method parameter with [MarshalAs(UnmanagedType.SafeArray, SafeArraySubTypes = new[] { ... })] produces

warning CS3016: Arrays as attribute arguments is not CLS-compliant

Examples in the generated surface include ITypeInfo, ITypeLib, IRecordInfo, IStream, IClassFactory, etc. Under TreatWarningsAsErrors=true (the default for official builds in many repos, including dotnet/msbuild) these become hard errors.

The warnings cannot be cleanly suppressed:

  • [SuppressMessage] in a global suppressions file does not work for CSxxxx warnings — the C# compiler only honors SuppressMessageAttribute for analyzer diagnostics. See CLSCompliant(true) check for arrays as attribute arguments triggers on internal types dotnet/roslyn#68526.
  • A blanket <NoWarn>CS3016</NoWarn> masks legitimate user code violations.
  • The generated structs are internal, so [CLSCompliant(false)] is semantically meaningless on them — but applying it via a hand-written partial declaration is currently the only targeted way to silence the diagnostic, at the cost of then
    having to suppress CS3019 ("CLS compliance checking will not be performed … because it is not visible from outside this assembly") for that same folder.

Expected

CsWin32-generated COM struct wrappers should not produce CLS-compliance warnings in a CLS-compliant assembly. They are internal and not part of the assembly's CLS surface; the generator could mark them [CLSCompliant(false)] (or omit the
SafeArraySubTypes array, or emit the structs in a form the CLS rules accept) so consumers do not have to maintain a hand-written [CLSCompliant(false)] partial for each generated COM type.

Actual

Each generated COM struct that has a SafeArraySubTypes-decorated parameter emits CS3016 at every such call-site declaration. Consumers must either:

  • disable CS3016 globally (loses real diagnostic value for user code), or
  • maintain a per-struct [CLSCompliant(false)] internal partial struct Foo { } declaration alongside an .editorconfig rule that suppresses the resulting CS3019 noise.

Environment

Component Version
Microsoft.Windows.CsWin32 0.3.275
.NET SDK 10.0.100
TFMs in repro net10.0;net472

How the workaround looks in production

dotnet/msbuild carries a dedicated file, src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs, that exists solely to attach [CLSCompliant(false)] to each generated COM struct that trips CS3016, plus an .editorconfig rule that suppresses the resulting
CS3019 warnings for files under the CsWin32-generated namespaces.

References

Suggested fix

Emit [CLSCompliant(false)] on generated COM struct wrappers that contain non-CLS-compliant attribute arguments (or otherwise structure the generated [MarshalAs(...)] usage so the array argument is not required). Either change removes the need for consumers to author per-struct partial declarations.

Sources

Drop these four files into a fresh folder and run dotnet build.

ClsCompliantSafeArray.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net10.0;net472</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.275" PrivateAssets="all" />
  </ItemGroup>
</Project>

NativeMethods.txt

ITypeInfo
ITypeLib
IRecordInfo

NativeMethods.json

{ "$schema": "https://aka.ms/CsWin32.schema.json", "allowMarshaling": false, "useSafeHandles": false }

Program.cs

using System;

[assembly: CLSCompliant(true)]

internal static class Program
{
    // The mere presence of the generated ITypeInfo / ITypeLib / IRecordInfo
    // structs surfaces CS3016. No call site is required.
    private static void Main() => Console.WriteLine("see warnings above");
}

Sketch of the fix

In the CsWin32 generator, when emitting a COM struct wrapper that contains any parameter decorated with
[MarshalAs(UnmanagedType.SafeArray, SafeArraySubTypes = new[] { ... })] (or any other attribute whose argument is an array), also emit [CLSCompliant(false)] on the struct declaration itself:

[CLSCompliant(false)] // <-- new
internal partial struct ITypeInfo
{
    public unsafe void Invoke(
        ...,
        [MarshalAs(UnmanagedType.SafeArray, SafeArraySubTypes = new[] { typeof(...) })]
        object[] rgvarg,
        ...);
}

Because the structs are internal, this attribute is semantically a no-op for the assembly's CLS surface but suppresses CS3016 at the source. The generator should not also need to suppress CS3019 — that warning only fires when the
attribute is applied to a member that the compiler thinks isn't externally visible and the assembly is CLS-compliant. Suppression of CS3019 for generated files remains the consumer's responsibility (e.g. via an
.editorconfig rule scoped to the generated namespace), but is one-line and stable across new COM types.

Alternative: emit SafeArraySubTypes as a single-valued attribute argument when only one subtype is needed, or omit it entirely when the generator can prove it is unnecessary for blittable signatures (allowMarshaling: false).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions