From f39bb4090cee0dc31fcb168b5283231167280289 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 28 Jul 2026 15:23:38 -0700 Subject: [PATCH 1/6] Add pseudo custom attribute lowering to managed ilasm Match native ilasm metadata emission for known pseudo custom attributes, including flag lowering, layout, P/Invoke, marshalling, and validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c177aade-9940-4e79-a9df-6747dcc22bb0 --- .../Actions/GrammarActions.BuildImage.cs | 1 + ...GrammarActions.CustomAttributes.Actions.cs | 9 +- .../GrammarActions.Manifest.Typedefs.cs | 2 +- src/tools/ilasm/src/ILAssembler/Diagnostic.cs | 12 + .../ilasm/src/ILAssembler/EntityRegistry.cs | 65 ++- .../src/ILAssembler/MetadataExtensions.cs | 45 ++ .../PseudoCustomAttributes.Application.cs | 356 ++++++++++++++ .../PseudoCustomAttributes.Decoding.cs | 305 ++++++++++++ .../PseudoCustomAttributes.Identity.cs | 182 ++++++++ .../PseudoCustomAttributes.KnownAttributes.cs | 260 +++++++++++ .../PseudoCustomAttributes.MarshalAs.cs | 434 ++++++++++++++++++ .../PseudoCustomAttributes.Security.cs | 56 +++ .../src/ILAssembler/PseudoCustomAttributes.cs | 141 ++++++ .../tests/ILAssembler.Tests/AssemblyTests.cs | 43 ++ .../ILAssembler.Tests/CustomAttributeTests.cs | 365 +++++++++++++++ .../tests/ILAssembler.Tests/FieldTests.cs | 47 ++ .../tests/ILAssembler.Tests/InteropTests.cs | 292 ++++++++++++ .../tests/ILAssembler.Tests/MethodTests.cs | 104 +++++ .../tests/ILAssembler.Tests/ParameterTests.cs | 31 ++ .../ILAssembler.Tests/PropertyEventTests.cs | 76 +++ 20 files changed, 2819 insertions(+), 7 deletions(-) create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs create mode 100644 src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs index ab104268eeeaab..fc2aa1ed2564cd 100644 --- a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs +++ b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs @@ -66,6 +66,7 @@ internal sealed partial class GrammarActions } BlobBuilder ilStream = new(); + PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics); Blob mvidFixup = _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames, _options.Deterministic); // MetadataRootBuilder only supports module-wide validation suppression, which is // required because wrapped GenericParam numbers intentionally violate table ordering. diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.CustomAttributes.Actions.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.CustomAttributes.Actions.cs index f6263647ff62d0..5d933ea2619af1 100644 --- a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.CustomAttributes.Actions.cs +++ b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.CustomAttributes.Actions.cs @@ -164,12 +164,14 @@ private void WriteCustomBlobNamedArguments( } private EntityRegistry.CustomAttributeEntity MaterializeCustomAttribute( - CustomAttributeDescriptorValue descriptor) + CustomAttributeDescriptorValue descriptor, + IToken location) { EntityRegistry.EntityBase constructor = MaterializeMethodReference(descriptor.Constructor); BlobBuilder value = MaterializeCustomAttributeBlob(descriptor.Value); EntityRegistry.CustomAttributeEntity attribute = _entityRegistry.CreateCustomAttribute(constructor, value); + attribute.Location = Location.From(location, _documents); if (descriptor.Owner is { } owner) { attribute.Owner = MaterializeOwnerType(owner); @@ -196,6 +198,7 @@ private EntityRegistry.CustomAttributeEntity MaterializeCustomAttribute( EntityRegistry.CustomAttributeEntity typedefAttribute = _entityRegistry.CreateCustomAttribute(resolved.Constructor, resolved.Value); typedefAttribute.Owner = resolved.Owner; + typedefAttribute.Location = Location.From(location, _documents); return typedefAttribute; } @@ -204,7 +207,7 @@ private EntityRegistry.CustomAttributeEntity MaterializeCustomAttribute( return null; } - EntityRegistry.CustomAttributeEntity attribute = MaterializeCustomAttribute(descriptor); + EntityRegistry.CustomAttributeEntity attribute = MaterializeCustomAttribute(descriptor, location); return descriptor.Owner is null ? attribute : null; } @@ -214,7 +217,7 @@ private EntityRegistry.CustomAttributeEntity MaterializeCustomAttribute( internal EntityRegistry.CustomAttributeEntity MaterializeCustomAttributeDescriptor( CILParser.CustomDescrContext context) - => MaterializeCustomAttribute(context.Value); + => MaterializeCustomAttribute(context.Value, context.Start); internal EntityRegistry.CustomAttributeEntity? MaterializeMethodBodyCustomAttributeDeclaration( CILParser.CustomDescrInMethodBodyContext context) diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Manifest.Typedefs.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Manifest.Typedefs.cs index 2507487a15cfe0..69c840c21f0ff8 100644 --- a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Manifest.Typedefs.cs +++ b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Manifest.Typedefs.cs @@ -55,7 +55,7 @@ private void MaterializeTypedef(TypedefDeclarationValue declaration) break; case CustomAttributeTypedefDeclarationValue customAttribute: EntityRegistry.CustomAttributeEntity attribute = - MaterializeCustomAttribute(customAttribute.Attribute); + MaterializeCustomAttribute(customAttribute.Attribute, customAttribute.Location); EntityRegistry.EntityBase? owner = attribute.Owner; attribute.Owner = null; _typedefs[customAttribute.Alias] = diff --git a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs index e33466df2a5c45..5f9953ef411221 100644 --- a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs +++ b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs @@ -64,6 +64,12 @@ public static class DiagnosticIds public const string DuplicateExportOrdinal = "ILA0044"; public const string UnsupportedVTableDeclaration = "ILA0045"; public const string InvalidOctalLiteral = "ILA0046"; + public const string PseudoCustomAttributeInvalidTarget = "ILA0047"; + public const string PseudoCustomAttributeInvalidValue = "ILA0048"; + public const string PseudoCustomAttributeInvalidBlob = "ILA0049"; + public const string PseudoCustomAttributeInvalidGuid = "ILA0050"; + public const string PseudoCustomAttributeUnknownArgument = "ILA0051"; + public const string PseudoCustomAttributeRepeatedArgument = "ILA0052"; } internal static class DiagnosticMessageTemplates @@ -114,4 +120,10 @@ internal static class DiagnosticMessageTemplates public const string DuplicateExportOrdinal = "Export '{0}' uses ordinal {1}, which is already assigned to a different VTable target"; public const string UnsupportedVTableDeclaration = "Raw .vtable declarations are not supported; use .vtfixup instead"; public const string InvalidOctalLiteral = "The value '{0}' is not a valid octal literal"; + public const string PseudoCustomAttributeInvalidTarget = "Custom attribute '{0}' cannot be applied to this target"; + public const string PseudoCustomAttributeInvalidValue = "Custom attribute '{0}' has an invalid argument value"; + public const string PseudoCustomAttributeInvalidBlob = "Custom attribute '{0}' has a malformed value blob"; + public const string PseudoCustomAttributeInvalidGuid = "Custom attribute '{0}' does not specify a valid GUID"; + public const string PseudoCustomAttributeUnknownArgument = "Custom attribute '{0}' does not have a field or property named '{1}'"; + public const string PseudoCustomAttributeRepeatedArgument = "Custom attribute '{0}' specifies '{1}' more than once"; } diff --git a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs index 24e392a9576009..fc759ee18d9d77 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -870,6 +870,34 @@ public TypeDefinitionEntity GetOrCreateTypeDefinition(TypeDefinitionEntity? cont return null; } + /// + /// Finds the local type definition that a type reference refers to, without assigning or + /// reading any handles. Passes that run before resolves + /// references need this, because handles are not assigned until emission. + /// + public TypeDefinitionEntity? FindLocalTypeDefinition(TypeReferenceEntity typeReference) + { + switch (typeReference.ResolutionScope) + { + case TypeReferenceEntity enclosing: + return FindLocalTypeDefinition(enclosing) is { } enclosingTypeDef + ? FindTypeDefinition(enclosingTypeDef, typeReference.Namespace, typeReference.Name) + : null; + + case AssemblyReferenceEntity assemblyReference: + return Assembly is not null + && string.Equals(assemblyReference.Name, Assembly.Name, StringComparison.OrdinalIgnoreCase) + ? FindTypeDefinition(null, typeReference.Namespace, typeReference.Name) + : null; + + case ModuleEntity or ModuleReferenceEntity: + return FindTypeDefinition(null, typeReference.Namespace, typeReference.Name); + + default: + return null; + } + } + public AssemblyReferenceEntity GetOrCreateAssemblyReference(string name, Action onCreateAssemblyReference) { return GetOrCreateEntity(name, TableIndex.AssemblyRef, _seenAssemblyRefs, _ => new(name), onCreateAssemblyReference); @@ -1678,6 +1706,31 @@ public CustomAttributeEntity CreateCustomAttribute(EntityBase constructor, BlobB return entity; } + /// + /// Removes custom attributes that were lowered to metadata bits or auxiliary table rows + /// by the pseudo custom attribute pass, and renumbers the handles of the survivors. + /// + /// + /// Nothing in metadata refers to a CustomAttribute handle (a CustomAttribute can never be + /// the parent of another custom attribute), so removing rows is safe. Handles are still + /// reassigned so that they stay consistent with the emitted row numbers. + /// + public void RemoveCustomAttributes(IReadOnlyCollection toRemove) + { + if (toRemove.Count == 0 || !_seenEntities.TryGetValue(TableIndex.CustomAttribute, out List? entities)) + { + return; + } + + var removeSet = new HashSet(toRemove); + entities.RemoveAll(entity => entity is CustomAttributeEntity customAttribute && removeSet.Contains(customAttribute)); + + for (int i = 0; i < entities.Count; i++) + { + ((IHasHandle)entities[i]).SetHandle(MetadataTokens.EntityHandle(TableIndex.CustomAttribute, i + 1)); + } + } + public static MethodImplementationEntity CreateUnrecordedMethodImplementation(MethodDefinitionEntity methodBody, MemberReferenceEntity methodDeclaration) { return new MethodImplementationEntity(methodBody.ContainingType, methodBody, methodDeclaration); @@ -2023,7 +2076,7 @@ public sealed class MethodDefinitionEntity(TypeDefinitionEntity containingType, public sealed class ParameterEntity(ParameterAttributes attributes, string? name, BlobBuilder marshallingDescriptor, int sequence) : EntityBase { - public ParameterAttributes Attributes { get; } = attributes; + public ParameterAttributes Attributes { get; set; } = attributes; public string? Name { get; } = name; public BlobBuilder MarshallingDescriptor { get; set; } = marshallingDescriptor; public bool HasCustomAttributes { get; set; } @@ -2088,6 +2141,12 @@ public sealed class CustomAttributeEntity(EntityBase constructor, BlobBuilder va public EntityBase? Owner { get; set; } public EntityBase Constructor { get; } = constructor; public BlobBuilder Value { get; } = value; + + /// + /// Source location of the originating .custom directive, used to report + /// diagnostics from the pseudo custom attribute lowering pass, which runs after parsing. + /// + public Location? Location { get; set; } } public sealed class MethodImplementationEntity(TypeDefinitionEntity type, EntityBase methodBody, MemberReferenceEntity methodDeclaration) : EntityBase @@ -2099,7 +2158,7 @@ public sealed class MethodImplementationEntity(TypeDefinitionEntity type, Entity public sealed class FieldDefinitionEntity(FieldAttributes attributes, TypeDefinitionEntity type, string name, BlobBuilder signature) : EntityBase { - public FieldAttributes Attributes { get; } = attributes; + public FieldAttributes Attributes { get; set; } = attributes; public TypeDefinitionEntity ContainingType { get; } = type; public string Name { get; } = name; public BlobBuilder Signature { get; } = signature; @@ -2123,7 +2182,7 @@ public sealed class InterfaceImplementationEntity(TypeDefinitionEntity type, Typ public sealed class EventEntity(EventAttributes attributes, TypeEntity? type, string name) : EntityBase { - public EventAttributes Attributes { get; } = attributes; + public EventAttributes Attributes { get; set; } = attributes; public TypeEntity? Type { get; } = type; public string Name { get; } = name; diff --git a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs index 3b3fd4321988dc..0f267e3fe4b463 100644 --- a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs +++ b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs @@ -3,15 +3,44 @@ using System.Reflection; using System.Reflection.Metadata; +using System.Runtime.InteropServices; namespace ILAssembler; internal static class MetadataExtensions { + extension(FieldAttributes) + { + public static FieldAttributes NotSerializedFlag => (FieldAttributes)0x0080; + } + + extension(MethodImplAttributes) + { + public static MethodImplAttributes UserMask => + MethodImplAttributes.ManagedMask | + MethodImplAttributes.ForwardRef | + MethodImplAttributes.PreserveSig | + MethodImplAttributes.InternalCall | + MethodImplAttributes.Synchronized | + MethodImplAttributes.NoInlining | + MethodImplAttributes.AggressiveInlining | + MethodImplAttributes.NoOptimization | + MethodImplAttributes.AggressiveOptimization | + MethodImplAttributes.Async; + } + extension(TypeAttributes) { public static TypeAttributes ExtendedLayout => (TypeAttributes)0x18; public static TypeAttributes Forwarder => (TypeAttributes)0x00200000; + public static TypeAttributes SerializableFlag => (TypeAttributes)0x00002000; + } + + extension(UnmanagedType) + { + public static int ArraySizeParamIndexSpecified => 0x0001; + public static UnmanagedType ByValStr => (UnmanagedType)0x22; + public static UnmanagedType Max => (UnmanagedType)0x50; } extension(DeclarativeSecurityAction) @@ -35,3 +64,19 @@ internal static class MetadataExtensions public static ILOpCode Unused => (ILOpCode)0xFE22; } } + +internal static class ClassInterfaceTypeExtensions +{ + extension(ClassInterfaceType) + { + public static ClassInterfaceType Last => (ClassInterfaceType)3; + } +} + +internal static class ComInterfaceTypeExtensions +{ + extension(ComInterfaceType) + { + public static ComInterfaceType Last => (ComInterfaceType)4; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs new file mode 100644 index 00000000000000..5dd6ca25e75014 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Runtime.InteropServices; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool Apply(in LoweringContext context, KnownAttribute known) + { + if ((known.Targets & GetTarget(context.Owner)) == 0) + { + return context.InvalidTarget(); + } + + if (!TryParseArguments(context, known, out CustomAttributeValue arguments)) + { + return false; + } + + return known.Kind switch + { + KnownAttributeKind.DllImport => ApplyDllImport(context, arguments), + KnownAttributeKind.Guid => ApplyGuid(context, arguments), + KnownAttributeKind.ComImport => AddTypeFlags(context, TypeAttributes.Import), + KnownAttributeKind.InterfaceType => + GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ComInterfaceType.Last || context.InvalidValue(), + KnownAttributeKind.ClassInterface => + GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ClassInterfaceType.Last || context.InvalidValue(), + KnownAttributeKind.Serializable => AddTypeFlags(context, TypeAttributes.SerializableFlag), + KnownAttributeKind.NonSerialized => AddFieldFlags(context, FieldAttributes.NotSerializedFlag), + KnownAttributeKind.MethodImpl1 or KnownAttributeKind.MethodImpl2 or KnownAttributeKind.MethodImpl3 => + ApplyMethodImpl(context, known.Kind, arguments), + KnownAttributeKind.MarshalAs1 or KnownAttributeKind.MarshalAs2 => ApplyMarshalAs(context, arguments), + KnownAttributeKind.PreserveSig => AddMethodImplFlags(context, MethodImplAttributes.PreserveSig), + KnownAttributeKind.In => AddParameterFlags(context, ParameterAttributes.In), + KnownAttributeKind.Out => AddParameterFlags(context, ParameterAttributes.Out), + KnownAttributeKind.Optional => AddParameterFlags(context, ParameterAttributes.Optional), + KnownAttributeKind.StructLayout1 or KnownAttributeKind.StructLayout2 => + ApplyStructLayout(context, known.Kind, arguments), + KnownAttributeKind.FieldOffset => ApplyFieldOffset(context, arguments), + KnownAttributeKind.TypeLibVersion or KnownAttributeKind.ComCompatibleVersion => + ValidateNonNegative(context, arguments.FixedArguments), + KnownAttributeKind.SpecialName => ApplySpecialName(context), + KnownAttributeKind.AllowPartiallyTrustedCallers => true, + KnownAttributeKind.WindowsRuntimeImport => AddTypeFlags(context, TypeAttributes.WindowsRuntime), + _ => true, + }; + } + + private static bool AddTypeFlags(in LoweringContext context, TypeAttributes flags) + { + ((EntityRegistry.TypeDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddFieldFlags(in LoweringContext context, FieldAttributes flags) + { + ((EntityRegistry.FieldDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddParameterFlags(in LoweringContext context, ParameterAttributes flags) + { + ((EntityRegistry.ParameterEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddMethodImplFlags(in LoweringContext context, MethodImplAttributes flags) + { + ((EntityRegistry.MethodDefinitionEntity)context.Owner).ImplementationAttributes |= flags; + return true; + } + + private static bool ApplySpecialName(in LoweringContext context) + { + switch (context.Owner) + { + case EntityRegistry.TypeDefinitionEntity type: + type.Attributes |= TypeAttributes.SpecialName; + return true; + case EntityRegistry.MethodDefinitionEntity method: + method.MethodAttributes |= MethodAttributes.SpecialName; + return true; + case EntityRegistry.FieldDefinitionEntity field: + field.Attributes |= FieldAttributes.SpecialName; + return true; + case EntityRegistry.PropertyEntity property: + property.Attributes |= PropertyAttributes.SpecialName; + return true; + case EntityRegistry.EventEntity @event: + @event.Attributes |= EventAttributes.SpecialName; + return true; + default: + return context.InvalidValue(); + } + } + + private static bool ValidateNonNegative( + in LoweringContext context, + ImmutableArray> arguments) + { + foreach (CustomAttributeTypedArgument argument in arguments) + { + if (GetInt32(argument.Value) < 0) + { + return context.InvalidValue(); + } + } + + return true; + } + + private static bool ApplyGuid( + in LoweringContext context, + in CustomAttributeValue arguments) + { + // The value is only validated; the attribute itself is still emitted. + string guid = GetString(arguments.FixedArguments[0].Value); + return guid.Length == 36 && Guid.TryParseExact(guid, "D", out _) + ? true + : context.InvalidGuid(); + } + + private static bool ApplyFieldOffset( + in LoweringContext context, + in CustomAttributeValue arguments) + { + uint offset = GetUInt32(arguments.FixedArguments[0].Value); + if (offset > int.MaxValue) + { + return context.InvalidValue(); + } + + ((EntityRegistry.FieldDefinitionEntity)context.Owner).Offset = (int)offset; + return true; + } + + private static bool ApplyMethodImpl( + in LoweringContext context, + KnownAttributeKind kind, + in CustomAttributeValue arguments) + { + var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; + CustomAttributeNamedArgument? codeTypeArgument = + FindNamedArgument(arguments, MethodImplCodeType); + + if (kind is not KnownAttributeKind.MethodImpl1) + { + // The I2 overload is widened before validation, matching the native emitter. + object? fixedValue = arguments.FixedArguments[0].Value; + ushort value = kind is KnownAttributeKind.MethodImpl2 + ? unchecked((ushort)GetInt16(fixedValue)) + : GetUInt16(fixedValue); + if (((MethodImplAttributes)value & ~MethodImplAttributes.UserMask) != 0) + { + return context.InvalidValue(); + } + + method.ImplementationAttributes |= (MethodImplAttributes)value; + + if (codeTypeArgument is null) + { + return true; + } + } + + ushort codeType = codeTypeArgument is { } argument ? GetUInt16(argument.Value) : (ushort)0; + if ((codeType & ~(ushort)MethodImplAttributes.CodeTypeMask) != 0) + { + return context.InvalidValue(); + } + + method.ImplementationAttributes = + (method.ImplementationAttributes & ~MethodImplAttributes.CodeTypeMask) | (MethodImplAttributes)codeType; + return true; + } + + private static bool ApplyStructLayout( + in LoweringContext context, + KnownAttributeKind kind, + in CustomAttributeValue arguments) + { + var type = (EntityRegistry.TypeDefinitionEntity)context.Owner; + + // The I2 overload is zero-extended through 16 bits before the layout kind is read. + object? fixedValue = arguments.FixedArguments[0].Value; + int layoutKind = kind is KnownAttributeKind.StructLayout1 + ? unchecked((ushort)GetInt16(fixedValue)) + : GetInt32(fixedValue); + + TypeAttributes layout = layoutKind switch + { + 0 => TypeAttributes.SequentialLayout, + 1 => TypeAttributes.ExtendedLayout, + 2 => TypeAttributes.ExplicitLayout, + 3 => TypeAttributes.AutoLayout, + _ => (TypeAttributes)(-1), + }; + + if (layout == (TypeAttributes)(-1)) + { + return context.InvalidValue(); + } + + TypeAttributes attributes = (type.Attributes & ~TypeAttributes.LayoutMask) | layout; + + if (FindNamedArgument(arguments, StructLayoutPack) is { } packArgument) + { + uint pack = GetUInt32(packArgument.Value); + if (pack > 128 || (pack & (pack - 1)) != 0) + { + return context.InvalidValue(); + } + + // An explicit .pack directive wins: the native assembler emits the ClassLayout row for + // explicit directives in a later phase than the one that applies this attribute. + type.PackingSize ??= (int)pack; + } + + if (FindNamedArgument(arguments, StructLayoutSize) is { } sizeArgument) + { + uint size = GetUInt32(sizeArgument.Value); + if (size > int.MaxValue) + { + return context.InvalidValue(); + } + + // An explicit .size directive wins, for the same reason as .pack above. + type.ClassSize ??= (int)size; + } + + if (FindNamedArgument(arguments, StructLayoutCharSet) is { } charSetArgument) + { + switch (GetUInt32(charSetArgument.Value)) + { + case 2: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.AnsiClass; + break; + case 3: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.UnicodeClass; + break; + case 4: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.AutoClass; + break; + default: + return context.InvalidValue(); + } + } + + type.Attributes = attributes; + return true; + } + + private static bool ApplyDllImport( + in LoweringContext context, + in CustomAttributeValue arguments) + { + var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; + + if (arguments.FixedArguments[0].Value is not string moduleName || moduleName.Length == 0) + { + return context.InvalidValue(); + } + + MethodImportAttributes flags = MethodImportAttributes.None; + + if (FindNamedArgument(arguments, DllImportCallingConvention) is { } callingConventionArgument) + { + flags = GetUInt32(callingConventionArgument.Value) switch + { + 0 => flags, + 1 => flags | MethodImportAttributes.CallingConventionWinApi, + 2 => flags | MethodImportAttributes.CallingConventionCDecl, + 3 => flags | MethodImportAttributes.CallingConventionStdCall, + 4 => flags | MethodImportAttributes.CallingConventionThisCall, + 5 => flags | MethodImportAttributes.CallingConventionFastCall, + _ => flags, + }; + } + else + { + flags |= MethodImportAttributes.CallingConventionWinApi; + } + + if (FindNamedArgument(arguments, DllImportCharSet) is { } charSetArgument) + { + flags = GetUInt32(charSetArgument.Value) switch + { + // 0 means "do nothing" and 1 is "not specified", which is the zero bit pattern. + 0 or 1 => flags, + 2 => flags | MethodImportAttributes.CharSetAnsi, + 3 => flags | MethodImportAttributes.CharSetUnicode, + 4 => flags | MethodImportAttributes.CharSetAuto, + _ => flags, + }; + } + + if (FindNamedArgument(arguments, DllImportExactSpelling) is { } exactSpellingArgument + && GetBoolean(exactSpellingArgument.Value)) + { + flags |= MethodImportAttributes.ExactSpelling; + } + + if (FindNamedArgument(arguments, DllImportSetLastError) is { } setLastErrorArgument + && GetBoolean(setLastErrorArgument.Value)) + { + flags |= MethodImportAttributes.SetLastError; + } + + if (FindNamedArgument(arguments, DllImportBestFitMapping) is { } bestFitMappingArgument) + { + flags |= GetBoolean(bestFitMappingArgument.Value) + ? MethodImportAttributes.BestFitMappingEnable + : MethodImportAttributes.BestFitMappingDisable; + } + + if (FindNamedArgument(arguments, DllImportThrowOnUnmappableChar) is { } throwOnUnmappableCharArgument) + { + flags |= GetBoolean(throwOnUnmappableCharArgument.Value) + ? MethodImportAttributes.ThrowOnUnmappableCharEnable + : MethodImportAttributes.ThrowOnUnmappableCharDisable; + } + + // PreserveSig defaults to set, and is only cleared by an explicit false value. + if (FindNamedArgument(arguments, DllImportPreserveSig) is { } preserveSigArgument + && !GetBoolean(preserveSigArgument.Value)) + { + method.ImplementationAttributes &= ~MethodImplAttributes.PreserveSig; + } + else + { + method.ImplementationAttributes |= MethodImplAttributes.PreserveSig; + } + + string entryPoint = FindNamedArgument(arguments, DllImportEntryPoint) is { } entryPointArgument + ? GetString(entryPointArgument.Value) + : method.Name; + + // The module reference is created even when an explicit pinvokeimpl clause takes precedence, + // because the native emitter resolves it before it discovers the existing ImplMap row. + var moduleReference = context.Registry.GetOrCreateModuleReference(moduleName, _ => { }); + + // An explicit pinvokeimpl clause wins: the native assembler emits the ImplMap row for + // explicit clauses in a later phase than the one that applies this attribute. + method.MethodImportInformation ??= (moduleReference, entryPoint, flags); + + return true; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs new file mode 100644 index 00000000000000..c4a672e136783c --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private readonly record struct EncodedArgumentType( + SerializationTypeCode Type, + SerializationTypeCode ArrayType = SerializationTypeCode.Invalid, + SerializationTypeCode EnumUnderlyingType = SerializationTypeCode.Invalid, + string? EnumName = null); + + private static EncodedArgumentType ReadEncodedArgumentType(ref BlobReader reader) + { + // The native CustomAttributeParser::GetTag method consumes one byte rather than a + // compressed integer, so use BlobReader.ReadByte instead of ReadSerializationTypeCode. + SerializationTypeCode type = (SerializationTypeCode)reader.ReadByte(); + SerializationTypeCode arrayType = SerializationTypeCode.Invalid; + if (type == SerializationTypeCode.SZArray) + { + arrayType = (SerializationTypeCode)reader.ReadByte(); + } + + SerializationTypeCode effectiveType = + type == SerializationTypeCode.SZArray ? arrayType : type; + string? enumName = null; + if (effectiveType == SerializationTypeCode.Enum) + { + enumName = reader.ReadSerializedString(); + if (enumName is null) + { + throw new BadImageFormatException(); + } + } + + return new(type, arrayType, EnumName: enumName); + } + + private static CustomAttributeTypedArgument ReadArgument( + ref BlobReader reader, + SerializationTypeCode type, + SerializationTypeCode enumUnderlyingType = SerializationTypeCode.Invalid) + { + SerializationTypeCode effectiveType = + type == SerializationTypeCode.Enum ? enumUnderlyingType : type; + + object? value = effectiveType switch + { + SerializationTypeCode.Boolean => reader.ReadBoolean(), + SerializationTypeCode.SByte => reader.ReadSByte(), + SerializationTypeCode.Byte => reader.ReadByte(), + SerializationTypeCode.Char => reader.ReadChar(), + SerializationTypeCode.Int16 => reader.ReadInt16(), + SerializationTypeCode.UInt16 => reader.ReadUInt16(), + SerializationTypeCode.Int32 => reader.ReadInt32(), + SerializationTypeCode.UInt32 => reader.ReadUInt32(), + SerializationTypeCode.Int64 => reader.ReadInt64(), + SerializationTypeCode.UInt64 => reader.ReadUInt64(), + SerializationTypeCode.Single => reader.ReadSingle(), + SerializationTypeCode.Double => reader.ReadDouble(), + SerializationTypeCode.String or SerializationTypeCode.Type => reader.ReadSerializedString(), + _ => throw new BadImageFormatException(), + }; + + return new(type, value); + } + + private static unsafe bool TryParseArguments( + in LoweringContext context, + KnownAttribute known, + out CustomAttributeValue arguments) + { + // The native emitter does not look at the blob at all when the attribute has neither + // fixed nor named arguments, so a malformed blob is tolerated for those attributes. + if (known.FixedArguments.Length == 0 && known.NamedArguments.Length == 0) + { + arguments = new( + ImmutableArray>.Empty, + ImmutableArray>.Empty); + return true; + } + + byte[] blob = context.Attribute.Value.ToArray(); + fixed (byte* blobPointer = blob) + { + var reader = new BlobReader(blobPointer, blob.Length); + try + { + if (reader.ReadUInt16() != 0x0001) + { + arguments = default; + return context.InvalidBlob(); + } + + var fixedArguments = + ImmutableArray.CreateBuilder>( + known.FixedArguments.Length); + foreach (SerializationTypeCode type in known.FixedArguments) + { + fixedArguments.Add(ReadArgument(ref reader, type)); + } + + ImmutableArray> namedArguments; + if (known.NamedArguments.Length == 0 && reader.RemainingBytes == 0) + { + namedArguments = ImmutableArray>.Empty; + } + else if (!TryParseNamedArguments(context, known, ref reader, out namedArguments)) + { + arguments = default; + return false; + } + + arguments = new(fixedArguments.MoveToImmutable(), namedArguments); + return true; + } + catch (BadImageFormatException) + { + arguments = default; + return context.InvalidBlob(); + } + } + } + + private static bool TryParseNamedArguments( + in LoweringContext context, + KnownAttribute known, + ref BlobReader reader, + out ImmutableArray> namedArguments) + { + // A missing count is treated as "no named arguments" rather than an error, matching the + // native emitter's documented Everett-compatible behavior. + if (reader.RemainingBytes < sizeof(ushort)) + { + namedArguments = ImmutableArray>.Empty; + return true; + } + + ushort actualCount = reader.ReadUInt16(); + var arguments = + ImmutableArray.CreateBuilder>( + Math.Min(actualCount, (ushort)known.NamedArguments.Length)); + var seenArguments = new bool[known.NamedArguments.Length]; + + // The count is deliberately read as a signed 16-bit value: the native emitter stores it in + // an INT16 and compares against a wider signed loop counter, so a count with the high bit + // set yields no named arguments rather than an error. + for (int i = 0; i < (short)actualCount; i++) + { + var kind = (CustomAttributeNamedArgumentKind)reader.ReadByte(); + if (kind is not (CustomAttributeNamedArgumentKind.Field or CustomAttributeNamedArgumentKind.Property)) + { + namedArguments = default; + return context.InvalidBlob(); + } + + EncodedArgumentType actual = ReadEncodedArgumentType(ref reader); + string? argumentName = reader.ReadSerializedString(); + if (string.IsNullOrEmpty(argumentName)) + { + namedArguments = default; + return context.InvalidBlob(); + } + + int match = -1; + for (int candidate = 0; candidate < known.NamedArguments.Length; candidate++) + { + NamedArgument descriptor = known.NamedArguments[candidate]; + + if (descriptor.Type != SerializationTypeCode.TaggedObject) + { + if (actual.Type != descriptor.Type) + { + continue; + } + + if (actual.Type == SerializationTypeCode.SZArray + && descriptor.ArrayType != SerializationTypeCode.TaggedObject + && actual.ArrayType != descriptor.ArrayType) + { + continue; + } + } + + if (descriptor.Name != argumentName) + { + continue; + } + + if (descriptor.Type == SerializationTypeCode.Enum + || (descriptor.Type == SerializationTypeCode.SZArray && descriptor.ArrayType == SerializationTypeCode.Enum)) + { + if (!EnumNameMatches(descriptor.EnumName, actual.EnumName)) + { + continue; + } + + actual = actual with { EnumUnderlyingType = descriptor.EnumType }; + } + + match = candidate; + break; + } + + if (match < 0) + { + namedArguments = default; + return context.UnknownArgument(argumentName); + } + + if (seenArguments[match]) + { + namedArguments = default; + return context.RepeatedArgument(argumentName); + } + + seenArguments[match] = true; + CustomAttributeTypedArgument value = + ReadArgument(ref reader, actual.Type, actual.EnumUnderlyingType); + arguments.Add(new(argumentName, kind, value.Type, value.Value)); + } + + namedArguments = arguments.ToImmutable(); + return true; + } + + private static CustomAttributeNamedArgument? FindNamedArgument( + in CustomAttributeValue arguments, + string name) + { + foreach (CustomAttributeNamedArgument argument in arguments.NamedArguments) + { + if (argument.Name == name) + { + return argument; + } + } + + return null; + } + + private static short GetInt16(object? value) => value switch + { + short signed => signed, + ushort unsigned => unchecked((short)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static ushort GetUInt16(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => unchecked((ushort)signed), + uint unsigned => unchecked((ushort)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static int GetInt32(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => signed, + uint unsigned => unchecked((int)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static uint GetUInt32(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => unchecked((uint)signed), + uint unsigned => unsigned, + _ => throw new BadImageFormatException(), + }; + + private static bool GetBoolean(object? value) => + value is bool boolean ? boolean : throw new BadImageFormatException(); + + private static string GetString(object? value) => value as string ?? ""; + + /// + /// Matches an enum type name against a descriptor name, allowing the blob to carry an + /// assembly-qualified name whose namespace-qualified prefix matches. + /// + private static bool EnumNameMatches(string descriptorName, string? actualName) + { + if (actualName is null || descriptorName.Length > actualName.Length) + { + return false; + } + + if (!actualName.AsSpan(0, descriptorName.Length).SequenceEqual(descriptorName)) + { + return false; + } + + return descriptorName.Length == actualName.Length || actualName[descriptorName.Length] == ','; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs new file mode 100644 index 00000000000000..98332f47f129da --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool TryGetAttributeTypeName(EntityRegistry.EntityBase constructor, out string @namespace, out string name) + { + switch (constructor) + { + case EntityRegistry.MemberReferenceEntity memberReference: + switch (memberReference.Parent) + { + case EntityRegistry.TypeReferenceEntity typeReference: + @namespace = typeReference.Namespace; + name = typeReference.Name; + return true; + case EntityRegistry.TypeDefinitionEntity typeDefinition: + @namespace = typeDefinition.Namespace; + name = typeDefinition.Name; + return true; + } + break; + + case EntityRegistry.MethodDefinitionEntity methodDefinition: + @namespace = methodDefinition.ContainingType.Namespace; + name = methodDefinition.ContainingType.Name; + return true; + } + + @namespace = ""; + name = ""; + return false; + } + + private static BlobBuilder? GetConstructorSignature(EntityRegistry.EntityBase constructor) => constructor switch + { + EntityRegistry.MemberReferenceEntity memberReference => memberReference.Signature, + EntityRegistry.MethodDefinitionEntity methodDefinition => methodDefinition.MethodSignature, + _ => null + }; + + /// + /// Resolves a type or member reference that designates an entity defined in this module to that + /// entity. References are otherwise only resolved while metadata rows are written, which happens + /// after this pass runs. Anything that does not designate a local entity is returned unchanged. + /// + private static EntityRegistry.EntityBase ResolveOwner(EntityRegistry registry, EntityRegistry.EntityBase owner) + { + switch (owner) + { + case EntityRegistry.TypeReferenceEntity typeReference: + return registry.FindLocalTypeDefinition(typeReference) ?? owner; + + case EntityRegistry.MemberReferenceEntity memberReference: + if (ResolveDeclaringType(registry, memberReference.Parent) is not { } declaringType) + { + return owner; + } + + byte[] signature = memberReference.Signature.ToArray(); + if (signature.Length == 0) + { + return owner; + } + + switch (new SignatureHeader(signature[0]).Kind) + { + case SignatureKind.Method: + foreach (EntityRegistry.MethodDefinitionEntity method in declaringType.Methods) + { + if (method.Name == memberReference.Name + && method.MethodSignature is { } methodSignature + && methodSignature.ContentEquals(memberReference.Signature)) + { + return method; + } + } + break; + + case SignatureKind.Field: + foreach (EntityRegistry.FieldDefinitionEntity field in declaringType.Fields) + { + if (field.Name == memberReference.Name + && field.Signature.ContentEquals(memberReference.Signature)) + { + return field; + } + } + break; + } + + return owner; + + default: + return owner; + } + } + + private static EntityRegistry.TypeDefinitionEntity? ResolveDeclaringType( + EntityRegistry registry, + EntityRegistry.EntityBase? declaringType) => declaringType switch + { + EntityRegistry.TypeDefinitionEntity typeDefinition => typeDefinition, + EntityRegistry.TypeReferenceEntity typeReference => registry.FindLocalTypeDefinition(typeReference), + _ => null, + }; + + private static KnownAttribute? TryFindKnownAttribute(EntityRegistry.EntityBase constructor, string @namespace, string name) + { + foreach (KnownAttribute candidate in s_knownAttributes) + { + if (candidate.Name != name || candidate.Namespace != @namespace) + { + continue; + } + + if (candidate.MatchBySignature && !SignatureMatches(constructor, candidate)) + { + continue; + } + + return candidate; + } + + return null; + } + + /// + /// Discriminates between overloads of a known attribute by comparing the constructor signature's + /// parameter count and element types against the descriptor's fixed arguments. The serialization + /// type tags of the primitive types used by the known attributes coincide with their + /// ELEMENT_TYPE values, so the tags can be compared directly. + /// + private static unsafe bool SignatureMatches(EntityRegistry.EntityBase constructor, KnownAttribute candidate) + { + BlobBuilder? signature = GetConstructorSignature(constructor); + if (signature is null) + { + return false; + } + + byte[] signatureBytes = signature.ToArray(); + fixed (byte* signaturePointer = signatureBytes) + { + var reader = new BlobReader(signaturePointer, signatureBytes.Length); + try + { + // Calling convention, then parameter count. + if (!reader.TryReadCompressedInteger(out _) + || !reader.TryReadCompressedInteger(out int parameterCount) + || parameterCount != candidate.FixedArguments.Length + // Skip the return type. + || !reader.TryReadCompressedInteger(out _)) + { + return false; + } + + for (int i = 0; i < parameterCount; i++) + { + if (!reader.TryReadCompressedInteger(out int element) + || (SerializationTypeCode)element != candidate.FixedArguments[i]) + { + return false; + } + } + + return true; + } + catch (BadImageFormatException) + { + return false; + } + } + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs new file mode 100644 index 00000000000000..cb900ccd07b1f9 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + [Flags] + private enum CaTargets + { + None = 0, + TypeDef = 1 << 0, + TypeRef = 1 << 1, + MethodDef = 1 << 2, + FieldDef = 1 << 3, + ParamDef = 1 << 4, + Property = 1 << 5, + Event = 1 << 6, + Module = 1 << 7, + Assembly = 1 << 8, + } + + private enum KnownAttributeKind + { + DllImport, + Guid, + ComImport, + InterfaceType, + ClassInterface, + Serializable, + NonSerialized, + MethodImpl1, + MethodImpl2, + MethodImpl3, + MarshalAs1, + MarshalAs2, + PreserveSig, + In, + Out, + Optional, + StructLayout1, + StructLayout2, + FieldOffset, + TypeLibVersion, + ComCompatibleVersion, + SpecialName, + AllowPartiallyTrustedCallers, + WindowsRuntimeImport, + } + + private sealed record NamedArgument( + string Name, + SerializationTypeCode Type, + SerializationTypeCode EnumType = SerializationTypeCode.Invalid, + string EnumName = "", + SerializationTypeCode ArrayType = SerializationTypeCode.Invalid); + + private sealed record KnownAttribute( + KnownAttributeKind Kind, + string Namespace, + string Name, + CaTargets Targets, + bool KeepAttribute = false, + SerializationTypeCode[]? FixedArgumentTypes = null, + NamedArgument[]? NamedArgumentDescriptors = null, + bool MatchBySignature = false) + { + public SerializationTypeCode[] FixedArguments { get; } = FixedArgumentTypes ?? []; + public NamedArgument[] NamedArguments { get; } = NamedArgumentDescriptors ?? []; + } + + private const string InteropNamespace = "System.Runtime.InteropServices"; + private const string CompilerServicesNamespace = "System.Runtime.CompilerServices"; + + private static NamedArgument Enum4(string name, string enumName) => + new(name, SerializationTypeCode.Enum, SerializationTypeCode.Int32, enumName); + + private const string DllImportCallingConvention = "CallingConvention"; + private const string DllImportCharSet = "CharSet"; + private const string DllImportEntryPoint = "EntryPoint"; + private const string DllImportExactSpelling = "ExactSpelling"; + private const string DllImportSetLastError = "SetLastError"; + private const string DllImportPreserveSig = "PreserveSig"; + private const string DllImportBestFitMapping = "BestFitMapping"; + private const string DllImportThrowOnUnmappableChar = "ThrowOnUnmappableChar"; + + private const string MethodImplCodeType = "MethodCodeType"; + + private const string MarshalArraySubType = "ArraySubType"; + private const string MarshalSafeArraySubType = "SafeArraySubType"; + private const string MarshalSafeArrayUserDefinedSubType = "SafeArrayUserDefinedSubType"; + private const string MarshalSizeParamIndex = "SizeParamIndex"; + private const string MarshalSizeConst = "SizeConst"; + private const string MarshalType = "MarshalType"; + private const string MarshalTypeRef = "MarshalTypeRef"; + private const string MarshalCookie = "MarshalCookie"; + private const string MarshalIidParameterIndex = "IidParameterIndex"; + + private const string StructLayoutPack = "Pack"; + private const string StructLayoutSize = "Size"; + private const string StructLayoutCharSet = "CharSet"; + + private static readonly NamedArgument[] s_dllImportNamedArguments = + [ + Enum4(DllImportCallingConvention, InteropNamespace + ".CallingConvention"), + Enum4(DllImportCharSet, InteropNamespace + ".CharSet"), + new(DllImportEntryPoint, SerializationTypeCode.String), + new(DllImportExactSpelling, SerializationTypeCode.Boolean), + new(DllImportSetLastError, SerializationTypeCode.Boolean), + new(DllImportPreserveSig, SerializationTypeCode.Boolean), + new(DllImportBestFitMapping, SerializationTypeCode.Boolean), + new(DllImportThrowOnUnmappableChar, SerializationTypeCode.Boolean), + ]; + + private static readonly NamedArgument[] s_methodImplNamedArguments = + [ + Enum4(MethodImplCodeType, CompilerServicesNamespace + ".MethodCodeType"), + ]; + + private static readonly NamedArgument[] s_marshalAsNamedArguments = + [ + Enum4(MarshalArraySubType, InteropNamespace + ".UnmanagedType"), + Enum4(MarshalSafeArraySubType, InteropNamespace + ".VarEnum"), + new(MarshalSafeArrayUserDefinedSubType, SerializationTypeCode.Type), + new(MarshalSizeParamIndex, SerializationTypeCode.Int16), + new(MarshalSizeConst, SerializationTypeCode.Int32), + new(MarshalType, SerializationTypeCode.String), + new(MarshalTypeRef, SerializationTypeCode.Type), + new(MarshalCookie, SerializationTypeCode.String), + new(MarshalIidParameterIndex, SerializationTypeCode.Int32), + ]; + + private static readonly NamedArgument[] s_structLayoutNamedArguments = + [ + new(StructLayoutPack, SerializationTypeCode.Int32), + new(StructLayoutSize, SerializationTypeCode.Int32), + Enum4(StructLayoutCharSet, InteropNamespace + ".CharSet"), + ]; + + private const CaTargets MarshalTargets = CaTargets.FieldDef | CaTargets.ParamDef | CaTargets.Property; + private const CaTargets InOutTargets = CaTargets.ParamDef; + private const CaTargets SpecialNameTargets = + CaTargets.TypeDef | CaTargets.MethodDef | CaTargets.FieldDef | CaTargets.Property | CaTargets.Event; + + /// + /// The known attributes, in the same order as the native KnownCaList. The order matters: + /// overloads that match by signature are tested before the match-by-name fallback overload. + /// + private static readonly KnownAttribute[] s_knownAttributes = + [ + new(KnownAttributeKind.DllImport, InteropNamespace, "DllImportAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.String], + NamedArgumentDescriptors: s_dllImportNamedArguments), + + new(KnownAttributeKind.Guid, InteropNamespace, "GuidAttribute", + CaTargets.TypeDef | CaTargets.TypeRef | CaTargets.Module | CaTargets.Assembly, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.String]), + + new(KnownAttributeKind.ComImport, InteropNamespace, "ComImportAttribute", CaTargets.TypeDef), + + new(KnownAttributeKind.InterfaceType, InteropNamespace, "InterfaceTypeAttribute", CaTargets.TypeDef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.UInt16]), + + new(KnownAttributeKind.ClassInterface, InteropNamespace, "ClassInterfaceAttribute", + CaTargets.TypeDef | CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.UInt16]), + + new(KnownAttributeKind.Serializable, "System", "SerializableAttribute", CaTargets.TypeDef), + + new(KnownAttributeKind.NonSerialized, "System", "NonSerializedAttribute", CaTargets.FieldDef), + + new(KnownAttributeKind.MethodImpl1, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + NamedArgumentDescriptors: s_methodImplNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MethodImpl2, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_methodImplNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MethodImpl3, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.UInt32], + NamedArgumentDescriptors: s_methodImplNamedArguments), + + new(KnownAttributeKind.MarshalAs1, InteropNamespace, "MarshalAsAttribute", MarshalTargets, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_marshalAsNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MarshalAs2, InteropNamespace, "MarshalAsAttribute", MarshalTargets, + FixedArgumentTypes: [SerializationTypeCode.UInt32], + NamedArgumentDescriptors: s_marshalAsNamedArguments), + + new(KnownAttributeKind.PreserveSig, InteropNamespace, "PreserveSigAttribute", CaTargets.MethodDef), + + new(KnownAttributeKind.In, InteropNamespace, "InAttribute", InOutTargets), + + new(KnownAttributeKind.Out, InteropNamespace, "OutAttribute", InOutTargets), + + new(KnownAttributeKind.Optional, InteropNamespace, "OptionalAttribute", InOutTargets), + + new(KnownAttributeKind.StructLayout1, InteropNamespace, "StructLayoutAttribute", CaTargets.TypeDef, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_structLayoutNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.StructLayout2, InteropNamespace, "StructLayoutAttribute", CaTargets.TypeDef, + FixedArgumentTypes: [SerializationTypeCode.Int32], + NamedArgumentDescriptors: s_structLayoutNamedArguments), + + new(KnownAttributeKind.FieldOffset, InteropNamespace, "FieldOffsetAttribute", CaTargets.FieldDef, + FixedArgumentTypes: [SerializationTypeCode.UInt32]), + + new(KnownAttributeKind.TypeLibVersion, InteropNamespace, "TypeLibVersionAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.Int32, SerializationTypeCode.Int32]), + + new(KnownAttributeKind.ComCompatibleVersion, InteropNamespace, "ComCompatibleVersionAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: + [ + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + ]), + + new(KnownAttributeKind.SpecialName, CompilerServicesNamespace, "SpecialNameAttribute", SpecialNameTargets), + + new(KnownAttributeKind.AllowPartiallyTrustedCallers, "System.Security", "AllowPartiallyTrustedCallersAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true), + + new(KnownAttributeKind.WindowsRuntimeImport, InteropNamespace + ".WindowsRuntime", + "WindowsRuntimeImportAttribute", CaTargets.TypeDef), + ]; + + private static CaTargets GetTarget(EntityRegistry.EntityBase owner) => owner switch + { + EntityRegistry.TypeDefinitionEntity => CaTargets.TypeDef, + EntityRegistry.TypeReferenceEntity => CaTargets.TypeRef, + EntityRegistry.MethodDefinitionEntity => CaTargets.MethodDef, + EntityRegistry.FieldDefinitionEntity => CaTargets.FieldDef, + EntityRegistry.ParameterEntity => CaTargets.ParamDef, + EntityRegistry.PropertyEntity => CaTargets.Property, + EntityRegistry.EventEntity => CaTargets.Event, + EntityRegistry.ModuleEntity => CaTargets.Module, + EntityRegistry.AssemblyEntity => CaTargets.Assembly, + _ => CaTargets.None, + }; +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs new file mode 100644 index 00000000000000..e070c29af39542 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -0,0 +1,434 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Text; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool ApplyMarshalAs( + in LoweringContext context, + in CustomAttributeValue arguments) + { + var descriptor = new BlobBuilder(); + if (!TryBuildMarshallingDescriptor(context, arguments, descriptor)) + { + return false; + } + + switch (context.Owner) + { + case EntityRegistry.FieldDefinitionEntity field: + field.MarshallingDescriptor = descriptor; + return true; + + case EntityRegistry.ParameterEntity parameter: + parameter.MarshallingDescriptor = descriptor; + return true; + + case EntityRegistry.PropertyEntity property: + ApplyMarshalAsToProperty(context.Registry, property, descriptor); + return true; + + default: + return context.InvalidTarget(); + } + } + + /// + /// Applies a property's marshalling descriptor to the getter's return parameter and to the + /// setter's last parameter, matching the native emitter. + /// + private static void ApplyMarshalAsToProperty( + EntityRegistry registry, + EntityRegistry.PropertyEntity property, + BlobBuilder descriptor) + { + EntityRegistry.TypeDefinitionEntity? containingType = FindContainingType(registry, property); + + foreach ((MethodSemanticsAttributes semantic, EntityRegistry.EntityBase accessor) in property.Accessors) + { + if (ResolveAccessor(registry, accessor, containingType) is not { } method) + { + continue; + } + + int targetSequence; + if (semantic == MethodSemanticsAttributes.Getter) + { + targetSequence = 0; + } + else if (semantic == MethodSemanticsAttributes.Setter) + { + if (!TryGetSignatureParameterCount(method.MethodSignature, out targetSequence) || targetSequence == 0) + { + continue; + } + } + else + { + continue; + } + + foreach (EntityRegistry.ParameterEntity parameter in method.Parameters) + { + if (parameter.Sequence == targetSequence) + { + // Each target gets its own builder so that the blobs stay independent. + var copy = new BlobBuilder(); + descriptor.WriteContentTo(copy); + parameter.MarshallingDescriptor = copy; + break; + } + } + } + } + + private static EntityRegistry.TypeDefinitionEntity? FindContainingType( + EntityRegistry registry, + EntityRegistry.PropertyEntity property) + { + foreach (var entity in registry.GetSeenEntities(TableIndex.TypeDef)) + { + if (entity is EntityRegistry.TypeDefinitionEntity type && type.Properties.Contains(property)) + { + return type; + } + } + + return null; + } + + /// + /// Property accessors are recorded as member references, so they are resolved to their local + /// method definitions the same way attribute owners are, falling back to a lookup in the + /// declaring type when the reference does not carry one. + /// + private static EntityRegistry.MethodDefinitionEntity? ResolveAccessor( + EntityRegistry registry, + EntityRegistry.EntityBase accessor, + EntityRegistry.TypeDefinitionEntity? containingType) + { + if (ResolveOwner(registry, accessor) is EntityRegistry.MethodDefinitionEntity resolved) + { + return resolved; + } + + if (accessor is not EntityRegistry.MemberReferenceEntity memberReference || containingType is null) + { + return null; + } + + foreach (EntityRegistry.MethodDefinitionEntity candidate in containingType.Methods) + { + if (candidate.Name == memberReference.Name + && candidate.MethodSignature is { } signature + && signature.ContentEquals(memberReference.Signature)) + { + return candidate; + } + } + + return null; + } + + private static unsafe bool TryGetSignatureParameterCount(BlobBuilder? signature, out int count) + { + count = 0; + if (signature is null) + { + return false; + } + + byte[] signatureBytes = signature.ToArray(); + fixed (byte* signaturePointer = signatureBytes) + { + var reader = new BlobReader(signaturePointer, signatureBytes.Length); + try + { + _ = reader.ReadByte(); + return reader.TryReadCompressedInteger(out count); + } + catch (BadImageFormatException) + { + return false; + } + } + } + + private static bool TryBuildMarshallingDescriptor( + in LoweringContext context, + in CustomAttributeValue arguments, + BlobBuilder descriptor) + { + CustomAttributeNamedArgument? arraySubTypeArgument = + FindNamedArgument(arguments, MarshalArraySubType); + CustomAttributeNamedArgument? safeArraySubTypeArgument = + FindNamedArgument(arguments, MarshalSafeArraySubType); + CustomAttributeNamedArgument? safeArrayUserDefinedSubTypeArgument = + FindNamedArgument(arguments, MarshalSafeArrayUserDefinedSubType); + CustomAttributeNamedArgument? sizeParamIndexArgument = + FindNamedArgument(arguments, MarshalSizeParamIndex); + CustomAttributeNamedArgument? sizeConstArgument = + FindNamedArgument(arguments, MarshalSizeConst); + CustomAttributeNamedArgument? marshalTypeArgument = + FindNamedArgument(arguments, MarshalType); + CustomAttributeNamedArgument? marshalTypeRefArgument = + FindNamedArgument(arguments, MarshalTypeRef); + CustomAttributeNamedArgument? marshalCookieArgument = + FindNamedArgument(arguments, MarshalCookie); + CustomAttributeNamedArgument? iidParameterIndexArgument = + FindNamedArgument(arguments, MarshalIidParameterIndex); + + // For the I2 overload the value was read as a 16-bit quantity and is zero-extended here, + // matching the native emitter which widens the I2 into a U4 before building the descriptor. + int nativeType = GetInt32(arguments.FixedArguments[0].Value); + if (!TryWriteCompressed(context, descriptor, nativeType)) + { + return false; + } + + switch (nativeType) + { + case (int)UnmanagedType.Interface: + case (int)UnmanagedType.IUnknown: + case (int)UnmanagedType.IDispatch: + if (iidParameterIndexArgument is { } iidParameterIndex) + { + int iidParameterIndexValue = GetInt32(iidParameterIndex.Value); + if (iidParameterIndexValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, iidParameterIndexValue)) + { + return false; + } + } + break; + + case (int)UnmanagedType.ByValArray: + if (safeArraySubTypeArgument is not null || sizeParamIndexArgument is not null) + { + return context.InvalidValue(); + } + + if (GetTarget(context.Owner) != CaTargets.FieldDef) + { + return context.InvalidTarget(); + } + + if (sizeConstArgument is { } sizeConst) + { + int sizeConstValue = GetInt32(sizeConst.Value); + if (sizeConstValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, sizeConstValue)) + { + return false; + } + } + else if (!TryWriteCompressed(context, descriptor, 1)) + { + return false; + } + + if (arraySubTypeArgument is { } arraySubType + && !TryWriteCompressed(context, descriptor, GetInt32(arraySubType.Value))) + { + return false; + } + break; + + case (int)UnmanagedType.ByValTStr: + if (sizeConstArgument is not { } fixedStringSize) + { + return context.InvalidValue(); + } + + if (arraySubTypeArgument is not null + || sizeParamIndexArgument is not null + || safeArraySubTypeArgument is not null) + { + return context.InvalidValue(); + } + + if (GetTarget(context.Owner) != CaTargets.FieldDef) + { + return context.InvalidTarget(); + } + + if (!TryWriteCompressed(context, descriptor, GetInt32(fixedStringSize.Value))) + { + return false; + } + break; + + case int value when value == (int)UnmanagedType.ByValStr: + if (GetTarget(context.Owner) != CaTargets.ParamDef) + { + return context.InvalidTarget(); + } + break; + + case (int)UnmanagedType.SafeArray: + if (arraySubTypeArgument is not null + || sizeParamIndexArgument is not null + || sizeConstArgument is not null) + { + return context.InvalidValue(); + } + + if (safeArraySubTypeArgument is { } safeArraySubType) + { + int safeArraySubTypeValue = GetInt32(safeArraySubType.Value); + if (!TryWriteCompressed(context, descriptor, safeArraySubTypeValue)) + { + return false; + } + + if (safeArrayUserDefinedSubTypeArgument is { } userDefinedSubType) + { + if (safeArraySubTypeValue != (int)VarEnum.VT_RECORD + && safeArraySubTypeValue != (int)VarEnum.VT_DISPATCH + && safeArraySubTypeValue != (int)VarEnum.VT_UNKNOWN) + { + return context.InvalidValue(); + } + + if (!TryWriteCountedString(context, descriptor, GetString(userDefinedSubType.Value))) + { + return false; + } + } + } + break; + + case (int)UnmanagedType.LPArray: + if (safeArraySubTypeArgument is not null) + { + return context.InvalidValue(); + } + + if (arraySubTypeArgument is { } lpArraySubType) + { + int arraySubTypeValue = GetInt32(lpArraySubType.Value); + if (arraySubTypeValue == (int)UnmanagedType.CustomMarshaler) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, arraySubTypeValue)) + { + return false; + } + } + else if (!TryWriteCompressed(context, descriptor, (int)UnmanagedType.Max)) + { + return false; + } + + if (sizeParamIndexArgument is { } sizeParamIndex) + { + int sizeParamIndexValue = GetInt32(sizeParamIndex.Value); + if (sizeParamIndexValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, GetInt16(sizeParamIndex.Value))) + { + return false; + } + + if (sizeConstArgument is { } indexedSizeConst) + { + int sizeConstValue = GetInt32(indexedSizeConst.Value); + if (sizeConstValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, sizeConstValue) + || !TryWriteCompressed(context, descriptor, UnmanagedType.ArraySizeParamIndexSpecified)) + { + return false; + } + } + } + else if (sizeConstArgument is { } unindexedSizeConst) + { + if (!TryWriteCompressed(context, descriptor, 0) + || !TryWriteCompressed(context, descriptor, GetInt32(unindexedSizeConst.Value)) + || !TryWriteCompressed(context, descriptor, 0)) + { + return false; + } + } + break; + + case (int)UnmanagedType.CustomMarshaler: + if (marshalTypeArgument is null && marshalTypeRefArgument is null) + { + return context.InvalidValue(); + } + + // Placeholders for the unmanaged type library GUID and the unmanaged type name. + if (!TryWriteCompressed(context, descriptor, 0) || !TryWriteCompressed(context, descriptor, 0)) + { + return false; + } + + string marshaler = marshalTypeArgument is { } marshalType + ? GetString(marshalType.Value) + : GetString(marshalTypeRefArgument!.Value.Value); + string cookie = marshalCookieArgument is { } marshalCookie + ? GetString(marshalCookie.Value) + : ""; + + if (!TryWriteCountedString(context, descriptor, marshaler) + || !TryWriteCountedString(context, descriptor, cookie)) + { + return false; + } + break; + } + + return true; + } + + private static bool TryWriteCompressed(in LoweringContext context, BlobBuilder builder, int value) + { + if (value < 0 || value > 0x1FFFFFFF) + { + return context.InvalidBlob(); + } + + builder.WriteCompressedInteger(value); + return true; + } + + private static bool TryWriteCountedString(in LoweringContext context, BlobBuilder builder, string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + if (!TryWriteCompressed(context, builder, bytes.Length)) + { + return false; + } + + builder.WriteBytes(bytes); + + return true; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs new file mode 100644 index 00000000000000..bc321fb1f52c07 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private const string DynamicSecurityMethodAttribute = "System.Security.DynamicSecurityMethodAttribute"; + private const string SuppressUnmanagedCodeSecurityAttribute = "System.Security.SuppressUnmanagedCodeSecurityAttribute"; + + private static bool IsSecurityAttribute(string @namespace, string name) => + @namespace == "System.Security" + && name is "DynamicSecurityMethodAttribute" or "SuppressUnmanagedCodeSecurityAttribute"; + + /// + /// Handles the two attributes that the native emitter recognizes by name outside the known + /// attribute table. + /// + private static bool ApplySecurityAttribute(in LoweringContext context, string @namespace, string name, out bool keep) + { + keep = true; + string fullName = @namespace.Length == 0 ? name : @namespace + "." + name; + + if (fullName == DynamicSecurityMethodAttribute) + { + if (context.Owner is not EntityRegistry.MethodDefinitionEntity method) + { + return false; + } + + method.MethodAttributes |= MethodAttributes.RequireSecObject; + keep = false; + return true; + } + + if (fullName == SuppressUnmanagedCodeSecurityAttribute) + { + switch (context.Owner) + { + case EntityRegistry.TypeDefinitionEntity type: + type.Attributes |= TypeAttributes.HasSecurity; + return true; + case EntityRegistry.MethodDefinitionEntity method: + method.MethodAttributes |= MethodAttributes.HasSecurity; + return true; + default: + return false; + } + } + + return false; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs new file mode 100644 index 00000000000000..f8876dbfc29adc --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection.Metadata.Ecma335; + +namespace ILAssembler; + +/// +/// Lowers "pseudo custom attributes" into the metadata flag bits and auxiliary table rows that they +/// represent, and suppresses the CustomAttribute row for the attributes that are not retained. +/// +/// +/// This mirrors RegMeta::DefineCustomAttribute in src/coreclr/md/compiler/custattr_emit.cpp, +/// which the native IL assembler relies on for the same behavior. Attributes are matched on the +/// namespace and name of the declaring type of the constructor only; the assembly the constructor +/// resolves to is deliberately not considered, matching the native implementation. +/// +internal static partial class PseudoCustomAttributes +{ + private static readonly Location s_unknownLocation = new(new SourceSpan(0, 0), new SourceText("", "")); + + public static void Lower(EntityRegistry registry, ImmutableArray.Builder diagnostics) + { + var attributes = new List(); + foreach (var entity in registry.GetSeenEntities(TableIndex.CustomAttribute)) + { + if (entity is EntityRegistry.CustomAttributeEntity attribute) + { + attributes.Add(attribute); + } + } + + if (attributes.Count == 0) + { + return; + } + + var lowered = new List(); + + // Attributes are processed in source order so that a later directive can override an + // earlier one, matching the native emitter which applies each attribute as it is defined. + foreach (var attribute in attributes) + { + if (attribute.Owner is null) + { + continue; + } + + if (!TryGetAttributeTypeName(attribute.Constructor, out string @namespace, out string name)) + { + continue; + } + + KnownAttribute? known = TryFindKnownAttribute(attribute.Constructor, @namespace, name); + if (known is null && !IsSecurityAttribute(@namespace, name)) + { + continue; + } + + var context = new LoweringContext(registry, diagnostics, attribute, @namespace, name); + if (known is not null) + { + // The native emitter abandons the whole DefineCustomAttribute call when a known + // attribute fails validation, so the row is never written regardless of KeepAttribute. + if (!Apply(context, known) || !known.KeepAttribute) + { + lowered.Add(attribute); + } + + continue; + } + + if (ApplySecurityAttribute(context, @namespace, name, out bool keepSecurityAttribute) + && !keepSecurityAttribute) + { + lowered.Add(attribute); + } + } + + registry.RemoveCustomAttributes(lowered); + } + + private readonly struct LoweringContext( + EntityRegistry registry, + ImmutableArray.Builder diagnostics, + EntityRegistry.CustomAttributeEntity attribute, + string @namespace, + string name) + { + public EntityRegistry Registry { get; } = registry; + public EntityRegistry.CustomAttributeEntity Attribute { get; } = attribute; + + /// + /// The attribute target. References to members and types defined in this module are only + /// resolved while metadata rows are written, which happens after this pass runs, so the + /// owner recorded during parsing is resolved to the entity it designates here. + /// + public EntityRegistry.EntityBase Owner { get; } = ResolveOwner(registry, attribute.Owner!); + + public string AttributeName { get; } = @namespace.Length == 0 ? name : @namespace + "." + name; + + public bool Error(string id, string message) + { + diagnostics.Add(new Diagnostic(id, DiagnosticSeverity.Error, message, Attribute.Location ?? s_unknownLocation)); + return false; + } + + public bool InvalidTarget() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidTarget, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidTarget, AttributeName)); + + public bool InvalidValue() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidValue, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidValue, AttributeName)); + + public bool InvalidBlob() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidBlob, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidBlob, AttributeName)); + + public bool InvalidGuid() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidGuid, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidGuid, AttributeName)); + + public bool UnknownArgument(string argumentName) => Error( + DiagnosticIds.PseudoCustomAttributeUnknownArgument, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeUnknownArgument, AttributeName, argumentName)); + + public bool RepeatedArgument(string argumentName) => Error( + DiagnosticIds.PseudoCustomAttributeRepeatedArgument, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeRepeatedArgument, AttributeName, argumentName)); + } + + + + + + +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs index a4366758e0e273..b9a076f6e17afb 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs @@ -986,5 +986,48 @@ .field public class [Alias]Contoso.External Value Assert.Single(reader.GetCustomAttributes(dependencyHandle)); Assert.Equal(dependencyHandle, externalType.ResolutionScope); } + + [Theory] + [InlineData("System.Security.AllowPartiallyTrustedCallersAttribute", ".ctor()", "( 01 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.TypeLibVersionAttribute", ".ctor(int32, int32)", "( 01 00 01 00 00 00 02 00 00 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.ComCompatibleVersionAttribute", ".ctor(int32, int32, int32, int32)", "( 01 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 00 00 )")] + public void PseudoCustomAttribute_OnAssembly_KeepsAttribute(string attributeType, string constructor, string value) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test + { + .custom instance void [mscorlib]{{attributeType}}::{{constructor}} = {{value}} + } + .class public auto ansi Test extends [mscorlib]System.Object + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(reader.GetAssemblyDefinition().GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_TypeLibVersionWithNegativeValue_ReportsDiagnostic() + { + string source = """ + .assembly extern mscorlib { } + .assembly test + { + .custom instance void [mscorlib]System.Runtime.InteropServices.TypeLibVersionAttribute::.ctor(int32, int32) = ( 01 00 FF FF FF FF 02 00 00 00 00 00 ) + } + .class public auto ansi Test extends [mscorlib]System.Object + { + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs index a6bb73eb2daae1..747422e45943cd 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -1069,5 +1069,370 @@ static string[] GetAttributeTypeNames( .Select(type => reader.GetString(type.Name)) .ToArray(); } + private const TypeAttributes SerializableType = (TypeAttributes)0x00002000; + private const TypeAttributes WindowsRuntimeType = (TypeAttributes)0x00004000; + private const TypeAttributes ExtendedLayoutType = (TypeAttributes)0x00000018; + private const FieldAttributes NotSerializedField = (FieldAttributes)0x0080; + + private static string TypeWithAttribute(string attributeType, string constructor = ".ctor()", string value = "( 01 00 00 00 )") => $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void [mscorlib]{{attributeType}}::{{constructor}} = {{value}} + } + """; + + private static TypeDefinition GetTestType(MetadataReader reader) => + reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test")); + + public static TheoryData PseudoAttributeTypeFlagData => new() + { + { "System.Runtime.InteropServices.ComImportAttribute", TypeAttributes.Import }, + { "System.SerializableAttribute", SerializableType }, + { "System.Runtime.CompilerServices.SpecialNameAttribute", TypeAttributes.SpecialName }, + { "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute", WindowsRuntimeType }, + }; + + [Theory] + [MemberData(nameof(PseudoAttributeTypeFlagData))] + public void PseudoCustomAttribute_OnType_LowersToFlagAndDropsAttribute(string attributeType, TypeAttributes expected) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(TypeWithAttribute(attributeType), new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(expected, testType.Attributes & expected); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Theory] + [InlineData("System.Runtime.InteropServices.GuidAttribute", ".ctor(string)", "( 01 00 24 30 31 32 33 34 35 36 37 2D 30 31 32 33 2D 30 31 32 33 2D 30 31 32 33 2D 30 30 31 31 32 32 33 33 34 34 35 35 00 00 )")] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 01 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 03 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.ClassInterfaceAttribute", ".ctor(int16)", "( 01 00 02 00 00 00 )")] + public void PseudoCustomAttribute_ValidateOnly_KeepsAttribute(string attributeType, string constructor, string value) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute(attributeType, constructor, value), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Theory] + [InlineData("System.Runtime.InteropServices.GuidAttribute", ".ctor(string)", "( 01 00 04 6E 6F 70 65 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidGuid)] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 07 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidValue)] + [InlineData("System.Runtime.InteropServices.ClassInterfaceAttribute", ".ctor(int16)", "( 01 00 09 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidValue)] + [InlineData("System.SerializableAttribute", ".ctor()", "( 01 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidTarget)] + public void PseudoCustomAttribute_InvalidValueOrTarget_ReportsDiagnostic( + string attributeType, + string constructor, + string value, + string expectedDiagnosticId) + { + // SerializableAttribute is only valid on a type, so applying it to a method is an invalid target. + string source = expectedDiagnosticId == DiagnosticIds.PseudoCustomAttributeInvalidTarget + ? $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]{{attributeType}}::{{constructor}} = {{value}} + ret + } + } + """ + : TypeWithAttribute(attributeType, constructor, value); + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(expectedDiagnosticId, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + + [Theory] + [InlineData("( 01 00 00 00 00 00 00 00 )", TypeAttributes.SequentialLayout)] + [InlineData("( 01 00 01 00 00 00 00 00 )", ExtendedLayoutType)] + [InlineData("( 01 00 02 00 00 00 00 00 )", TypeAttributes.ExplicitLayout)] + [InlineData("( 01 00 03 00 00 00 00 00 )", TypeAttributes.AutoLayout)] + public void PseudoCustomAttribute_StructLayout_SetsLayoutMask(string value, TypeAttributes expected) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(expected, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_StructLayout_NamedArgumentsSetClassLayoutAndCharSet() + { + // LayoutKind.Explicit with Pack = 4, Size = 16 and CharSet = Unicode (3). + string value = "( 01 00 02 00 00 00 03 00 53 08 04 50 61 63 6B 04 00 00 00 " + + "53 08 04 53 69 7A 65 10 00 00 00 " + + "53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 " + + "07 43 68 61 72 53 65 74 03 00 00 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(TypeAttributes.ExplicitLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Equal(TypeAttributes.UnicodeClass, testType.Attributes & TypeAttributes.StringFormatMask); + + var layout = testType.GetLayout(); + Assert.Equal(4, layout.PackingSize); + Assert.Equal(16, layout.Size); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_DynamicSecurityMethod_SetsRequireSecObjectAndIsDropped() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Security.DynamicSecurityMethodAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodAttributes.RequireSecObject, method.Attributes & MethodAttributes.RequireSecObject); + Assert.Empty(method.GetCustomAttributes()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PseudoCustomAttribute_SuppressUnmanagedCodeSecurity_SetsHasSecurityAndIsKept(bool onType) + { + string attribute = ".custom instance void [mscorlib]System.Security.SuppressUnmanagedCodeSecurityAttribute::.ctor() = ( 01 00 00 00 )"; + string source = onType + ? $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + {{attribute}} + } + """ + : $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + {{attribute}} + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + if (onType) + { + var testType = GetTestType(reader); + Assert.Equal(TypeAttributes.HasSecurity, testType.Attributes & TypeAttributes.HasSecurity); + Assert.Single(testType.GetCustomAttributes()); + } + else + { + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + Assert.Equal(MethodAttributes.HasSecurity, method.Attributes & MethodAttributes.HasSecurity); + Assert.Single(method.GetCustomAttributes()); + } + } + + [Fact] + public void PseudoCustomAttribute_UnknownNamedArgument_ReportsDiagnostic() + { + // StructLayoutAttribute with a named field named "Bogus". + string value = "( 01 00 00 00 00 00 01 00 53 08 05 42 6F 67 75 73 01 00 00 00 )"; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeUnknownArgument, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_StructLayout_ExplicitPackAndSizeDirectivesWin() + { + // LayoutKind.Sequential with Pack = 16, on a type that also declares .pack and .size. + // The native assembler emits the ClassLayout row for the explicit directives in a later + // phase than the one that applies the attribute, so the directives take precedence. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType + { + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 00 00 00 00 01 00 53 08 04 50 61 63 6B 10 00 00 00 ) + .pack 4 + .size 32 + .field public int32 Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var layout = GetTestType(reader).GetLayout(); + + Assert.Equal(4, layout.PackingSize); + Assert.Equal(32, layout.Size); + } + + [Fact] + public void PseudoCustomAttribute_NonPseudoAttribute_IsStillEmitted() + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.ObsoleteAttribute"), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_LoweredAttribute_DoesNotShiftRemainingAttributeOwners() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi First extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 ) + .custom instance void [mscorlib]System.SerializableAttribute::.ctor() = ( 01 00 00 00 ) + } + .class public auto ansi Second extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 ) + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + foreach (string typeName in new[] { "First", "Second" }) + { + var typeDefinition = reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == typeName)); + var attribute = reader.GetCustomAttribute(Assert.Single(typeDefinition.GetCustomAttributes())); + var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var declaringType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + Assert.Equal("ObsoleteAttribute", reader.GetString(declaringType.Name)); + } + + var first = reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "First")); + Assert.Equal(SerializableType, first.Attributes & SerializableType); + } + + [Fact] + public void PseudoCustomAttribute_ZeroArgDescriptor_MalformedBlobSkipped() + { + // SerializableAttribute has zero fixed and zero named-arg descriptors. The native + // emitter does not parse the blob at all for such attributes, so even a completely + // malformed blob (bad prolog or arbitrary bytes) must produce no diagnostics. + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.SerializableAttribute", ".ctor()", "( FF FF DE AD BE EF )"), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(SerializableType, testType.Attributes & SerializableType); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_FixedArgsNoNamedDescriptors_EverettBlobWithNoNamedCountAccepted() + { + // GuidAttribute has one fixed string arg and no named-arg descriptors. When the blob + // ends immediately after the fixed argument with no 2-byte named-arg count, the native + // emitter accepts it as Everett-compatible behavior. Compilation must succeed and the + // attribute row must be retained (GuidAttribute has KeepAttribute = true). + // Blob: 01 00 (prolog) 24 (SerString length = 36) + 36 UTF-8 bytes of GUID -- no trailing 00 00. + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute( + "System.Runtime.InteropServices.GuidAttribute", + ".ctor(string)", + "( 01 00 24 30 31 32 33 34 35 36 37 2D 30 31 32 33 2D 30 31 32 33 2D 30 31 32 33 2D 30 30 31 31 32 32 33 33 34 34 35 35 )"), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_NamedArgCount0x8000_TreatedAsSignedNegativeAndSkipped() + { + // The named-argument count is stored and compared as a signed INT16 in the native + // emitter. A count of 0x8000 (-32768 when sign-extended) causes the loop to execute + // zero times, so no named arguments are consumed. The fixed layout effect is applied + // and the CA row is dropped. + // Blob: 01 00 (prolog) 02 00 (I2 = LayoutKind.Explicit) 00 80 (count = 0x8000 LE). + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute( + "System.Runtime.InteropServices.StructLayoutAttribute", + ".ctor(int16)", + "( 01 00 02 00 00 80 )"), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(TypeAttributes.ExplicitLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_TruncatedFixedArg_ReportsInvalidBlobDiagnostic() + { + // A blob that is too short to supply all fixed arguments must produce + // PseudoCustomAttributeInvalidBlob and no output image in normal mode. + // Blob: 01 00 (valid prolog) 01 (1 byte; I4 requires 4 bytes) -- truncated. + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText( + TypeWithAttribute( + "System.Runtime.InteropServices.StructLayoutAttribute", + ".ctor(int32)", + "( 01 00 01 )"), + "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidBlob, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + Assert.Null(result); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs index 65c46d10d7afca..561f5b82705bd8 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs @@ -21,6 +21,8 @@ namespace ILAssembler.Tests { public class FieldTests { + private const FieldAttributes NotSerializedField = (FieldAttributes)0x0080; + [Fact] public void TrailingCustomAttribute_AttachesToField() { @@ -251,6 +253,51 @@ .field public volatile int32 VolatileField Assert.Equal(ConstantTypeCode.NullReference, nullConstant.TypeCode); } + [Theory] + [InlineData("System.NonSerializedAttribute", NotSerializedField)] + [InlineData("System.Runtime.CompilerServices.SpecialNameAttribute", FieldAttributes.SpecialName)] + public void PseudoCustomAttribute_OnField_LowersToFlagAndDropsAttribute(string attributeType, FieldAttributes expected) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .field public int32 Value + } + + .custom (field int32 Test::Value) instance void [mscorlib]{{attributeType}}::.ctor() = ( 01 00 00 00 ) + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + + Assert.Equal(expected, field.Attributes & expected); + Assert.Empty(field.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_FieldOffset_CreatesFieldLayoutAndDropsAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public explicit ansi Test extends [mscorlib]System.Object + { + .field public int32 Value + } + + .custom (field int32 Test::Value) instance void [mscorlib]System.Runtime.InteropServices.FieldOffsetAttribute::.ctor(uint32) = ( 01 00 08 00 00 00 00 00 ) + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + + Assert.Equal(8, field.GetOffset()); + Assert.Empty(field.GetCustomAttributes()); + } [Fact] public void FieldRVA_MultipleDataSections() diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs index 6ed929c08f0000..6bc2c4f5d9627e 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -1219,5 +1219,297 @@ private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) Assert.Equal("value", reader.GetString(parameter.Name)); return parameter.GetMarshallingDescriptor(); } + + private static MethodDefinition GetMethod(MetadataReader reader, string name) => + reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == name); + + [Fact] + public void PseudoCustomAttribute_PreserveSig_LowersToImplFlagAndIsDropped() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.PreserveSigAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "M"); + + Assert.Equal(MethodImplAttributes.PreserveSig, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + } + + private static string DllImportSource(string blob) => $$""" + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Native() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.DllImportAttribute::.ctor(string) = {{blob}} + ret + } + } + """; + + [Fact] + public void PseudoCustomAttribute_DllImport_CreatesImplMapAndIsDropped() + { + // DllImportAttribute("kernel32.dll") with no named arguments. + string blob = "( 01 00 0C 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(DllImportSource(blob), new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "Native"); + + Assert.Equal(MethodAttributes.PinvokeImpl, method.Attributes & MethodAttributes.PinvokeImpl); + Assert.Equal(MethodImplAttributes.PreserveSig, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + + var import = method.GetImport(); + Assert.Equal("kernel32.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); + // The entry point defaults to the method name. + Assert.Equal("Native", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CallingConventionWinApi, import.Attributes & MethodImportAttributes.CallingConventionMask); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_NamedArgumentsSetImportAttributes() + { + // DllImportAttribute("kernel32.dll") with EntryPoint = "GetLastError", CharSet = Unicode (3), + // CallingConvention = Cdecl (2), SetLastError = true, ExactSpelling = true and PreserveSig = false. + string blob = "( 01 00 0C 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 06 00 " + + "53 0E 0A 45 6E 74 72 79 50 6F 69 6E 74 0C 47 65 74 4C 61 73 74 45 72 72 6F 72 " + + "53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 07 43 68 61 72 53 65 74 03 00 00 00 " + + "53 55 30 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E 11 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E 02 00 00 00 " + + "53 02 0C 53 65 74 4C 61 73 74 45 72 72 6F 72 01 " + + "53 02 0D 45 78 61 63 74 53 70 65 6C 6C 69 6E 67 01 " + + "53 02 0B 50 72 65 73 65 72 76 65 53 69 67 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(DllImportSource(blob), new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "Native"); + var import = method.GetImport(); + + Assert.Equal("GetLastError", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CharSetUnicode, import.Attributes & MethodImportAttributes.CharSetMask); + Assert.Equal(MethodImportAttributes.CallingConventionCDecl, import.Attributes & MethodImportAttributes.CallingConventionMask); + Assert.Equal(MethodImportAttributes.SetLastError, import.Attributes & MethodImportAttributes.SetLastError); + Assert.Equal(MethodImportAttributes.ExactSpelling, import.Attributes & MethodImportAttributes.ExactSpelling); + Assert.Equal(default, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_EmptyModuleName_ReportsInvalidValue() + { + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics( + DllImportSource("( 01 00 00 00 00 )"), + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_ExplicitPinvokeImplWinsButModuleReferenceIsStillCreated() + { + // The native assembler emits the ImplMap row for an explicit pinvokeimpl clause in a + // later phase than the one that applies the attribute, so the clause takes precedence. + // The attribute's module reference is still resolved, and so is still emitted. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static pinvokeimpl("explicit.dll" as "ExplicitEntry" cdecl) void Native() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.DllImportAttribute::.ctor(string) = ( 01 00 08 61 74 74 72 2E 64 6C 6C 00 00 ) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var import = GetMethod(reader, "Native").GetImport(); + + Assert.Equal("explicit.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); + Assert.Equal("ExplicitEntry", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CallingConventionCDecl, import.Attributes & MethodImportAttributes.CallingConventionMask); + + Assert.Contains( + "attr.dll", + Enumerable.Range(1, reader.GetTableRowCount(TableIndex.ModuleRef)) + .Select(rid => reader.GetString( + reader.GetModuleReference(MetadataTokens.ModuleReferenceHandle(rid)).Name))); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnParameter_CreatesFieldMarshalAndIsDropped() + { + // MarshalAsAttribute(UnmanagedType.Bool) applied to the parameter via .param [1]. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32 value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "M"); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); + + Assert.Equal(ParameterAttributes.HasFieldMarshal, parameter.Attributes & ParameterAttributes.HasFieldMarshal); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(parameter.GetMarshallingDescriptor())); + Assert.Empty(parameter.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_ByValArrayOnField_EncodesSizeAndSubType() + { + // MarshalAsAttribute(UnmanagedType.ByValArray) with SizeConst = 4 and ArraySubType = I4. + string blob = "( 01 00 1E 00 00 00 02 00 " + + "53 08 09 53 69 7A 65 43 6F 6E 73 74 04 00 00 00 " + + "53 55 2C 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 55 6E 6D 61 6E 61 67 65 64 54 79 70 65 " + + "0C 41 72 72 61 79 53 75 62 54 79 70 65 07 00 00 00 )"; + + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType + { + .field public int32[] Values + .custom (field int32[] Test::Values) instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = {{blob}} + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + + Assert.Equal(FieldAttributes.HasFieldMarshal, field.Attributes & FieldAttributes.HasFieldMarshal); + Assert.Equal( + [(byte)UnmanagedType.ByValArray, 0x04, (byte)UnmanagedType.I4], + reader.GetBlobBytes(field.GetMarshallingDescriptor())); + Assert.Empty(field.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_ByValArrayOnParameter_ReportsInvalidTarget() + { + // UnmanagedType.ByValArray is only valid on fields. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32[] value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = ( 01 00 1E 00 00 00 00 00 ) + ret + } + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidTarget, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnProperty_AppliesToAccessorParameters() + { + // MarshalAsAttribute(UnmanagedType.Bool) on a property fans out to the getter's return + // parameter and to the setter's last parameter. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public instance int32 get_Value() cil managed + { + ldc.i4.0 + ret + } + .method public instance void set_Value(int32 'value') cil managed + { + ret + } + .property instance int32 Value() + { + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + .get instance int32 Test::get_Value() + .set instance void Test::set_Value(int32) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var property = reader.GetPropertyDefinition(Assert.Single(reader.PropertyDefinitions)); + Assert.Empty(property.GetCustomAttributes()); + + var getterReturn = reader.GetParameter(GetMethod(reader, "get_Value").GetParameters().Single()); + Assert.Equal(0, getterReturn.SequenceNumber); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(getterReturn.GetMarshallingDescriptor())); + + var setterValue = reader.GetParameter(GetMethod(reader, "set_Value").GetParameters() + .Single(handle => reader.GetParameter(handle).SequenceNumber == 1)); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(setterValue.GetMarshallingDescriptor())); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_CustomMarshaler_EncodesMarshalerAndCookie() + { + // MarshalAsAttribute(UnmanagedType.CustomMarshaler) with MarshalType = "M" and MarshalCookie = "C". + string blob = "( 01 00 2C 00 00 00 02 00 " + + "53 0E 0B 4D 61 72 73 68 61 6C 54 79 70 65 01 4D " + + "53 0E 0D 4D 61 72 73 68 61 6C 43 6F 6F 6B 69 65 01 43 )"; + + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(object value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = {{blob}} + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var parameter = reader.GetParameter(Assert.Single(GetMethod(reader, "M").GetParameters())); + + // Native type, empty GUID placeholder, empty native type name placeholder, marshaler name, cookie. + Assert.Equal( + [(byte)UnmanagedType.CustomMarshaler, 0x00, 0x00, 0x01, (byte)'M', 0x01, (byte)'C'], + reader.GetBlobBytes(parameter.GetMarshallingDescriptor())); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs index 900af3c5c59d44..20279ec14f5cb7 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs @@ -159,6 +159,110 @@ .method public static void FlaggedImplMethod() flags(0x1000) { } Assert.Equal((MethodImplAttributes)0x1000, methods["FlaggedImplMethod"].ImplAttributes); } + [Fact] + public void PseudoCustomAttribute_SpecialNameOnMethod_LowersToFlagAndDropsAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.SpecialNameAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodAttributes.SpecialName, method.Attributes & MethodAttributes.SpecialName); + Assert.Empty(method.GetCustomAttributes()); + } + + [Theory] + [InlineData("08 00", MethodImplAttributes.NoInlining)] + [InlineData("00 20", MethodImplAttributes.Async)] + public void PseudoCustomAttribute_MethodImpl_LowersToImplAttributeAndDropsAttribute( + string valueBytes, + MethodImplAttributes expected) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor(int16) = ( 01 00 {{valueBytes}} 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(expected, method.ImplAttributes & expected); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MethodImplWithMethodCodeTypeNamedArgument_LowersToImplAttributeAndDropsAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor() = ( 01 00 01 00 53 55 2E 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 43 6F 6D 70 69 6C 65 72 53 65 72 76 69 63 65 73 2E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 0E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodImplAttributes.Native, method.ImplAttributes & MethodImplAttributes.CodeTypeMask); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MethodImplInvalidValue_ReportsDiagnostic() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + ret + } + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + [Fact] public void MethodBodyDirectives_EmitRawInstructionLocalsInitializationAndMappedData() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs index 3931daef2d36e1..16671a51242d72 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs @@ -229,6 +229,37 @@ .method public static void M( Assert.Equal((ParameterAttributes)8, parameters["rawFlags"].Attributes); } + [Theory] + [InlineData("System.Runtime.InteropServices.InAttribute", ParameterAttributes.In)] + [InlineData("System.Runtime.InteropServices.OutAttribute", ParameterAttributes.Out)] + [InlineData("System.Runtime.InteropServices.OptionalAttribute", ParameterAttributes.Optional)] + public void PseudoCustomAttribute_OnParameter_LowersToFlagAndDropsAttribute(string attributeType, ParameterAttributes expected) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32 x) cil managed + { + .param [1] + .custom instance void [mscorlib]{{attributeType}}::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); + + Assert.Equal(expected, parameter.Attributes & expected); + Assert.Empty(parameter.GetCustomAttributes()); + } + [Fact] public void UnnamedInstanceParam_EmitsParamRow() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs index 7e508c4907f526..505163557c9979 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Xunit; @@ -84,5 +85,80 @@ .event MyDelegate Changed Assert.Equal("Value", reader.GetString(property.Name)); Assert.Equal("Changed", reader.GetString(@event.Name)); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PseudoCustomAttribute_SpecialNameOnPropertyOrEvent_LowersToFlagAndDropsAttribute(bool onProperty) + { + string propertyAttribute = onProperty + ? ".custom instance void [mscorlib]System.Runtime.CompilerServices.SpecialNameAttribute::.ctor() = ( 01 00 00 00 )" + : ""; + string eventAttribute = onProperty + ? "" + : ".custom instance void [mscorlib]System.Runtime.CompilerServices.SpecialNameAttribute::.ctor() = ( 01 00 00 00 )"; + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi MyDelegate extends [mscorlib]System.MulticastDelegate + { + .method public specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed + { + } + .method public virtual instance void Invoke() runtime managed + { + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public hidebysig specialname instance int32 get_Value() cil managed + { + ldc.i4.0 + ret + } + + .method public hidebysig specialname instance void add_Changed(class MyDelegate value) cil managed + { + ret + } + + .method public hidebysig specialname instance void remove_Changed(class MyDelegate value) cil managed + { + ret + } + + .property int32 Value() + { + {{propertyAttribute}} + .get instance int32 Test::get_Value() + } + + .event MyDelegate Changed + { + {{eventAttribute}} + .addon instance void Test::add_Changed(class MyDelegate) + .removeon instance void Test::remove_Changed(class MyDelegate) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var property = reader.GetPropertyDefinition(reader.PropertyDefinitions.Single()); + var @event = reader.GetEventDefinition(reader.EventDefinitions.Single()); + + if (onProperty) + { + Assert.Equal(PropertyAttributes.SpecialName, property.Attributes & PropertyAttributes.SpecialName); + Assert.Empty(property.GetCustomAttributes()); + Assert.Equal(default, @event.Attributes & EventAttributes.SpecialName); + } + else + { + Assert.Equal(EventAttributes.SpecialName, @event.Attributes & EventAttributes.SpecialName); + Assert.Empty(@event.GetCustomAttributes()); + Assert.Equal(default, property.Attributes & PropertyAttributes.SpecialName); + } + } } } From c8e479af55d73d94d14b6e451eb45128db8b1f30 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 28 Jul 2026 17:32:59 -0700 Subject: [PATCH 2/6] Simplify pseudo attribute lowering context Use a reference type for LoweringContext and remove unnecessary readonly-reference parameter modifiers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c177aade-9940-4e79-a9df-6747dcc22bb0 --- .../PseudoCustomAttributes.Application.cs | 34 +++++++++---------- .../PseudoCustomAttributes.Decoding.cs | 6 ++-- .../PseudoCustomAttributes.MarshalAs.cs | 12 +++---- .../PseudoCustomAttributes.Security.cs | 2 +- .../src/ILAssembler/PseudoCustomAttributes.cs | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs index 5dd6ca25e75014..98cbfcc4319aa3 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -12,7 +12,7 @@ namespace ILAssembler; internal static partial class PseudoCustomAttributes { - private static bool Apply(in LoweringContext context, KnownAttribute known) + private static bool Apply(LoweringContext context, KnownAttribute known) { if ((known.Targets & GetTarget(context.Owner)) == 0) { @@ -54,31 +54,31 @@ private static bool Apply(in LoweringContext context, KnownAttribute known) }; } - private static bool AddTypeFlags(in LoweringContext context, TypeAttributes flags) + private static bool AddTypeFlags(LoweringContext context, TypeAttributes flags) { ((EntityRegistry.TypeDefinitionEntity)context.Owner).Attributes |= flags; return true; } - private static bool AddFieldFlags(in LoweringContext context, FieldAttributes flags) + private static bool AddFieldFlags(LoweringContext context, FieldAttributes flags) { ((EntityRegistry.FieldDefinitionEntity)context.Owner).Attributes |= flags; return true; } - private static bool AddParameterFlags(in LoweringContext context, ParameterAttributes flags) + private static bool AddParameterFlags(LoweringContext context, ParameterAttributes flags) { ((EntityRegistry.ParameterEntity)context.Owner).Attributes |= flags; return true; } - private static bool AddMethodImplFlags(in LoweringContext context, MethodImplAttributes flags) + private static bool AddMethodImplFlags(LoweringContext context, MethodImplAttributes flags) { ((EntityRegistry.MethodDefinitionEntity)context.Owner).ImplementationAttributes |= flags; return true; } - private static bool ApplySpecialName(in LoweringContext context) + private static bool ApplySpecialName(LoweringContext context) { switch (context.Owner) { @@ -103,7 +103,7 @@ private static bool ApplySpecialName(in LoweringContext context) } private static bool ValidateNonNegative( - in LoweringContext context, + LoweringContext context, ImmutableArray> arguments) { foreach (CustomAttributeTypedArgument argument in arguments) @@ -118,8 +118,8 @@ private static bool ValidateNonNegative( } private static bool ApplyGuid( - in LoweringContext context, - in CustomAttributeValue arguments) + LoweringContext context, + CustomAttributeValue arguments) { // The value is only validated; the attribute itself is still emitted. string guid = GetString(arguments.FixedArguments[0].Value); @@ -129,8 +129,8 @@ private static bool ApplyGuid( } private static bool ApplyFieldOffset( - in LoweringContext context, - in CustomAttributeValue arguments) + LoweringContext context, + CustomAttributeValue arguments) { uint offset = GetUInt32(arguments.FixedArguments[0].Value); if (offset > int.MaxValue) @@ -143,9 +143,9 @@ private static bool ApplyFieldOffset( } private static bool ApplyMethodImpl( - in LoweringContext context, + LoweringContext context, KnownAttributeKind kind, - in CustomAttributeValue arguments) + CustomAttributeValue arguments) { var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; CustomAttributeNamedArgument? codeTypeArgument = @@ -183,9 +183,9 @@ private static bool ApplyMethodImpl( } private static bool ApplyStructLayout( - in LoweringContext context, + LoweringContext context, KnownAttributeKind kind, - in CustomAttributeValue arguments) + CustomAttributeValue arguments) { var type = (EntityRegistry.TypeDefinitionEntity)context.Owner; @@ -259,8 +259,8 @@ private static bool ApplyStructLayout( } private static bool ApplyDllImport( - in LoweringContext context, - in CustomAttributeValue arguments) + LoweringContext context, + CustomAttributeValue arguments) { var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs index c4a672e136783c..0b96622ed379e3 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs @@ -72,7 +72,7 @@ private static CustomAttributeTypedArgument ReadArgument( } private static unsafe bool TryParseArguments( - in LoweringContext context, + LoweringContext context, KnownAttribute known, out CustomAttributeValue arguments) { @@ -129,7 +129,7 @@ private static unsafe bool TryParseArguments( } private static bool TryParseNamedArguments( - in LoweringContext context, + LoweringContext context, KnownAttribute known, ref BlobReader reader, out ImmutableArray> namedArguments) @@ -231,7 +231,7 @@ private static bool TryParseNamedArguments( } private static CustomAttributeNamedArgument? FindNamedArgument( - in CustomAttributeValue arguments, + CustomAttributeValue arguments, string name) { foreach (CustomAttributeNamedArgument argument in arguments.NamedArguments) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs index e070c29af39542..32cbbc6e547490 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -14,8 +14,8 @@ internal static partial class PseudoCustomAttributes { private static bool ApplyMarshalAs( - in LoweringContext context, - in CustomAttributeValue arguments) + LoweringContext context, + CustomAttributeValue arguments) { var descriptor = new BlobBuilder(); if (!TryBuildMarshallingDescriptor(context, arguments, descriptor)) @@ -164,8 +164,8 @@ private static unsafe bool TryGetSignatureParameterCount(BlobBuilder? signature, } private static bool TryBuildMarshallingDescriptor( - in LoweringContext context, - in CustomAttributeValue arguments, + LoweringContext context, + CustomAttributeValue arguments, BlobBuilder descriptor) { CustomAttributeNamedArgument? arraySubTypeArgument = @@ -408,7 +408,7 @@ private static bool TryBuildMarshallingDescriptor( return true; } - private static bool TryWriteCompressed(in LoweringContext context, BlobBuilder builder, int value) + private static bool TryWriteCompressed(LoweringContext context, BlobBuilder builder, int value) { if (value < 0 || value > 0x1FFFFFFF) { @@ -419,7 +419,7 @@ private static bool TryWriteCompressed(in LoweringContext context, BlobBuilder b return true; } - private static bool TryWriteCountedString(in LoweringContext context, BlobBuilder builder, string value) + private static bool TryWriteCountedString(LoweringContext context, BlobBuilder builder, string value) { byte[] bytes = Encoding.UTF8.GetBytes(value); if (!TryWriteCompressed(context, builder, bytes.Length)) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs index bc321fb1f52c07..01d3b9399086df 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs @@ -19,7 +19,7 @@ private static bool IsSecurityAttribute(string @namespace, string name) => /// Handles the two attributes that the native emitter recognizes by name outside the known /// attribute table. /// - private static bool ApplySecurityAttribute(in LoweringContext context, string @namespace, string name, out bool keep) + private static bool ApplySecurityAttribute(LoweringContext context, string @namespace, string name, out bool keep) { keep = true; string fullName = @namespace.Length == 0 ? name : @namespace + "." + name; diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs index f8876dbfc29adc..2edfe3e4424d45 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs @@ -83,7 +83,7 @@ public static void Lower(EntityRegistry registry, ImmutableArray.Bui registry.RemoveCustomAttributes(lowered); } - private readonly struct LoweringContext( + private sealed class LoweringContext( EntityRegistry registry, ImmutableArray.Builder diagnostics, EntityRegistry.CustomAttributeEntity attribute, From f6e4ac43e482c3399e877eb83e529e3bd86257aa Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 28 Jul 2026 17:04:40 -0700 Subject: [PATCH 3/6] Read int16 instead of int32 as the attribute field is int16 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs index 32cbbc6e547490..16ef7289b5c52b 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -341,13 +341,13 @@ private static bool TryBuildMarshallingDescriptor( if (sizeParamIndexArgument is { } sizeParamIndex) { - int sizeParamIndexValue = GetInt32(sizeParamIndex.Value); + int sizeParamIndexValue = GetInt16(sizeParamIndex.Value); if (sizeParamIndexValue < 0) { return context.InvalidValue(); } - if (!TryWriteCompressed(context, descriptor, GetInt16(sizeParamIndex.Value))) + if (!TryWriteCompressed(context, descriptor, sizeParamIndexValue)) { return false; } From f5eb81f3eae9757e16635d2656f27abd19460dfe Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 29 Jul 2026 12:13:00 -0700 Subject: [PATCH 4/6] Use public obsolete serialization flags with local suppression in ilasm --- .../ilasm/src/ILAssembler/MetadataExtensions.cs | 6 ------ .../PseudoCustomAttributes.Application.cs | 6 ++++-- .../ILAssembler.Tests/CustomAttributeTests.cs | 14 +++++--------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs index 0f267e3fe4b463..94401cf5194058 100644 --- a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs +++ b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs @@ -9,11 +9,6 @@ namespace ILAssembler; internal static class MetadataExtensions { - extension(FieldAttributes) - { - public static FieldAttributes NotSerializedFlag => (FieldAttributes)0x0080; - } - extension(MethodImplAttributes) { public static MethodImplAttributes UserMask => @@ -33,7 +28,6 @@ internal static class MetadataExtensions { public static TypeAttributes ExtendedLayout => (TypeAttributes)0x18; public static TypeAttributes Forwarder => (TypeAttributes)0x00200000; - public static TypeAttributes SerializableFlag => (TypeAttributes)0x00002000; } extension(UnmanagedType) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs index 98cbfcc4319aa3..9de78ccc76acfb 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -33,8 +33,10 @@ private static bool Apply(LoweringContext context, KnownAttribute known) GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ComInterfaceType.Last || context.InvalidValue(), KnownAttributeKind.ClassInterface => GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ClassInterfaceType.Last || context.InvalidValue(), - KnownAttributeKind.Serializable => AddTypeFlags(context, TypeAttributes.SerializableFlag), - KnownAttributeKind.NonSerialized => AddFieldFlags(context, FieldAttributes.NotSerializedFlag), +#pragma warning disable SYSLIB0050 // Formatter-based serialization APIs are obsolete. + KnownAttributeKind.Serializable => AddTypeFlags(context, TypeAttributes.Serializable), + KnownAttributeKind.NonSerialized => AddFieldFlags(context, FieldAttributes.NotSerialized), +#pragma warning restore SYSLIB0050 // Formatter-based serialization APIs are obsolete. KnownAttributeKind.MethodImpl1 or KnownAttributeKind.MethodImpl2 or KnownAttributeKind.MethodImpl3 => ApplyMethodImpl(context, known.Kind, arguments), KnownAttributeKind.MarshalAs1 or KnownAttributeKind.MarshalAs2 => ApplyMarshalAs(context, arguments), diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs index 747422e45943cd..442cbc96d0390e 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -1069,10 +1069,6 @@ static string[] GetAttributeTypeNames( .Select(type => reader.GetString(type.Name)) .ToArray(); } - private const TypeAttributes SerializableType = (TypeAttributes)0x00002000; - private const TypeAttributes WindowsRuntimeType = (TypeAttributes)0x00004000; - private const TypeAttributes ExtendedLayoutType = (TypeAttributes)0x00000018; - private const FieldAttributes NotSerializedField = (FieldAttributes)0x0080; private static string TypeWithAttribute(string attributeType, string constructor = ".ctor()", string value = "( 01 00 00 00 )") => $$""" .assembly extern mscorlib { } @@ -1090,9 +1086,9 @@ private static TypeDefinition GetTestType(MetadataReader reader) => public static TheoryData PseudoAttributeTypeFlagData => new() { { "System.Runtime.InteropServices.ComImportAttribute", TypeAttributes.Import }, - { "System.SerializableAttribute", SerializableType }, + { "System.SerializableAttribute", TypeAttributes.Serializable }, { "System.Runtime.CompilerServices.SpecialNameAttribute", TypeAttributes.SpecialName }, - { "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute", WindowsRuntimeType }, + { "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute", TypeAttributes.WindowsRuntime }, }; [Theory] @@ -1157,7 +1153,7 @@ .method public static void M() cil managed [Theory] [InlineData("( 01 00 00 00 00 00 00 00 )", TypeAttributes.SequentialLayout)] - [InlineData("( 01 00 01 00 00 00 00 00 )", ExtendedLayoutType)] + [InlineData("( 01 00 01 00 00 00 00 00 )", TypeAttributes.ExtendedLayout)] [InlineData("( 01 00 02 00 00 00 00 00 )", TypeAttributes.ExplicitLayout)] [InlineData("( 01 00 03 00 00 00 00 00 )", TypeAttributes.AutoLayout)] public void PseudoCustomAttribute_StructLayout_SetsLayoutMask(string value, TypeAttributes expected) @@ -1352,7 +1348,7 @@ .class public auto ansi Second extends [mscorlib]System.Object var first = reader.GetTypeDefinition(reader.TypeDefinitions .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "First")); - Assert.Equal(SerializableType, first.Attributes & SerializableType); + Assert.Equal(TypeAttributes.Serializable, first.Attributes & TypeAttributes.Serializable); } [Fact] @@ -1367,7 +1363,7 @@ public void PseudoCustomAttribute_ZeroArgDescriptor_MalformedBlobSkipped() var reader = pe.GetMetadataReader(); var testType = GetTestType(reader); - Assert.Equal(SerializableType, testType.Attributes & SerializableType); + Assert.Equal(TypeAttributes.Serializable, testType.Attributes & TypeAttributes.Serializable); Assert.Empty(testType.GetCustomAttributes()); } From 916428e0d22d6dd6d9edc6add7174268e55701db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:43:25 +0000 Subject: [PATCH 5/6] Remove duplicated ILAssembler interop test block Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- .../tests/ILAssembler.Tests/InteropTests.cs | 445 ------------------ 1 file changed, 445 deletions(-) diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs index 6bc2c4f5d9627e..9cb39842614b1e 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -775,451 +775,6 @@ private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) return parameter.GetMarshallingDescriptor(); } - [Fact] - public void MarshalLpStrParameter_EmitsMarshallingDescriptorBlob() - { - string source = """ - .assembly extern mscorlib { } - .assembly test { } - .class public auto ansi Test extends [mscorlib]System.Object - { - .method public static void Print(string marshal(lpstr) text) cil managed - { - ret - } - } - """; - - using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); - var reader = pe.GetMetadataReader(); - - var method = reader.MethodDefinitions - .Select(h => reader.GetMethodDefinition(h)) - .First(m => reader.GetString(m.Name) == "Print"); - var parameterHandle = Assert.Single(method.GetParameters()); - var parameter = reader.GetParameter(parameterHandle); - var descriptor = parameter.GetMarshallingDescriptor(); - - Assert.False(descriptor.IsNil); - Assert.Equal("text", reader.GetString(parameter.Name)); - Assert.Equal(1, reader.GetTableRowCount(TableIndex.FieldMarshal)); - BlobReader descriptorReader = reader.GetBlobReader(descriptor); - Assert.Equal((byte)UnmanagedType.LPStr, descriptorReader.ReadByte()); - Assert.Equal(0, descriptorReader.RemainingBytes); - } - - [Fact] - public void PinvokeMethod_WithAlias_EmitsImportNameAndModuleReference() - { - string source = """ - .assembly extern mscorlib { } - .assembly test { } - .module test.dll - .class public auto ansi beforefieldinit NativeMethods extends [mscorlib]System.Object - { - .method public static pinvokeimpl("kernel32.dll" as "GetTickCount" winapi) - int32 GetTickCountManaged() cil managed preservesig - { - } - } - """; - - using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); - var reader = pe.GetMetadataReader(); - var method = reader.MethodDefinitions - .Select(reader.GetMethodDefinition) - .First(definition => reader.GetString(definition.Name) == "GetTickCountManaged"); - var import = method.GetImport(); - var moduleRef = reader.GetModuleReference(import.Module); - - Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); - Assert.Equal("GetTickCount", reader.GetString(import.Name)); - Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); - } - - [Fact] - public void PinvokeMethod_EmitsImportMetadata() - { - string source = """ - .assembly extern mscorlib { } - .assembly test { } - .module test.dll - .class public auto ansi beforefieldinit NativeMethods extends [mscorlib]System.Object - { - .method public static pinvokeimpl("kernel32.dll" winapi) - int32 GetCurrentProcessId() cil managed preservesig - { - } - } - """; - - using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); - var reader = pe.GetMetadataReader(); - var method = reader.MethodDefinitions - .Select(reader.GetMethodDefinition) - .First(definition => reader.GetString(definition.Name) == "GetCurrentProcessId"); - var import = method.GetImport(); - var moduleRef = reader.GetModuleReference(import.Module); - - Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); - Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); - Assert.Equal(1, reader.GetTableRowCount(TableIndex.ModuleRef)); - } - - [Theory] - [InlineData("nomangle", (int)MethodImportAttributes.ExactSpelling)] - [InlineData("ansi", (int)MethodImportAttributes.CharSetAnsi)] - [InlineData("unicode", (int)MethodImportAttributes.CharSetUnicode)] - [InlineData("autochar", (int)MethodImportAttributes.CharSetAuto)] - [InlineData("lasterr", (int)MethodImportAttributes.SetLastError)] - [InlineData("cdecl", (int)MethodImportAttributes.CallingConventionCDecl)] - [InlineData("stdcall", (int)MethodImportAttributes.CallingConventionStdCall)] - [InlineData("thiscall", (int)MethodImportAttributes.CallingConventionThisCall)] - [InlineData("fastcall", (int)MethodImportAttributes.CallingConventionFastCall)] - [InlineData("bestfit:on", (int)MethodImportAttributes.BestFitMappingEnable)] - [InlineData("bestfit:off", (int)MethodImportAttributes.BestFitMappingDisable)] - [InlineData("charmaperror:on", (int)MethodImportAttributes.ThrowOnUnmappableCharEnable)] - [InlineData("charmaperror:off", (int)MethodImportAttributes.ThrowOnUnmappableCharDisable)] - [InlineData("flags(0x1234)", 0x1234)] - public void PinvokeAttribute_EmitsExpectedImportFlags(string attribute, int expectedFlags) - { - string source = $$""" - .assembly extern mscorlib { } - .assembly test { } - .class public auto ansi NativeMethods extends [mscorlib]System.Object - { - .method public static pinvokeimpl("native.dll" {{attribute}}) - void M() cil managed preservesig - { - } - } - """; - - using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); - var reader = pe.GetMetadataReader(); - var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); - var import = method.GetImport(); - - Assert.Equal((MethodImportAttributes)expectedFlags, import.Attributes); - Assert.Equal("M", reader.GetString(method.Name)); - Assert.Equal(string.Empty, reader.GetString(import.Name)); - Assert.Equal("native.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); - } - - [Theory] - [InlineData("bool", 0x02)] - [InlineData("int8", 0x03)] - [InlineData("uint8", 0x04)] - [InlineData("int16", 0x05)] - [InlineData("uint16", 0x06)] - [InlineData("int32", 0x07)] - [InlineData("uint32", 0x08)] - [InlineData("int64", 0x09)] - [InlineData("uint64", 0x0A)] - [InlineData("float32", 0x0B)] - [InlineData("float64", 0x0C)] - [InlineData("currency", 0x0F)] - [InlineData("bstr", 0x13)] - [InlineData("lpstr", 0x14)] - [InlineData("lpwstr", 0x15)] - [InlineData("lptstr", 0x16)] - [InlineData("iunknown", 0x19)] - [InlineData("idispatch", 0x1A)] - [InlineData("struct", 0x1B)] - [InlineData("interface", 0x1C)] - [InlineData("int", 0x1F)] - [InlineData("uint", 0x20)] - [InlineData("byvalstr", 0x22)] - [InlineData("ansi bstr", 0x23)] - [InlineData("tbstr", 0x24)] - [InlineData("method", 0x26)] - [InlineData("as any", 0x28)] - [InlineData("lpstruct", 0x2B)] - [InlineData("error", 0x2D)] - public void MarshalSimpleNativeType_EmitsExpectedDescriptor(string nativeType, int expectedTypeCode) - { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - Assert.Empty(diagnostics); - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); - - Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); - Assert.Equal(0, descriptor.RemainingBytes); - } - - [Theory] - [InlineData("fixed systring[5]")] - [InlineData("fixed array[3] int16")] - [InlineData("iunknown(iidparam=2)")] - [InlineData("idispatch(iidparam=3)")] - [InlineData("interface(iidparam=4)")] - [InlineData("int32*")] - [InlineData("int32[]")] - [InlineData("int32[3]")] - [InlineData("int32[3+2]")] - [InlineData("int32[+2]")] - public void MarshalNativeTypeWithPayload_EmitsExpectedDescriptor(string nativeType) - { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - if (nativeType == "int32*") - { - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); - } - else - { - Assert.Empty(diagnostics); - } - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); - - switch (nativeType) - { - case "fixed systring[5]": - Assert.Equal((byte)UnmanagedType.ByValTStr, descriptor.ReadByte()); - Assert.Equal(5, descriptor.ReadCompressedInteger()); - break; - - case "fixed array[3] int16": - Assert.Equal((byte)UnmanagedType.ByValArray, descriptor.ReadByte()); - Assert.Equal(3, descriptor.ReadCompressedInteger()); - Assert.Equal((byte)UnmanagedType.I2, descriptor.ReadByte()); - break; - - case "iunknown(iidparam=2)": - Assert.Equal((byte)UnmanagedType.IUnknown, descriptor.ReadByte()); - Assert.Equal(2, descriptor.ReadCompressedInteger()); - break; - - case "idispatch(iidparam=3)": - Assert.Equal((byte)UnmanagedType.IDispatch, descriptor.ReadByte()); - Assert.Equal(3, descriptor.ReadCompressedInteger()); - break; - - case "interface(iidparam=4)": - Assert.Equal((byte)UnmanagedType.Interface, descriptor.ReadByte()); - Assert.Equal(4, descriptor.ReadCompressedInteger()); - break; - - case "int32*": - Assert.Equal(0x10, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - break; - - case "int32[]": - Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - break; - - case "int32[3]": - Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - Assert.Equal(0, descriptor.ReadCompressedInteger()); - Assert.Equal(3, descriptor.ReadCompressedInteger()); - Assert.Equal(0, descriptor.ReadCompressedInteger()); - break; - - case "int32[3+2]": - Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - Assert.Equal(2, descriptor.ReadCompressedInteger()); - Assert.Equal(3, descriptor.ReadCompressedInteger()); - Assert.Equal(1, descriptor.ReadCompressedInteger()); - break; - - case "int32[+2]": - Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - Assert.Equal(2, descriptor.ReadCompressedInteger()); - break; - } - - Assert.Equal(0, descriptor.RemainingBytes); - } - - [Theory] - [InlineData("custom(\"Marshaller.Type\", \"cookie\")", false)] - [InlineData("custom(\"guid\", \"native\", \"Marshaller.Type\", \"cookie\")", true)] - public void MarshalCustomMarshaller_EmitsStructuredDescriptor( - string nativeType, - bool usesDeprecatedForm) - { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - if (usesDeprecatedForm) - { - Assert.Contains( - diagnostics, - diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedCustomMarshaller); - } - else - { - Assert.Empty(diagnostics); - } - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); - - Assert.Equal((byte)UnmanagedType.CustomMarshaler, descriptor.ReadByte()); - if (usesDeprecatedForm) - { - Assert.Equal("guid", descriptor.ReadSerializedString()); - Assert.Equal("native", descriptor.ReadSerializedString()); - } - else - { - Assert.Equal(0, descriptor.ReadCompressedInteger()); - Assert.Equal(0, descriptor.ReadCompressedInteger()); - } - - Assert.Equal("Marshaller.Type", descriptor.ReadSerializedString()); - Assert.Equal("cookie", descriptor.ReadSerializedString()); - Assert.Equal(0, descriptor.RemainingBytes); - } - - [Theory] - [InlineData("variant", (int)VarEnum.VT_VARIANT)] - [InlineData("currency", (int)VarEnum.VT_CY)] - [InlineData("void", (int)VarEnum.VT_VOID)] - [InlineData("bool", (int)VarEnum.VT_BOOL)] - [InlineData("int8", (int)VarEnum.VT_I1)] - [InlineData("int16", (int)VarEnum.VT_I2)] - [InlineData("int32", (int)VarEnum.VT_I4)] - [InlineData("int64", (int)VarEnum.VT_I8)] - [InlineData("float32", (int)VarEnum.VT_R4)] - [InlineData("float64", (int)VarEnum.VT_R8)] - [InlineData("uint8", (int)VarEnum.VT_UI1)] - [InlineData("uint16", (int)VarEnum.VT_UI2)] - [InlineData("uint32", (int)VarEnum.VT_UI4)] - [InlineData("uint64", (int)VarEnum.VT_UI8)] - [InlineData("decimal", (int)VarEnum.VT_DECIMAL)] - [InlineData("date", (int)VarEnum.VT_DATE)] - [InlineData("bstr", (int)VarEnum.VT_BSTR)] - [InlineData("lpstr", (int)VarEnum.VT_LPSTR)] - [InlineData("lpwstr", (int)VarEnum.VT_LPWSTR)] - [InlineData("iunknown", (int)VarEnum.VT_UNKNOWN)] - [InlineData("idispatch", (int)VarEnum.VT_DISPATCH)] - [InlineData("safearray", (int)VarEnum.VT_SAFEARRAY)] - [InlineData("int", (int)VarEnum.VT_INT)] - [InlineData("uint", (int)VarEnum.VT_UINT)] - [InlineData("error", (int)VarEnum.VT_ERROR)] - [InlineData("hresult", (int)VarEnum.VT_HRESULT)] - [InlineData("carray", (int)VarEnum.VT_CARRAY)] - [InlineData("userdefined", (int)VarEnum.VT_USERDEFINED)] - [InlineData("record", (int)VarEnum.VT_RECORD)] - [InlineData("filetime", (int)VarEnum.VT_FILETIME)] - [InlineData("blob", (int)VarEnum.VT_BLOB)] - [InlineData("stream", (int)VarEnum.VT_STREAM)] - [InlineData("storage", (int)VarEnum.VT_STORAGE)] - [InlineData("streamed_object", (int)VarEnum.VT_STREAMED_OBJECT)] - [InlineData("stored_object", (int)VarEnum.VT_STORED_OBJECT)] - [InlineData("blob_object", (int)VarEnum.VT_BLOB_OBJECT)] - [InlineData("cf", (int)VarEnum.VT_CF)] - [InlineData("clsid", (int)VarEnum.VT_CLSID)] - [InlineData("int32[]", (int)(VarEnum.VT_I4 | VarEnum.VT_ARRAY))] - [InlineData("int32 vector", (int)(VarEnum.VT_I4 | VarEnum.VT_VECTOR))] - [InlineData("int32&", (int)(VarEnum.VT_I4 | VarEnum.VT_BYREF))] - public void MarshalSafeArrayVariantType_EmitsExpectedDescriptor(string variantType, int expectedVariantType) - { - var (diagnostics, image) = CompileParameterMarshallingDescriptor($"safearray {variantType}"); - Assert.Empty(diagnostics); - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobHandle descriptorHandle = GetOnlyMarshallingDescriptor(reader); - BlobReader descriptor = reader.GetBlobReader(descriptorHandle); - - Assert.True( - descriptor.ReadByte() == (byte)UnmanagedType.SafeArray, - Convert.ToHexString(reader.GetBlobBytes(descriptorHandle))); - Assert.Equal(expectedVariantType, descriptor.ReadCompressedInteger()); - Assert.Equal(0, descriptor.ReadCompressedInteger()); - Assert.Equal(0, descriptor.RemainingBytes); - } - - [Theory] - [InlineData("variant", 0x0E)] - [InlineData("syschar", 0x0D)] - [InlineData("void", 0x01)] - [InlineData("decimal", 0x11)] - [InlineData("date", 0x12)] - [InlineData("objectref", 0x18)] - [InlineData("nested struct", 0x21)] - public void DeprecatedMarshalNativeType_EmitsDescriptorAndWarning( - string nativeType, - int expectedTypeCode) - { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); - - Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); - Assert.Equal(0, descriptor.RemainingBytes); - } - - [Fact] - public void MarshalSafeArrayWithUserDefinedType_EmitsVariantAndTypeName() - { - var (diagnostics, image) = - CompileParameterMarshallingDescriptor("safearray int32, \"Contoso.Element\""); - Assert.Empty(diagnostics); - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobHandle descriptorHandle = GetOnlyMarshallingDescriptor(reader); - BlobReader descriptor = reader.GetBlobReader(descriptorHandle); - - Assert.True( - descriptor.ReadByte() == (byte)UnmanagedType.SafeArray, - Convert.ToHexString(reader.GetBlobBytes(descriptorHandle))); - Assert.Equal((int)VarEnum.VT_I4, descriptor.ReadCompressedInteger()); - Assert.Equal("Contoso.Element", descriptor.ReadSerializedString()); - Assert.Equal(0, descriptor.RemainingBytes); - } - - private static (ImmutableArray Diagnostics, ImmutableArray Image) - CompileParameterMarshallingDescriptor(string nativeType) - { - string source = $$""" - .assembly extern mscorlib { } - .assembly test { } - .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object - { - .method public static void M(object marshal({{nativeType}}) value) cil managed - { - ret - } - } - """; - - var compiler = new DocumentCompiler(); - var (diagnostics, result) = compiler.Compile( - new SourceText(source, "test.il"), - _ => { Assert.Fail("Expected no includes"); return default; }, - _ => { Assert.Fail("Expected no resources"); return default; }, - new Options()); - - Assert.NotNull(result); - var image = new BlobBuilder(); - result!.Serialize(image); - return (diagnostics, image.ToImmutableArray()); - } - - private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) - { - var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); - var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); - Assert.Equal(1, reader.GetTableRowCount(TableIndex.FieldMarshal)); - Assert.Equal("value", reader.GetString(parameter.Name)); - return parameter.GetMarshallingDescriptor(); - } - private static MethodDefinition GetMethod(MetadataReader reader, string name) => reader.MethodDefinitions .Select(reader.GetMethodDefinition) From 0a78466f8dd93961d8c75ebd5cfc4e9cd41b72c4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 14:30:35 -0700 Subject: [PATCH 6/6] Address pseudoattribute review feedback Make invalid pseudoattribute lowering atomic, preserve native precedence across repeated layout attributes and explicit directives, and avoid resolving external module type references as local definitions. Handle generic accessor signatures and add parity tests for the reviewed edge cases. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 806a996a-9fcc-42b7-a902-cac954582b24 --- .../Actions/GrammarActions.BuildImage.cs | 2 +- .../Actions/GrammarActions.Members.Class.cs | 2 + .../ilasm/src/ILAssembler/EntityRegistry.cs | 4 +- .../PseudoCustomAttributes.Application.cs | 42 +++++--- .../PseudoCustomAttributes.MarshalAs.cs | 7 +- .../src/ILAssembler/PseudoCustomAttributes.cs | 6 -- .../ILAssembler.Tests/CustomAttributeTests.cs | 97 ++++++++++++++++++- .../tests/ILAssembler.Tests/InteropTests.cs | 90 ++++++++++++++++- .../tests/ILAssembler.Tests/MethodTests.cs | 62 ++++++++++++ .../ILAssembler.Tests/PropertyEventTests.cs | 4 +- 10 files changed, 286 insertions(+), 30 deletions(-) diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs index fc2aa1ed2564cd..9763765e2a9b02 100644 --- a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs +++ b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs @@ -54,6 +54,7 @@ internal sealed partial class GrammarActions ValidateDataLabelFixups(); ImmutableArray validatedExports = ValidateExports(validatedVTableAssociations, machine); + PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics); // Return early if there are structural errors that prevent building valid metadata. // However, allow errors in method bodies (ILA0016-0019) to pass through so we can @@ -66,7 +67,6 @@ internal sealed partial class GrammarActions } BlobBuilder ilStream = new(); - PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics); Blob mvidFixup = _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames, _options.Deterministic); // MetadataRootBuilder only supports module-wide validation suppression, which is // required because wrapped GenericParam numbers intentionally violate table ordering. diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Members.Class.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Members.Class.cs index 000a81d67bcf7d..528cceb9ce2d99 100644 --- a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Members.Class.cs +++ b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Members.Class.cs @@ -79,6 +79,7 @@ internal void SetClassSize(IToken token) _currentTypeDefinition.PeekOrDefault() is { } currentType) { currentType.ClassSize = ParseInt32(token); + currentType.HasExplicitClassSize = true; } } @@ -88,6 +89,7 @@ internal void SetClassPackingSize(IToken token) _currentTypeDefinition.PeekOrDefault() is { } currentType) { currentType.PackingSize = ParseInt32(token); + currentType.HasExplicitPackingSize = true; } } diff --git a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs index fc759ee18d9d77..6036622370fa87 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -890,7 +890,7 @@ public TypeDefinitionEntity GetOrCreateTypeDefinition(TypeDefinitionEntity? cont ? FindTypeDefinition(null, typeReference.Namespace, typeReference.Name) : null; - case ModuleEntity or ModuleReferenceEntity: + case ModuleEntity: return FindTypeDefinition(null, typeReference.Namespace, typeReference.Name); default: @@ -1925,6 +1925,8 @@ static string CreateReflectionNotation(TypeDefinitionEntity typeDefinition) // ClassLayout table fields public int? PackingSize { get; set; } public int? ClassSize { get; set; } + public bool HasExplicitPackingSize { get; set; } + public bool HasExplicitClassSize { get; set; } } public sealed class TypeReferenceEntity(EntityBase resolutionScope, string @namespace, string name) : TypeEntity, IHasReflectionNotation diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs index 9de78ccc76acfb..8c2af349876f98 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -100,7 +100,7 @@ private static bool ApplySpecialName(LoweringContext context) @event.Attributes |= EventAttributes.SpecialName; return true; default: - return context.InvalidValue(); + return context.InvalidTarget(); } } @@ -152,6 +152,7 @@ private static bool ApplyMethodImpl( var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; CustomAttributeNamedArgument? codeTypeArgument = FindNamedArgument(arguments, MethodImplCodeType); + MethodImplAttributes fixedAttributes = 0; if (kind is not KnownAttributeKind.MethodImpl1) { @@ -165,22 +166,23 @@ private static bool ApplyMethodImpl( return context.InvalidValue(); } - method.ImplementationAttributes |= (MethodImplAttributes)value; - - if (codeTypeArgument is null) - { - return true; - } + fixedAttributes = (MethodImplAttributes)value; } + bool applyCodeType = kind is KnownAttributeKind.MethodImpl1 || codeTypeArgument is not null; ushort codeType = codeTypeArgument is { } argument ? GetUInt16(argument.Value) : (ushort)0; - if ((codeType & ~(ushort)MethodImplAttributes.CodeTypeMask) != 0) + if (applyCodeType && (codeType & ~(ushort)MethodImplAttributes.CodeTypeMask) != 0) { return context.InvalidValue(); } - method.ImplementationAttributes = - (method.ImplementationAttributes & ~MethodImplAttributes.CodeTypeMask) | (MethodImplAttributes)codeType; + method.ImplementationAttributes |= fixedAttributes; + if (applyCodeType) + { + method.ImplementationAttributes = + (method.ImplementationAttributes & ~MethodImplAttributes.CodeTypeMask) | (MethodImplAttributes)codeType; + } + return true; } @@ -212,6 +214,8 @@ private static bool ApplyStructLayout( } TypeAttributes attributes = (type.Attributes & ~TypeAttributes.LayoutMask) | layout; + int? packingSize = null; + int? classSize = null; if (FindNamedArgument(arguments, StructLayoutPack) is { } packArgument) { @@ -221,9 +225,7 @@ private static bool ApplyStructLayout( return context.InvalidValue(); } - // An explicit .pack directive wins: the native assembler emits the ClassLayout row for - // explicit directives in a later phase than the one that applies this attribute. - type.PackingSize ??= (int)pack; + packingSize = (int)pack; } if (FindNamedArgument(arguments, StructLayoutSize) is { } sizeArgument) @@ -234,8 +236,7 @@ private static bool ApplyStructLayout( return context.InvalidValue(); } - // An explicit .size directive wins, for the same reason as .pack above. - type.ClassSize ??= (int)size; + classSize = (int)size; } if (FindNamedArgument(arguments, StructLayoutCharSet) is { } charSetArgument) @@ -257,6 +258,17 @@ private static bool ApplyStructLayout( } type.Attributes = attributes; + // Explicit directives are emitted after pseudo custom attributes by native ilasm, so they + // win regardless of source order. Otherwise, later attributes overwrite earlier ones. + if (packingSize is not null && !type.HasExplicitPackingSize) + { + type.PackingSize = packingSize; + } + if (classSize is not null && !type.HasExplicitClassSize) + { + type.ClassSize = classSize; + } + return true; } diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs index 16ef7289b5c52b..f75e8e8bfb7b9e 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -153,7 +153,12 @@ private static unsafe bool TryGetSignatureParameterCount(BlobBuilder? signature, var reader = new BlobReader(signaturePointer, signatureBytes.Length); try { - _ = reader.ReadByte(); + var header = new SignatureHeader(reader.ReadByte()); + if (header.IsGeneric && !reader.TryReadCompressedInteger(out _)) + { + return false; + } + return reader.TryReadCompressedInteger(out count); } catch (BadImageFormatException) diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs index 2edfe3e4424d45..8aebe59a1c0972 100644 --- a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs @@ -132,10 +132,4 @@ public bool RepeatedArgument(string argumentName) => Error( DiagnosticIds.PseudoCustomAttributeRepeatedArgument, string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeRepeatedArgument, AttributeName, argumentName)); } - - - - - - } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs index 442cbc96d0390e..7fa69ba46f5d63 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -1083,7 +1083,23 @@ private static TypeDefinition GetTestType(MetadataReader reader) => reader.GetTypeDefinition(reader.TypeDefinitions .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test")); - public static TheoryData PseudoAttributeTypeFlagData => new() + private static (ImmutableArray Diagnostics, ImmutableArray Image) + CompileErrorTolerant(string source) + { + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + Assert.NotNull(result); + var image = new BlobBuilder(); + result!.Serialize(image); + return (diagnostics, image.ToImmutableArray()); + } + + public static TheoryData PseudoAttributeTypeFlagData { get; } = new() { { "System.Runtime.InteropServices.ComImportAttribute", TypeAttributes.Import }, { "System.SerializableAttribute", TypeAttributes.Serializable }, @@ -1192,6 +1208,59 @@ public void PseudoCustomAttribute_StructLayout_NamedArgumentsSetClassLayoutAndCh Assert.Empty(testType.GetCustomAttributes()); } + [Fact] + public void PseudoCustomAttribute_LaterStructLayoutAttributeOverwritesEarlierAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed Test extends [mscorlib]System.ValueType + { + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 00 00 00 00 02 00 53 08 04 50 61 63 6B 04 00 00 00 53 08 04 53 69 7A 65 10 00 00 00 ) + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 02 00 00 00 02 00 53 08 04 50 61 63 6B 08 00 00 00 53 08 04 53 69 7A 65 20 00 00 00 ) + .field public int32 Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + var layout = testType.GetLayout(); + + Assert.Equal(TypeAttributes.ExplicitLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Equal(8, layout.PackingSize); + Assert.Equal(32, layout.Size); + } + + [Fact] + public void PseudoCustomAttribute_InvalidStructLayoutDoesNotPartiallyOverwriteEarlierAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed Test extends [mscorlib]System.ValueType + { + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 00 00 00 00 03 00 53 08 04 50 61 63 6B 04 00 00 00 53 08 04 53 69 7A 65 10 00 00 00 53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 07 43 68 61 72 53 65 74 03 00 00 00 ) + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 02 00 00 00 02 00 53 08 04 50 61 63 6B 08 00 00 00 53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 07 43 68 61 72 53 65 74 01 00 00 00 ) + .field public int32 Value + } + """; + + var (diagnostics, image) = CompileErrorTolerant(source); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + var layout = testType.GetLayout(); + + Assert.Equal(TypeAttributes.SequentialLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Equal(TypeAttributes.UnicodeClass, testType.Attributes & TypeAttributes.StringFormatMask); + Assert.Equal(4, layout.PackingSize); + Assert.Equal(16, layout.Size); + } + [Fact] public void PseudoCustomAttribute_DynamicSecurityMethod_SetsRequireSecObjectAndIsDropped() { @@ -1305,6 +1374,32 @@ .field public int32 Value Assert.Equal(32, layout.Size); } + [Fact] + public void PseudoCustomAttribute_ModuleScopedOwnerDoesNotResolveToCurrentModule() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .module extern Other.netmodule + .class public auto ansi Contoso.External extends [mscorlib]System.Object + { + } + .custom (class [.module Other.netmodule]Contoso.External) instance void [mscorlib]System.SerializableAttribute::.ctor() = ( 01 00 00 00 ) + """; + + var (diagnostics, image) = CompileErrorTolerant(source); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidTarget, diagnostic.Id); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + var localType = reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "External")); + + Assert.Equal(default, localType.Attributes & TypeAttributes.Serializable); + Assert.Equal(0, reader.GetTableRowCount(TableIndex.CustomAttribute)); + } + [Fact] public void PseudoCustomAttribute_NonPseudoAttribute_IsStillEmitted() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs index 9cb39842614b1e..6509ae8b5aaed8 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -866,6 +866,20 @@ public void PseudoCustomAttribute_DllImport_NamedArgumentsSetImportAttributes() Assert.Empty(method.GetCustomAttributes()); } + [Fact] + public void PseudoCustomAttribute_DllImportExplicitZeroCallingConventionDoesNotDefaultToWinApi() + { + string blob = "( 01 00 0C 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 01 00 " + + "53 55 30 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E " + + "11 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E 00 00 00 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(DllImportSource(blob), new Options()); + var reader = pe.GetMetadataReader(); + MethodImport import = GetMethod(reader, "Native").GetImport(); + + Assert.Equal(default, import.Attributes & MethodImportAttributes.CallingConventionMask); + } + [Fact] public void PseudoCustomAttribute_DllImport_EmptyModuleName_ReportsInvalidValue() { @@ -920,7 +934,7 @@ .assembly extern mscorlib { } .assembly test { } .class public auto ansi Test extends [mscorlib]System.Object { - .method public static void M(int32 value) cil managed + .method public static void M(int32 'value') cil managed { .param [1] .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) @@ -978,7 +992,7 @@ .assembly extern mscorlib { } .assembly test { } .class public auto ansi Test extends [mscorlib]System.Object { - .method public static void M(int32[] value) cil managed + .method public static void M(int32[] 'value') cil managed { .param [1] .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = ( 01 00 1E 00 00 00 00 00 ) @@ -1035,6 +1049,76 @@ .property instance int32 Value() Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(setterValue.GetMarshallingDescriptor())); } + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnPropertyHandlesGenericSetterSignature() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void set_Value(int32 'value') cil managed + { + ret + } + .property int32 Value() + { + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + .set void Test::set_Value<[2]>(int32) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var setterValue = reader.GetParameter(GetMethod(reader, "set_Value").GetParameters() + .Single(handle => reader.GetParameter(handle).SequenceNumber == 1)); + + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(setterValue.GetMarshallingDescriptor())); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAsNegativeSizeParamIndexReportsInvalidValue() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32[] 'value') cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = ( 01 00 2A 00 00 00 01 00 53 06 0E 53 69 7A 65 50 61 72 61 6D 49 6E 64 65 78 FF FF ) + ret + } + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAsNegativeByValTStrSizeReportsInvalidBlob() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType + { + .field public string Value + .custom (field string Test::Value) instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = ( 01 00 17 00 00 00 01 00 53 08 09 53 69 7A 65 43 6F 6E 73 74 FF FF FF FF ) + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidBlob, diagnostic.Id); + } + [Fact] public void PseudoCustomAttribute_MarshalAs_CustomMarshaler_EncodesMarshalerAndCookie() { @@ -1048,7 +1132,7 @@ .assembly extern mscorlib { } .assembly test { } .class public auto ansi Test extends [mscorlib]System.Object { - .method public static void M(object value) cil managed + .method public static void M(object 'value') cil managed { .param [1] .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = {{blob}} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs index 20279ec14f5cb7..b54b8004c77c32 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs @@ -263,6 +263,68 @@ .method public static void M() cil managed Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); } + [Fact] + public void PseudoCustomAttribute_InvalidMethodCodeTypeDoesNotApplyFixedFlags() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor(int16) = ( 01 00 08 00 01 00 53 55 2E 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 43 6F 6D 70 69 6C 65 72 53 65 72 76 69 63 65 73 2E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 0E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 04 00 00 00 ) + ret + } + } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + Assert.NotNull(result); + + var image = new BlobBuilder(); + result!.Serialize(image); + using var pe = new PEReader(image.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(default, method.ImplAttributes & MethodImplAttributes.NoInlining); + } + + [Fact] + public void PseudoCustomAttribute_ParameterlessMethodImplClearsExistingCodeType() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() native unmanaged + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor() = ( 01 00 00 00 ) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(default, method.ImplAttributes & MethodImplAttributes.CodeTypeMask); + } + [Fact] public void MethodBodyDirectives_EmitRawInstructionLocalsInitializationAndMappedData() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs index 505163557c9979..370280cd777e4d 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs @@ -117,12 +117,12 @@ .method public hidebysig specialname instance int32 get_Value() cil managed ret } - .method public hidebysig specialname instance void add_Changed(class MyDelegate value) cil managed + .method public hidebysig specialname instance void add_Changed(class MyDelegate 'value') cil managed { ret } - .method public hidebysig specialname instance void remove_Changed(class MyDelegate value) cil managed + .method public hidebysig specialname instance void remove_Changed(class MyDelegate 'value') cil managed { ret }