diff --git a/eng/generators.targets b/eng/generators.targets index c24eca9be55dd1..3290f37580be29 100644 --- a/eng/generators.targets +++ b/eng/generators.targets @@ -48,12 +48,6 @@ " /> - - - - - diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/LibraryImportAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/LibraryImportAttribute.cs index b5dd3804d58f34..c4dedf19b05fbc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/LibraryImportAttribute.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/LibraryImportAttribute.cs @@ -14,16 +14,7 @@ namespace System.Runtime.InteropServices /// applied to static, partial, non-generic methods. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -#if SYSTEM_PRIVATE_CORELIB - public -#else -#pragma warning disable CS0436 // Type conflicts with imported type - // Some assemblies that target downlevel have InternalsVisibleTo to their test assemblies. - // As this is only used in this repo and isn't a problem in shipping code, - // just disable the duplicate type warning. - internal -#endif - sealed class LibraryImportAttribute : Attribute + public sealed class LibraryImportAttribute : Attribute { /// /// Initializes a new instance of the . diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StringMarshalling.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StringMarshalling.cs index b31d024cea8e68..0970d95135b9b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StringMarshalling.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StringMarshalling.cs @@ -13,12 +13,7 @@ namespace System.Runtime.InteropServices /// /// Specifies how strings should be marshalled for generated p/invokes /// -#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_INTEROP_SOURCEGENERATION - public -#else - internal -#endif - enum StringMarshalling + public enum StringMarshalling { /// /// Indicates the user is supplying a specific marshaller in . diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs index 63ca6ca49ff81d..aa84f8615811f9 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs @@ -35,8 +35,42 @@ public static class StepNames public const string GenerateSingleStub = nameof(GenerateSingleStub); } + // Internal definitions of LibraryImportAttribute and StringMarshalling that are injected + // into user projects targeting downlevel frameworks that don't have these types in the BCL. + // These definitions only expose the properties and enum values supported downlevel. + private const string GeneratedInteropTypes = """ + // + #nullable enable + namespace System.Runtime.InteropServices + { + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [global::Microsoft.CodeAnalysis.Embedded] + internal sealed partial class LibraryImportAttribute : global::System.Attribute + { + public LibraryImportAttribute(string libraryName) { } + public string? EntryPoint { get; set; } + public StringMarshalling StringMarshalling { get; set; } + public bool SetLastError { get; set; } + } + + [global::Microsoft.CodeAnalysis.Embedded] + internal enum StringMarshalling + { + Utf16 = 2, + } + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) { + // Inject internal definitions of LibraryImportAttribute and StringMarshalling into the compilation + // so that users targeting downlevel frameworks can apply [LibraryImport] to their methods. + context.RegisterPostInitializationOutput(static ctx => + { + ctx.AddEmbeddedAttributeDefinition(); + ctx.AddSource("LibraryImportInteropTypes.g.cs", GeneratedInteropTypes); + }); + // Collect all methods adorned with LibraryImportAttribute and filter out invalid ones // (diagnostics for invalid methods are reported by the analyzer) var methodsToGenerate = context.SyntaxProvider diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AdditionalAttributesOnStub.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AdditionalAttributesOnStub.cs index ebb295f6d3fe79..8d58aa3679f45c 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AdditionalAttributesOnStub.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AdditionalAttributesOnStub.cs @@ -128,7 +128,6 @@ public async Task SkipLocalsInitOnDownlevelTargetFrameworks(TestTargetFramework { string source = $$""" using System.Runtime.InteropServices; - {{CodeSnippets.LibraryImportAttributeDeclaration}} partial class C { [LibraryImportAttribute("DoesNotExist")] diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/CodeSnippets.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/CodeSnippets.cs index c0896cc09e8f5d..08c9804f7f0993 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/CodeSnippets.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/CodeSnippets.cs @@ -46,28 +46,6 @@ string ICustomMarshallingSignatureTestProvider.MarshalUsingCollectionReturnValue string ICustomMarshallingSignatureTestProvider.CustomElementMarshalling(string type, string marshallerType, string preDeclaration) => CustomElementMarshalling(type, marshallerType, preDeclaration); - /// - /// Partially define attribute for pre-.NET 7.0 - /// - public static readonly string LibraryImportAttributeDeclaration = """ - namespace System.Runtime.InteropServices - { - internal enum StringMarshalling - { - Custom = 0, - Utf8, - Utf16, - } - - sealed class LibraryImportAttribute : System.Attribute - { - public LibraryImportAttribute(string a) { } - public StringMarshalling StringMarshalling { get; set; } - public Type StringMarshallingCustomType { get; set; } - } - } - """; - /// /// Trivial declaration of LibraryImport usage /// diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs index 225ae48288ebc1..81b10efc82d4af 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs @@ -500,8 +500,9 @@ protected override ParseOptions CreateParseOptions() public static IEnumerable CodeSnippetsToValidateFallbackForwarder() { // Confirm that all unsupported target frameworks can be generated. + // LibraryImportAttribute and StringMarshalling are injected by the generator via RegisterPostInitializationOutput. { - string code = CodeSnippets.BasicParametersAndModifiers(CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicParametersAndModifiers(); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, false }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, false }; yield return new object[] { ID(), code, TestTargetFramework.Framework, false }; @@ -509,7 +510,7 @@ public static IEnumerable CodeSnippetsToValidateFallbackForwarder() // Confirm that all unsupported target frameworks fall back to a forwarder. { - string code = CodeSnippets.BasicParametersAndModifiers(CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicParametersAndModifiers(); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; @@ -517,7 +518,7 @@ public static IEnumerable CodeSnippetsToValidateFallbackForwarder() // Confirm that all unsupported target frameworks fall back to a forwarder. { - string code = CodeSnippets.BasicParametersAndModifiersWithStringMarshalling(StringMarshalling.Utf16, CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicParametersAndModifiersWithStringMarshalling(StringMarshalling.Utf16); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; @@ -525,25 +526,25 @@ public static IEnumerable CodeSnippetsToValidateFallbackForwarder() // Confirm that if support is missing for a type with an ITypeBasedMarshallingInfoProvider (like arrays and SafeHandles), we fall back to a forwarder even if other types are supported. { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "System.Runtime.InteropServices.SafeHandle", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "System.Runtime.InteropServices.SafeHandle"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; } { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("System.Runtime.InteropServices.SafeHandle", "int", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("System.Runtime.InteropServices.SafeHandle", "int"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; } { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "int[]", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "int[]"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; } { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("int", "int[]", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("int", "int[]"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; @@ -551,13 +552,13 @@ public static IEnumerable CodeSnippetsToValidateFallbackForwarder() // Confirm that if support is missing for a type without an ITypeBasedMarshallingInfoProvider (like StringBuilder), we fall back to a forwarder even if other types are supported. { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "System.Text.StringBuilder", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("void", "System.Text.StringBuilder"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; } { - string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("int", "System.Text.StringBuilder", CodeSnippets.LibraryImportAttributeDeclaration); + string code = CodeSnippets.BasicReturnAndParameterWithAlwaysSupportedParameter("int", "System.Text.StringBuilder"); yield return new object[] { ID(), code, TestTargetFramework.Standard2_0, true }; yield return new object[] { ID(), code, TestTargetFramework.Standard2_1, true }; yield return new object[] { ID(), code, TestTargetFramework.Framework, true }; @@ -761,14 +762,17 @@ public class Basic { } [InlineData(TestTargetFramework.Standard2_0)] [InlineData(TestTargetFramework.Framework)] [Theory] - public async Task ValidateNoGeneratedOutputForNoImportDownlevel(TestTargetFramework framework) + public async Task ValidateNoStubOutputForNoImportDownlevel(TestTargetFramework framework) { + // The DownlevelLibraryImportGenerator always injects type definitions for LibraryImportAttribute + // and StringMarshalling via RegisterPostInitializationOutput. When there are no [LibraryImport] + // usages, no method stubs should be generated beyond those type definitions. string source = """ using System.Runtime.InteropServices; public class Basic { } """; - var test = new NoChangeTest(framework) + var test = new OnlyTypeDefinitionsOutputTest(framework) { TestCode = source, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck @@ -777,6 +781,28 @@ public class Basic { } await test.RunAsync(); } + class OnlyTypeDefinitionsOutputTest : Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier.Test + { + public OnlyTypeDefinitionsOutputTest(TestTargetFramework framework) + : base(framework) + { + } + + protected override async Task<(Compilation compilation, ImmutableArray generatorDiagnostics)> GetProjectCompilationAsync(Project project, IVerifier verifier, CancellationToken cancellationToken) + { + var originalCompilation = await project.GetCompilationAsync(cancellationToken); + var (newCompilation, diagnostics) = await base.GetProjectCompilationAsync(project, verifier, cancellationToken); + // The DownlevelLibraryImportGenerator should inject type definitions (for example, via + // RegisterPostInitializationOutput) but, when there are no [LibraryImport] usages, it + // must not emit any stub output such as the generated "LibraryImports" stub tree. + var originalTrees = originalCompilation!.SyntaxTrees.ToImmutableArray(); + var newTrees = newCompilation.SyntaxTrees.ToImmutableArray(); + var addedTrees = newTrees.Except(originalTrees).ToImmutableArray(); + Assert.DoesNotContain(addedTrees, tree => tree.FilePath.Contains("LibraryImports", StringComparison.Ordinal)); + return (newCompilation, diagnostics); + } + } + class NoChangeTest : Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier.Test where TSourceGenerator : new() where TAnalyzer : DiagnosticAnalyzer, new() diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs index b33c26d2cb22a6..9894e9141ed14d 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs @@ -241,40 +241,26 @@ await VerifyCS.VerifyAnalyzerAsync(source, [OuterLoop("Uses the network for downlevel ref packs")] public async Task StringMarshallingForwardingNotSupported_ReportsDiagnostic() { + // The downlevel injected StringMarshalling enum only has Utf16 = 2. + // Use a numeric cast to exercise a non-Utf16 value that should trigger CannotForwardToDllImport. string source = """ - using System.Runtime.InteropServices; partial class Test { - [LibraryImport("DoesNotExist", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport("DoesNotExist", StringMarshalling = (StringMarshalling)1)] public static partial void {|#0:Method1|}(string s); - - [LibraryImport("DoesNotExist", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Native))] - public static partial void {|#1:Method2|}(string s); - - struct Native - { - public Native(string s) { } - public string ToManaged() => default; - } } - """ + CodeSnippets.LibraryImportAttributeDeclaration; - DiagnosticResult[] expectedDiags = - [ - VerifyCS.Diagnostic(GeneratorDiagnostics.CannotForwardToDllImport) - .WithLocation(0) - .WithArguments($"{nameof(TypeNames.LibraryImportAttribute)}{Type.Delimiter}{nameof(StringMarshalling)}={nameof(StringMarshalling)}{Type.Delimiter}{nameof(StringMarshalling.Utf8)}"), - VerifyCS.Diagnostic(GeneratorDiagnostics.CannotForwardToDllImport) - .WithLocation(1) - .WithArguments($"{nameof(TypeNames.LibraryImportAttribute)}{Type.Delimiter}{nameof(StringMarshalling)}={nameof(StringMarshalling)}{Type.Delimiter}{nameof(StringMarshalling.Custom)}") - ]; + """; var test = new Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier.Test(TestTargetFramework.Standard2_0) { TestCode = source, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; - test.ExpectedDiagnostics.AddRange(expectedDiags); + test.ExpectedDiagnostics.Add( + VerifyCS.Diagnostic(GeneratorDiagnostics.CannotForwardToDllImport) + .WithLocation(0) + .WithArguments($"{nameof(TypeNames.LibraryImportAttribute)}{Type.Delimiter}{nameof(StringMarshalling)}={nameof(StringMarshalling)}{Type.Delimiter}{nameof(StringMarshalling.Utf8)}")); await test.RunAsync(); }