diff --git a/DllImportGenerator/Ancillary.Interop/Ancillary.Interop.csproj b/DllImportGenerator/Ancillary.Interop/Ancillary.Interop.csproj new file mode 100644 index 000000000000..ad8edc594033 --- /dev/null +++ b/DllImportGenerator/Ancillary.Interop/Ancillary.Interop.csproj @@ -0,0 +1,8 @@ + + + + net5.0 + 8.0 + + + diff --git a/DllImportGenerator/Ancillary.Interop/GeneratedDllImportAttribute.cs b/DllImportGenerator/Ancillary.Interop/GeneratedDllImportAttribute.cs new file mode 100644 index 000000000000..0f0d322da4dd --- /dev/null +++ b/DllImportGenerator/Ancillary.Interop/GeneratedDllImportAttribute.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace System.Runtime.InteropServices +{ + // [TODO] Remove once the attribute has been added to the BCL + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class GeneratedDllImportAttribute : Attribute + { + public bool BestFitMapping; + public CallingConvention CallingConvention; + public CharSet CharSet; + public string? EntryPoint; + public bool ExactSpelling; + public bool PreserveSig; + public bool SetLastError; + public bool ThrowOnUnmappableChar; + + public GeneratedDllImportAttribute(string dllName) + { + this.Value = dllName; + } + + public string Value { get; private set; } + } +} diff --git a/DllImportGenerator/Demo/Demo.csproj b/DllImportGenerator/Demo/Demo.csproj index f03d8a3f6c20..09299b00addb 100644 --- a/DllImportGenerator/Demo/Demo.csproj +++ b/DllImportGenerator/Demo/Demo.csproj @@ -8,6 +8,7 @@ + diff --git a/DllImportGenerator/DllImportGenerator.Test/CodeSnippets.cs b/DllImportGenerator/DllImportGenerator.Test/CodeSnippets.cs index 18888cf58795..b69856e49064 100644 --- a/DllImportGenerator/DllImportGenerator.Test/CodeSnippets.cs +++ b/DllImportGenerator/DllImportGenerator.Test/CodeSnippets.cs @@ -153,6 +153,50 @@ partial class Test [GeneratedDllImport(""DoesNotExist"", EntryPoint=""UserDefinedEntryPoint"")] public static partial void NotAnExport(); } +"; + + /// + /// Declaration with all DllImport named arguments. + /// + public static readonly string AllDllImportNamedArguments = @" +using System.Runtime.InteropServices; +partial class Test +{ + [GeneratedDllImport(""DoesNotExist"", + BestFitMapping = false, + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Unicode, + EntryPoint = ""UserDefinedEntryPoint"", + ExactSpelling = true, + PreserveSig = false, + SetLastError = true, + ThrowOnUnmappableChar = true)] + public static partial void Method(); +} +"; + + /// + /// Declaration using various methods to compute constants in C#. + /// + public static readonly string UseCSharpFeaturesForConstants = @" +using System.Runtime.InteropServices; +partial class Test +{ + private const bool IsTrue = true; + private const bool IsFalse = false; + private const string EntryPointName = nameof(Test) + nameof(IsFalse); + + [GeneratedDllImport(nameof(Test), + BestFitMapping = 0 != 1, + CallingConvention = (CallingConvention)1, + CharSet = (CharSet)2, + EntryPoint = EntryPointName, + ExactSpelling = IsTrue, + PreserveSig = IsFalse, + SetLastError = !IsFalse, + ThrowOnUnmappableChar = !IsTrue)] + public static partial void Method(); +} "; /// @@ -181,6 +225,52 @@ partial class Test [GeneratedDllImport(""DoesNotExist"")] public static partial void Method(int t = 0); } +"; + + /// + /// Apply MarshalAsAttribute to parameters and return types. + /// + public static readonly string MarshalAsAttributeOnTypes = @" +using System; +using System.Runtime.InteropServices; +namespace NS +{ + class MyCustomMarshaler : ICustomMarshaler + { + static ICustomMarshaler GetInstance(string pstrCookie) + => new MyCustomMarshaler(); + + public void CleanUpManagedData(object ManagedObj) + => throw new NotImplementedException(); + + public void CleanUpNativeData(IntPtr pNativeData) + => throw new NotImplementedException(); + + public int GetNativeDataSize() + => throw new NotImplementedException(); + + public IntPtr MarshalManagedToNative(object ManagedObj) + => throw new NotImplementedException(); + + public object MarshalNativeToManaged(IntPtr pNativeData) + => throw new NotImplementedException(); + } +} + +partial class Test +{ + [GeneratedDllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.LPWStr)] + public static partial string Method1([MarshalAs(UnmanagedType.LPStr)]string t); + + [GeneratedDllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.LPWStr)] + public static partial string Method2([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(NS.MyCustomMarshaler), MarshalCookie=""COOKIE1"")]string t); + + [GeneratedDllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.LPWStr)] + public static partial string Method3([MarshalAs(UnmanagedType.CustomMarshaler, MarshalType = ""NS.MyCustomMarshaler"", MarshalCookie=""COOKIE2"")]string t); +} "; /// diff --git a/DllImportGenerator/DllImportGenerator.Test/Compiles.cs b/DllImportGenerator/DllImportGenerator.Test/Compiles.cs index 5375ab26a7e7..85cd5cad87a8 100644 --- a/DllImportGenerator/DllImportGenerator.Test/Compiles.cs +++ b/DllImportGenerator/DllImportGenerator.Test/Compiles.cs @@ -14,8 +14,11 @@ public static IEnumerable CodeSnippetsToCompile() yield return new[] { CodeSnippets.NestedNamespace }; yield return new[] { CodeSnippets.NestedTypes }; yield return new[] { CodeSnippets.UserDefinedEntryPoint }; + yield return new[] { CodeSnippets.AllDllImportNamedArguments }; yield return new[] { CodeSnippets.BasicParametersAndModifiers }; yield return new[] { CodeSnippets.DefaultParameters }; + yield return new[] { CodeSnippets.UseCSharpFeaturesForConstants }; + yield return new[] { CodeSnippets.MarshalAsAttributeOnTypes }; } [Theory] diff --git a/DllImportGenerator/DllImportGenerator.Test/DllImportGenerator.Test.csproj b/DllImportGenerator/DllImportGenerator.Test/DllImportGenerator.Test.csproj index 74398ed619e6..4cba239e624e 100644 --- a/DllImportGenerator/DllImportGenerator.Test/DllImportGenerator.Test.csproj +++ b/DllImportGenerator/DllImportGenerator.Test/DllImportGenerator.Test.csproj @@ -26,6 +26,7 @@ + diff --git a/DllImportGenerator/DllImportGenerator.Test/TestUtils.cs b/DllImportGenerator/DllImportGenerator.Test/TestUtils.cs index 0afb4144787a..a52480f22ea5 100644 --- a/DllImportGenerator/DllImportGenerator.Test/TestUtils.cs +++ b/DllImportGenerator/DllImportGenerator.Test/TestUtils.cs @@ -1,8 +1,10 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Xunit; namespace DllImportGenerator.Test @@ -34,10 +36,26 @@ public static void AssertPreSourceGeneratorCompilation(Compilation comp) /// Output type /// The resulting compilation public static Compilation CreateCompilation(string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary) - => CSharpCompilation.Create("compilation", + { + var mdRefs = new List(); + + // Include the assembly containing the new attribute and all of its references. + // [TODO] Remove once the attribute has been added to the BCL + var attrAssem = typeof(GeneratedDllImportAttribute).GetTypeInfo().Assembly; + mdRefs.Add(MetadataReference.CreateFromFile(attrAssem.Location)); + foreach (var assemName in attrAssem.GetReferencedAssemblies()) + { + var assemRef = Assembly.Load(assemName); + mdRefs.Add(MetadataReference.CreateFromFile(assemRef.Location)); + } + + // Add a CoreLib reference + mdRefs.Add(MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location)); + return CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview)) }, - new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) }, + mdRefs, new CSharpCompilationOptions(outputKind)); + } /// /// Run the supplied generators on the compilation. diff --git a/DllImportGenerator/DllImportGenerator.sln b/DllImportGenerator/DllImportGenerator.sln index 0a48b1b9a82b..2ecea783df57 100644 --- a/DllImportGenerator/DllImportGenerator.sln +++ b/DllImportGenerator/DllImportGenerator.sln @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllImportGenerator", "DllIm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllImportGenerator.Test", "DllImportGenerator.Test\DllImportGenerator.Test.csproj", "{B8CA13C4-F41B-4ABD-A9F3-63A02C53B96E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{E478EE77-E072-4A42-B453-EBFDA7728717}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{E478EE77-E072-4A42-B453-EBFDA7728717}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ancillary.Interop", "Ancillary.Interop\Ancillary.Interop.csproj", "{E59F0B6A-1137-4179-A91D-33464A775DEB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +29,10 @@ Global {E478EE77-E072-4A42-B453-EBFDA7728717}.Debug|Any CPU.Build.0 = Debug|Any CPU {E478EE77-E072-4A42-B453-EBFDA7728717}.Release|Any CPU.ActiveCfg = Release|Any CPU {E478EE77-E072-4A42-B453-EBFDA7728717}.Release|Any CPU.Build.0 = Release|Any CPU + {E59F0B6A-1137-4179-A91D-33464A775DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E59F0B6A-1137-4179-A91D-33464A775DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E59F0B6A-1137-4179-A91D-33464A775DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E59F0B6A-1137-4179-A91D-33464A775DEB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DllImportGenerator/DllImportGenerator/DllImportGenerator.cs b/DllImportGenerator/DllImportGenerator/DllImportGenerator.cs index 801c8b059770..35bca6a05fa6 100644 --- a/DllImportGenerator/DllImportGenerator/DllImportGenerator.cs +++ b/DllImportGenerator/DllImportGenerator/DllImportGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; - +using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -17,31 +17,6 @@ public class DllImportGenerator : ISourceGenerator { private const string GeneratedDllImport = nameof(GeneratedDllImport); private const string GeneratedDllImportAttribute = nameof(GeneratedDllImportAttribute); - private static readonly string GeneratedDllImportAttributeSource = $@" -#nullable enable -namespace System.Runtime.InteropServices -{{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class {nameof(GeneratedDllImportAttribute)} : Attribute - {{ - public bool BestFitMapping; - public CallingConvention CallingConvention; - public CharSet CharSet; - public string? EntryPoint; - public bool ExactSpelling; - public bool PreserveSig; - public bool SetLastError; - public bool ThrowOnUnmappableChar; - - public {nameof(GeneratedDllImportAttribute)}(string dllName) - {{ - this.Value = dllName; - }} - - public string Value {{ get; private set; }} - }} -}} -"; public void Execute(SourceGeneratorContext context) { @@ -57,8 +32,6 @@ public void Execute(SourceGeneratorContext context) // this caching. var syntaxToModel = new Dictionary(); - context.AddSource(nameof(GeneratedDllImportAttributeSource), SourceText.From(GeneratedDllImportAttributeSource, Encoding.UTF8)); - var generatedDllImports = new StringBuilder(); foreach (SyntaxReference synRef in synRec.Methods) { @@ -74,32 +47,37 @@ public void Execute(SourceGeneratorContext context) // Process the method syntax and get its SymbolInfo. var methodSymbolInfo = sm.GetDeclaredSymbol(methodSyntax, context.CancellationToken); - // Create the stub details. - var dllImportStub = DllImportStub.Create(methodSymbolInfo, context.CancellationToken); + // Process the attributes on the method. + DllImportStub.GeneratedDllImportData dllImportData; + AttributeSyntax dllImportAttr = this.ProcessAttributes(methodSymbolInfo, context.CancellationToken, out dllImportData); + Debug.Assert(!(dllImportAttr is null) && !(dllImportData is null)); + + // Create the stub. + var dllImportStub = DllImportStub.Create(methodSymbolInfo, dllImportData, context.CancellationToken); - // Report any diagnostics from the stub genertion step. + // Report any diagnostics from the stub generation step. foreach (var diag in dllImportStub.Diagnostics) { context.ReportDiagnostic(diag); } - // Process the attributes on the method. - AttributeSyntax dllImportAttr; - var additionalAttrs = this.ProcessAttributes(methodSymbolInfo.Name, methodSyntax.AttributeLists, out dllImportAttr); - - PrintGeneratedSource(generatedDllImports, methodSyntax, ref dllImportStub, dllImportAttr, additionalAttrs); + PrintGeneratedSource(generatedDllImports, methodSyntax, dllImportStub, dllImportAttr); } Debug.WriteLine(generatedDllImports.ToString()); // [TODO] Find some way to emit this for debugging - logs? context.AddSource("DllImportGenerator.g.cs", SourceText.From(generatedDllImports.ToString(), Encoding.UTF8)); } + public void Initialize(InitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + private void PrintGeneratedSource( StringBuilder builder, MethodDeclarationSyntax userDeclaredMethod, - ref DllImportStub stub, - AttributeSyntax dllImportAttr, - IEnumerable additionalAttrDecls) + DllImportStub stub, + AttributeSyntax dllImportAttr) { const string SingleDepth = " "; var currentIndent = string.Empty; @@ -180,11 +158,6 @@ private void PrintGeneratedSource( } } - public void Initialize(InitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - private static bool IsGeneratedDllImportAttribute(AttributeSyntax attrSyntaxMaybe) { var attrName = attrSyntaxMaybe.Name.ToString(); @@ -206,76 +179,150 @@ private static bool IsGeneratedDllImportAttribute(AttributeSyntax attrSyntaxMayb || attrName.EndsWith(PrefixedGeneratedDllImportAttribute); } - private IEnumerable ProcessAttributes( - string methodName, - SyntaxList attributes, - out AttributeSyntax dllImportAttr) + private AttributeSyntax ProcessAttributes( + IMethodSymbol method, + CancellationToken cancelToken, + out DllImportStub.GeneratedDllImportData dllImportData) { - dllImportAttr = default; - - var retainedAttrs = new List(); + dllImportData = new DllImportStub.GeneratedDllImportData(); // Process all attributes - foreach (AttributeListSyntax listSyntax in attributes) + foreach (AttributeData attrData in method.GetAttributes()) { - foreach (AttributeSyntax attrSyntax in listSyntax.Attributes) + if (attrData.ApplicationSyntaxReference is null) { - // Retain the attribute if not GeneratedDllImport. - if (!IsGeneratedDllImportAttribute(attrSyntax)) - { - retainedAttrs.Add(attrSyntax.ToString()); - continue; - } + continue; + } - // Determine if the attribute has the EntryPoint property set. - bool hasEntryPoint = false; - if (!(attrSyntax.ArgumentList is null)) - { - foreach (var arg in attrSyntax.ArgumentList.Arguments) - { - if (arg.NameEquals is null) - { - continue; - } - - hasEntryPoint = nameof(DllImportAttribute.EntryPoint).Equals(arg.NameEquals.Name.ToString()); - if (hasEntryPoint) - { - break; - } - } - } + var attrSyntax = (AttributeSyntax)attrData.ApplicationSyntaxReference.GetSyntax(cancelToken); - // Don't retain the GeneratedDllImport attribute. - // However, we use its settings for the real DllImport. - AttributeSyntax newAttr = attrSyntax; - if (!hasEntryPoint) + // Skip the attribute if not GeneratedDllImport. + if (!IsGeneratedDllImportAttribute(attrSyntax)) + { + continue; + } + + // Found the GeneratedDllImport, but it has an error so report the error. + // This is most likely an issue with targeting an incorrect TFM. + if (attrData.AttributeClass.TypeKind == TypeKind.Error) + { + // [TODO] Report GeneratedDllImport has an error - corrupt metadata? + throw new InvalidProgramException(); + } + + var newAttributeArgs = new List(); + + // Populate the DllImport data from the GeneratedDllImportAttribute attribute. + dllImportData.ModuleName = attrData.ConstructorArguments[0].Value.ToString(); + + newAttributeArgs.Add(SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(dllImportData.ModuleName)))); + + // All other data on attribute is defined as NamedArguments. + foreach (var namedArg in attrData.NamedArguments) + { + ExpressionSyntax expSyntaxMaybe = null; + switch (namedArg.Key) { - // If the EntryPoint property is not set, we will compute and - // add it based on existing semantics (i.e. method name). - // - // N.B. The export discovery logic is identical regardless of where - // the name is defined (i.e. method name vs EntryPoint property). - var entryPointName = SyntaxFactory.NameEquals(nameof(DllImportAttribute.EntryPoint)); - - // The name of the method is the entry point name to use. - var entryPointValue = SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(methodName)); - - var entryPointProp = SyntaxFactory.AttributeArgument(entryPointName, null, entryPointValue); - - // Add the new property to the existing attribute thus creating a new attribute. - newAttr = attrSyntax.AddArgumentListArguments(entryPointProp); + default: + Debug.Fail($"An unknown member was found on {GeneratedDllImport}"); + continue; + case nameof(DllImportStub.GeneratedDllImportData.BestFitMapping): + dllImportData.BestFitMapping = (bool)namedArg.Value.Value; + expSyntaxMaybe = CreateBoolExpressionSyntax(dllImportData.BestFitMapping); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.BestFitMapping; + break; + case nameof(DllImportStub.GeneratedDllImportData.CallingConvention): + dllImportData.CallingConvention = (CallingConvention)namedArg.Value.Value; + expSyntaxMaybe = CreateEnumExpressionSyntax(dllImportData.CallingConvention); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.CallingConvention; + break; + case nameof(DllImportStub.GeneratedDllImportData.CharSet): + dllImportData.CharSet = (CharSet)namedArg.Value.Value; + expSyntaxMaybe = CreateEnumExpressionSyntax(dllImportData.CharSet); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.CharSet; + break; + case nameof(DllImportStub.GeneratedDllImportData.EntryPoint): + dllImportData.EntryPoint = (string)namedArg.Value.Value; + expSyntaxMaybe = CreateStringExpressionSyntax(dllImportData.EntryPoint); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.EntryPoint; + break; + case nameof(DllImportStub.GeneratedDllImportData.ExactSpelling): + dllImportData.ExactSpelling = (bool)namedArg.Value.Value; + expSyntaxMaybe = CreateBoolExpressionSyntax(dllImportData.ExactSpelling); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.ExactSpelling; + break; + case nameof(DllImportStub.GeneratedDllImportData.PreserveSig): + dllImportData.PreserveSig = (bool)namedArg.Value.Value; + expSyntaxMaybe = CreateBoolExpressionSyntax(dllImportData.PreserveSig); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.PreserveSig; + break; + case nameof(DllImportStub.GeneratedDllImportData.SetLastError): + dllImportData.SetLastError = (bool)namedArg.Value.Value; + expSyntaxMaybe = CreateBoolExpressionSyntax(dllImportData.SetLastError); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.SetLastError; + break; + case nameof(DllImportStub.GeneratedDllImportData.ThrowOnUnmappableChar): + dllImportData.ThrowOnUnmappableChar = (bool)namedArg.Value.Value; + expSyntaxMaybe = CreateBoolExpressionSyntax(dllImportData.ThrowOnUnmappableChar); + dllImportData.IsUserDefined |= DllImportStub.DllImportMember.ThrowOnUnmappableChar; + break; } - // Replace the name of the attribute - NameSyntax dllImportName = SyntaxFactory.ParseName(typeof(DllImportAttribute).FullName); - dllImportAttr = newAttr.WithName(dllImportName); + Debug.Assert(!(expSyntaxMaybe is null)); + + // Defer the name equals syntax till we know the value means something. If we created + // an expression we know the key value was valid. + NameEqualsSyntax nameSyntax = SyntaxFactory.NameEquals(namedArg.Key); + newAttributeArgs.Add(SyntaxFactory.AttributeArgument(nameSyntax, null, expSyntaxMaybe)); + } + + // If the EntryPoint property is not set, we will compute and + // add it based on existing semantics (i.e. method name). + // + // N.B. The export discovery logic is identical regardless of where + // the name is defined (i.e. method name vs EntryPoint property). + if (!dllImportData.IsUserDefined.HasFlag(DllImportStub.DllImportMember.EntryPoint)) + { + var entryPointName = SyntaxFactory.NameEquals(nameof(DllImportAttribute.EntryPoint)); + + // The name of the method is the entry point name to use. + var entryPointValue = CreateStringExpressionSyntax(method.Name); + newAttributeArgs.Add(SyntaxFactory.AttributeArgument(entryPointName, null, entryPointValue)); } + + // Create new attribute + return SyntaxFactory.Attribute( + SyntaxFactory.ParseName(typeof(DllImportAttribute).FullName), + SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(newAttributeArgs))); + } + + // [TODO] Report the missing GeneratedDllImportAttribute + throw new NotSupportedException(); + + static ExpressionSyntax CreateBoolExpressionSyntax(bool trueOrFalse) + { + return SyntaxFactory.LiteralExpression( + trueOrFalse + ? SyntaxKind.TrueLiteralExpression + : SyntaxKind.FalseLiteralExpression); + } + + static ExpressionSyntax CreateStringExpressionSyntax(string str) + { + return SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(str)); } - return retainedAttrs; + static ExpressionSyntax CreateEnumExpressionSyntax(T value) where T : Enum + { + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(typeof(T).FullName), + SyntaxFactory.IdentifierName(value.ToString())); + } } private class SyntaxReceiver : ISyntaxReceiver diff --git a/DllImportGenerator/DllImportGenerator/DllImportGenerator.csproj b/DllImportGenerator/DllImportGenerator/DllImportGenerator.csproj index 45150f884505..8082fd111732 100644 --- a/DllImportGenerator/DllImportGenerator/DllImportGenerator.csproj +++ b/DllImportGenerator/DllImportGenerator/DllImportGenerator.csproj @@ -1,17 +1,19 @@ - + netstandard2.0 false true True + true + GENERATE_FORWARDER Preview DllImportGenerator 1.0.0.0 - arobins + Microsoft http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE http://ICON_URL_HERE_OR_DELETE_THIS_LINE @@ -28,11 +30,6 @@ https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources) - - true - GENERATE_FORWARDER - - diff --git a/DllImportGenerator/DllImportGenerator/DllImportStub.cs b/DllImportGenerator/DllImportGenerator/DllImportStub.cs index 511e75862d42..c5fe2532a9ec 100644 --- a/DllImportGenerator/DllImportGenerator/DllImportStub.cs +++ b/DllImportGenerator/DllImportGenerator/DllImportStub.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -11,6 +12,9 @@ namespace Microsoft.Interop { internal class DllImportStub { + private TypePositionInfo returnTypeInfo; + private IEnumerable paramsTypeInfo; + private DllImportStub() { } @@ -19,21 +23,94 @@ private DllImportStub() public IEnumerable StubContainingTypesDecl { get; private set; } - public string StubReturnType { get; private set; } + public string StubReturnType { get => this.returnTypeInfo.ManagedTypeDecl; } - public IEnumerable<(string Type, string Name, RefKind refKind)> StubParameters { get; private set; } + public IEnumerable<(string Type, string Name)> StubParameters + { + get + { + foreach (var typeinfo in paramsTypeInfo) + { + //if (typeinfo.ManagedIndex != TypePositionInfo.UnsetIndex) + { + yield return (typeinfo.ManagedTypeDecl, typeinfo.InstanceIdentifier); + } + } + } + } public IEnumerable StubCode { get; private set; } - public string DllImportReturnType { get; private set; } + public string DllImportReturnType { get => this.returnTypeInfo.UnmanagedTypeDecl; } public string DllImportMethodName { get; private set; } - public IEnumerable<(string Type, string Name, RefKind refKind)> DllImportParameters { get; private set; } + public IEnumerable<(string Type, string Name)> DllImportParameters + { + get + { + foreach (var typeinfo in paramsTypeInfo) + { + //if (typeinfo.UnmanagedIndex != TypePositionInfo.UnsetIndex) + { + yield return (typeinfo.UnmanagedTypeDecl, typeinfo.InstanceIdentifier); + } + } + } + } public IEnumerable Diagnostics { get; private set; } - public static DllImportStub Create(IMethodSymbol method, CancellationToken token = default) + /// + /// Flags used to indicate members on GeneratedDllImport attribute. + /// + [Flags] + public enum DllImportMember + { + None = 0, + BestFitMapping = 1 << 0, + CallingConvention = 1 << 1, + CharSet = 1 << 2, + EntryPoint = 1 << 3, + ExactSpelling = 1 << 4, + PreserveSig = 1 << 5, + SetLastError = 1 << 6, + ThrowOnUnmappableChar = 1 << 7, + } + + /// + /// GeneratedDllImportAttribute data + /// + /// + /// The names of these members map directly to those on the + /// DllImportAttribute and should not be changed. + /// + public class GeneratedDllImportData + { + public string ModuleName { get; set; } + + /// + /// Value set by the user on the original declaration. + /// + public DllImportMember IsUserDefined = DllImportMember.None; + + // Default values for the below fields are based on the + // documented semanatics of DllImportAttribute: + // - https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.dllimportattribute + public bool BestFitMapping { get; set; } = true; + public CallingConvention CallingConvention { get; set; } = CallingConvention.Winapi; + public CharSet CharSet { get; set; } = CharSet.Ansi; + public string EntryPoint { get; set; } = null; + public bool ExactSpelling { get; set; } = false; // VB has different and unusual default behavior here. + public bool PreserveSig { get; set; } = true; + public bool SetLastError { get; set; } = false; + public bool ThrowOnUnmappableChar { get; set; } = false; + } + + public static DllImportStub Create( + IMethodSymbol method, + GeneratedDllImportData dllImportData, + CancellationToken token = default) { // Cancel early if requested token.ThrowIfCancellationRequested(); @@ -75,35 +152,33 @@ public static DllImportStub Create(IMethodSymbol method, CancellationToken token stubContainingTypes.Reverse(); // Determine parameter types - var stubParams = new List<(string Type, string Name, RefKind RefKind)>(); - var dllImportParams = new List<(string Type, string Name, RefKind RefKind)>(); - foreach (var namePair in method.Parameters) + var paramsTypeInfo = new List(); + foreach (var paramSymbol in method.Parameters) { - stubParams.Add((ComputeTypeForStub(namePair.Type, namePair.RefKind), namePair.Name, namePair.RefKind)); - dllImportParams.Add((ComputeTypeForDllImport(namePair.Type, namePair.RefKind), namePair.Name, namePair.RefKind)); + paramsTypeInfo.Add(TypePositionInfo.CreateForParameter(paramSymbol)); } + var retTypeInfo = TypePositionInfo.CreateForType(method.ReturnType, method.GetReturnTypeAttributes()); + string dllImportName = method.Name + "__PInvoke__"; #if !GENERATE_FORWARDER var dispatchCall = new StringBuilder($"throw new System.{nameof(NotSupportedException)}();"); #else // Forward call to generated P/Invoke - var returnMaybe = method.ReturnType.SpecialType == SpecialType.System_Void - ? string.Empty - : "return "; + var returnMaybe = method.ReturnsVoid ? string.Empty : "return "; var dispatchCall = new StringBuilder($"{returnMaybe}{dllImportName}"); - if (!dllImportParams.Any()) + if (!paramsTypeInfo.Any()) { dispatchCall.Append("();"); } else { char delim = '('; - foreach (var param in dllImportParams) + foreach (var param in paramsTypeInfo) { - dispatchCall.Append($"{delim}{RefKindToString(param.RefKind)}{param.Name}"); + dispatchCall.Append($"{delim}{param.RefKindDecl}{param.InstanceIdentifier}"); delim = ','; } dispatchCall.Append(");"); @@ -112,97 +187,14 @@ public static DllImportStub Create(IMethodSymbol method, CancellationToken token return new DllImportStub() { + returnTypeInfo = retTypeInfo, + paramsTypeInfo = paramsTypeInfo, StubTypeNamespace = stubTypeNamespace, StubContainingTypesDecl = stubContainingTypes, - StubReturnType = ComputeTypeForStub(method.ReturnType), - StubParameters = stubParams, StubCode = new[] { dispatchCall.ToString() }, - DllImportReturnType = ComputeTypeForDllImport(method.ReturnType), DllImportMethodName = dllImportName, - DllImportParameters = dllImportParams, Diagnostics = Enumerable.Empty(), }; } - - private static string RefKindToString(RefKind refKind) - { - return refKind switch - { - RefKind.In => "in ", - RefKind.Ref => "ref ", - RefKind.Out => "out ", - RefKind.None => string.Empty, - _ => throw new NotImplementedException("Support for some RefKind"), - }; - } - - private static string ComputeTypeForStub(ITypeSymbol type, RefKind refKind = RefKind.None) - { - var typeAsString = type.SpecialType switch - { - SpecialType.System_Void => "void", - SpecialType.System_SByte => "sbyte", - SpecialType.System_Byte => "byte", - SpecialType.System_Int16 => "short", - SpecialType.System_UInt16 => "ushort", - SpecialType.System_Int32 => "int", - SpecialType.System_UInt32 => "uint", - SpecialType.System_Int64 => "long", - SpecialType.System_UInt64 => "ulong", - SpecialType.System_Single => "float", - SpecialType.System_Double => "double", - SpecialType.System_String => "string", - SpecialType.System_IntPtr => "System.IntPtr", - SpecialType.System_UIntPtr => "System.UIntPtr", - _ => null, - }; - - var typePrefix = string.Empty; - if (typeAsString is null) - { - // Determine the namespace - if (!(type.ContainingNamespace is null) - && !type.ContainingNamespace.IsGlobalNamespace) - { - typePrefix = $"{type.ContainingNamespace}{Type.Delimiter}"; - } - - typeAsString = type.ToString(); - } - - string refKindAsString = RefKindToString(refKind); - return $"{refKindAsString}{typePrefix}{typeAsString}"; - } - - private static string ComputeTypeForDllImport(ITypeSymbol type, RefKind refKind = RefKind.None) - { -#if GENERATE_FORWARDER - return ComputeTypeForStub(type, refKind); -#else - if (!type.IsUnmanagedType) - { - return "void*"; - } - - return type.SpecialType switch - { - SpecialType.System_Void => "void", - SpecialType.System_SByte => "sbyte", - SpecialType.System_Byte => "byte", - SpecialType.System_Int16 => "short", - SpecialType.System_UInt16 => "ushort", - SpecialType.System_Int32 => "int", - SpecialType.System_UInt32 => "uint", - SpecialType.System_Int64 => "long", - SpecialType.System_UInt64 => "ulong", - SpecialType.System_Single => "float", - SpecialType.System_Double => "double", - SpecialType.System_String => "char*", // [TODO] Consider encoding here - SpecialType.System_IntPtr => "void*", - SpecialType.System_UIntPtr => "void*", - _ => "void*", - }; -#endif - } } } diff --git a/DllImportGenerator/DllImportGenerator/TypePositionInfo.cs b/DllImportGenerator/DllImportGenerator/TypePositionInfo.cs new file mode 100644 index 000000000000..34e7f77dd9dd --- /dev/null +++ b/DllImportGenerator/DllImportGenerator/TypePositionInfo.cs @@ -0,0 +1,232 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Interop +{ + /// + /// Collected MarshalAsAttribute info. + /// + internal sealed class MarshalAsInfo + { + public UnmanagedType UnmanagedType { get; set; } + public string CustomMarshallerTypeName { get; set; } + public string CustomMarshallerCookie { get; set; } + + public UnmanagedType UnmanagedArraySubType { get; set; } + public int ArraySizeConst { get; set; } + public short ArraySizeParamIndex { get; set; } + } + + /// + /// Positional type information involved in unmanaged/managed scenarios. + /// + internal sealed class TypePositionInfo + { + public const int UnsetIndex = int.MinValue; + public const int ReturnIndex = UnsetIndex + 1; + + private TypePositionInfo() + { + this.ManagedIndex = UnsetIndex; + this.UnmanagedIndex = UnsetIndex; + this.UnmanagedLCIDConversionArgIndex = UnsetIndex; + } + + public ITypeSymbol TypeSymbol { get; private set; } + public string InstanceIdentifier { get; private set; } + + public RefKind RefKind { get; private set; } + public string RefKindDecl { get => RefKindToString(this.RefKind); } + public string ManagedTypeDecl { get; private set; } + public string UnmanagedTypeDecl { get; private set; } + + public bool IsManagedReturnPosition { get => this.ManagedIndex == ReturnIndex; } + public bool IsUnmanagedReturnPosition { get => this.UnmanagedIndex == ReturnIndex; } + + public int ManagedIndex { get; set; } + public int UnmanagedIndex { get; set; } + public int UnmanagedLCIDConversionArgIndex { get; private set; } + + public MarshalAsInfo MarshalAsInfo { get; private set; } + + public static TypePositionInfo CreateForParameter(IParameterSymbol paramSymbol) + { + var typeInfo = new TypePositionInfo() + { + TypeSymbol = paramSymbol.Type, + InstanceIdentifier = paramSymbol.Name, + ManagedTypeDecl = ComputeTypeForManaged(paramSymbol.Type, paramSymbol.RefKind), + UnmanagedTypeDecl = ComputeTypeForUnmanaged(paramSymbol.Type, paramSymbol.RefKind), + RefKind = paramSymbol.RefKind + }; + + UpdateWithAttributeData(paramSymbol.GetAttributes(), ref typeInfo); + + return typeInfo; + } + + public static TypePositionInfo CreateForType(ITypeSymbol type, IEnumerable attributes) + { + var typeInfo = new TypePositionInfo() + { + TypeSymbol = type, + InstanceIdentifier = string.Empty, + ManagedTypeDecl = ComputeTypeForManaged(type, RefKind.None), + UnmanagedTypeDecl = ComputeTypeForUnmanaged(type, RefKind.None), + RefKind = RefKind.None + }; + + UpdateWithAttributeData(attributes, ref typeInfo); + + return typeInfo; + } + + private static void UpdateWithAttributeData(IEnumerable attributes, ref TypePositionInfo typeInfo) + { + // Look at attributes on the type. + foreach (var attrData in attributes) + { + string attributeName = attrData.AttributeClass.Name; + + if (nameof(MarshalAsAttribute).Equals(attributeName)) + { + // https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshalasattribute + typeInfo.MarshalAsInfo = CreateMarshalAsInfo(attrData); + } + else if (nameof(LCIDConversionAttribute).Equals(attributeName)) + { + // https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.lcidconversionattribute + typeInfo.UnmanagedLCIDConversionArgIndex = (int)attrData.ConstructorArguments[0].Value; + } + } + + static MarshalAsInfo CreateMarshalAsInfo(AttributeData attrData) + { + var info = new MarshalAsInfo + { + UnmanagedType = (UnmanagedType)attrData.ConstructorArguments[0].Value + }; + + // All other data on attribute is defined as NamedArguments. + foreach (var namedArg in attrData.NamedArguments) + { + switch (namedArg.Key) + { + default: + Debug.Fail($"An unknown member was found on {nameof(MarshalAsAttribute)}"); + continue; + case nameof(MarshalAsAttribute.SafeArraySubType): + case nameof(MarshalAsAttribute.SafeArrayUserDefinedSubType): + case nameof(MarshalAsAttribute.IidParameterIndex): + // [TODO] Report not supported + break; + case nameof(MarshalAsAttribute.MarshalTypeRef): + case nameof(MarshalAsAttribute.MarshalType): + // Call ToString() to handle INamedTypeSymbol as well. + info.CustomMarshallerTypeName = namedArg.Value.Value.ToString(); + break; + case nameof(MarshalAsAttribute.MarshalCookie): + info.CustomMarshallerCookie = (string)namedArg.Value.Value; + break; + case nameof(MarshalAsAttribute.ArraySubType): + info.UnmanagedArraySubType = (UnmanagedType)namedArg.Value.Value; + break; + case nameof(MarshalAsAttribute.SizeConst): + info.ArraySizeConst = (int)namedArg.Value.Value; + break; + case nameof(MarshalAsAttribute.SizeParamIndex): + info.ArraySizeParamIndex = (short)namedArg.Value.Value; + break; + } + } + + return info; + } + } + + private static string ComputeTypeForManaged(ITypeSymbol type, RefKind refKind) + { + var typeAsString = type.SpecialType switch + { + SpecialType.System_Void => "void", + SpecialType.System_SByte => "sbyte", + SpecialType.System_Byte => "byte", + SpecialType.System_Int16 => "short", + SpecialType.System_UInt16 => "ushort", + SpecialType.System_Int32 => "int", + SpecialType.System_UInt32 => "uint", + SpecialType.System_Int64 => "long", + SpecialType.System_UInt64 => "ulong", + SpecialType.System_Single => "float", + SpecialType.System_Double => "double", + SpecialType.System_String => "string", + SpecialType.System_IntPtr => "System.IntPtr", + SpecialType.System_UIntPtr => "System.UIntPtr", + _ => null, + }; + + var typePrefix = string.Empty; + if (typeAsString is null) + { + // Determine the namespace + if (!(type.ContainingNamespace is null) + && !type.ContainingNamespace.IsGlobalNamespace) + { + typePrefix = $"{type.ContainingNamespace}{Type.Delimiter}"; + } + + typeAsString = type.ToString(); + } + + string refKindAsString = RefKindToString(refKind); + return $"{refKindAsString}{typePrefix}{typeAsString}"; + } + + private static string ComputeTypeForUnmanaged(ITypeSymbol type, RefKind refKind) + { +#if GENERATE_FORWARDER + return ComputeTypeForManaged(type, refKind); +#else + if (!type.IsUnmanagedType) + { + return "void*"; + } + + return type.SpecialType switch + { + SpecialType.System_Void => "void", + SpecialType.System_SByte => "sbyte", + SpecialType.System_Byte => "byte", + SpecialType.System_Int16 => "short", + SpecialType.System_UInt16 => "ushort", + SpecialType.System_Int32 => "int", + SpecialType.System_UInt32 => "uint", + SpecialType.System_Int64 => "long", + SpecialType.System_UInt64 => "ulong", + SpecialType.System_Single => "float", + SpecialType.System_Double => "double", + SpecialType.System_String => "char*", // [TODO] Consider encoding here + SpecialType.System_IntPtr => "void*", + SpecialType.System_UIntPtr => "void*", + _ => "void*", + }; +#endif + } + + private static string RefKindToString(RefKind refKind) + { + return refKind switch + { + RefKind.In => "in ", + RefKind.Ref => "ref ", + RefKind.Out => "out ", + RefKind.None => string.Empty, + _ => throw new NotImplementedException("Support for some RefKind"), + }; + } + } +} diff --git a/DllImportGenerator/readme.md b/DllImportGenerator/readme.md index 3a3f5d25594a..e54d73e8164e 100644 --- a/DllImportGenerator/readme.md +++ b/DllImportGenerator/readme.md @@ -1,3 +1,16 @@ # DllImport Generator -See [P/Invoke source generator proposal](https://github.com/dotnet/runtime/blob/master/docs/design/features/source-generator-pinvokes.md) for design and goals. \ No newline at end of file +See [P/Invoke source generator proposal](https://github.com/dotnet/runtime/blob/master/docs/design/features/source-generator-pinvokes.md) for design and goals. + +## Additional work + +Below are additional work items that are not presently captured in this repository. + +### Required + +* Add `GeneratedDllImportAttribute` to the BCL. See [`GeneratedDllImportAttribute.cs`](./DllImportGeneratorAttribute/GeneratedDllImportAttribute.cs). +* Add support for calling [`SetLastError()`](https://docs.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror) from managed code with the expected semantics. During P/Invoke transitions the runtime manipulates the associated thread's error code and thus breaks `SetLastError()` semantics. + +### Optional + +* A tool to compare the resulting IL from the generated source to that generated by the built-in IL Marshaller system. This would help with validation of what is being generated. \ No newline at end of file