diff --git a/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs b/src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs index ab104268eeeaab..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 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/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/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..6036622370fa87 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: + 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); @@ -1872,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 @@ -2023,7 +2078,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 +2143,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 +2160,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 +2184,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..94401cf5194058 100644 --- a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs +++ b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs @@ -3,17 +3,40 @@ using System.Reflection; using System.Reflection.Metadata; +using System.Runtime.InteropServices; namespace ILAssembler; internal static class MetadataExtensions { + 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; } + extension(UnmanagedType) + { + public static int ArraySizeParamIndexSpecified => 0x0001; + public static UnmanagedType ByValStr => (UnmanagedType)0x22; + public static UnmanagedType Max => (UnmanagedType)0x50; + } + extension(DeclarativeSecurityAction) { public static DeclarativeSecurityAction Request => (DeclarativeSecurityAction)1; @@ -35,3 +58,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..8c2af349876f98 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -0,0 +1,370 @@ +// 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(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(), +#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), + 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(LoweringContext context, TypeAttributes flags) + { + ((EntityRegistry.TypeDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddFieldFlags(LoweringContext context, FieldAttributes flags) + { + ((EntityRegistry.FieldDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddParameterFlags(LoweringContext context, ParameterAttributes flags) + { + ((EntityRegistry.ParameterEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddMethodImplFlags(LoweringContext context, MethodImplAttributes flags) + { + ((EntityRegistry.MethodDefinitionEntity)context.Owner).ImplementationAttributes |= flags; + return true; + } + + private static bool ApplySpecialName(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.InvalidTarget(); + } + } + + private static bool ValidateNonNegative( + LoweringContext context, + ImmutableArray> arguments) + { + foreach (CustomAttributeTypedArgument argument in arguments) + { + if (GetInt32(argument.Value) < 0) + { + return context.InvalidValue(); + } + } + + return true; + } + + private static bool ApplyGuid( + LoweringContext context, + 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( + LoweringContext context, + 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( + LoweringContext context, + KnownAttributeKind kind, + CustomAttributeValue arguments) + { + var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; + CustomAttributeNamedArgument? codeTypeArgument = + FindNamedArgument(arguments, MethodImplCodeType); + MethodImplAttributes fixedAttributes = 0; + + 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(); + } + + fixedAttributes = (MethodImplAttributes)value; + } + + bool applyCodeType = kind is KnownAttributeKind.MethodImpl1 || codeTypeArgument is not null; + ushort codeType = codeTypeArgument is { } argument ? GetUInt16(argument.Value) : (ushort)0; + if (applyCodeType && (codeType & ~(ushort)MethodImplAttributes.CodeTypeMask) != 0) + { + return context.InvalidValue(); + } + + method.ImplementationAttributes |= fixedAttributes; + if (applyCodeType) + { + method.ImplementationAttributes = + (method.ImplementationAttributes & ~MethodImplAttributes.CodeTypeMask) | (MethodImplAttributes)codeType; + } + + return true; + } + + private static bool ApplyStructLayout( + LoweringContext context, + KnownAttributeKind kind, + 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; + int? packingSize = null; + int? classSize = null; + + if (FindNamedArgument(arguments, StructLayoutPack) is { } packArgument) + { + uint pack = GetUInt32(packArgument.Value); + if (pack > 128 || (pack & (pack - 1)) != 0) + { + return context.InvalidValue(); + } + + packingSize = (int)pack; + } + + if (FindNamedArgument(arguments, StructLayoutSize) is { } sizeArgument) + { + uint size = GetUInt32(sizeArgument.Value); + if (size > int.MaxValue) + { + return context.InvalidValue(); + } + + 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; + // 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; + } + + private static bool ApplyDllImport( + LoweringContext context, + 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..0b96622ed379e3 --- /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( + 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( + 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( + 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..f75e8e8bfb7b9e --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -0,0 +1,439 @@ +// 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( + LoweringContext context, + 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 + { + var header = new SignatureHeader(reader.ReadByte()); + if (header.IsGeneric && !reader.TryReadCompressedInteger(out _)) + { + return false; + } + + return reader.TryReadCompressedInteger(out count); + } + catch (BadImageFormatException) + { + return false; + } + } + } + + private static bool TryBuildMarshallingDescriptor( + LoweringContext context, + 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 = GetInt16(sizeParamIndex.Value); + if (sizeParamIndexValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, sizeParamIndexValue)) + { + 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(LoweringContext context, BlobBuilder builder, int value) + { + if (value < 0 || value > 0x1FFFFFFF) + { + return context.InvalidBlob(); + } + + builder.WriteCompressedInteger(value); + return true; + } + + private static bool TryWriteCountedString(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..01d3b9399086df --- /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(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..8aebe59a1c0972 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs @@ -0,0 +1,135 @@ +// 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 sealed class 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..7fa69ba46f5d63 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -1069,5 +1069,461 @@ static string[] GetAttributeTypeNames( .Select(type => reader.GetString(type.Name)) .ToArray(); } + + 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")); + + 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 }, + { "System.Runtime.CompilerServices.SpecialNameAttribute", TypeAttributes.SpecialName }, + { "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute", TypeAttributes.WindowsRuntime }, + }; + + [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 )", 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) + { + 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_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() + { + 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_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() + { + 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(TypeAttributes.Serializable, first.Attributes & TypeAttributes.Serializable); + } + + [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(TypeAttributes.Serializable, testType.Attributes & TypeAttributes.Serializable); + 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..6509ae8b5aaed8 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -775,16 +775,22 @@ private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) 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 MarshalLpStrParameter_EmitsMarshallingDescriptorBlob() + 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 Print(string marshal(lpstr) text) cil managed + .method public static void M() cil managed { + .custom instance void [mscorlib]System.Runtime.InteropServices.PreserveSigAttribute::.ctor() = ( 01 00 00 00 ) ret } } @@ -792,432 +798,357 @@ .method public static void Print(string marshal(lpstr) text) cil managed using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "M"); - 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.Equal(MethodImplAttributes.PreserveSig, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + } - 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); + 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 PinvokeMethod_WithAlias_EmitsImportNameAndModuleReference() + 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_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() + { + 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 beforefieldinit NativeMethods extends [mscorlib]System.Object + .class public auto ansi Test extends [mscorlib]System.Object { - .method public static pinvokeimpl("kernel32.dll" as "GetTickCount" winapi) - int32 GetTickCountManaged() cil managed preservesig + .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 method = reader.MethodDefinitions - .Select(reader.GetMethodDefinition) - .First(definition => reader.GetString(definition.Name) == "GetTickCountManaged"); - var import = method.GetImport(); - var moduleRef = reader.GetModuleReference(import.Module); + var import = GetMethod(reader, "Native").GetImport(); - Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); - Assert.Equal("GetTickCount", reader.GetString(import.Name)); - Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); + 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 PinvokeMethod_EmitsImportMetadata() + public void PseudoCustomAttribute_MarshalAs_OnParameter_CreatesFieldMarshalAndIsDropped() { + // MarshalAsAttribute(UnmanagedType.Bool) applied to the parameter via .param [1]. string source = """ .assembly extern mscorlib { } .assembly test { } - .module test.dll - .class public auto ansi beforefieldinit NativeMethods extends [mscorlib]System.Object + .class public auto ansi Test extends [mscorlib]System.Object { - .method public static pinvokeimpl("kernel32.dll" winapi) - int32 GetCurrentProcessId() cil managed preservesig + .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 = reader.MethodDefinitions - .Select(reader.GetMethodDefinition) - .First(definition => reader.GetString(definition.Name) == "GetCurrentProcessId"); - var import = method.GetImport(); - var moduleRef = reader.GetModuleReference(import.Module); + var method = GetMethod(reader, "M"); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); - Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); - Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); - Assert.Equal(1, reader.GetTableRowCount(TableIndex.ModuleRef)); + Assert.Equal(ParameterAttributes.HasFieldMarshal, parameter.Attributes & ParameterAttributes.HasFieldMarshal); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(parameter.GetMarshallingDescriptor())); + Assert.Empty(parameter.GetCustomAttributes()); } - [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) + [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 auto ansi NativeMethods extends [mscorlib]System.Object + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType { - .method public static pinvokeimpl("native.dll" {{attribute}}) - void M() cil managed preservesig - { - } + .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 method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); - var import = method.GetImport(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); - 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)); + 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()); } - [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) + [Fact] + public void PseudoCustomAttribute_MarshalAs_ByValArrayOnParameter_ReportsInvalidTarget() { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - Assert.Empty(diagnostics); - - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + // 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 + } + } + """; - Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); - Assert.Equal(0, descriptor.RemainingBytes); + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidTarget, diagnostic.Id); } - [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) + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnProperty_AppliesToAccessorParameters() { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - if (nativeType == "int32*") - { - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); - } - else - { - Assert.Empty(diagnostics); - } + // 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 = new PEReader(image); + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); 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; + var property = reader.GetPropertyDefinition(Assert.Single(reader.PropertyDefinitions)); + Assert.Empty(property.GetCustomAttributes()); - case "int32[+2]": - Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); - Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); - Assert.Equal(2, descriptor.ReadCompressedInteger()); - break; - } + var getterReturn = reader.GetParameter(GetMethod(reader, "get_Value").GetParameters().Single()); + Assert.Equal(0, getterReturn.SequenceNumber); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(getterReturn.GetMarshallingDescriptor())); - Assert.Equal(0, descriptor.RemainingBytes); + 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())); } - [Theory] - [InlineData("custom(\"Marshaller.Type\", \"cookie\")", false)] - [InlineData("custom(\"guid\", \"native\", \"Marshaller.Type\", \"cookie\")", true)] - public void MarshalCustomMarshaller_EmitsStructuredDescriptor( - string nativeType, - bool usesDeprecatedForm) + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnPropertyHandlesGenericSetterSignature() { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - if (usesDeprecatedForm) - { - Assert.Contains( - diagnostics, - diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedCustomMarshaller); - } - else - { - Assert.Empty(diagnostics); - } + 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 = new PEReader(image); + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + var setterValue = reader.GetParameter(GetMethod(reader, "set_Value").GetParameters() + .Single(handle => reader.GetParameter(handle).SequenceNumber == 1)); - 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); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(setterValue.GetMarshallingDescriptor())); } - [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) + [Fact] + public void PseudoCustomAttribute_MarshalAsNegativeSizeParamIndexReportsInvalidValue() { - var (diagnostics, image) = CompileParameterMarshallingDescriptor($"safearray {variantType}"); - Assert.Empty(diagnostics); + 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 + } + } + """; - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobHandle descriptorHandle = GetOnlyMarshallingDescriptor(reader); - BlobReader descriptor = reader.GetBlobReader(descriptorHandle); + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); - 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); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); } - [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) + [Fact] + public void PseudoCustomAttribute_MarshalAsNegativeByValTStrSizeReportsInvalidBlob() { - var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); + 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 ) + } + """; - using var pe = new PEReader(image); - var reader = pe.GetMetadataReader(); - BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); - Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); - Assert.Equal(0, descriptor.RemainingBytes); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidBlob, diagnostic.Id); } [Fact] - public void MarshalSafeArrayWithUserDefinedType_EmitsVariantAndTypeName() + public void PseudoCustomAttribute_MarshalAs_CustomMarshaler_EncodesMarshalerAndCookie() { - 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); - } + // 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 )"; - 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 + .class public auto ansi Test extends [mscorlib]System.Object { - .method public static void M(object marshal({{nativeType}}) 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}} 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()); - } + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var parameter = reader.GetParameter(Assert.Single(GetMethod(reader, "M").GetParameters())); - 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(); + // 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..b54b8004c77c32 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs @@ -159,6 +159,172 @@ .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 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/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..370280cd777e4d 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); + } + } } }