From e10e5f3910130b4212cc89fb2d1bedaf59b6c2a4 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Wed, 14 Oct 2026 18:06:33 -0700 Subject: [PATCH] Support AssociatedEnum annotation on parameters and fields Project ABI-compatible enum storage directly for native signatures and buffer elements. Preserve native-backed scalar field properties and report PInvoke015 with native fallback for incompatible metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 551c26b9-812d-47f4-9c39-9fd3344e17ce --- docfx/docs/3rdPartyMetadata.md | 3 + docfx/docs/associated-enums.md | 79 +++ docfx/docs/features.md | 1 + docfx/docs/toc.yml | 1 + src/CsWin32Generator/Program.cs | 13 +- .../AnalyzerReleases.Unshipped.md | 1 + .../Generator.AssociatedEnum.cs | 257 ++++++++ .../Generator.CustomMarshaller.cs | 101 --- .../Generator.Extern.cs | 48 +- .../Generator.MetadataHelpers.cs | 11 - .../Generator.Struct.cs | 37 +- .../HandleTypeHandleInfo.cs | 2 + .../PointerTypeHandleInfo.cs | 4 +- .../PrimitiveTypeHandleInfo.cs | 37 +- .../SourceGenerator.cs | 16 + .../SuperGenerator.cs | 5 + .../CsWin32GeneratorTests.cs | 47 +- .../COMTests.cs | 4 +- .../NativeMethods.txt | 1 + test/GenerationSandbox.Tests/BasicTests.cs | 6 +- .../ComRuntimeTests.cs | 4 +- test/GenerationSandbox.Tests/GeneratedForm.cs | 6 +- .../GenerationSandbox.Tests/NativeMethods.txt | 2 + test/GenerationSandbox.props | 3 + .../AssociatedEnumTests.cs | 574 ++++++++++++++++++ .../COMTests.cs | 5 +- .../ExternMethodTests.cs | 10 +- .../ExternalMetadata/AssociatedEnumArrays.il | 53 ++ .../AssociatedEnumArrays.winmd | Bin 0 -> 2560 bytes .../ExternalMetadata/NOTICE.md | 1 + .../SourceGeneratorTests.cs | 25 + .../StructTests.cs | 7 +- 32 files changed, 1127 insertions(+), 237 deletions(-) create mode 100644 docfx/docs/associated-enums.md create mode 100644 src/Microsoft.Windows.CsWin32/Generator.AssociatedEnum.cs create mode 100644 test/Microsoft.Windows.CsWin32.Tests/AssociatedEnumTests.cs create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.il create mode 100644 test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.winmd diff --git a/docfx/docs/3rdPartyMetadata.md b/docfx/docs/3rdPartyMetadata.md index 3fa335e4..409b5a34 100644 --- a/docfx/docs/3rdPartyMetadata.md +++ b/docfx/docs/3rdPartyMetadata.md @@ -8,6 +8,9 @@ But the general transformation from metadata to C# code may be applied to other Constructing metadata is outside the scope of this document. However you may find [the win32metadata architecture](https://github.com/microsoft/win32metadata/blob/main/docs/architecture.md) document instructive. +Use [associated enum annotations](associated-enums.md) to expose enum parameters and fields +while retaining native integer and pointer signatures in metadata. + ## Hooking metadata into CsWin32 Metadata is fed into CsWin32 through MSBuild items. diff --git a/docfx/docs/associated-enums.md b/docfx/docs/associated-enums.md new file mode 100644 index 00000000..7cb287f9 --- /dev/null +++ b/docfx/docs/associated-enums.md @@ -0,0 +1,79 @@ +# Associated enums + +Win32 metadata can annotate a native integer, pointer or buffer with +`Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute(string Name)`. +CsWin32 uses the named enum in the generated API when its declared underlying type +matches the native value's width and signedness. + +For example, an enum declared as `MODE : uint` can represent an annotated native +`uint` parameter or the elements of an annotated `uint*` buffer: + +```csharp +public enum MODE : uint { First = 1, Second = 2 } + +// Native metadata: +public static extern void SetMode([AssociatedEnum("MODE")] uint mode); +public static extern unsafe void ReadModes( + [In, AssociatedEnum("MODE"), NativeArrayInfo(CountParamIndex = 1)] uint* modes, + uint count); + +// Generated signatures, with import attributes omitted: +public static extern void SetMode(MODE mode); +public static extern unsafe void ReadModes(MODE* modes, uint count); +public static unsafe void ReadModes(ReadOnlySpan modes); +``` + +The enum is ABI-compatible with the native value: no scalar conversion or enum +marshaler is required. Its underlying type, member names, values and `[Flags]` +attribute remain as declared in metadata. +This applies with runtime marshaling enabled or disabled, including return +parameters, callbacks, multiple pointer levels, and fixed or flexible buffers. + +## Fields + +Annotated scalar fields retain the existing native-backed enum-property shape: + +```csharp +private uint _Mode; + +public MODE Mode +{ + get => (MODE)this._Mode; + set => this._Mode = (uint)value; +} +``` + +This preserves callers that implement interface properties or use property reflection. +Pointer and buffer fields expose enum elements directly when their storage matches. + +## Resolving enum names + +An unqualified name such as `MODE` prefers the annotated member's declaring namespace. +Otherwise, the name must identify exactly one type in that metadata assembly and +its loaded assembly-reference closure. +A qualified name such as `Windows.Win32.Test.MODE` selects that namespace explicitly. +An unrelated loaded metadata assembly is not searched. +Missing, ambiguous, non-enum, or platform-incompatible targets produce generation errors. + +## Incompatible metadata + +If the native value and enum have different underlying types, CsWin32 reports +warning **PInvoke015** and ignores that association. +The native parameter, pointer, buffer or field representation is preserved. +For example, `uint*` cannot be projected as a pointer to an enum declared with +`ushort` or `int` storage. +The same warning applies when an existing C# enum has incompatible storage, the +annotated native shape is not integral, or a field is also a `NativeBitfield` backing field. + +CsWin32 does not change an enum's underlying type to compensate for inconsistent +metadata, generate converting enum overloads for it, or reinterpret incompatible buffers. +Correct the annotation or enum definition in the metadata. + +This also applies to existing annotations in the Windows metadata packages. +For example, the currently shipped annotations for `SHObjectProperties`, +`PathCleanupSpec`, `IShellFolderView.Select`, and `SHDESCRIPTIONID.dwDescriptionId` +disagree with their associated enums' signedness. +These APIs now retain their native integer types and produce PInvoke015 instead +of exposing a converting enum view. +Callers of affected APIs must use native integers or explicit enum casts until +the metadata is corrected. diff --git a/docfx/docs/features.md b/docfx/docs/features.md index f912c88d..9ed661fa 100644 --- a/docfx/docs/features.md +++ b/docfx/docs/features.md @@ -5,5 +5,6 @@ - Generates xml documentation based on and links back to learn.microsoft.com - Ships no bulky assemblies alongside your application. - [Layered composition](composition.md): multiple assemblies can extend a single shared `PInvoke` static class so callers reach every native API through one symbol. +- [Associated enums](associated-enums.md): expose ABI-compatible enum parameters and buffer elements from native metadata annotations. ![Animation demonstrating p/invoke code generation](../images/demo.gif) diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml index 184674de..fe36a206 100644 --- a/docfx/docs/toc.yml +++ b/docfx/docs/toc.yml @@ -5,3 +5,4 @@ items: - href: composition.md - href: ArchSpecificAPIs.md - href: 3rdPartyMetadata.md +- href: associated-enums.md diff --git a/src/CsWin32Generator/Program.cs b/src/CsWin32Generator/Program.cs index 28d96a84..f455ef21 100644 --- a/src/CsWin32Generator/Program.cs +++ b/src/CsWin32Generator/Program.cs @@ -629,6 +629,11 @@ private bool GenerateAndWriteFiles(SuperGenerator superGenerator, DirectoryInfo fileCount++; } + foreach (Diagnostic diagnostic in superGenerator.Diagnostics) + { + this.ReportWarning(diagnostic.GetMessage(), code: diagnostic.Id); + } + var generatedFilesTxt = Path.Combine(outputPath.FullName, "CsWin32GeneratedFiles.txt"); File.WriteAllLines(generatedFilesTxt, outputFiles); @@ -661,19 +666,19 @@ private void ReportError(string message, string? file = null, int? line = null) } } - private void ReportWarning(string message, string? file = null, int? line = null) + private void ReportWarning(string message, string? file = null, int? line = null, string? code = null) { if (file is not null && line is not null) { - this.output.WriteLine($"{file}({line}): CsWin32 warning : {message}"); + this.output.WriteLine($"{file}({line}): CsWin32 warning {code}: {message}"); } else if (file is not null) { - this.output.WriteLine($"{file}: CsWin32 warning : {message}"); + this.output.WriteLine($"{file}: CsWin32 warning {code}: {message}"); } else { - this.output.WriteLine($"CsWin32 : warning : {message}"); + this.output.WriteLine($"CsWin32 : warning {code}: {message}"); } } diff --git a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md index 01505536..21ed9fa2 100644 --- a/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md +++ b/src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md @@ -19,3 +19,4 @@ PInvoke011 | Configuration | Error | SourceGenerator PInvoke012 | Configuration | Error | SourceGenerator PInvoke013 | Configuration | Error | SourceGenerator PInvoke014 | Configuration | Error | SourceGenerator +PInvoke015 | Functionality | Warning | SourceGenerator diff --git a/src/Microsoft.Windows.CsWin32/Generator.AssociatedEnum.cs b/src/Microsoft.Windows.CsWin32/Generator.AssociatedEnum.cs new file mode 100644 index 00000000..596fc33e --- /dev/null +++ b/src/Microsoft.Windows.CsWin32/Generator.AssociatedEnum.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Windows.CsWin32; + +public partial class Generator +{ + private readonly Dictionary associatedEnums = new(); + private readonly List generationDiagnostics = new(); + private Dictionary? associatedEnumParameterOwners; + + /// + /// Gets diagnostics produced while projecting metadata. + /// + public IReadOnlyList Diagnostics => this.generationDiagnostics; + + /// + /// Resolves an enum association whose storage matches the annotated native value. + /// + /// The attributes on the native value. + /// The associated enum, or null if absent or incompatible with the native representation. + internal HandleTypeHandleInfo? FindAssociatedEnum(CustomAttributeHandleCollection? customAttributeHandles) + { + if (this.FindAttribute(customAttributeHandles, InteropDecorationNamespace, AssociatedEnumAttribute) is not CustomAttribute attribute) + { + return null; + } + + if (this.associatedEnums.TryGetValue(attribute.Parent, out HandleTypeHandleInfo? cached)) + { + return cached; + } + + CustomAttributeValue arguments = attribute.DecodeValue(CustomAttributeTypeProvider.Instance); + if (arguments.FixedArguments is not [{ Value: string name }] || string.IsNullOrWhiteSpace(name)) + { + throw new GenerationFailedException("AssociatedEnum requires a nonempty enum name."); + } + + (TypeHandleInfo nativeType, string scope, string site) = this.GetAssociatedEnumUseSite(attribute.Parent); + HandleTypeHandleInfo enumType = this.ResolveAssociatedEnum(name, scope); + PrimitiveTypeCode enumStorage = GetAssociatedEnumStorage(enumType); + while (nativeType is ITypeHandleContainer container) + { + nativeType = container.ElementType; + } + + string? incompatibility = null; + if (this.FindAttribute(customAttributeHandles, InteropDecorationNamespace, NativeBitfieldAttribute) is not null) + { + incompatibility = "the annotated field is also a NativeBitfield backing field"; + } + else if (nativeType is PrimitiveTypeHandleInfo primitive) + { + if (primitive.PrimitiveTypeCode != enumStorage) + { + incompatibility = $"native storage is {primitive.PrimitiveTypeCode}, but the enum's underlying type is {enumStorage}"; + } + } + else if (nativeType is not HandleTypeHandleInfo nativeHandle + || !nativeHandle.Generator.TryGetTypeDefHandle(nativeHandle.Handle, out QualifiedTypeDefinitionHandle definition) + || definition.Generator != enumType.Generator || definition.DefinitionHandle != enumType.Handle) + { + incompatibility = $"native type {nativeType} is not the enum's underlying integral type"; + } + + TypeDefinition enumDefinition = enumType.Reader.GetTypeDefinition((TypeDefinitionHandle)enumType.Handle); + string qualifiedName = $"{enumType.Reader.GetString(enumDefinition.Namespace)}.{enumType.Reader.GetString(enumDefinition.Name)}"; + if (incompatibility is null && enumType.Generator.FindTypeSymbolIfAlreadyAvailable(qualifiedName) is INamedTypeSymbol availableType + && availableType.EnumUnderlyingType?.SpecialType != GetEnumSpecialType(enumStorage)) + { + incompatibility = $"the available C# type {qualifiedName} does not have the enum's {enumStorage} storage"; + } + + if (incompatibility is not null) + { + this.generationDiagnostics.Add(Diagnostic.Create(SourceGenerator.IncompatibleAssociatedEnum, Location.None, name, site, incompatibility)); + this.associatedEnums.Add(attribute.Parent, null); + return null; + } + + this.associatedEnums.Add(attribute.Parent, enumType); + return enumType; + } + + private static PrimitiveTypeCode GetAssociatedEnumStorage(HandleTypeHandleInfo enumType) + { + TypeDefinition definition = enumType.Reader.GetTypeDefinition((TypeDefinitionHandle)enumType.Handle); + enumType.Generator.GetBaseTypeInfo(definition, out StringHandle baseName, out StringHandle baseNamespace); + if (!enumType.Reader.StringComparer.Equals(baseName, nameof(Enum)) || !enumType.Reader.StringComparer.Equals(baseNamespace, nameof(System))) + { + throw new GenerationFailedException($"AssociatedEnum \"{enumType}\" does not refer to an enum."); + } + + FieldDefinition storage = definition.GetFields().Select(enumType.Reader.GetFieldDefinition).Single(field => !field.Attributes.HasFlag(FieldAttributes.Static)); + if (storage.DecodeSignature(enumType.Generator.SignatureHandleProvider, null) is not PrimitiveTypeHandleInfo underlying + || underlying.PrimitiveTypeCode is not (PrimitiveTypeCode.SByte or PrimitiveTypeCode.Byte or PrimitiveTypeCode.Int16 or PrimitiveTypeCode.UInt16 + or PrimitiveTypeCode.Int32 or PrimitiveTypeCode.UInt32 or PrimitiveTypeCode.Int64 or PrimitiveTypeCode.UInt64)) + { + throw new GenerationFailedException($"AssociatedEnum \"{enumType}\" has unsupported storage."); + } + + return underlying.PrimitiveTypeCode; + } + + private static SpecialType GetEnumSpecialType(PrimitiveTypeCode storage) => storage switch + { + PrimitiveTypeCode.SByte => SpecialType.System_SByte, + PrimitiveTypeCode.Byte => SpecialType.System_Byte, + PrimitiveTypeCode.Int16 => SpecialType.System_Int16, + PrimitiveTypeCode.UInt16 => SpecialType.System_UInt16, + PrimitiveTypeCode.Int32 => SpecialType.System_Int32, + PrimitiveTypeCode.UInt32 => SpecialType.System_UInt32, + PrimitiveTypeCode.Int64 => SpecialType.System_Int64, + PrimitiveTypeCode.UInt64 => SpecialType.System_UInt64, + _ => throw new GenerationFailedException($"Unsupported enum storage {storage}."), + }; + + private static PrimitiveTypeCode GetFixedBufferElementType(CustomAttribute fixedBuffer) + { + string elementName = fixedBuffer.DecodeValue(CustomAttributeTypeProvider.Instance).FixedArguments[0].Value!.ToString()!; + return elementName switch + { + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Byte" => PrimitiveTypeCode.Byte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + "System.Char" => PrimitiveTypeCode.Char, + "System.Single" => PrimitiveTypeCode.Single, + "System.Double" => PrimitiveTypeCode.Double, + "System.Boolean" => PrimitiveTypeCode.Boolean, + _ => throw new GenerationFailedException($"Unsupported fixed-buffer element type {elementName}."), + }; + } + + private HandleTypeHandleInfo ResolveAssociatedEnum(string name, string scope) + { + TrySplitPossiblyQualifiedName(name, out string? enumNamespace, out string enumName); + List matches = new(); + bool incompatiblePlatform = false; + foreach (Generator generator in this.GetAssociatedEnumReferenceClosure()) + { + foreach (NamespaceMetadata ns in generator.GetNamespacesToSearch(enumNamespace)) + { + if (ns.Types.TryGetValue(enumName, out TypeDefinitionHandle handle)) + { + matches.Add(new HandleTypeHandleInfo(generator, generator.Reader, handle)); + } + + incompatiblePlatform |= ns.TypesForOtherPlatform.Contains(enumName); + } + } + + if (enumNamespace is null) + { + List localMatches = matches.Where(match => + match.Reader.StringComparer.Equals(match.Reader.GetTypeDefinition((TypeDefinitionHandle)match.Handle).Namespace, scope)).ToList(); + if (localMatches.Count > 0) + { + matches = localMatches; + } + } + + if (matches.Count != 1) + { + if (matches.Count == 0 && incompatiblePlatform) + { + throw new PlatformIncompatibleException($"AssociatedEnum \"{name}\" is not available for the target platform."); + } + + throw new GenerationFailedException(matches.Count == 0 + ? $"AssociatedEnum \"{name}\" was not found in the metadata reference closure." + : $"AssociatedEnum \"{name}\" is ambiguous. Use a namespace-qualified enum name."); + } + + return matches[0]; + } + + private (TypeHandleInfo Type, string Namespace, string Site) GetAssociatedEnumUseSite(EntityHandle owner) + { + if (owner.Kind == HandleKind.FieldDefinition) + { + FieldDefinition field = this.Reader.GetFieldDefinition((FieldDefinitionHandle)owner); + TypeDefinition declaringType = this.Reader.GetTypeDefinition(field.GetDeclaringType()); + string scope = this.GetNamespaceForPossiblyNestedType(declaringType); + TypeHandleInfo nativeType = field.DecodeSignature(this.SignatureHandleProvider, null); + if (this.FindAttribute(field.GetCustomAttributes(), SystemRuntimeCompilerServices, nameof(FixedBufferAttribute)) is CustomAttribute fixedBuffer) + { + nativeType = new PrimitiveTypeHandleInfo(GetFixedBufferElementType(fixedBuffer)); + } + + return (nativeType, scope, $"{scope}.{this.Reader.GetString(declaringType.Name)}.{this.Reader.GetString(field.Name)}"); + } + + if (owner.Kind != HandleKind.Parameter) + { + throw new GenerationFailedException("AssociatedEnum is only supported on fields, parameters and return values."); + } + + ParameterHandle parameterHandle = (ParameterHandle)owner; + if (this.associatedEnumParameterOwners is null) + { + this.associatedEnumParameterOwners = new(); + foreach (MethodDefinitionHandle methodHandle in this.Reader.MethodDefinitions) + { + foreach (ParameterHandle handle in this.Reader.GetMethodDefinition(methodHandle).GetParameters()) + { + this.associatedEnumParameterOwners.Add(handle, methodHandle); + } + } + } + + Parameter parameter = this.Reader.GetParameter(parameterHandle); + MethodDefinition method = this.Reader.GetMethodDefinition(this.associatedEnumParameterOwners[parameterHandle]); + TypeDefinition type = this.Reader.GetTypeDefinition(method.GetDeclaringType()); + MethodSignature signature = method.DecodeSignature(this.SignatureHandleProvider, null); + string ns = this.GetNamespaceForPossiblyNestedType(type); + return ( + parameter.SequenceNumber == 0 ? signature.ReturnType : signature.ParameterTypes[parameter.SequenceNumber - 1], + ns, + $"{ns}.{this.Reader.GetString(type.Name)}.{this.Reader.GetString(method.Name)}::{(parameter.SequenceNumber == 0 ? "return" : this.Reader.GetString(parameter.Name))}"); + } + + private IEnumerable GetAssociatedEnumReferenceClosure() + { + HashSet visited = new(); + Queue pending = new(); + pending.Enqueue(this); + while (pending.Count > 0) + { + Generator generator = pending.Dequeue(); + if (!visited.Add(generator)) + { + continue; + } + + yield return generator; + if (this.SuperGenerator is not SuperGenerator super) + { + continue; + } + + foreach (AssemblyReferenceHandle referenceHandle in generator.Reader.AssemblyReferences) + { + AssemblyReference reference = generator.Reader.GetAssemblyReference(referenceHandle); + if (super.TryGetGenerator(generator.Reader.GetString(reference.Name), out Generator? referencedGenerator)) + { + pending.Enqueue(referencedGenerator); + } + } + } + } +} diff --git a/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs b/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs index 3e5ac809..71c6fa45 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.CustomMarshaller.cs @@ -5,107 +5,6 @@ namespace Microsoft.Windows.CsWin32; public partial class Generator { - internal string RequestCustomEnumMarshaller(string qualifiedEnumTypeName, UnmanagedType unmanagedType) - { - // Create type syntax for the unmanaged type (uint for U4) - TypeSyntax unmanagedTypeSyntax = unmanagedType switch - { - UnmanagedType.U4 => PredefinedType(Token(SyntaxKind.UIntKeyword)), - UnmanagedType.I4 => PredefinedType(Token(SyntaxKind.IntKeyword)), - UnmanagedType.U1 => PredefinedType(Token(SyntaxKind.ByteKeyword)), - _ => throw new InvalidOperationException($"Unsupported unmanaged type: {unmanagedType}"), - }; - - if (!TrySplitPossiblyQualifiedName(qualifiedEnumTypeName, out string? @namespace, out string enumTypeName) || - !this.TryStripCommonNamespace(@namespace, out string? shortNamespace)) - { - throw new InvalidOperationException($"This generator doesn't share a prefix with this enum {qualifiedEnumTypeName}"); - } - - // Custom marshallers should go in a InteropServices sub-namespace. - shortNamespace = shortNamespace.Length > 0 ? $"{shortNamespace}.InteropServices" : "InteropServices"; - - string customTypeMarshallerName = $"{enumTypeName}To{unmanagedType}Marshaller"; - - return this.volatileCode.GenerateCustomTypeMarshaller(customTypeMarshallerName, delegate - { - // Create type syntax for the enum type - TypeSyntax enumTypeSyntax = IdentifierName(enumTypeName); - - // Create the CustomMarshaller attributes for all required marshal modes - var marshalModes = new[] - { - "ManagedToUnmanagedIn", - "ManagedToUnmanagedOut", - "UnmanagedToManagedIn", - "UnmanagedToManagedOut", - "ElementIn", - "ElementOut", - }; - - // [CustomMarshaller(typeof(Enum), MarshalMode.ManagedToUnmanagedIn, typeof(EnumToUintMarshaller))] - // [CustomMarshaller(typeof(Enum), MarshalMode.ManagedToUnmanagedOut, typeof(EnumToUintMarshaller))] - // [CustomMarshaller(typeof(Enum), MarshalMode.UnmanagedToManagedIn, typeof(EnumToUintMarshaller))] - // [CustomMarshaller(typeof(Enum), MarshalMode.UnmanagedToManagedOut, typeof(EnumToUintMarshaller))] - // [CustomMarshaller(typeof(Enum), MarshalMode.ElementIn, typeof(EnumToUintMarshaller))] - // [CustomMarshaller(typeof(Enum), MarshalMode.ElementOut, typeof(EnumToUintMarshaller))] - var customMarshallerAttributes = marshalModes.Select(mode => - Attribute(ParseName("global::System.Runtime.InteropServices.Marshalling.CustomMarshaller")) - .AddArgumentListArguments( - AttributeArgument(TypeOfExpression(enumTypeSyntax)), - AttributeArgument(MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - ParseName("global::System.Runtime.InteropServices.Marshalling.MarshalMode"), - IdentifierName(mode))), - AttributeArgument(TypeOfExpression(IdentifierName(customTypeMarshallerName))))) - .ToArray(); - - // Create ConvertToManaged method - // public static unsafe Enum ConvertToManaged(uint unmanaged) - // { - // return (Enum)unmanaged; - // } - MethodDeclarationSyntax convertToManagedMethod = MethodDeclaration(enumTypeSyntax, Identifier("ConvertToManaged")) - .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.UnsafeKeyword)) - .AddParameterListParameters(Parameter(unmanagedTypeSyntax.WithTrailingTrivia(Space), Identifier("unmanaged"))) - .WithBody(Block(ReturnStatement(UncheckedExpression(CastExpression(enumTypeSyntax, IdentifierName("unmanaged")))))); - - // Create ConvertToUnmanaged method - // public static uint ConvertToUnmanaged(Enum managed) - // { - // return (uint)managed; - // } - MethodDeclarationSyntax convertToUnmanagedMethod = MethodDeclaration(unmanagedTypeSyntax, Identifier("ConvertToUnmanaged")) - .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) - .AddParameterListParameters(Parameter(enumTypeSyntax.WithTrailingTrivia(Space), Identifier("managed"))) - .WithBody(Block(ReturnStatement(UncheckedExpression(CastExpression(unmanagedTypeSyntax, IdentifierName("managed")))))); - - // Create Free method - // public static void Free(uint unmanaged) - // { - // } - MethodDeclarationSyntax freeMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("Free")) - .AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword)) - .AddParameterListParameters(Parameter(unmanagedTypeSyntax.WithTrailingTrivia(Space), Identifier("unmanaged"))) - .WithBody(Block()); // Empty body - - // Create the class declaration - ClassDeclarationSyntax marshallerClass = ClassDeclaration(Identifier(customTypeMarshallerName), [convertToManagedMethod, convertToUnmanagedMethod, freeMethod]) - .AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword)) - .AddAttributeLists(customMarshallerAttributes.Select(attr => AttributeList(attr)).ToArray()); - - marshallerClass = marshallerClass.WithAdditionalAnnotations(new SyntaxAnnotation(NamespaceContainerAnnotation, shortNamespace)); - - string qualifiedName = $"global::{this.Namespace}.{shortNamespace}.{customTypeMarshallerName}"; - - CustomMarshallerTypeRecord typeRecord = new(marshallerClass, qualifiedName); - - this.volatileCode.AddCustomTypeMarshaller(customTypeMarshallerName, typeRecord); - - return typeRecord; - }); - } - internal string RequestCustomTypeDefMarshaller(string fullyQualifiedTypeName, TypeSyntax unmanagedTypeSyntax) { if (!TrySplitPossiblyQualifiedName(fullyQualifiedTypeName, out string? @namespace, out string typeDefName) || diff --git a/src/Microsoft.Windows.CsWin32/Generator.Extern.cs b/src/Microsoft.Windows.CsWin32/Generator.Extern.cs index ff3004f9..10cde2ab 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Extern.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Extern.cs @@ -220,26 +220,6 @@ private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle) CustomAttributeHandleCollection? returnTypeAttributes = this.GetReturnTypeCustomAttributes(methodDefinition); TypeSyntaxAndMarshaling returnType = signature.ReturnType.ToTypeSyntax(typeSettings with { IsReturnValue = true }, GeneratingElement.ExternMethod, returnTypeAttributes?.QualifyWith(this), ParameterAttributes.Out); - bool customMarshaling = this.options.AllowMarshaling && this.useSourceGenerators; - - // Search for any enum substitutions. - TypeSyntax? returnTypeEnumName = customMarshaling ? null : this.FindAssociatedEnum(returnTypeAttributes); - TypeSyntax?[]? parameterEnumType = null; - foreach (ParameterHandle parameterHandle in methodDefinition.GetParameters()) - { - Parameter parameter = this.Reader.GetParameter(parameterHandle); - if (parameter.SequenceNumber == 0) - { - continue; - } - - if (this.FindAssociatedEnum(parameter.GetCustomAttributes()) is IdentifierNameSyntax parameterEnumName && !customMarshaling) - { - parameterEnumType ??= new TypeSyntax?[signature.ParameterTypes.Length]; - parameterEnumType[parameter.SequenceNumber - 1] = parameterEnumName; - } - } - bool setLastError = (import.Attributes & MethodImportAttributes.SetLastError) == MethodImportAttributes.SetLastError; bool setLastErrorViaMarshaling = setLastError && (this.Options.AllowMarshaling || !this.canUseSetLastPInvokeError); bool setLastErrorManually = setLastError && !setLastErrorViaMarshaling; @@ -293,34 +273,19 @@ AttributeListSyntax CreateDllImportAttributeList() } MethodDeclarationSyntax exposedMethod; - if (returnTypeEnumName is null && parameterEnumType is null && !setLastErrorManually) + if (!setLastErrorManually) { // No need for wrapping the extern method, so just expose it directly. exposedMethod = externDeclaration.WithModifiers(externDeclaration.Modifiers.Insert(0, TokenWithSpace(this.Visibility))); } else { - string ns = this.GetMethodNamespace(methodDefinition); - NameSyntax nsSyntax = ParseName(ReplaceCommonNamespaceWithAlias(this, ns)); ParameterListSyntax exposedParameterList = this.CreateParameterList(methodDefinition, signature, typeSettings, GeneratingElement.ExternMethod); static SyntaxToken RefInOutKeyword(ParameterSyntax p) => p.Modifiers.Any(SyntaxKind.OutKeyword) ? TokenWithSpace(SyntaxKind.OutKeyword) : p.Modifiers.Any(SyntaxKind.RefKeyword) ? TokenWithSpace(SyntaxKind.RefKeyword) : default; ArgumentListSyntax argumentList = ArgumentList([.. exposedParameterList.Parameters.Select(p => Argument(IdentifierName(p.Identifier.ValueText)).WithRefKindKeyword(RefInOutKeyword(p)))]); - if (parameterEnumType is not null) - { - for (int i = 0; i < parameterEnumType.Length; i++) - { - if (parameterEnumType[i] is TypeSyntax parameterType) - { - NameSyntax qualifiedParameterType = QualifiedName(nsSyntax, (SimpleNameSyntax)parameterType); - exposedParameterList = exposedParameterList.ReplaceNode(exposedParameterList.Parameters[i], exposedParameterList.Parameters[i].WithType(qualifiedParameterType.WithTrailingTrivia(Space))); - this.RequestInteropType(ns, parameterEnumType[i]!.ToString(), this.DefaultContext); - argumentList = argumentList.ReplaceNode(argumentList.Arguments[i], argumentList.Arguments[i].WithExpression(CastExpression(externDeclaration.ParameterList.Parameters[i].Type!.WithTrailingTrivia(default(SyntaxTriviaList)), argumentList.Arguments[i].Expression))); - } - } - } // We need to specify Entrypoint because our local function will have a different name. // It must have a unique name because some functions will have the same signature as our exposed method except for the return type. @@ -328,13 +293,6 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) => IdentifierNameSyntax localExternFunctionName = IdentifierName("LocalExternFunction"); ExpressionSyntax invocation = InvocationExpression(localExternFunctionName, argumentList); - if (returnTypeEnumName is not null) - { - this.RequestInteropType(ns, returnTypeEnumName.ToString(), this.DefaultContext); - returnTypeEnumName = QualifiedName(nsSyntax, (SimpleNameSyntax)returnTypeEnumName); - invocation = CastExpression(returnTypeEnumName, invocation); - } - BlockSyntax body = Block(); IdentifierNameSyntax? retValLocalName = returnType.Type is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword } ? null : IdentifierName("__retVal"); @@ -352,7 +310,7 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) => // var __retVal = LocalExternFunction(...); body = body.AddStatements( LocalDeclarationStatement( - VariableDeclaration(returnTypeEnumName ?? returnType.Type, [VariableDeclarator(retValLocalName.Identifier, EqualsValueClause(invocation))]))); + VariableDeclaration(returnType.Type, [VariableDeclarator(retValLocalName.Identifier, EqualsValueClause(invocation))]))); } else { @@ -381,7 +339,7 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) => .WithSemicolonToken(SemicolonWithLineFeed); body = body.AddStatements(externFunction); - exposedMethod = MethodDeclaration(returnTypeEnumName ?? returnType.Type, externDeclaration.Identifier) + exposedMethod = MethodDeclaration(returnType.Type, externDeclaration.Identifier) .AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword)) .WithParameterList(exposedParameterList) .WithBody(body); diff --git a/src/Microsoft.Windows.CsWin32/Generator.MetadataHelpers.cs b/src/Microsoft.Windows.CsWin32/Generator.MetadataHelpers.cs index 49987978..4d9aa35c 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.MetadataHelpers.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.MetadataHelpers.cs @@ -18,17 +18,6 @@ public partial class Generator internal CustomAttribute? FindAttribute(CustomAttributeHandleCollection? customAttributeHandles, string attributeNamespace, string attributeName) => MetadataUtilities.FindAttribute(this.Reader, customAttributeHandles, attributeNamespace, attributeName); - internal IdentifierNameSyntax? FindAssociatedEnum(CustomAttributeHandleCollection? customAttributeHandles) - { - if (this.FindAttribute(customAttributeHandles, InteropDecorationNamespace, AssociatedEnumAttribute) is CustomAttribute att) - { - CustomAttributeValue args = att.DecodeValue(CustomAttributeTypeProvider.Instance); - return IdentifierName((string)args.FixedArguments[0].Value!); - } - - return null; - } - internal bool TryGetTypeDefHandle(TypeReferenceHandle typeRefHandle, out QualifiedTypeDefinitionHandle typeDefHandle) { if (this.SuperGenerator is object) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Struct.cs b/src/Microsoft.Windows.CsWin32/Generator.Struct.cs index 74712676..680e88d5 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Struct.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Struct.cs @@ -116,11 +116,21 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle if (fixedBufferAttribute.HasValue) { CustomAttributeValue attributeArgs = fixedBufferAttribute.Value.DecodeValue(CustomAttributeTypeProvider.Instance); - var fieldType = (TypeSyntax)attributeArgs.FixedArguments[0].Value!; + TypeSyntax fieldType = PrimitiveTypeHandleInfo.ToTypeSyntax(GetFixedBufferElementType(fixedBufferAttribute.Value), typeSettings.PreferNativeInt); ExpressionSyntax size = LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal((int)attributeArgs.FixedArguments[1].Value!)); - field = FieldDeclaration( - [TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword), Token(SyntaxKind.FixedKeyword)], - VariableDeclaration(fieldType, [fieldDeclarator.WithArgumentList(BracketedArgumentList(Argument(size)))])); + if (this.FindAssociatedEnum(fieldDef.GetCustomAttributes()) is HandleTypeHandleInfo enumType) + { + TypeSyntax elementType = enumType.ToTypeSyntax(typeSettings, GeneratingElement.StructMember, null).Type; + var fieldInfo = this.ReinterpretFieldType(fieldDef, ArrayType(elementType, [ArrayRankSpecifier([size])]), fieldDef.GetCustomAttributes(), context); + additionalMembers = additionalMembers.AddRange(fieldInfo.AdditionalMembers); + field = FieldDeclaration([TokenWithSpace(this.Visibility)], VariableDeclaration(fieldInfo.FieldType, [fieldDeclarator])); + } + else + { + field = FieldDeclaration( + [TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword), Token(SyntaxKind.FixedKeyword)], + VariableDeclaration(fieldType, [fieldDeclarator.WithArgumentList(BracketedArgumentList(Argument(size)))])); + } } else if (fieldDefHandle == flexibleArrayFieldHandle) { @@ -186,28 +196,30 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle } } - TypeSyntaxAndMarshaling fieldTypeSyntax = fieldTypeInfo.ToTypeSyntax(thisFieldTypeSettings, GeneratingElement.StructMember, fieldAttributes.QualifyWith(this)); + HandleTypeHandleInfo? propertyEnum = fieldTypeInfo is PrimitiveTypeHandleInfo ? this.FindAssociatedEnum(fieldAttributes) : null; + TypeSyntaxAndMarshaling fieldTypeSyntax = fieldTypeInfo.ToTypeSyntax(thisFieldTypeSettings, GeneratingElement.StructMember, propertyEnum is null ? fieldAttributes.QualifyWith(this) : null); (TypeSyntax FieldType, SyntaxList AdditionalMembers, AttributeSyntax? MarshalAsAttribute) fieldInfo = this.ReinterpretFieldType(fieldDef, fieldTypeSyntax.Type, fieldAttributes, context); additionalMembers = additionalMembers.AddRange(fieldInfo.AdditionalMembers); PropertyDeclarationSyntax? property = null; - if (this.FindAssociatedEnum(fieldAttributes) is { } propertyType) + if (propertyEnum is not null) { // Keep the field with its original type, but then add a property that returns the enum type. + TypeSyntax propertyType = propertyEnum.ToTypeSyntax(thisFieldTypeSettings, GeneratingElement.StructMember, null).Type; fieldDeclarator = VariableDeclarator(SafeIdentifier($"_{fieldName}")); field = FieldDeclaration([TokenWithSpace(SyntaxKind.PrivateKeyword)], VariableDeclaration(fieldInfo.FieldType, [fieldDeclarator])); - // internal EnumType FieldName { - // get => (EnumType)this._fieldName; - // set => this._fieldName = (UnderlyingType)value; - // } - this.RequestInteropType(this.GetNamespaceForPossiblyNestedType(typeDef), propertyType.Identifier.ValueText, context); ExpressionSyntax fieldAccess = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName(fieldDeclarator.Identifier)); property = PropertyDeclaration(propertyType.WithTrailingTrivia(Space), Identifier(fieldName).WithTrailingTrivia(LineFeed)) .AddModifiers(TokenWithSpace(this.Visibility)) .WithAccessorList(AccessorList( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithExpressionBody(ArrowExpressionClause(CastExpression(propertyType, fieldAccess))).WithSemicolonToken(Semicolon), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, fieldAccess, CastExpression(fieldInfo.FieldType, IdentifierName("value"))))).WithSemicolonToken(Semicolon))); + if (this.HasObsoleteAttribute(fieldAttributes)) + { + property = property.AddAttributeLists(AttributeList(ObsoleteAttributeSyntax)); + } + additionalMembers = additionalMembers.Add(property); } else @@ -223,7 +235,6 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle if (this.HasObsoleteAttribute(fieldDef.GetCustomAttributes())) { field = field.AddAttributeLists(AttributeList(ObsoleteAttributeSyntax)); - property = property?.AddAttributeLists(AttributeList(ObsoleteAttributeSyntax)); } if (RequiresUnsafe(fieldInfo.FieldType)) @@ -814,7 +825,7 @@ private void GatherFlattenedAnonymousAccessors( } // Skip fields whose generated representation is not a plain, ref-returnable field of the decoded type. - if (this.FindAssociatedEnum(fieldAttributes) is not null) + if (fieldDef.DecodeSignature(this.SignatureHandleProvider, null) is PrimitiveTypeHandleInfo && this.FindAssociatedEnum(fieldAttributes) is not null) { // Surfaced as a by-value property rather than a field. continue; diff --git a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs index a7c5316e..fdd7e055 100644 --- a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs +++ b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs @@ -78,6 +78,8 @@ internal bool IsType(string leafName) internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs, Generator.GeneratingElement forElement, QualifiedCustomAttributeHandleCollection? customAttributes, ParameterAttributes parameterAttributes = default) { + _ = customAttributes?.Generator.FindAssociatedEnum(customAttributes.Value.Collection); + NameSyntax? nameSyntax; bool isInterface; bool isNonCOMConformingInterface; diff --git a/src/Microsoft.Windows.CsWin32/PointerTypeHandleInfo.cs b/src/Microsoft.Windows.CsWin32/PointerTypeHandleInfo.cs index b2795747..c4f32f6a 100644 --- a/src/Microsoft.Windows.CsWin32/PointerTypeHandleInfo.cs +++ b/src/Microsoft.Windows.CsWin32/PointerTypeHandleInfo.cs @@ -48,7 +48,9 @@ inputs with TypeSyntaxAndMarshaling elementTypeDetails = this.ElementType.ToTypeSyntax(inputs with { PreferInOutRef = false }, forElement, customAttributes, parameterAttributes); if (elementTypeDetails.MarshalAsAttribute is object || elementTypeDetails.MarshalUsingType is string || - (inputs.Generator?.IsManagedType(this.ElementType) is true) || (inputs.PreferInOutRef && !xOptional && this.ElementType is PrimitiveTypeHandleInfo { PrimitiveTypeCode: not PrimitiveTypeCode.Void })) + (inputs.Generator?.IsManagedType(this.ElementType) is true) || (inputs.PreferInOutRef && !xOptional + && this.ElementType is PrimitiveTypeHandleInfo { PrimitiveTypeCode: not PrimitiveTypeCode.Void } + && customAttributes?.Generator.FindAssociatedEnum(customAttributes.Value.Collection) is null)) { bool xIn = (parameterAttributes & ParameterAttributes.In) == ParameterAttributes.In; bool xOut = (parameterAttributes & ParameterAttributes.Out) == ParameterAttributes.Out; diff --git a/src/Microsoft.Windows.CsWin32/PrimitiveTypeHandleInfo.cs b/src/Microsoft.Windows.CsWin32/PrimitiveTypeHandleInfo.cs index 55f04773..48fbac22 100644 --- a/src/Microsoft.Windows.CsWin32/PrimitiveTypeHandleInfo.cs +++ b/src/Microsoft.Windows.CsWin32/PrimitiveTypeHandleInfo.cs @@ -9,26 +9,9 @@ internal record PrimitiveTypeHandleInfo(PrimitiveTypeCode PrimitiveTypeCode) : T internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs, Generator.GeneratingElement forElement, QualifiedCustomAttributeHandleCollection? customAttributes, ParameterAttributes parameterAttributes) { - // We want to expose the enum type when there is one, but doing it *properly* requires marshaling to the underlying type. - // The length of the enum may differ from the length of the primitive type, which means we can't just use the enum type directly. - // Even if the lengths match, if the enum's underlying base type conflicts with the primitive's signed type (e.g. int != uint), - // certain CPU architectures can fail as well. - // So we just have to use the primitive type when marshaling is not allowed. - if (inputs.AllowMarshaling && customAttributes?.Generator.FindAssociatedEnum(customAttributes.Value.Collection) is NameSyntax enumTypeName && inputs.Generator!.TryGenerateType(enumTypeName.ToString(), out IReadOnlyCollection preciseMatch)) + if (customAttributes?.Generator.FindAssociatedEnum(customAttributes.Value.Collection) is HandleTypeHandleInfo enumType) { - // Use the qualified name. - enumTypeName = ParseName(Generator.ReplaceCommonNamespaceWithAlias(inputs.Generator, preciseMatch.First())); - UnmanagedType unmanagedType = GetUnmanagedType(this.PrimitiveTypeCode); - - // If marshaling using source generators, we need to generate a custom marshaler and a MarshalUsing(...) attribute. - if (inputs.AllowMarshaling && inputs.Generator?.UseSourceGenerators == true) - { - string marshalerTypeName = inputs.Generator!.RequestCustomEnumMarshaller(preciseMatch.First(), unmanagedType); - return new TypeSyntaxAndMarshaling(enumTypeName) { MarshalUsingType = marshalerTypeName }; - } - - MarshalAsAttribute marshalAs = new(unmanagedType); - return new TypeSyntaxAndMarshaling(enumTypeName, marshalAs, nativeArrayInfo: null); + return enumType.ToTypeSyntax(inputs, forElement, null); } else { @@ -71,20 +54,4 @@ internal static TypeSyntax ToTypeSyntax(PrimitiveTypeCode typeCode, bool preferN _ => throw new NotSupportedException("Unsupported type code: " + typeCode), }; } - - private static UnmanagedType GetUnmanagedType(PrimitiveTypeCode typeCode) - { - return typeCode switch - { - PrimitiveTypeCode.SByte => UnmanagedType.I1, - PrimitiveTypeCode.Byte => UnmanagedType.U1, - PrimitiveTypeCode.Int16 => UnmanagedType.I2, - PrimitiveTypeCode.UInt16 => UnmanagedType.U2, - PrimitiveTypeCode.Int32 => UnmanagedType.I4, - PrimitiveTypeCode.UInt32 => UnmanagedType.U4, - PrimitiveTypeCode.Int64 => UnmanagedType.I8, - PrimitiveTypeCode.UInt64 => UnmanagedType.U8, - _ => throw new NotSupportedException("Unsupported type code: " + typeCode), - }; - } } diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 4a1a6267..cb7c10db 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -178,6 +178,17 @@ public partial class SourceGenerator : ISourceGenerator isEnabledByDefault: true); #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + /// + /// Warns that an enum association cannot preserve the native representation. + /// + public static readonly DiagnosticDescriptor IncompatibleAssociatedEnum = new( + "PInvoke015", + "Incompatible AssociatedEnum annotation", + "AssociatedEnum \"{0}\" on \"{1}\" is ignored because {2}. The native type is preserved.", + "Functionality", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + private const string InputProjectionErrorId = "PInvoke010"; private const string InputProjectionErrorTitle = "Input projection error"; @@ -438,6 +449,11 @@ public void Execute(GeneratorExecutionContext context) context.AddSource(unit.Key, unit.Value.GetText(Encoding.UTF8)); } + foreach (Diagnostic diagnostic in superGenerator.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + string ConcatSuggestions(IReadOnlyCollection suggestions) { var suggestionBuilder = new StringBuilder(); diff --git a/src/Microsoft.Windows.CsWin32/SuperGenerator.cs b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs index bb48386b..235c488f 100644 --- a/src/Microsoft.Windows.CsWin32/SuperGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SuperGenerator.cs @@ -18,6 +18,11 @@ private SuperGenerator(ImmutableDictionary generators) /// public ImmutableDictionary Generators { get; } + /// + /// Gets diagnostics produced by the constituent generators while projecting metadata. + /// + public IEnumerable Diagnostics => this.Generators.Values.SelectMany(generator => generator.Diagnostics); + /// /// Initializes a new instance of the class. /// diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index 63ec3d8d..ccd1030e 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -720,12 +720,12 @@ public async Task NativeTypedefMarshallerNamespace( } /// - /// Verifies associated enum marshaller namespaces through build-task generation and compilation. + /// Verifies native fallback for inconsistent root and nested enum annotations through the build task. /// /// Whether the enum is in a child of the common namespace. /// The target framework of the generated projection. [Theory, CombinatorialData] - public async Task AssociatedEnumMarshallerNamespace( + public async Task IncompatibleAssociatedEnumUsesNativeStorage( bool nestedNamespace, [CombinatorialValues("net8.0", "net9.0", "net10.0")] string tfm) { @@ -735,18 +735,49 @@ public async Task AssociatedEnumMarshallerNamespace( this.win32winmdPaths = [FlatNamespaceMetadataPath]; string typeName = nestedNamespace ? "NESTED_ENUM" : "TEST_ENUM"; string typeNamespace = nestedNamespace ? "FlatNamespace.Nested" : "FlatNamespace"; - string marshallerName = $"{typeNamespace}.InteropServices.{typeName}ToU4Marshaller"; string apiName = nestedNamespace ? "UseNestedEnum" : "UseRootEnum"; this.nativeMethods.Add(apiName); - await this.InvokeGeneratorAndCompile($"{nameof(this.AssociatedEnumMarshallerNamespace)}_{nestedNamespace}_{tfm}"); + await this.InvokeGeneratorAndCompile($"{nameof(this.IncompatibleAssociatedEnumUsesNativeStorage)}_{nestedNamespace}_{tfm}"); MethodDeclarationSyntax method = Assert.Single(this.FindGeneratedMethod(apiName), m => FindAttribute(m.AttributeLists, "LibraryImport").Any()); ParameterSyntax parameter = Assert.Single(method.ParameterList.Parameters); - AttributeSyntax attribute = Assert.Single(FindAttribute(parameter.AttributeLists, "global::System.Runtime.InteropServices.Marshalling.MarshalUsing")); - TypeOfExpressionSyntax marshallerType = Assert.IsType(Assert.Single(attribute.ArgumentList!.Arguments).Expression); - Assert.Equal($"global::{marshallerName}", marshallerType.Type.ToString()); - Assert.NotNull(this.compilation.GetTypeByMetadataName(marshallerName)); + Assert.Equal(SyntaxKind.UIntKeyword, Assert.IsType(parameter.Type).Keyword.Kind()); + Assert.Empty(FindAttribute(parameter.AttributeLists, "global::System.Runtime.InteropServices.Marshalling.MarshalUsing")); + Assert.Null(this.compilation.GetTypeByMetadataName($"{typeNamespace}.{typeName}")); + Assert.Contains("CsWin32 : warning PInvoke015:", this.Logger.Output); + } + + /// + /// Compiles direct enum parameters and buffers through the platform LibraryImport generator. + /// + /// The matching native and enum storage type. + /// The target framework. + [Theory, CombinatorialData] + public async Task AssociatedEnumLibraryImport( + [CombinatorialValues("sbyte", "byte", "short", "ushort", "int", "uint", "long", "ulong")] string storage, + [CombinatorialValues("net8.0", "net10.0")] string tfm) + { + this.compilation = this.starterCompilations[tfm]; + this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm(tfm)!.Value); + this.tfm = tfm; + string path = Microsoft.Windows.CsWin32.Tests.AssociatedEnumTests.EmitMetadata( + Microsoft.Windows.CsWin32.Tests.AssociatedEnumTests.GetMetadataSource(storage), this.compilation, this.parseOptions); + try + { + this.win32winmdPaths = [path]; + this.nativeMethods.AddRange(["RoundTrip", "Buffer", "Output", "DoublePointer", "PointerReturn"]); + await this.InvokeGeneratorAndCompile($"{nameof(this.AssociatedEnumLibraryImport)}_{storage}_{tfm}"); + INamedTypeSymbol mode = this.compilation.GetTypeByMetadataName("Windows.Win32.Test.MODE")!; + Assert.Equal(storage, mode.EnumUnderlyingType!.ToDisplayString()); + Assert.DoesNotContain("PInvoke015", this.Logger.Output); + MethodDeclarationSyntax method = Assert.Single(this.FindGeneratedMethod("RoundTrip"), m => FindAttribute(m.AttributeLists, "LibraryImport").Any()); + Assert.Empty(FindAttribute(Assert.Single(method.ParameterList.Parameters).AttributeLists, "global::System.Runtime.InteropServices.Marshalling.MarshalUsing")); + } + finally + { + File.Delete(path); + } } [Fact] diff --git a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs index d031a266..186b97ae 100644 --- a/test/GenerationSandbox.BuildTask.Tests/COMTests.cs +++ b/test/GenerationSandbox.BuildTask.Tests/COMTests.cs @@ -248,7 +248,7 @@ public void IWbemServices_GetObject_Works() dwAuthnLevel: RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_CALL, dwImpLevel: RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, pAuthInfo: null, - dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + dwCapabilities: (uint)EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); } var className = new SysFreeStringSafeHandle(Marshal.StringToBSTR("MSFT_MpScan"), true); @@ -354,7 +354,7 @@ public void IShellItem_BindToHandler_IStream_ReadWorks() Assert.Equal(0UL, newPosition); STATSTG stat; - stream.Stat(&stat, STATFLAG.STATFLAG_NONAME); + stream.Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME); Assert.True(stat.cbSize > 0, "Expected Stat to report a non-empty file size."); // Read again after Seek to confirm the seek took effect on the same vtable slot. diff --git a/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt b/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt index 2734c657..849ae204 100644 --- a/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.BuildTask.Tests/NativeMethods.txt @@ -17,6 +17,7 @@ DebugPropertyInfo DefWindowProc DestroyWindow DeviceIoControl +EOLE_AUTHENTICATION_CAPABILITIES FILE_FLAGS_AND_ATTRIBUTES GetFileVersionInfoEx GetFileVersionInfoSizeEx diff --git a/test/GenerationSandbox.Tests/BasicTests.cs b/test/GenerationSandbox.Tests/BasicTests.cs index cbf827e9..f41f8265 100644 --- a/test/GenerationSandbox.Tests/BasicTests.cs +++ b/test/GenerationSandbox.Tests/BasicTests.cs @@ -649,11 +649,11 @@ public void FlattenedBitfieldPropertyWritesAreIsolatedFromNeighbors() } [Fact] - public void FieldWithAssociatedEnum() + public void FieldWithIncompatibleAssociatedEnumRetainsNativeStorage() { SHDESCRIPTIONID s = default; - s.dwDescriptionId = SHDID_ID.SHDID_FS_FILE; - Assert.Equal(SHDID_ID.SHDID_FS_FILE, s.dwDescriptionId); + s.dwDescriptionId = 1; + Assert.Equal(1U, s.dwDescriptionId); } // Test for https://github.com/microsoft/CsWin32/issues/1421 diff --git a/test/GenerationSandbox.Tests/ComRuntimeTests.cs b/test/GenerationSandbox.Tests/ComRuntimeTests.cs index 7adefd6c..3e5e39f9 100644 --- a/test/GenerationSandbox.Tests/ComRuntimeTests.cs +++ b/test/GenerationSandbox.Tests/ComRuntimeTests.cs @@ -150,7 +150,7 @@ public void IWbemServices_GetObject_Works() dwAuthnLevel: RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_CALL, dwImpLevel: RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, pAuthInfo: null, - dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + dwCapabilities: (uint)EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); } var className = new SysFreeStringSafeHandle(Marshal.StringToBSTR("MSFT_MpScan"), true); @@ -266,7 +266,7 @@ public void IShellItem_BindToHandler_IStream_ReadWorks() Assert.Equal(0UL, newPosition); Windows.Win32.System.Com.STATSTG stat; - stream.Stat(&stat, STATFLAG.STATFLAG_NONAME); + stream.Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME); Assert.True(stat.cbSize > 0, "Expected Stat to report a non-empty file size."); stream.Read(buffer, out uint bytesReadAfterSeek); diff --git a/test/GenerationSandbox.Tests/GeneratedForm.cs b/test/GenerationSandbox.Tests/GeneratedForm.cs index 5c560fe1..54ac180e 100644 --- a/test/GenerationSandbox.Tests/GeneratedForm.cs +++ b/test/GenerationSandbox.Tests/GeneratedForm.cs @@ -118,7 +118,7 @@ public void CopyTo(IStream pstm, ulong cb, ulong* pcbRead, ulong* pcbWritten) throw new NotImplementedException(); } - public void Commit([MarshalAs(UnmanagedType.U4)] STGC grfCommitFlags) + public void Commit(uint grfCommitFlags) { throw new NotImplementedException(); } @@ -128,7 +128,7 @@ public void Revert() throw new NotImplementedException(); } - public void LockRegion(ulong libOffset, ulong cb, [MarshalAs(UnmanagedType.U4)] LOCKTYPE dwLockType) + public void LockRegion(ulong libOffset, ulong cb, uint dwLockType) { throw new NotImplementedException(); } @@ -138,7 +138,7 @@ public void UnlockRegion(ulong libOffset, ulong cb, uint dwLockType) throw new NotImplementedException(); } - public void Stat(Windows.Win32.System.Com.STATSTG* pstatstg, [MarshalAs(UnmanagedType.U4)] STATFLAG grfStatFlag) + public void Stat(Windows.Win32.System.Com.STATSTG* pstatstg, uint grfStatFlag) { throw new NotImplementedException(); } diff --git a/test/GenerationSandbox.Tests/NativeMethods.txt b/test/GenerationSandbox.Tests/NativeMethods.txt index 84a6cba0..1290a3e8 100644 --- a/test/GenerationSandbox.Tests/NativeMethods.txt +++ b/test/GenerationSandbox.Tests/NativeMethods.txt @@ -15,6 +15,7 @@ DefWindowProc DestroyWindow DISPLAYCONFIG_VIDEO_SIGNAL_INFO EnumWindows +EOLE_AUTHENTICATION_CAPABILITIES FILE_ACCESS_RIGHTS FLICK_DATA GetForegroundWindow @@ -81,6 +82,7 @@ ShellWindowFindWindowOptions ShellWindows ShellWindowTypeConstants SHFILEOPSTRUCTW +STATFLAG SIZE SYSTEM_INFO TrySubmitThreadpoolCallback diff --git a/test/GenerationSandbox.props b/test/GenerationSandbox.props index 7605c447..41da3e8a 100644 --- a/test/GenerationSandbox.props +++ b/test/GenerationSandbox.props @@ -13,6 +13,9 @@ x64 true + + $(NoWarn);PInvoke015 + $(MSBuildWarningsAsMessages);PInvoke015 diff --git a/test/Microsoft.Windows.CsWin32.Tests/AssociatedEnumTests.cs b/test/Microsoft.Windows.CsWin32.Tests/AssociatedEnumTests.cs new file mode 100644 index 00000000..bc032b6a --- /dev/null +++ b/test/Microsoft.Windows.CsWin32.Tests/AssociatedEnumTests.cs @@ -0,0 +1,574 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Windows.CsWin32.Tests; + +/// +/// Verifies ABI-compatible enum associations and warnings for incompatible metadata. +/// +public class AssociatedEnumTests : GeneratorTestBase +{ + /// + /// Metadata shared by the core and command-line projection tests. + /// + public const string MetadataSource = """ + using System; + using System.Runtime.InteropServices; + using Windows.Win32.Foundation.Metadata; + using MODE_STORAGE = System.UInt32; + + namespace Windows.Win32.Foundation.Metadata + { + public sealed class AssociatedEnumAttribute : Attribute + { + public AssociatedEnumAttribute(string name) { } + } + public sealed class NativeArrayInfoAttribute : Attribute + { + public short CountParamIndex; + public int CountConst; + } + } + namespace Windows.Win32.Test + { + [Flags] + public enum MODE : uint { First = 1, Second = 2, High = 0x80000000 } + public unsafe struct RECORD + { + [AssociatedEnum("MODE")] public MODE_STORAGE Mode; + [AssociatedEnum("Windows.Win32.Test.MODE")] public MODE_STORAGE* Modes; + } + public unsafe struct FIXED_RECORD + { + [AssociatedEnum("MODE")] public fixed uint Modes[2]; + } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: AssociatedEnum("MODE")] + public unsafe delegate MODE_STORAGE CALLBACK([AssociatedEnum("MODE")] MODE_STORAGE mode, [AssociatedEnum("MODE")] MODE_STORAGE* data); + public static unsafe class Apis + { + [DllImport("fixture.dll", ExactSpelling = true)] + [return: AssociatedEnum("MODE")] + public static extern MODE_STORAGE RoundTrip([AssociatedEnum("MODE")] MODE_STORAGE mode); + [DllImport("fixture.dll", ExactSpelling = true)] + [return: AssociatedEnum("Windows.Win32.Test.MODE")] + public static extern MODE_STORAGE* PointerReturn(); + [DllImport("fixture.dll", ExactSpelling = true)] + public static extern void Buffer([In, AssociatedEnum("MODE"), NativeArrayInfo(CountParamIndex = 1)] MODE_STORAGE* modes, uint count); + [DllImport("fixture.dll", ExactSpelling = true)] + public static extern void Output([Out, AssociatedEnum("MODE")] MODE_STORAGE* mode); + [DllImport("fixture.dll", ExactSpelling = true)] + public static extern void DoublePointer([Out, AssociatedEnum("MODE")] MODE_STORAGE** modes); + [DllImport("fixture.dll", ExactSpelling = true)] + public static extern void UseCallback(CALLBACK callback); + [DllImport("fixture.dll", ExactSpelling = true)] + public static extern void Unannotated(uint mode); + } + } + """; + + private const string Caller = """ + using System; + using Windows.Win32; + using Windows.Win32.Test; + static unsafe class Caller + { + static void Use(MODE* modes, MODE** indirect, RECORD record) + { + MODE flags = MODE.First | MODE.Second | MODE.High; + MODE result = PInvoke.RoundTrip(flags); + record.Mode = flags; + MODE* fields = record.Modes; + PInvoke.Buffer(modes, 2); + PInvoke.Buffer(new ReadOnlySpan(modes, 2)); + PInvoke.Output(modes); + PInvoke.Output(out MODE output); + PInvoke.DoublePointer(indirect); + MODE* returned = PInvoke.PointerReturn(); + PInvoke.Unannotated(42); + } + } + namespace Windows.Win32.Test + { + interface IModeProperty { MODE Mode { get; set; } } + public partial struct RECORD : IModeProperty { } + } + """; + + /// + /// Initializes a new instance of the class. + /// + /// The test output. + public AssociatedEnumTests(ITestOutputHelper logger) + : base(logger) + { + } + + /// + /// Creates fixture declarations with matching native and enum storage. + /// + /// The integral storage type. + /// The metadata declarations. + public static string GetMetadataSource(string storage) + { + string systemType = storage switch + { + "sbyte" => "SByte", "byte" => "Byte", "short" => "Int16", "ushort" => "UInt16", + "int" => "Int32", "uint" => "UInt32", "long" => "Int64", "ulong" => "UInt64", + _ => throw new ArgumentException("Unsupported storage.", nameof(storage)), + }; + return MetadataSource.Replace("using MODE_STORAGE = System.UInt32;", $"using MODE_STORAGE = System.{systemType};", StringComparison.Ordinal) + .Replace("enum MODE : uint { First = 1, Second = 2, High = 0x80000000 }", $"enum MODE : {storage} {{ First = 1, Second = 2, High = 4 }}", StringComparison.Ordinal) + .Replace("fixed uint Modes", $"fixed {storage} Modes", StringComparison.Ordinal); + } + + /// + /// Emits a metadata fixture shared with the command-line generator tests. + /// + /// The fixture declarations. + /// The reference compilation. + /// The language options. + /// The metadata assembly identity. + /// The temporary metadata path, owned by the caller. + public static string EmitMetadata(string source, CSharpCompilation references, CSharpParseOptions parseOptions, string assemblyName = "Windows.Win32.winmd") + { + CSharpCompilation metadata = CSharpCompilation.Create( + assemblyName, + [CSharpSyntaxTree.ParseText(source, parseOptions)], + references.References, + references.Options.WithDeterministic(true).WithPlatform(Platform.AnyCpu)); + string path = Path.Combine(Path.GetTempPath(), $"CsWin32-AssociatedEnum-{Guid.NewGuid():N}.winmd"); + using FileStream stream = File.Create(path); + var result = metadata.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + return path; + } + + /// + /// Compiles scalar, pointer, span, output and callback callers in both marshaling modes. + /// + /// Whether runtime marshaling is enabled. + /// Whether native error preservation needs a wrapper. + [Theory, CombinatorialData] + public void MatchingStorageProjectsEnums(bool allowMarshaling, bool setLastError) + { + string source = setLastError ? MetadataSource.Replace("ExactSpelling = true", "ExactSpelling = true, SetLastError = true", StringComparison.Ordinal) : MetadataSource; + this.GenerateSource(source, allowMarshaling, Platform.X64, "RECORD", "RoundTrip", "PointerReturn", "Buffer", "Output", "DoublePointer", "UseCallback", "Unannotated"); + this.compilation = this.AddCode(Caller); + string callback = allowMarshaling + ? "static void Register() => PInvoke.UseCallback(Callback); static MODE Callback(MODE mode, MODE* data) => mode;" + : "static void Register() => PInvoke.UseCallback(&Callback); [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] static MODE Callback(MODE mode, MODE* data) => mode;"; + this.compilation = this.AddCode("using System.Runtime.InteropServices; using Windows.Win32; using Windows.Win32.Test; static unsafe class CallbackCaller { " + callback + " }"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Empty(((Generator)this.generator!).Diagnostics); + INamedTypeSymbol record = this.compilation.GetTypeByMetadataName("Windows.Win32.Test.RECORD")!; + Assert.IsAssignableFrom(Assert.Single(record.GetMembers("Mode"))); + Assert.Equal(SpecialType.System_UInt32, Assert.Single(record.GetMembers("_Mode").OfType()).Type.SpecialType); + Assert.Equal(SpecialType.System_UInt32, this.compilation.GetTypeByMetadataName("Windows.Win32.Test.MODE")!.EnumUnderlyingType!.SpecialType); + } + + /// + /// Preserves an annotation on a signature already typed as the same enum. + /// + [Fact] + public void NativeEnumSignatureIsUnchanged() + { + string source = MetadataSource.Replace("MODE_STORAGE RoundTrip([AssociatedEnum(\"MODE\")] MODE_STORAGE mode)", "MODE RoundTrip([AssociatedEnum(\"MODE\")] MODE mode)", StringComparison.Ordinal); + this.GenerateSource(source, false, Platform.X64, "RoundTrip"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Empty(((Generator)this.generator!).Diagnostics); + IMethodSymbol method = Assert.Single(this.compilation.GetTypeByMetadataName("Windows.Win32.PInvoke")!.GetMembers("RoundTrip").OfType()); + Assert.Equal(TypeKind.Enum, method.ReturnType.TypeKind); + Assert.True(SymbolEqualityComparer.Default.Equals(method.ReturnType, method.Parameters[0].Type)); + } + + /// + /// Preserves all eight enum storage types, independent of CPU, marshaling and request order. + /// + /// The matching native and enum storage type. + /// Whether runtime marshaling is enabled. + /// Whether the enum is requested before its consumers. + /// The target CPU. + [Theory, CombinatorialData] + public void ExactIntegralStorage( + [CombinatorialValues("sbyte", "byte", "short", "ushort", "int", "uint", "long", "ulong")] string storage, + bool allowMarshaling, + bool enumFirst, + [CombinatorialValues(Platform.X86, Platform.X64, Platform.Arm64)] Platform platform) + { + string source = GetMetadataSource(storage); + string[] requests = ["RECORD", "RoundTrip", "PointerReturn", "Output", "DoublePointer", "Buffer", "Unannotated", "FIXED_RECORD"]; + this.GenerateSource(source, allowMarshaling, platform, enumFirst ? ["MODE", .. requests] : requests); + this.compilation = this.AddCode(Caller); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Empty(((Generator)this.generator!).Diagnostics); + IMethodSymbol method = Assert.Single(this.compilation.GetTypeByMetadataName("Windows.Win32.PInvoke")!.GetMembers("RoundTrip").OfType()); + Assert.Equal(storage, ((INamedTypeSymbol)method.Parameters[0].Type).EnumUnderlyingType!.ToDisplayString()); + Assert.True(SymbolEqualityComparer.Default.Equals(method.ReturnType, method.Parameters[0].Type)); + } + + /// + /// Warns once per annotation and preserves the complete native shape when enum storage differs. + /// + /// The incompatible enum storage. + /// Whether runtime marshaling is enabled. + /// Whether the enum is independently requested first. + [Theory, CombinatorialData] + public void IncompatibleStoragePreservesNativeTypes( + [CombinatorialValues("ushort", "int", "ulong")] string storage, + bool allowMarshaling, + bool enumFirst) + { + string source = MetadataSource.Replace("enum MODE : uint { First = 1, Second = 2, High = 0x80000000 }", $"enum MODE : {storage} {{ First = 1, Second = 2, High = 4 }}", StringComparison.Ordinal); + string[] requests = ["RECORD", "FIXED_RECORD", "RoundTrip", "PointerReturn", "Buffer", "Output", "DoublePointer", "UseCallback"]; + this.GenerateSource(source, allowMarshaling, Platform.X64, enumFirst ? ["MODE", .. requests] : requests); + this.compilation = this.AddCode(""" + using System; + using Windows.Win32; + using Windows.Win32.Test; + static unsafe class RawCaller + { + static void Use(uint* modes, uint** indirect, RECORD record) + { + uint result = PInvoke.RoundTrip(42); + ref uint field = ref record.Mode; + uint* fields = record.Modes; + PInvoke.Buffer(modes, 2); + PInvoke.Buffer(new ReadOnlySpan(modes, 2)); + PInvoke.Output(modes); + PInvoke.Output(out uint output); + PInvoke.DoublePointer(indirect); + uint* returned = PInvoke.PointerReturn(); + } + } + """); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + IReadOnlyList diagnostics = ((Generator)this.generator!).Diagnostics; + Assert.Equal(12, diagnostics.Count); + Assert.All(diagnostics, diagnostic => + { + Assert.Equal("PInvoke015", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("native storage is UInt32", diagnostic.GetMessage(), StringComparison.Ordinal); + }); + INamedTypeSymbol? mode = this.compilation.GetTypeByMetadataName("Windows.Win32.Test.MODE"); + if (enumFirst) + { + Assert.Equal(storage, mode!.EnumUnderlyingType!.ToDisplayString()); + } + else + { + Assert.Null(mode); + } + } + + /// + /// Executes fixed-buffer access and verifies that scalar fields remain native-backed properties. + /// + /// Whether runtime marshaling is enabled. + [Theory, CombinatorialData] + public void FieldLayoutAndPropertyContract(bool allowMarshaling) + { + this.GenerateSource(MetadataSource, allowMarshaling, Platform.X64, "RECORD", "FIXED_RECORD"); + this.compilation = this.AddCode(""" + using Windows.Win32.Test; + /// Exercises the projected field layout. + public static unsafe class Layout + { + /// Returns the fixed-buffer size after checking both representations. + public static int Run() + { + RECORD scalar = default; + scalar.Mode = MODE.High | MODE.Second; + if (*(uint*)&scalar != (uint)(MODE.High | MODE.Second)) throw new System.Exception(); + FIXED_RECORD buffer = default; + buffer.Modes.AsSpan()[0] = MODE.High | MODE.First; + if (*(uint*)&buffer != (uint)(MODE.High | MODE.First)) throw new System.Exception(); + return sizeof(FIXED_RECORD); + } + } + """); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + using MemoryStream stream = new(); + var result = this.compilation.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + Assert.Equal(8, Assembly.Load(stream.ToArray()).GetType("Layout")!.GetMethod("Run")!.Invoke(null, null)); + } + + /// + /// Rejects invalid enum targets rather than generating unresolved or ambiguous C# types. + /// + /// The invalid association name. + /// The expected error detail. + [Theory] + [InlineData("Missing", "was not found")] + [InlineData("RECORD", "does not refer to an enum")] + [InlineData("", "nonempty enum name")] + public void InvalidAssociationsFailExplicitly(string name, string message) + { + string source = MetadataSource.Replace("AssociatedEnum(\"MODE\")", $"AssociatedEnum(\"{name}\")", StringComparison.Ordinal); + GenerationFailedException exception = Assert.Throws(() => this.GenerateSource(source, false, Platform.X64, "RoundTrip")); + Assert.Contains(message, exception.ToString(), StringComparison.Ordinal); + } + + /// + /// Warns for nonintegral native values without changing their representation. + /// + [Fact] + public void NonintegralNativeValueUsesNativeStorage() + { + string source = MetadataSource.Replace("public static extern MODE_STORAGE RoundTrip", "public static extern float RoundTrip", StringComparison.Ordinal); + this.GenerateSource(source, false, Platform.X64, "RoundTrip"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Diagnostic warning = Assert.Single(((Generator)this.generator!).Diagnostics); + Assert.Contains("native storage is Single", warning.GetMessage(), StringComparison.Ordinal); + IMethodSymbol method = Assert.Single(this.compilation.GetTypeByMetadataName("Windows.Win32.PInvoke")!.GetMembers("RoundTrip").OfType()); + Assert.Equal(SpecialType.System_Single, method.ReturnType.SpecialType); + } + + /// + /// Keeps bitfield backing storage primitive instead of applying an enum association to its arithmetic. + /// + [Fact] + public void BitfieldAssociationUsesNativeStorage() + { + string source = MetadataSource.Replace( + "public sealed class NativeArrayInfoAttribute", + "public sealed class NativeBitfieldAttribute : Attribute { public NativeBitfieldAttribute(string name, long offset, long length) { } } public sealed class NativeArrayInfoAttribute", + StringComparison.Ordinal) + .Replace("[AssociatedEnum(\"MODE\")] public MODE_STORAGE Mode;", "[AssociatedEnum(\"MODE\"), NativeBitfield(\"FirstBit\", 0, 1)] public MODE_STORAGE Mode;", StringComparison.Ordinal); + this.GenerateSource(source, false, Platform.X64, "RECORD"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Contains("NativeBitfield", Assert.Single(((Generator)this.generator!).Diagnostics).GetMessage(), StringComparison.Ordinal); + INamedTypeSymbol record = this.compilation.GetTypeByMetadataName("Windows.Win32.Test.RECORD")!; + Assert.Equal(SpecialType.System_UInt32, Assert.Single(record.GetMembers("Mode").OfType()).Type.SpecialType); + Assert.Single(record.GetMembers("FirstBit").OfType()); + } + + /// + /// Executes direct enum value, return and pointer parameters through a native COM vtable. + /// + [Fact] + public void UnmanagedComCallPreservesEnumBits() + { + const string InterfaceSource = """ + namespace Windows.Win32.Foundation + { + public struct HRESULT { public int Value; } + } + namespace Windows.Win32.Test + { + [ComImport, Guid("E783A761-BE81-4FD7-83F7-80DB453FB7EA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public unsafe interface IScalar + { + [PreserveSig] + [return: AssociatedEnum("MODE")] + uint Convert([AssociatedEnum("MODE")] uint mode, [AssociatedEnum("MODE")] uint* output); + } + } + """; + this.GenerateSource(MetadataSource + InterfaceSource, false, Platform.X64, "IScalar"); + this.compilation = this.AddCode(""" + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + using Windows.Win32.Test; + /// Executes an enum-typed native call. + public static unsafe class NativeCaller + { + /// Returns the native return value and output bits. + public static ulong Run() + { + void** vtable = stackalloc void*[4]; + for (int i = 0; i < 4; i++) vtable[i] = (void*)(delegate* unmanaged[Stdcall])&Convert; + IScalar instance = default; + *(void***)&instance = vtable; + MODE output; + MODE result = instance.Convert(MODE.High, &output); + return ((ulong)(uint)result << 32) | (uint)output; + } + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + private static uint Convert(IScalar* instance, uint mode, uint* output) + { + *output = mode ^ 1; + return mode ^ 2; + } + } + """); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Empty(((Generator)this.generator!).Diagnostics); + using MemoryStream stream = new(); + var result = this.compilation.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + Assert.Equal(0x8000000280000001UL, Assembly.Load(stream.ToArray()).GetType("NativeCaller")!.GetMethod("Run")!.Invoke(null, null)); + } + + /// + /// Prefers the declaring namespace and diagnoses ambiguous unqualified names. + /// + [Fact] + public void NamespaceResolutionAndAmbiguity() + { + const string OtherEnums = "namespace Windows.Win32.Other { public enum MODE : uint { Other = 1 } } namespace Windows.Win32.Another { public enum MODE : uint { Another = 1 } }"; + this.GenerateSource(MetadataSource + OtherEnums, false, Platform.X64, "RoundTrip"); + Assert.Equal("Windows.Win32.Test.MODE", Assert.Single(this.compilation.GetTypeByMetadataName("Windows.Win32.PInvoke")!.GetMembers("RoundTrip").OfType()).ReturnType.ToDisplayString()); + this.generator!.Dispose(); + this.generator = null; + string source = MetadataSource.Replace("enum MODE : uint", "enum LOCAL_MODE : uint", StringComparison.Ordinal) + OtherEnums; + GenerationFailedException exception = Assert.Throws(() => this.GenerateSource(source, false, Platform.X64, "RoundTrip")); + Assert.Contains("ambiguous", exception.ToString(), StringComparison.Ordinal); + } + + /// + /// Resolves qualified names through actual assembly references, not arbitrary loaded metadata. + /// + /// Whether the native metadata references the enum's assembly. + [Theory, CombinatorialData] + public void QualifiedEnumReferenceClosure(bool referenced) + { + this.compilation = this.starterCompilations["net8.0-x64"]; + string external = EmitMetadata("namespace Windows.Win32.External { public enum EXTERNAL_MODE : uint { First = 1 } }", this.compilation, this.parseOptions, "Windows.Win32.External.winmd"); + string source = MetadataSource.Replace("AssociatedEnum(\"MODE\")", "AssociatedEnum(\"Windows.Win32.External.EXTERNAL_MODE\")", StringComparison.Ordinal); + CSharpCompilation references = this.compilation; + if (referenced) + { + source += "namespace Windows.Win32.Test { public struct ANCHOR { public const Windows.Win32.External.EXTERNAL_MODE Value = Windows.Win32.External.EXTERNAL_MODE.First; } }"; + references = references.AddReferences(MetadataReference.CreateFromFile(external)); + } + + string native = EmitMetadata(source, references, this.parseOptions); + try + { + GeneratorOptions options = DefaultTestGeneratorOptions with { Public = true, AllowMarshaling = false }; + this.generator = SuperGenerator.Combine( + new Generator(native, null, [], options, this.compilation, this.parseOptions), + new Generator(external, null, [], options, this.compilation, this.parseOptions)); + if (referenced) + { + Assert.True(this.generator.TryGenerate("RoundTrip", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(this.generator); + this.compilation = this.AddCode("static class ExternalCaller { static void Use() => Windows.Win32.PInvoke.RoundTrip(Windows.Win32.External.EXTERNAL_MODE.First); }"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Empty(((SuperGenerator)this.generator).Diagnostics); + } + else + { + GenerationFailedException exception = Assert.Throws(() => this.generator.TryGenerate("RoundTrip", TestContext.Current.CancellationToken)); + Assert.Contains("metadata reference closure", exception.ToString(), StringComparison.Ordinal); + } + } + finally + { + this.generator?.Dispose(); + this.generator = null; + File.Delete(native); + File.Delete(external); + } + } + + /// + /// Warns and keeps native storage if an already available C# enum has a different underlying type. + /// + [Fact] + public void ReferencedCSharpEnumMustMatchStorage() + { + this.compilation = this.starterCompilations["net8.0-x64"]; + this.compilation = this.AddCode("namespace Windows.Win32.Test { internal enum MODE : ushort { First = 1 } }"); + string path = EmitMetadata(MetadataSource, this.compilation, this.parseOptions); + try + { + Generator generator = new(path, null, [], DefaultTestGeneratorOptions with { Public = true }, this.compilation, this.parseOptions); + this.generator = generator; + Assert.True(generator.TryGenerate("RoundTrip", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(generator); + this.compilation = this.AddCode("static class RawCaller { static uint Use() => Windows.Win32.PInvoke.RoundTrip(42); }"); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Assert.Equal(2, generator.Diagnostics.Count); + Assert.All(generator.Diagnostics, diagnostic => Assert.Contains("available C# type", diagnostic.GetMessage(), StringComparison.Ordinal)); + } + finally + { + File.Delete(path); + } + } + + /// + /// Preserves fixed and flexible array elements and rejects a signedness mismatch without changing layout. + /// + [Fact] + public void MetadataArrayElements() + { + this.compilation = this.starterCompilations["net8.0-x64"]; + string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ExternalMetadata", "AssociatedEnumArrays.winmd"); + Generator generator = new(path, null, [], DefaultTestGeneratorOptions with { Public = true, AllowMarshaling = false }, this.compilation, this.parseOptions); + this.generator = generator; + Assert.True(generator.TryGenerate("ARRAY_RECORD", TestContext.Current.CancellationToken)); + Assert.True(generator.TryGenerate("ARRAY_FLEX", TestContext.Current.CancellationToken)); + Assert.True(generator.TryGenerate("SIGNED_ARRAY_RECORD", TestContext.Current.CancellationToken)); + this.CollectGeneratedCode(generator); + this.compilation = this.AddCode(""" + using Windows.Win32.Test; + static unsafe class ArrayCaller + { + static void Use(ARRAY_RECORD record, ARRAY_FLEX flexible, SIGNED_ARRAY_RECORD signed) + { + record.Modes.AsSpan()[0] = ARRAY_MODE.First; + flexible.Modes.AsSpan(1)[0] = ARRAY_MODE.First; + signed.Modes.AsSpan()[0] = -1; + } + } + """); + this.AssertNoDiagnostics(logAllGeneratedCode: false); + Diagnostic warning = Assert.Single(generator.Diagnostics); + Assert.Contains("native storage is Int32", warning.GetMessage(), StringComparison.Ordinal); + Assert.Contains("underlying type is UInt32", warning.GetMessage(), StringComparison.Ordinal); + Assert.Null(this.compilation.GetTypeByMetadataName("Windows.Win32.Test.SIGNED_MODE")); + } + + /// + /// Reports the storage warning through the Roslyn source-generator host without failing generation. + /// + [Fact] + public async Task SourceGeneratorReportsStorageMismatch() + { + string source = MetadataSource.Replace("enum MODE : uint { First = 1, Second = 2, High = 0x80000000 }", "enum MODE : ushort { First = 1 }", StringComparison.Ordinal); + string path = EmitMetadata(source, this.starterCompilations["net8.0"], this.parseOptions); + try + { + await new CSharpSourceGeneratorVerifier.Test(this.logger) + { + ReferenceAssemblies = MyReferenceAssemblies.Net.Net80, + NativeMethodsTxt = "Buffer", + GeneratorConfiguration = GeneratorConfiguration.Default with { InputMetadataPaths = [path], InputDocPaths = [] }, + ExpectedDiagnostics = + { + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("MODE", "Windows.Win32.Test.Apis.Buffer::modes", "native storage is UInt32, but the enum's underlying type is UInt16"), + }, + }.RunAsync(TestContext.Current.CancellationToken); + } + finally + { + File.Delete(path); + } + } + + private void GenerateSource(string source, bool allowMarshaling, Platform platform, params string[] apis) + { + this.compilation = this.starterCompilations["net8.0"].WithOptions(this.starterCompilations["net8.0"].Options.WithPlatform(platform)); + string path = EmitMetadata(source, this.compilation, this.parseOptions); + try + { + this.generator = new Generator(path, null, [], DefaultTestGeneratorOptions with { AllowMarshaling = allowMarshaling, Public = true }, this.compilation, this.parseOptions); + foreach (string api in apis) + { + Assert.True(this.generator.TryGenerate(api, TestContext.Current.CancellationToken)); + } + + this.CollectGeneratedCode(this.generator); + } + finally + { + File.Delete(path); + } + } +} diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs index ad5b55ca..38e3e87a 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs @@ -317,14 +317,15 @@ public void MethodWithHRParameter() } [Fact] - public void AssociatedEnumOnMethodParameters() + public void AssociatedEnumOnMethodParametersWithIncompatibleSignedness() { this.GenerateApi("IShellFolderView"); InterfaceDeclarationSyntax ifaceSyntax = Assert.Single(this.FindGeneratedType("IShellFolderView").OfType()); MethodDeclarationSyntax methodSyntax = Assert.Single(ifaceSyntax.Members.OfType(), m => m.Identifier.ValueText == "Select"); ParameterSyntax parameter = Assert.Single(methodSyntax.ParameterList.Parameters); - Assert.Equal("SFVS_SELECT", Assert.IsType(parameter.Type).Right.Identifier.ValueText); + Assert.Equal(SyntaxKind.UIntKeyword, Assert.IsType(parameter.Type).Keyword.Kind()); + Assert.Contains(((SuperGenerator)this.generator!).Diagnostics, diagnostic => diagnostic.Id == "PInvoke015" && diagnostic.GetMessage().Contains("SFVS_SELECT", StringComparison.Ordinal)); } [Fact] diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternMethodTests.cs b/test/Microsoft.Windows.CsWin32.Tests/ExternMethodTests.cs index 9e0fda80..94586623 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/ExternMethodTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternMethodTests.cs @@ -9,18 +9,20 @@ public ExternMethodTests(ITestOutputHelper logger) } [Fact] - public void AssociatedEnumOnParameter() + public void AssociatedEnumOnParameterWithIncompatibleSignedness() { MethodDeclarationSyntax method = Assert.Single(this.GenerateMethod("SHObjectProperties"), IsOrContainsExternMethod); ParameterSyntax enumParam = method.ParameterList.Parameters[1]; - Assert.Equal("SHOP_TYPE", Assert.IsType(enumParam.Type).Right.Identifier.ValueText); + Assert.Equal(SyntaxKind.UIntKeyword, Assert.IsType(enumParam.Type).Keyword.Kind()); + Assert.Contains(((SuperGenerator)this.generator!).Diagnostics, diagnostic => diagnostic.Id == "PInvoke015" && diagnostic.GetMessage().Contains("SHOP_TYPE", StringComparison.Ordinal)); } [Fact] - public void AssociatedEnumOnReturnValue() + public void AssociatedEnumOnReturnValueWithIncompatibleSignedness() { MethodDeclarationSyntax method = Assert.Single(this.GenerateMethod("PathCleanupSpec"), IsOrContainsExternMethod); - Assert.Equal("PCS_RET", Assert.IsType(method.ReturnType).Right.Identifier.ValueText); + Assert.Equal(SyntaxKind.IntKeyword, Assert.IsType(method.ReturnType).Keyword.Kind()); + Assert.Contains(((SuperGenerator)this.generator!).Diagnostics, diagnostic => diagnostic.Id == "PInvoke015" && diagnostic.GetMessage().Contains("PCS_RET", StringComparison.Ordinal)); } [Fact] diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.il b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.il new file mode 100644 index 00000000..ad581a1d --- /dev/null +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.il @@ -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. +// +// Assemble with: ilasm /dll /output=AssociatedEnumArrays.winmd AssociatedEnumArrays.il + +.assembly extern netstandard { .publickeytoken = (CC 7B 13 FF CD 2D DD 51) .ver 2:1:0:0 } +.assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 4:0:0:0 } +.assembly extern Windows.Win32.winmd { .ver 0:0:0:0 } +.assembly Windows.Win32.winmd { .ver 1:0:0:0 } +.module AssociatedEnumArrays.winmd +.corflags 0x00000001 + +.class public sequential ansi sealed Windows.Win32.Foundation.ANCHOR + extends [netstandard]System.ValueType +{ + .field public uint32 Value +} + +.class public auto ansi sealed Windows.Win32.Test.ARRAY_MODE + extends [netstandard]System.Enum +{ + .field public specialname rtspecialname uint32 value__ + .field public static literal valuetype Windows.Win32.Test.ARRAY_MODE First = uint32(1) +} + +.class public sequential ansi sealed Windows.Win32.Test.ARRAY_RECORD + extends [netstandard]System.ValueType +{ + .field public uint32[2] Modes + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute::.ctor(string) = (01 00 0A 41 52 52 41 59 5F 4D 4F 44 45 00 00) +} + +.class public sequential ansi sealed Windows.Win32.Test.ARRAY_FLEX + extends [netstandard]System.ValueType +{ + .field public uint32[1] Modes + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute::.ctor(string) = (01 00 0A 41 52 52 41 59 5F 4D 4F 44 45 00 00) + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.FlexibleArrayAttribute::.ctor() = (01 00 00 00) +} + +.class public auto ansi sealed Windows.Win32.Test.SIGNED_MODE + extends [netstandard]System.Enum +{ + .field public specialname rtspecialname uint32 value__ + .field public static literal valuetype Windows.Win32.Test.SIGNED_MODE First = uint32(1) +} + +.class public sequential ansi sealed Windows.Win32.Test.SIGNED_ARRAY_RECORD + extends [netstandard]System.ValueType +{ + .field public int32[2] Modes + .custom instance void [Windows.Win32.winmd]Windows.Win32.Foundation.Metadata.AssociatedEnumAttribute::.ctor(string) = (01 00 0B 53 49 47 4E 45 44 5F 4D 4F 44 45 00 00) +} diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.winmd b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/AssociatedEnumArrays.winmd new file mode 100644 index 0000000000000000000000000000000000000000..83648fb4baf22a14386fa88b0f275c3e9e61cb14 GIT binary patch literal 2560 zcmeHI&ubiY6#slT$+){oV?&A>jPx5e1(7;Unt%urcaz;TE!k|`O;as}WOjFGM`mZr z%xu$C(0cITMZ}92DR}bYQLrd@@NDo85Fsc31}j4RyqQ@tgwjLtQs~?LzV|-w*Y|zj zcV@SK{Ywl180I=U0v=Kdm1F*7cAxI?vpnev|9{v8|V7Ei$W=7R0V9=6FtQ(S`xyoF^ zGzERqx6nXhmmtjAAza^lNsE_AmVC?apCtEd;&qU;j*D~0Q?7I>1 zmNZUZ_N+uPX<*k3Y}fCkJ(Ovdvt1FIm#$u} zQdWs?30`*nHl+~9p4P=exEMothFc)^M9wb+*@GBB^E zVHj_bZ{L4cou#FwUXOVtW4{txHZX{xE@qmi>DpqAO1DeExftrz$8Phv;zuR~1CA9X#$nIg=a{Kny{%spIuiwKp$Lsog zp-H)L(OmWWUe}5o-!toWWKp$H2}8f*Sdrb8gi6G_Z}%e`W+(Ckth)AHr|sI6Ah7n2 zohOpS8Jc^J*XzQwqcF0#P0&Rz?D&D}v`@5=1Xeay)*8(duHz0-X*Me#?9?08+KHGg zJB(6qv$oP`R#ULLUV9&{%a=B4)g&#|lR{}|JRjVVDedfF)d^UOBedmdAJXwiFSPHS z|D^xT+JnEpm|A+{>j1-Asc|}0{^nSf6ITQ(P;k$#`I`M*x!KT+A@umAu6 literal 0 HcmV?d00001 diff --git a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md index 2ae27f2d..e69ef981 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md +++ b/test/Microsoft.Windows.CsWin32.Tests/ExternalMetadata/NOTICE.md @@ -8,3 +8,4 @@ WnfWithoutStatusSuccess.winmd | Authored fixture for issue #1677 regression cove SelfContained.winmd | Authored fixture reproducing the OSClient single-`Generator` build configuration (a non-`Windows.Win32` metadata assembly with the Windows.Win32 projection provided by a reference), where special string typedefs (`PCWSTR`) must be referenced under `Windows.Win32.Foundation`. Its source is checked in as [SelfContained.il](SelfContained.il). Kept as IL for the same reason as WnfWithoutStatusSuccess.winmd. FlatNamespace.winmd | Authored fixture for issue #1848 regression coverage, with native typedefs and associated enums at the common namespace root and in a nested namespace. Its source and regeneration command are checked in as [FlatNamespace.il](FlatNamespace.il). TypeDefConstant.winmd | Authored fixture verifying that struct-typed constants defined in the same metadata module using TypeDefinition handles (rather than TypeReference handles) correctly nest into the struct and attach to extension blocks. Its source is checked in as [TypeDefConstant.il](TypeDefConstant.il). Kept as IL for the same reason as WnfWithoutStatusSuccess.winmd. +AssociatedEnumArrays.winmd | Authored fixed-rank and flexible array fixture with native primitive elements and enum associations, including an incompatible signedness annotation for warning/native-fallback coverage. Its source and regeneration command are in [AssociatedEnumArrays.il](AssociatedEnumArrays.il). diff --git a/test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs index 915eb0e7..006c347e 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs @@ -78,6 +78,31 @@ public async Task Gdi32() ReferenceAssemblies = ReferenceAssemblies.Net.Net80.WithNuGetConfigFilePath(MyReferenceAssemblies.NuGetConfigPath), LanguageVersion = LanguageVersion.CSharp12, NativeMethodsTxt = "gdi32.*", + ExpectedDiagnostics = + { + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("BACKGROUND_MODE", "Windows.Win32.Graphics.Gdi.Apis.GetBkMode::return", "native storage is Int32, but the enum's underlying type is UInt32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("BACKGROUND_MODE", "Windows.Win32.Graphics.Gdi.Apis.SetBkMode::mode", "native storage is Int32, but the enum's underlying type is UInt32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("FONT_CHARSET", "Windows.Win32.Graphics.Gdi.Apis.CreateFontW::iCharSet", "native storage is UInt32, but the enum's underlying type is Byte"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("FONT_CLIP_PRECISION", "Windows.Win32.Graphics.Gdi.Apis.CreateFontW::iClipPrecision", "native storage is UInt32, but the enum's underlying type is Byte"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("FONT_OUTPUT_PRECISION", "Windows.Win32.Graphics.Gdi.Apis.CreateFontW::iOutPrecision", "native storage is UInt32, but the enum's underlying type is Byte"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("FONT_QUALITY", "Windows.Win32.Graphics.Gdi.Apis.CreateFontW::iQuality", "native storage is UInt32, but the enum's underlying type is Byte"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("GET_DEVICE_CAPS_INDEX", "Windows.Win32.Graphics.Gdi.Apis.GetDeviceCaps::index", "native storage is Int32, but the enum's underlying type is UInt32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("OBJ_TYPE", "Windows.Win32.Graphics.Gdi.Apis.GetCurrentObject::type", "native storage is UInt32, but the enum's underlying type is Int32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("PEN_STYLE", "Windows.Win32.Graphics.Gdi.Apis.ExtCreatePen::iPenStyle", "native storage is UInt32, but the enum's underlying type is Int32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("PEN_STYLE", "Windows.Win32.Graphics.Gdi.LOGPEN.lopnStyle", "native storage is UInt32, but the enum's underlying type is Int32"), + new DiagnosticResult(SourceGenerator.IncompatibleAssociatedEnum) + .WithArguments("PFD_LAYER_TYPE", "Windows.Win32.Graphics.OpenGL.PIXELFORMATDESCRIPTOR.iLayerType", "native storage is Byte, but the enum's underlying type is SByte"), + }, }.RunAsync(TestContext.Current.CancellationToken); } diff --git a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs index 1dac4bc5..a5f37efc 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/StructTests.cs @@ -147,13 +147,14 @@ public void PointerFieldIsDeclaredAsPointer() } [Fact] - public void FieldWithAssociatedEnum() + public void FieldWithIncompatibleAssociatedEnum() { this.GenerateApi("SHDESCRIPTIONID"); var type = (StructDeclarationSyntax)Assert.Single(this.FindGeneratedType("SHDESCRIPTIONID")); - PropertyDeclarationSyntax property = Assert.Single(type.Members.OfType(), m => m.Identifier.ValueText == "dwDescriptionId"); - Assert.Equal("SHDID_ID", Assert.IsType(property.Type).Identifier.ValueText); + FieldDeclarationSyntax field = Assert.Single(type.Members.OfType(), m => m.Declaration.Variables.Any(v => v.Identifier.ValueText == "dwDescriptionId")); + Assert.Equal(SyntaxKind.UIntKeyword, Assert.IsType(field.Declaration.Type).Keyword.Kind()); + Assert.Contains(((SuperGenerator)this.generator!).Diagnostics, diagnostic => diagnostic.Id == "PInvoke015" && diagnostic.GetMessage().Contains("SHDID_ID", StringComparison.Ordinal)); } [Fact]