From 1ca9e84a426e2d99c4553ac12c273b16e47a795d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 27 Jun 2023 10:44:28 -0600 Subject: [PATCH] Add `IVTable` interfaces to COM structs Closes #831 --- .../Generator.Com.cs | 29 ++++++++-- .../Generator.Invariants.cs | 9 ++-- .../Generator.Templates.cs | 11 ++++ src/Microsoft.Windows.CsWin32/Generator.cs | 36 +++++++++---- .../HandleTypeHandleInfo.cs | 6 ++- .../templates/ComHelpers.cs | 15 ++++++ .../templates/IVTable.cs | 8 +++ .../templates/IVTable`2.cs | 16 ++++++ .../ComHelpers.cs | 53 +++++++++++++++++++ .../GeneratedForm.cs | 11 ++++ .../COMTests.cs | 31 +++++++++++ .../GeneratorTestBase.cs | 4 +- .../GeneratorTests.cs | 2 +- 13 files changed, 207 insertions(+), 24 deletions(-) create mode 100644 src/Microsoft.Windows.CsWin32/templates/IVTable.cs create mode 100644 src/Microsoft.Windows.CsWin32/templates/IVTable`2.cs create mode 100644 test/GenerationSandbox.Unmarshalled.Tests/ComHelpers.cs diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs index c4cf8436..2a5ad7fb 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs @@ -33,6 +33,10 @@ private static Guid DecodeGuidFromAttribute(CustomAttribute guidAttribute) private static bool IsHresult(TypeHandleInfo? typeHandleInfo) => typeHandleInfo is HandleTypeHandleInfo handleInfo && handleInfo.IsType("HRESULT"); + private static bool GenerateCcwFor(string interfaceName) => interfaceName is not ("IUnknown" or "IDispatch" or "IInspectable"); + + private static bool GenerateCcwFor(MetadataReader reader, StringHandle typeName) => !(reader.StringComparer.Equals(typeName, "IUnknown") || reader.StringComparer.Equals(typeName, "IDispatch") || reader.StringComparer.Equals(typeName, "IInspectable")); + /// /// Generates a type to represent a COM interface. /// @@ -121,8 +125,7 @@ private TypeDeclarationSyntax DeclareInterfaceAsStruct(TypeDefinitionHandle type allMethods.AddRange(methodsThisType); // We do *not* emit CCW methods for IUnknown, because those are provided by ComWrappers. - if (ccwThisParameter is not null && - (qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IUnknown") || qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IDispatch") || qualifiedBaseType.Reader.StringComparer.Equals(baseType.Name, "IInspectable"))) + if (ccwThisParameter is not null && !GenerateCcwFor(qualifiedBaseType.Reader, baseType.Name)) { ccwMethodsToSkip.AddRange(methodsThisType); } @@ -482,7 +485,7 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null) if (ccwThisParameter is not null) { - // PopulateVTable must be public in order to (implicitly) implement an interface that WinForms declares. + // PopulateVTable must be public in order to (implicitly) implement the IVTable interface. // public static void PopulateVTable(Vtbl* vtable) MethodDeclarationSyntax populateVtblMethodDecl = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("PopulateVTable")) .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) @@ -503,7 +506,7 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null) BaseListSyntax baseList = BaseList(SeparatedList()); CustomAttribute? guidAttribute = this.FindGuidAttribute(typeDef.GetCustomAttributes()); - var staticMembers = this.DeclareStaticCOMInterfaceMembers(guidAttribute); + var staticMembers = this.DeclareStaticCOMInterfaceMembers(originalIfaceName, ifaceName, ccwThisParameter is not null, guidAttribute, context); members.AddRange(staticMembers.Members); baseList = baseList.AddTypes(staticMembers.BaseTypes.ToArray()); @@ -755,11 +758,27 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null) return ifaceDeclaration; } - private unsafe (List Members, List BaseTypes) DeclareStaticCOMInterfaceMembers(CustomAttribute? guidAttribute) + private unsafe (IReadOnlyList Members, IReadOnlyList BaseTypes) DeclareStaticCOMInterfaceMembers( + string originalIfaceName, + IdentifierNameSyntax ifaceName, + bool populateVtblDeclared, + CustomAttribute? guidAttribute, + Context context) { List members = new(); List baseTypes = new(); + // IVTable + // Static interface members require C# 11 and .NET 7 at minimum. + if (populateVtblDeclared && this.IsFeatureAvailable(Feature.InterfaceStaticMembers) && !context.AllowMarshaling && GenerateCcwFor(originalIfaceName)) + { + this.RequestComHelpers(context); + baseTypes.Add(SimpleBaseType(GenericName("IVTable").AddTypeArgumentListArguments( + ifaceName, + QualifiedName(ifaceName, IdentifierName("Vtbl"))))); + } + + // IComIID if (guidAttribute.HasValue) { Guid guidAttributeValue = DecodeGuidFromAttribute(guidAttribute.Value); diff --git a/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs b/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs index d99782ab..15ab3d78 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs @@ -81,7 +81,8 @@ public partial class Generator private const string OriginalDelegateAnnotation = "OriginalDelegate"; private static readonly Dictionary PInvokeHelperMethods; - private static readonly ClassDeclarationSyntax ComHelperClass; + private static readonly InterfaceDeclarationSyntax IVTableInterface; + private static readonly InterfaceDeclarationSyntax IVTableGenericInterface; private static readonly Dictionary Win32SdkMacros; private static readonly string AutoGeneratedHeader = @"// ------------------------------------------------------------------------------ @@ -324,13 +325,13 @@ public partial class Generator .Add("ULARGE_INTEGER", "Use the C# ulong keyword instead.") .Add("OVERLAPPED", "Use System.Threading.NativeOverlapped instead.") .Add("POINT", "Use System.Drawing.Point instead.") - .Add("POINTF", "Use System.Drawing.PointF instead.") - .Add("IUnknown", "This COM interface is implicit in the runtime. Interfaces that derive from it should apply the [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] attribute instead.") - .Add("IDispatch", "This COM interface is implicit in the runtime. Interfaces that derive from it should apply the [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] attribute instead."); + .Add("POINTF", "Use System.Drawing.PointF instead."); /// /// Gets a map of interop APIs that should not be generated when marshaling is allowed, and messages to emit in diagnostics if these APIs are ever directly requested. /// internal static ImmutableDictionary BannedAPIsWithMarshaling { get; } = BannedAPIsWithoutMarshaling + .Add("IUnknown", "This COM interface is implicit in the runtime. Interfaces that derive from it should apply the [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] attribute instead.") + .Add("IDispatch", "This COM interface is implicit in the runtime. Interfaces that derive from it should apply the [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] attribute instead.") .Add("VARIANT", "Use `object` instead of VARIANT when in COM interface mode. VARIANT can only be emitted when emitting COM interfaces as structs."); } diff --git a/src/Microsoft.Windows.CsWin32/Generator.Templates.cs b/src/Microsoft.Windows.CsWin32/Generator.Templates.cs index 4f28ca6e..435aa336 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Templates.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Templates.cs @@ -38,6 +38,17 @@ private static bool TryFetchTemplate(string name, Generator? generator, [NotNull return true; } + private static void FetchTemplate(string name, Generator? generator, out T member) + where T : MemberDeclarationSyntax + { + if (!TryFetchTemplate(name, generator, out MemberDeclarationSyntax? localMember)) + { + throw new GenerationFailedException("Missing embedded resource."); + } + + member = (T)localMember; + } + private IEnumerable ExtractMembersFromTemplate(string name) => ((TypeDeclarationSyntax)this.FetchTemplate($"{name}")).Members; /// diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 64d6ed3c..f652eaa3 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -21,6 +21,8 @@ public partial class Generator : IGenerator, IDisposable private readonly TypeSyntaxSettings functionPointerTypeSettings; private readonly TypeSyntaxSettings errorMessageTypeSettings; + private readonly ClassDeclarationSyntax comHelperClass; + private readonly Dictionary> findTypeSymbolIfAlreadyAvailableCache = new(StringComparer.Ordinal); private readonly Rental metadataReader; private readonly GeneratorOptions options; @@ -53,12 +55,8 @@ static Generator() Win32SdkMacros = ((ClassDeclarationSyntax)member).Members.OfType().ToDictionary(m => m.Identifier.ValueText, m => m); - if (!TryFetchTemplate("ComHelpers", null, out member)) - { - throw new GenerationFailedException("Missing embedded resource."); - } - - ComHelperClass = (ClassDeclarationSyntax)member; + FetchTemplate("IVTable", null, out IVTableInterface); + FetchTemplate("IVTable`2", null, out IVTableGenericInterface); } /// @@ -113,6 +111,7 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option AddSymbolIf(this.canUseUnsafeAsRef, "canUseUnsafeAsRef"); AddSymbolIf(this.canUseUnsafeNullRef, "canUseUnsafeNullRef"); AddSymbolIf(compilation?.GetTypeByMetadataName("System.Drawing.Point") is not null, "canUseSystemDrawing"); + AddSymbolIf(this.IsFeatureAvailable(Feature.InterfaceStaticMembers), "canUseInterfaceStaticMembers"); if (extraSymbols.Count > 0) { @@ -147,10 +146,15 @@ void AddSymbolIf(bool condition, string symbol) this.errorMessageTypeSettings = this.generalTypeSettings with { QualifyNames = true, Generator = null }; // Avoid risk of infinite recursion from errors in ToTypeSyntax this.methodsAndConstantsClassName = IdentifierName(options.ClassName); + + FetchTemplate("ComHelpers", this, out this.comHelperClass); } private enum Feature { + /// + /// Indicates that interfaces can declare static members. This requires at least .NET 7 and C# 11. + /// InterfaceStaticMembers, } @@ -794,9 +798,17 @@ internal void RequestComHelpers(Context context) { if (this.IsWin32Sdk) { - const string specialType = "ComHelpers"; this.RequestInteropType("Windows.Win32.Foundation", "HRESULT", context); - this.volatileCode.GenerateSpecialType(specialType, () => this.volatileCode.AddSpecialType(specialType, ComHelperClass)); + this.volatileCode.GenerateSpecialType("ComHelpers", () => this.volatileCode.AddSpecialType("ComHelpers", this.comHelperClass)); + if (this.IsFeatureAvailable(Feature.InterfaceStaticMembers) && !context.AllowMarshaling) + { + this.volatileCode.GenerateSpecialType("IVTable", () => this.volatileCode.AddSpecialType("IVTable", IVTableInterface)); + this.volatileCode.GenerateSpecialType("IVTable`2", () => this.volatileCode.AddSpecialType("IVTable`2", IVTableGenericInterface)); + if (!this.TryGenerate("IUnknown", default)) + { + throw new GenerationFailedException("Unable to generate IUnknown."); + } + } } else if (this.SuperGenerator is not null && this.SuperGenerator.TryGetGenerator("Windows.Win32", out Generator? generator)) { @@ -865,7 +877,7 @@ internal void RequestInteropType(TypeDefinitionHandle typeDefHandle, Context con } } - bool hasUnmanagedName = this.HasUnmanagedSuffix(context.AllowMarshaling, this.IsManagedType(typeDefHandle)); + bool hasUnmanagedName = this.HasUnmanagedSuffix(this.Reader, typeDef.Name, context.AllowMarshaling, this.IsManagedType(typeDefHandle)); this.volatileCode.GenerateType(typeDefHandle, hasUnmanagedName, delegate { if (this.RequestInteropTypeHelper(typeDefHandle, context) is MemberDeclarationSyntax typeDeclaration) @@ -1027,10 +1039,12 @@ internal void GetBaseTypeInfo(TypeDefinition typeDef, out StringHandle baseTypeN return specialDeclaration; } - internal bool HasUnmanagedSuffix(bool allowMarshaling, bool isManagedType) => !allowMarshaling && isManagedType && this.options.AllowMarshaling; + internal bool HasUnmanagedSuffix(string originalName, bool allowMarshaling, bool isManagedType) => !allowMarshaling && isManagedType && this.options.AllowMarshaling && originalName is not "IUnknown"; + + internal bool HasUnmanagedSuffix(MetadataReader reader, StringHandle typeName, bool allowMarshaling, bool isManagedType) => !allowMarshaling && isManagedType && this.options.AllowMarshaling && !reader.StringComparer.Equals(typeName, "IUnknown"); internal string GetMangledIdentifier(string normalIdentifier, bool allowMarshaling, bool isManagedType) => - this.HasUnmanagedSuffix(allowMarshaling, isManagedType) ? normalIdentifier + UnmanagedInteropSuffix : normalIdentifier; + this.HasUnmanagedSuffix(normalIdentifier, allowMarshaling, isManagedType) ? normalIdentifier + UnmanagedInteropSuffix : normalIdentifier; /// /// Disposes of managed and unmanaged resources. diff --git a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs index f9bc784b..7e17b595 100644 --- a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs +++ b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs @@ -47,12 +47,12 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs bool isInterface; bool isNonCOMConformingInterface; bool isManagedType = inputs.Generator?.IsManagedType(this) ?? false; - bool hasUnmanagedSuffix = inputs.Generator?.HasUnmanagedSuffix(inputs.AllowMarshaling, isManagedType) ?? false; - string simpleNameSuffix = hasUnmanagedSuffix ? Generator.UnmanagedInteropSuffix : string.Empty; switch (this.Handle.Kind) { case HandleKind.TypeDefinition: TypeDefinition td = this.reader.GetTypeDefinition((TypeDefinitionHandle)this.Handle); + bool hasUnmanagedSuffix = inputs.Generator?.HasUnmanagedSuffix(this.reader, td.Name, inputs.AllowMarshaling, isManagedType) ?? false; + string simpleNameSuffix = hasUnmanagedSuffix ? Generator.UnmanagedInteropSuffix : string.Empty; nameSyntax = inputs.QualifyNames ? GetNestingQualifiedName(inputs.Generator, this.reader, td, hasUnmanagedSuffix, isInterfaceNestedInStruct: false) : IdentifierName(this.reader.GetString(td.Name) + simpleNameSuffix); isInterface = (td.Attributes & TypeAttributes.Interface) == TypeAttributes.Interface; isNonCOMConformingInterface = isInterface && inputs.Generator?.IsNonCOMInterface(td) is true; @@ -60,6 +60,8 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs case HandleKind.TypeReference: var trh = (TypeReferenceHandle)this.Handle; TypeReference tr = this.reader.GetTypeReference(trh); + hasUnmanagedSuffix = inputs.Generator?.HasUnmanagedSuffix(this.reader, tr.Name, inputs.AllowMarshaling, isManagedType) ?? false; + simpleNameSuffix = hasUnmanagedSuffix ? Generator.UnmanagedInteropSuffix : string.Empty; nameSyntax = inputs.QualifyNames ? GetNestingQualifiedName(inputs, this.reader, tr, hasUnmanagedSuffix) : IdentifierName(this.reader.GetString(tr.Name) + simpleNameSuffix); isInterface = inputs.Generator?.IsInterface(trh) is true; isNonCOMConformingInterface = isInterface && inputs.Generator?.IsNonCOMInterface(trh) is true; diff --git a/src/Microsoft.Windows.CsWin32/templates/ComHelpers.cs b/src/Microsoft.Windows.CsWin32/templates/ComHelpers.cs index 318cc38e..6cd3454e 100644 --- a/src/Microsoft.Windows.CsWin32/templates/ComHelpers.cs +++ b/src/Microsoft.Windows.CsWin32/templates/ComHelpers.cs @@ -10,4 +10,19 @@ internal static winmdroot.Foundation.HRESULT UnwrapCCW(TThis* @object = ComWrappers.ComInterfaceDispatch.GetInstance((ComWrappers.ComInterfaceDispatch*)@this); return @object is null ? COR_E_OBJECTDISPOSED : S_OK; } + +#if canUseInterfaceStaticMembers + internal static void PopulateIUnknown(System.Com.IUnknown.Vtbl* vtable) + where TComInterface : unmanaged + { + PopulateIUnknownImpl(vtable); + if (vtable->QueryInterface_1 is null) + { + throw new NotImplementedException("v-tables cannot be accessed unless the Windows.Win32.ComHelpers.PopulateIUnknownImpl partial method is implemented."); + } + } + + static partial void PopulateIUnknownImpl(System.Com.IUnknown.Vtbl* vtable) + where TComInterface : unmanaged; +#endif } diff --git a/src/Microsoft.Windows.CsWin32/templates/IVTable.cs b/src/Microsoft.Windows.CsWin32/templates/IVTable.cs new file mode 100644 index 00000000..4c035c3b --- /dev/null +++ b/src/Microsoft.Windows.CsWin32/templates/IVTable.cs @@ -0,0 +1,8 @@ +/// +/// Non generic interface that allows constraining against a COM wrapper type directly. COM structs should +/// implement . +/// +internal unsafe interface IVTable +{ + static abstract System.Com.IUnknown.Vtbl* VTable { get; } +} diff --git a/src/Microsoft.Windows.CsWin32/templates/IVTable`2.cs b/src/Microsoft.Windows.CsWin32/templates/IVTable`2.cs new file mode 100644 index 00000000..da046b63 --- /dev/null +++ b/src/Microsoft.Windows.CsWin32/templates/IVTable`2.cs @@ -0,0 +1,16 @@ +internal unsafe interface IVTable : IVTable + where TVTable : unmanaged + where TComInterface : unmanaged, IVTable +{ + private protected static abstract void PopulateVTable(TVTable* vtable); + + static System.Com.IUnknown.Vtbl* IVTable.VTable { get; } = (System.Com.IUnknown.Vtbl*)CreateVTable(); + + private static TVTable* CreateVTable() + { + TVTable* vtbl = (TVTable*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(TVTable), sizeof(TVTable)); + ComHelpers.PopulateIUnknown((System.Com.IUnknown.Vtbl*)vtbl); + TComInterface.PopulateVTable(vtbl); + return vtbl; + } +} diff --git a/test/GenerationSandbox.Unmarshalled.Tests/ComHelpers.cs b/test/GenerationSandbox.Unmarshalled.Tests/ComHelpers.cs new file mode 100644 index 00000000..024068da --- /dev/null +++ b/test/GenerationSandbox.Unmarshalled.Tests/ComHelpers.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET7_0_OR_GREATER + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; + +namespace Windows.Win32; + +// The `unsafe` modifier is only allowed to appear on the class declaration -- not the partial method declaration. +// See https://github.com/dotnet/csharplang/discussions/7298 for more. +internal unsafe partial class ComHelpers +{ + static partial void PopulateIUnknownImpl(IUnknown.Vtbl* vtable) + where TComInterface : unmanaged + { + // IUnknown member initialization of the v-table would go here. + vtable->QueryInterface_1 = TestComWrappers.ComWrappersForIUnknown.QueryInterface_1; + vtable->AddRef_2 = TestComWrappers.ComWrappersForIUnknown.AddRef_2; + vtable->Release_3 = TestComWrappers.ComWrappersForIUnknown.Release_3; + } + + private unsafe class TestComWrappers : ComWrappers + { + internal static readonly IUnknown.Vtbl ComWrappersForIUnknown = GetComWrappersUnknown(); + + // Abstracts that need implementation + protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) + { + count = 0; + return null; + } + + protected override object? CreateObject(nint externalComObject, CreateObjectFlags flags) => null; + + protected override void ReleaseObjects(global::System.Collections.IEnumerable objects) => throw new NotImplementedException(); + + private static IUnknown.Vtbl GetComWrappersUnknown() + { + GetIUnknownImpl(out IntPtr fpQueryInterface, out IntPtr fpAddRef, out IntPtr fpRelease); + return new IUnknown.Vtbl() + { + QueryInterface_1 = (delegate* unmanaged[Stdcall])fpQueryInterface, + AddRef_2 = (delegate* unmanaged[Stdcall])fpAddRef, + Release_3 = (delegate* unmanaged[Stdcall])fpRelease, + }; + } + } +} + +#endif diff --git a/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs b/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs index e6033b26..0f29efa0 100644 --- a/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs +++ b/test/GenerationSandbox.Unmarshalled.Tests/GeneratedForm.cs @@ -37,6 +37,17 @@ private static unsafe void IStream_GetsCCW() } #endif +#if NET7_0_OR_GREATER + private static unsafe void GetVTable() + { + IUnknown.Vtbl* vtbl = GetVtable(); + + static IUnknown.Vtbl* GetVtable() + where T : IVTable + => T.VTable; + } +#endif + private static unsafe void IUnknownGetsVtbl() { // WinForms needs the v-table to be declared for these base interfaces. diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index 59664a9c..948f5b32 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -254,6 +254,23 @@ public void AssociatedEnumOnMethodParameters() Assert.Equal("SFVS_SELECT", Assert.IsType(parameter.Type).Right.Identifier.ValueText); } + [Theory, CombinatorialData] + public void InterestingUnmarshaledComInterfaces( + [CombinatorialValues( + "IUnknown", + "IDispatch", + "IInspectable")] + string api, + [CombinatorialMemberData(nameof(TFMDataNoNetFx35))] + string tfm) + { + this.compilation = this.starterCompilations[tfm]; + this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); + Assert.True(this.generator.TryGenerate(api, CancellationToken.None)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + } + [Theory] [CombinatorialData] public void InterestingComInterfaces( @@ -349,6 +366,20 @@ public void IStream_ProducesPopulateVTable() Assert.Single(iface.Members.OfType(), m => m.Identifier.ValueText == "PopulateVTable"); } + [Fact] + public void IPersistFile_DerivesFromIComIID() + { + this.compilation = this.starterCompilations["net7.0"]; + const string typeName = "IPersistFile"; + this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false }); + Assert.True(this.generator.TryGenerateType(typeName)); + this.CollectGeneratedCode(this.generator); + this.AssertNoDiagnostics(); + var iface = (StructDeclarationSyntax)this.FindGeneratedType(typeName).Single(); + Assert.NotNull(iface.BaseList); + Assert.Single(iface.BaseList.Types, bt => bt.Type.ToString().Contains("IComIID")); + } + [Theory] [CombinatorialData] public void COMInterfaceIIDInterfaceOnAppropriateTFMs( diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs index e26ea4c8..548d0b18 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs @@ -25,7 +25,7 @@ public GeneratorTestBase(ITestOutputHelper logger) this.parseOptions = CSharpParseOptions.Default .WithDocumentationMode(DocumentationMode.Diagnose) - .WithLanguageVersion(LanguageVersion.CSharp9); + .WithLanguageVersion(LanguageVersion.CSharp11); // set in InitializeAsync this.compilation = null!; @@ -45,6 +45,7 @@ public enum MarshalingOptions new object[] { "net472" }, new object[] { "netstandard2.0" }, new object[] { "net6.0" }, + new object[] { "net7.0" }, }; public static IEnumerable TFMDataNoNetFx35MemberData => TFMDataNoNetFx35.Select(tfm => new object[] { tfm }).ToArray(); @@ -55,6 +56,7 @@ public enum MarshalingOptions "net472", "netstandard2.0", "net6.0", + "net7.0", }; public static Platform[] SpecificCpuArchitectures => diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index 1ad6e6e5..c726beaa 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -71,7 +71,7 @@ public void SimplestMethod(string tfm) this.AssertNoDiagnostics(); var generatedMethod = this.FindGeneratedMethod(methodName).Single(); - if (tfm == "net6.0") + if (tfm is "net6.0" or "net7.0") { Assert.Contains(generatedMethod.AttributeLists, al => IsAttributePresent(al, "SupportedOSPlatform")); }