Skip to content
6 changes: 0 additions & 6 deletions eng/generators.targets
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@
" />
</ItemGroup>

<ItemGroup Condition="'@(EnabledGenerators)' != '' and
@(EnabledGenerators->AnyHaveMetadataValue('Identity', 'DownlevelLibraryImportGenerator'))">
<Compile Include="$(CoreLibSharedDir)System\Runtime\InteropServices\LibraryImportAttribute.cs" />
<Compile Include="$(CoreLibSharedDir)System\Runtime\InteropServices\StringMarshalling.cs" />
</ItemGroup>

<!-- Use this complex item list based filtering to add the ProjectReference to make sure dotnet/runtime stays compatible with NuGet Static Graph Restore.
That is required as the EnabledGenerators condition checks on the ProjectReference items and hence can't be a property condition. -->
<ItemGroup Condition="'@(EnabledGenerators)' != ''">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,7 @@ namespace System.Runtime.InteropServices
/// applied to static, partial, non-generic methods.
/// </remarks>
[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
{
/// <summary>
/// Initializes a new instance of the <see cref="LibraryImportAttribute"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ namespace System.Runtime.InteropServices
/// <summary>
/// Specifies how strings should be marshalled for generated p/invokes
/// </summary>
#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_INTEROP_SOURCEGENERATION
public
#else
internal
#endif
enum StringMarshalling
public enum StringMarshalling
{
/// <summary>
/// Indicates the user is supplying a specific marshaller in <see cref="LibraryImportAttribute.StringMarshallingCustomType"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
// <auto-generated/>
#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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ public async Task SkipLocalsInitOnDownlevelTargetFrameworks(TestTargetFramework
{
string source = $$"""
using System.Runtime.InteropServices;
{{CodeSnippets.LibraryImportAttributeDeclaration}}
partial class C
{
[LibraryImportAttribute("DoesNotExist")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,6 @@ string ICustomMarshallingSignatureTestProvider.MarshalUsingCollectionReturnValue
string ICustomMarshallingSignatureTestProvider.CustomElementMarshalling(string type, string marshallerType, string preDeclaration)
=> CustomElementMarshalling(type, marshallerType, preDeclaration);

/// <summary>
/// Partially define attribute for pre-.NET 7.0
/// </summary>
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; }
}
}
""";

/// <summary>
/// Trivial declaration of LibraryImport usage
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,64 +500,65 @@ protected override ParseOptions CreateParseOptions()
public static IEnumerable<object[]> CodeSnippetsToValidateFallbackForwarder()
{
// Confirm that all unsupported target frameworks can be generated.
// LibraryImportAttribute and StringMarshalling are injected by the generator via RegisterPostInitializationOutput.
{
string code = CodeSnippets.BasicParametersAndModifiers<byte>(CodeSnippets.LibraryImportAttributeDeclaration);
Comment thread
jkoritzinsky marked this conversation as resolved.
string code = CodeSnippets.BasicParametersAndModifiers<byte>();
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 };
}

// Confirm that all unsupported target frameworks fall back to a forwarder.
{
string code = CodeSnippets.BasicParametersAndModifiers<byte[]>(CodeSnippets.LibraryImportAttributeDeclaration);
string code = CodeSnippets.BasicParametersAndModifiers<byte[]>();
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 };
}

// Confirm that all unsupported target frameworks fall back to a forwarder.
{
string code = CodeSnippets.BasicParametersAndModifiersWithStringMarshalling<string>(StringMarshalling.Utf16, CodeSnippets.LibraryImportAttributeDeclaration);
string code = CodeSnippets.BasicParametersAndModifiersWithStringMarshalling<string>(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 };
}

// 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 };
}

// 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 };
Expand Down Expand Up @@ -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<Microsoft.Interop.DownlevelLibraryImportGenerator, Microsoft.CodeAnalysis.Testing.EmptyDiagnosticAnalyzer>(framework)
var test = new OnlyTypeDefinitionsOutputTest(framework)
{
TestCode = source,
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
Expand All @@ -777,6 +781,28 @@ public class Basic { }
await test.RunAsync();
}

class OnlyTypeDefinitionsOutputTest : Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.Interop.DownlevelLibraryImportGenerator, Microsoft.CodeAnalysis.Testing.EmptyDiagnosticAnalyzer>.Test
{
public OnlyTypeDefinitionsOutputTest(TestTargetFramework framework)
: base(framework)
{
}

protected override async Task<(Compilation compilation, ImmutableArray<Diagnostic> 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<TSourceGenerator, TAnalyzer> : Microsoft.Interop.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<TSourceGenerator, TAnalyzer>.Test
where TSourceGenerator : new()
where TAnalyzer : DiagnosticAnalyzer, new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DownlevelLibraryImportGenerator, Microsoft.Interop.Analyzers.DownlevelLibraryImportDiagnosticsAnalyzer>.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();
}

Expand Down
Loading