Skip to content

Commit

Permalink
Add TFM and version support query to marshaller interface. (#61064)
Browse files Browse the repository at this point in the history
* Add TFM and version support query to marshaller interface.

* If any marshaller fails to support the current tfm version, fallback
to using the built-in DllImport attribute.

* Add tests for fallback generation on downlevel or generation if marshallers
  are supported on the target framework.

* Convert CreateCompilationWithReferenceAssemblies to CreateCompilation overload.

Co-authored-by: Elinor Fung <elfung@microsoft.com>
  • Loading branch information
AaronRobinsonMSFT and elinor-fung authored Nov 3, 2021
1 parent 62148b6 commit 6527f54
Show file tree
Hide file tree
Showing 24 changed files with 306 additions and 161 deletions.
8 changes: 6 additions & 2 deletions docs/design/libraries/DllImportGenerator/Compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ Documentation on compatibility guidance and the current state. The version headi

## Version 1

The focus of version 1 is to support `NetCoreApp`. This implies that anything not needed by `NetCoreApp` is subject to change. Only .NET 6+ is supported.
The focus of version 1 is to support `NetCoreApp`. This implies that anything not needed by `NetCoreApp` is subject to change.

### Fallback mechanism

In the event a marshaller would generate code that has a specific target framework or version requirement that is not satisfied, the generator will instead produce a normal `DllImportAttribute` declaration. This fallback mechanism enables the use of `GeneratedDllImportAttribute` in most circumstances and permits the conversion from `DllImportAttribute` to `GeneratedDllImportAttribute` to be across most code bases. There are instances where the generator will not be able to handle signatures or configuration. For example, uses of `StringBuilder` are not supported in any form and consumers should retain uses of `DllImportAttribute`. Additionally, `GeneratedDllImportAttribute` cannot represent all settings available on `DllImportAttribute`&mdash;see below for details.

### Semantic changes compared to `DllImportAttribute`

Expand Down Expand Up @@ -92,4 +96,4 @@ Unlike the built-in system, the source generator does not support marshalling fo

## Version 0

This version is the built-in IL Stub generation system that is triggered whenever a method marked with `DllImport` is invoked.
This version is the built-in IL Stub generation system that is triggered whenever a method marked with `DllImportAttribute` is invoked.
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ internal static class Comparers
/// <summary>
/// Comparer for an individual generated stub source as a syntax tree and the generated diagnostics for the stub.
/// </summary>
public static readonly IEqualityComparer<(MemberDeclarationSyntax Syntax, ImmutableArray<Diagnostic> Diagnostics)> GeneratedSyntax = new CustomValueTupleElementComparer<MemberDeclarationSyntax, ImmutableArray<Diagnostic>>(new SyntaxEquivalentComparer(), new ImmutableArraySequenceEqualComparer<Diagnostic>(EqualityComparer<Diagnostic>.Default));
public static readonly IEqualityComparer<(MemberDeclarationSyntax Syntax, ImmutableArray<Diagnostic> Diagnostics)> GeneratedSyntax = new CustomValueTupleElementComparer<MemberDeclarationSyntax, ImmutableArray<Diagnostic>>(SyntaxEquivalentComparer.Instance, new ImmutableArraySequenceEqualComparer<Diagnostic>(EqualityComparer<Diagnostic>.Default));

/// <summary>
/// Comparer for the context used to generate a stub and the original user-provided syntax that triggered stub creation.
/// </summary>
public static readonly IEqualityComparer<(MethodDeclarationSyntax Syntax, DllImportGenerator.IncrementalStubGenerationContext StubContext)> CalculatedContextWithSyntax = new CustomValueTupleElementComparer<MethodDeclarationSyntax, DllImportGenerator.IncrementalStubGenerationContext>(new SyntaxEquivalentComparer(), EqualityComparer<DllImportGenerator.IncrementalStubGenerationContext>.Default);
public static readonly IEqualityComparer<(MethodDeclarationSyntax Syntax, DllImportGenerator.IncrementalStubGenerationContext StubContext)> CalculatedContextWithSyntax = new CustomValueTupleElementComparer<MethodDeclarationSyntax, DllImportGenerator.IncrementalStubGenerationContext>(SyntaxEquivalentComparer.Instance, EqualityComparer<DllImportGenerator.IncrementalStubGenerationContext>.Default);
}

/// <summary>
Expand Down Expand Up @@ -63,6 +63,10 @@ public int GetHashCode(ImmutableArray<T> obj)

internal class SyntaxEquivalentComparer : IEqualityComparer<SyntaxNode>
{
public static readonly SyntaxEquivalentComparer Instance = new();

private SyntaxEquivalentComparer() { }

public bool Equals(SyntaxNode x, SyntaxNode y)
{
return x.IsEquivalentTo(y);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ public sealed class DllImportGenerator : IIncrementalGenerator
private const string GeneratedDllImport = nameof(GeneratedDllImport);
private const string GeneratedDllImportAttribute = nameof(GeneratedDllImportAttribute);

private static readonly Version s_minimumSupportedFrameworkVersion = new Version(5, 0);

internal sealed record IncrementalStubGenerationContext(DllImportStubContext StubContext, ImmutableArray<AttributeSyntax> ForwardedAttributes, GeneratedDllImportData DllImportData, ImmutableArray<Diagnostic> Diagnostics)
internal sealed record IncrementalStubGenerationContext(
StubEnvironment Environment,
DllImportStubContext StubContext,
ImmutableArray<AttributeSyntax> ForwardedAttributes,
GeneratedDllImportData DllImportData,
ImmutableArray<Diagnostic> Diagnostics)
{
public bool Equals(IncrementalStubGenerationContext? other)
{
return other is not null
&& StubEnvironment.AreCompilationSettingsEqual(Environment, other.Environment)
&& StubContext.Equals(other.StubContext)
&& DllImportData.Equals(other.DllImportData)
&& ForwardedAttributes.SequenceEqual(other.ForwardedAttributes, (IEqualityComparer<AttributeSyntax>)new SyntaxEquivalentComparer())
&& ForwardedAttributes.SequenceEqual(other.ForwardedAttributes, (IEqualityComparer<AttributeSyntax>)SyntaxEquivalentComparer.Instance)
&& Diagnostics.SequenceEqual(other.Diagnostics);
}

Expand Down Expand Up @@ -93,28 +97,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.ReportDiagnostic(Diagnostic.Create(GeneratorDiagnostics.ReturnConfigurationNotSupported, refReturnMethod.Syntax.GetLocation(), "ref return", refReturnMethod.Symbol.ToDisplayString()));
});

IncrementalValueProvider<(Compilation compilation, bool isSupported, Version targetFrameworkVersion)> compilationAndTargetFramework = context.CompilationProvider
IncrementalValueProvider<(Compilation compilation, TargetFramework targetFramework, Version targetFrameworkVersion)> compilationAndTargetFramework = context.CompilationProvider
.Select(static (compilation, ct) =>
{
bool isSupported = IsSupportedTargetFramework(compilation, out Version targetFrameworkVersion);
return (compilation, isSupported, targetFrameworkVersion);
TargetFramework fmk = DetermineTargetFramework(compilation, out Version targetFrameworkVersion);
return (compilation, fmk, targetFrameworkVersion);
});

context.RegisterSourceOutput(
compilationAndTargetFramework
.Combine(methodsToGenerate.Collect()),
static (context, data) =>
{
if (!data.Left.isSupported && data.Right.Any())
if (data.Left.targetFramework is TargetFramework.Unknown && data.Right.Any())
{
// We don't block source generation when the TFM is unsupported.
// We don't block source generation when the TFM is unknown.
// This allows a user to copy generated source and use it as a starting point
// for manual marshalling if desired.
context.ReportDiagnostic(
Diagnostic.Create(
GeneratorDiagnostics.TargetFrameworkNotSupported,
Location.None,
s_minimumSupportedFrameworkVersion.ToString(2)));
data.Left.targetFrameworkVersion));
}
});

Expand All @@ -127,7 +131,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static (data, ct) =>
new StubEnvironment(
data.Left.compilation,
data.Left.isSupported,
data.Left.targetFramework,
data.Left.targetFrameworkVersion,
data.Left.compilation.SourceModule.GetAttributes().Any(attr => attr.AttributeClass?.ToDisplayString() == TypeNames.System_Runtime_CompilerServices_SkipLocalsInitAttribute),
data.Right)
Expand Down Expand Up @@ -312,20 +316,21 @@ private static MemberDeclarationSyntax WrapMethodInContainingScopes(DllImportStu
return toPrint;
}

private static bool IsSupportedTargetFramework(Compilation compilation, out Version version)
private static TargetFramework DetermineTargetFramework(Compilation compilation, out Version version)
{
IAssemblySymbol systemAssembly = compilation.GetSpecialType(SpecialType.System_Object).ContainingAssembly;
version = systemAssembly.Identity.Version;

return systemAssembly.Identity.Name switch
{
// .NET Framework
"mscorlib" => false,
"mscorlib" => TargetFramework.Framework,
// .NET Standard
"netstandard" => false,
"netstandard" => TargetFramework.Standard,
// .NET Core (when version < 5.0) or .NET
"System.Runtime" or "System.Private.CoreLib" => version >= s_minimumSupportedFrameworkVersion,
_ => false,
"System.Runtime" or "System.Private.CoreLib" =>
(version.Major < 5) ? TargetFramework.Core : TargetFramework.Net,
_ => TargetFramework.Unknown,
};
}

Expand All @@ -339,7 +344,6 @@ private static GeneratedDllImportData ProcessGeneratedDllImportAttribute(Attribu
throw new InvalidProgramException();
}


// Default values for these properties are based on the
// documented semanatics of DllImportAttribute:
// - https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.dllimportattribute
Expand Down Expand Up @@ -473,7 +477,7 @@ private static IncrementalStubGenerationContext CalculateStubInformation(IMethod
// Create the stub.
var dllImportStub = DllImportStubContext.Create(symbol, stubDllImportData, environment, generatorDiagnostics, ct);

return new IncrementalStubGenerationContext(dllImportStub, additionalAttributes.ToImmutableArray(), stubDllImportData, generatorDiagnostics.Diagnostics.ToImmutableArray());
return new IncrementalStubGenerationContext(environment, dllImportStub, additionalAttributes.ToImmutableArray(), stubDllImportData, generatorDiagnostics.Diagnostics.ToImmutableArray());
}

private (MemberDeclarationSyntax, ImmutableArray<Diagnostic>) GenerateSource(
Expand All @@ -490,12 +494,16 @@ private static IncrementalStubGenerationContext CalculateStubInformation(IMethod

// Generate stub code
var stubGenerator = new PInvokeStubCodeGenerator(
dllImportStub.Environment,
dllImportStub.StubContext.ElementTypeInformation,
dllImportStub.DllImportData.SetLastError && !options.GenerateForwarders,
(elementInfo, ex) => diagnostics.ReportMarshallingNotSupported(originalSyntax, elementInfo, ex.NotSupportedDetails),
dllImportStub.StubContext.GeneratorFactory);

if (stubGenerator.StubIsBasicForwarder)
// Check if the generator should produce a forwarder stub - regular DllImport.
// This is done if the signature is blittable or the target framework is not supported.
if (stubGenerator.StubIsBasicForwarder
|| !stubGenerator.SupportsTargetFramework)
{
return (PrintForwarderStub(originalSyntax, dllImportStub), dllImportStub.Diagnostics.AddRange(diagnostics.Diagnostics));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,27 @@ namespace Microsoft.Interop
{
internal record StubEnvironment(
Compilation Compilation,
bool SupportedTargetFramework,
TargetFramework TargetFramework,
Version TargetFrameworkVersion,
bool ModuleSkipLocalsInit,
DllImportGeneratorOptions Options);
DllImportGeneratorOptions Options)
{
/// <summary>
/// Override for determining if two StubEnvironment instances are
/// equal. This intentionally excludes the Compilation instance
/// since that represents the actual compilation and not just the settings.
/// </summary>
/// <param name="env1">The first StubEnvironment</param>
/// <param name="env2">The second StubEnvironment</param>
/// <returns>True if the settings are equal, otherwise false.</returns>
public static bool AreCompilationSettingsEqual(StubEnvironment env1, StubEnvironment env2)
{
return env1.TargetFramework == env2.TargetFramework
&& env1.TargetFrameworkVersion == env2.TargetFrameworkVersion
&& env1.ModuleSkipLocalsInit == env2.ModuleSkipLocalsInit
&& env1.Options.Equals(env2.Options);
}
}

internal sealed class DllImportStubContext : IEquatable<DllImportStubContext>
{
Expand Down Expand Up @@ -164,7 +181,6 @@ private static (ImmutableArray<TypePositionInfo>, IMarshallingGeneratorFactory)
NativeIndex = typeInfos.Count
};
typeInfos.Add(typeInfo);

}

TypePositionInfo retTypeInfo = new(ManagedTypeInfo.CreateTypeInfoForTypeSymbol(method.ReturnType), marshallingAttributeParser.ParseMarshallingInfo(method.ReturnType, method.GetReturnTypeAttributes()));
Expand Down Expand Up @@ -234,9 +250,9 @@ public bool Equals(DllImportStubContext other)
return other is not null
&& StubTypeNamespace == other.StubTypeNamespace
&& ElementTypeInformation.SequenceEqual(other.ElementTypeInformation)
&& StubContainingTypes.SequenceEqual(other.StubContainingTypes, (IEqualityComparer<TypeDeclarationSyntax>)new SyntaxEquivalentComparer())
&& StubContainingTypes.SequenceEqual(other.StubContainingTypes, (IEqualityComparer<TypeDeclarationSyntax>)SyntaxEquivalentComparer.Instance)
&& StubReturnType.IsEquivalentTo(other.StubReturnType)
&& AdditionalAttributes.SequenceEqual(other.AdditionalAttributes, (IEqualityComparer<AttributeListSyntax>)new SyntaxEquivalentComparer())
&& AdditionalAttributes.SequenceEqual(other.AdditionalAttributes, (IEqualityComparer<AttributeListSyntax>)SyntaxEquivalentComparer.Instance)
&& Options.Equals(other.Options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ private record struct BoundGenerator(TypePositionInfo TypeInfo, IMarshallingGene

public override bool AdditionalTemporaryStateLivesAcrossStages => true;

public bool SupportsTargetFramework { get; init; }

public bool StubIsBasicForwarder { get; init; }

/// <summary>
/// Identifier for managed return value
/// </summary>
Expand All @@ -58,16 +62,28 @@ private record struct BoundGenerator(TypePositionInfo TypeInfo, IMarshallingGene
private readonly List<BoundGenerator> _sortedMarshallers;
private readonly bool _stubReturnsVoid;

public bool StubIsBasicForwarder { get; }

public PInvokeStubCodeGenerator(
StubEnvironment environment,
IEnumerable<TypePositionInfo> argTypes,
bool setLastError,
Action<TypePositionInfo, MarshallingNotSupportedException> marshallingNotSupportedCallback,
IMarshallingGeneratorFactory generatorFactory)
{
_setLastError = setLastError;

// Support for SetLastError logic requires .NET 6+. Initialize the
// supports target framework value with this value.
if (_setLastError)
{
SupportsTargetFramework = environment.TargetFramework == TargetFramework.Net
&& environment.TargetFrameworkVersion.Major >= 6;
}
else
{
SupportsTargetFramework = true;
}

bool noMarshallingNeeded = true;
List<BoundGenerator> allMarshallers = new();
List<BoundGenerator> paramMarshallers = new();
bool foundNativeRetMarshaller = false;
Expand All @@ -78,6 +94,14 @@ public PInvokeStubCodeGenerator(
foreach (TypePositionInfo argType in argTypes)
{
BoundGenerator generator = CreateGenerator(argType);

// Check each marshaler if the current target framework is supported or not.
SupportsTargetFramework &= generator.Generator.IsSupported(environment.TargetFramework, environment.TargetFrameworkVersion);

// Check if generator is either blittable or just a forwarder.
noMarshallingNeeded &= generator is { Generator: BlittableMarshaller, TypeInfo: { IsByRef: false } }
or { Generator: Forwarder };

allMarshallers.Add(generator);
if (argType.IsManagedReturnPosition)
{
Expand Down Expand Up @@ -139,9 +163,7 @@ public PInvokeStubCodeGenerator(

StubIsBasicForwarder = !setLastError
&& managedRetMarshaller.TypeInfo.IsNativeReturnPosition // If the managed return has native return position, then it's the return for both.
&& _sortedMarshallers.All(
m => m is { Generator: BlittableMarshaller, TypeInfo: { IsByRef: false } }
or { Generator: Forwarder });
&& noMarshallingNeeded;

if (managedRetMarshaller.Generator.UsesNativeIdentifier(managedRetMarshaller.TypeInfo, this))
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6527f54

Please sign in to comment.