diff --git a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs index df69247b9f9b68..31b5d87398fcf7 100644 --- a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs +++ b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs @@ -53,6 +53,12 @@ public static class DiagnosticIds public const string MissingExportedTypeImplementation = "ILA0031"; public const string KeyFileError = "ILA0032"; public const string TooManyGenericParameters = "ILA0033"; + public const string PseudoCustomAttributeInvalidTarget = "ILA0034"; + public const string PseudoCustomAttributeInvalidValue = "ILA0035"; + public const string PseudoCustomAttributeInvalidBlob = "ILA0036"; + public const string PseudoCustomAttributeInvalidGuid = "ILA0037"; + public const string PseudoCustomAttributeUnknownArgument = "ILA0038"; + public const string PseudoCustomAttributeRepeatedArgument = "ILA0039"; } internal static class DiagnosticMessageTemplates @@ -89,4 +95,10 @@ internal static class DiagnosticMessageTemplates public const string DuplicateMethod = "Duplicate method definition"; public const string MissingExportedTypeImplementation = "Undefined implementation in ExportedType '{0}' -- ExportedType not emitted"; public const string TooManyGenericParameters = "Generic parameter count {0} exceeds the maximum of {1}"; + 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 6c78be229f35b8..0503736c99f5cf 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -819,6 +819,34 @@ public TypeDefinitionEntity GetOrCreateTypeDefinition(TypeDefinitionEntity? cont return null; } + /// + /// Finds the local type definition that a type reference refers to, without assigning or + /// reading any handles. Passes that run before resolves + /// references need this, because handles are not assigned until emission. + /// + public TypeDefinitionEntity? FindLocalTypeDefinition(TypeReferenceEntity typeReference) + { + switch (typeReference.ResolutionScope) + { + case TypeReferenceEntity enclosing: + return FindLocalTypeDefinition(enclosing) is { } enclosingTypeDef + ? FindTypeDefinition(enclosingTypeDef, typeReference.Namespace, typeReference.Name) + : null; + + case AssemblyReferenceEntity assemblyReference: + return Assembly is not null + && string.Equals(assemblyReference.Name, Assembly.Name, StringComparison.OrdinalIgnoreCase) + ? FindTypeDefinition(null, typeReference.Namespace, typeReference.Name) + : null; + + case ModuleEntity or ModuleReferenceEntity: + return FindTypeDefinition(null, typeReference.Namespace, typeReference.Name); + + default: + return null; + } + } + public AssemblyReferenceEntity GetOrCreateAssemblyReference(string name, Action onCreateAssemblyReference) { return GetOrCreateEntity(name, TableIndex.AssemblyRef, _seenAssemblyRefs, _ => new(name), onCreateAssemblyReference); @@ -1599,6 +1627,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, methodDeclaration); @@ -1939,7 +1992,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; } @@ -2004,6 +2057,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(MethodDefinitionEntity methodBody, MemberReferenceEntity methodDeclaration) : EntityBase @@ -2014,7 +2073,7 @@ public sealed class MethodImplementationEntity(MethodDefinitionEntity methodBody 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; @@ -2038,7 +2097,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/GrammarVisitor.cs b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs index 8543239d09a5ca..c2181e51def26f 100644 --- a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs +++ b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs @@ -176,6 +176,7 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC } BlobBuilder ilStream = new(); + PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics); _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames); MetadataRootBuilder rootBuilder = new(_metadataBuilder, _options.MetadataVersion); @@ -1866,7 +1867,9 @@ private static GrammarResult.String VisitCompQstring(CILParser.CompQstringContex var resolved = TryResolveTypedefAsCustomAttribute(alias); if (resolved is not null) { - return new(_entityRegistry.CreateCustomAttribute(resolved.Value.Constructor, resolved.Value.Value)); + var typedefAttribute = _entityRegistry.CreateCustomAttribute(resolved.Value.Constructor, resolved.Value.Value); + typedefAttribute.Location = Location.From(context.Start, _documents); + return new(typedefAttribute); } // Typedef not found - could report diagnostic here return new(null); @@ -1983,10 +1986,13 @@ public GrammarResult.FormattedBlob VisitCustomBlobNVPairs(CILParser.CustomBlobNV value.WriteUInt16(0); } - return new(_entityRegistry.CreateCustomAttribute(ctor, value)); + var attribute = _entityRegistry.CreateCustomAttribute(ctor, value); + attribute.Location = Location.From(context.Start, _documents); + return new(attribute); } GrammarResult ICILVisitor.VisitCustomDescrWithOwner(CILParser.CustomDescrWithOwnerContext context) => VisitCustomDescrWithOwner(context); + public GrammarResult.Literal VisitCustomDescrWithOwner(CILParser.CustomDescrWithOwnerContext context) { var ctor = VisitCustomType(context.customType()).Value; @@ -2016,6 +2022,7 @@ public GrammarResult.FormattedBlob VisitCustomBlobNVPairs(CILParser.CustomBlobNV var attr = _entityRegistry.CreateCustomAttribute(ctor, value); + attr.Location = Location.From(context.Start, _documents); attr.Owner = VisitOwnerType(context.ownerType()).Value; return new(attr); diff --git a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs index 7f030940cdabd4..657f2f66924643 100644 --- a/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs +++ b/src/tools/ilasm/src/ILAssembler/MetadataExtensions.cs @@ -3,15 +3,44 @@ using System; using System.Reflection; +using System.Runtime.InteropServices; namespace ILAssembler; internal static class MetadataExtensions { + extension(FieldAttributes) + { + public static FieldAttributes NotSerializedFlag => (FieldAttributes)0x0080; + } + + extension(MethodImplAttributes) + { + public static MethodImplAttributes UserMask => + MethodImplAttributes.ManagedMask | + MethodImplAttributes.ForwardRef | + MethodImplAttributes.PreserveSig | + MethodImplAttributes.InternalCall | + MethodImplAttributes.Synchronized | + MethodImplAttributes.NoInlining | + MethodImplAttributes.AggressiveInlining | + MethodImplAttributes.NoOptimization | + MethodImplAttributes.AggressiveOptimization | + MethodImplAttributes.Async; + } + extension(TypeAttributes) { public static TypeAttributes ExtendedLayout => (TypeAttributes)0x18; public static TypeAttributes Forwarder => (TypeAttributes)0x00200000; + public static TypeAttributes SerializableFlag => (TypeAttributes)0x00002000; + } + + extension(UnmanagedType) + { + public static int ArraySizeParamIndexSpecified => 0x0001; + public static UnmanagedType ByValStr => (UnmanagedType)0x22; + public static UnmanagedType Max => (UnmanagedType)0x50; } extension(DeclarativeSecurityAction) @@ -30,3 +59,19 @@ internal static class MetadataExtensions public static AssemblyFlags ArchitectureMask => (AssemblyFlags)0xF0; } } + +internal static class ClassInterfaceTypeExtensions +{ + extension(ClassInterfaceType) + { + public static ClassInterfaceType Last => (ClassInterfaceType)3; + } +} + +internal static class ComInterfaceTypeExtensions +{ + extension(ComInterfaceType) + { + public static ComInterfaceType Last => (ComInterfaceType)4; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs new file mode 100644 index 00000000000000..5dd6ca25e75014 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Application.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Runtime.InteropServices; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool Apply(in LoweringContext context, KnownAttribute known) + { + if ((known.Targets & GetTarget(context.Owner)) == 0) + { + return context.InvalidTarget(); + } + + if (!TryParseArguments(context, known, out CustomAttributeValue arguments)) + { + return false; + } + + return known.Kind switch + { + KnownAttributeKind.DllImport => ApplyDllImport(context, arguments), + KnownAttributeKind.Guid => ApplyGuid(context, arguments), + KnownAttributeKind.ComImport => AddTypeFlags(context, TypeAttributes.Import), + KnownAttributeKind.InterfaceType => + GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ComInterfaceType.Last || context.InvalidValue(), + KnownAttributeKind.ClassInterface => + GetUInt16(arguments.FixedArguments[0].Value) < (ushort)ClassInterfaceType.Last || context.InvalidValue(), + KnownAttributeKind.Serializable => AddTypeFlags(context, TypeAttributes.SerializableFlag), + KnownAttributeKind.NonSerialized => AddFieldFlags(context, FieldAttributes.NotSerializedFlag), + KnownAttributeKind.MethodImpl1 or KnownAttributeKind.MethodImpl2 or KnownAttributeKind.MethodImpl3 => + ApplyMethodImpl(context, known.Kind, arguments), + KnownAttributeKind.MarshalAs1 or KnownAttributeKind.MarshalAs2 => ApplyMarshalAs(context, arguments), + KnownAttributeKind.PreserveSig => AddMethodImplFlags(context, MethodImplAttributes.PreserveSig), + KnownAttributeKind.In => AddParameterFlags(context, ParameterAttributes.In), + KnownAttributeKind.Out => AddParameterFlags(context, ParameterAttributes.Out), + KnownAttributeKind.Optional => AddParameterFlags(context, ParameterAttributes.Optional), + KnownAttributeKind.StructLayout1 or KnownAttributeKind.StructLayout2 => + ApplyStructLayout(context, known.Kind, arguments), + KnownAttributeKind.FieldOffset => ApplyFieldOffset(context, arguments), + KnownAttributeKind.TypeLibVersion or KnownAttributeKind.ComCompatibleVersion => + ValidateNonNegative(context, arguments.FixedArguments), + KnownAttributeKind.SpecialName => ApplySpecialName(context), + KnownAttributeKind.AllowPartiallyTrustedCallers => true, + KnownAttributeKind.WindowsRuntimeImport => AddTypeFlags(context, TypeAttributes.WindowsRuntime), + _ => true, + }; + } + + private static bool AddTypeFlags(in LoweringContext context, TypeAttributes flags) + { + ((EntityRegistry.TypeDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddFieldFlags(in LoweringContext context, FieldAttributes flags) + { + ((EntityRegistry.FieldDefinitionEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddParameterFlags(in LoweringContext context, ParameterAttributes flags) + { + ((EntityRegistry.ParameterEntity)context.Owner).Attributes |= flags; + return true; + } + + private static bool AddMethodImplFlags(in LoweringContext context, MethodImplAttributes flags) + { + ((EntityRegistry.MethodDefinitionEntity)context.Owner).ImplementationAttributes |= flags; + return true; + } + + private static bool ApplySpecialName(in LoweringContext context) + { + switch (context.Owner) + { + case EntityRegistry.TypeDefinitionEntity type: + type.Attributes |= TypeAttributes.SpecialName; + return true; + case EntityRegistry.MethodDefinitionEntity method: + method.MethodAttributes |= MethodAttributes.SpecialName; + return true; + case EntityRegistry.FieldDefinitionEntity field: + field.Attributes |= FieldAttributes.SpecialName; + return true; + case EntityRegistry.PropertyEntity property: + property.Attributes |= PropertyAttributes.SpecialName; + return true; + case EntityRegistry.EventEntity @event: + @event.Attributes |= EventAttributes.SpecialName; + return true; + default: + return context.InvalidValue(); + } + } + + private static bool ValidateNonNegative( + in LoweringContext context, + ImmutableArray> arguments) + { + foreach (CustomAttributeTypedArgument argument in arguments) + { + if (GetInt32(argument.Value) < 0) + { + return context.InvalidValue(); + } + } + + return true; + } + + private static bool ApplyGuid( + in LoweringContext context, + in CustomAttributeValue arguments) + { + // The value is only validated; the attribute itself is still emitted. + string guid = GetString(arguments.FixedArguments[0].Value); + return guid.Length == 36 && Guid.TryParseExact(guid, "D", out _) + ? true + : context.InvalidGuid(); + } + + private static bool ApplyFieldOffset( + in LoweringContext context, + in CustomAttributeValue arguments) + { + uint offset = GetUInt32(arguments.FixedArguments[0].Value); + if (offset > int.MaxValue) + { + return context.InvalidValue(); + } + + ((EntityRegistry.FieldDefinitionEntity)context.Owner).Offset = (int)offset; + return true; + } + + private static bool ApplyMethodImpl( + in LoweringContext context, + KnownAttributeKind kind, + in CustomAttributeValue arguments) + { + var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; + CustomAttributeNamedArgument? codeTypeArgument = + FindNamedArgument(arguments, MethodImplCodeType); + + if (kind is not KnownAttributeKind.MethodImpl1) + { + // The I2 overload is widened before validation, matching the native emitter. + object? fixedValue = arguments.FixedArguments[0].Value; + ushort value = kind is KnownAttributeKind.MethodImpl2 + ? unchecked((ushort)GetInt16(fixedValue)) + : GetUInt16(fixedValue); + if (((MethodImplAttributes)value & ~MethodImplAttributes.UserMask) != 0) + { + return context.InvalidValue(); + } + + method.ImplementationAttributes |= (MethodImplAttributes)value; + + if (codeTypeArgument is null) + { + return true; + } + } + + ushort codeType = codeTypeArgument is { } argument ? GetUInt16(argument.Value) : (ushort)0; + if ((codeType & ~(ushort)MethodImplAttributes.CodeTypeMask) != 0) + { + return context.InvalidValue(); + } + + method.ImplementationAttributes = + (method.ImplementationAttributes & ~MethodImplAttributes.CodeTypeMask) | (MethodImplAttributes)codeType; + return true; + } + + private static bool ApplyStructLayout( + in LoweringContext context, + KnownAttributeKind kind, + in CustomAttributeValue arguments) + { + var type = (EntityRegistry.TypeDefinitionEntity)context.Owner; + + // The I2 overload is zero-extended through 16 bits before the layout kind is read. + object? fixedValue = arguments.FixedArguments[0].Value; + int layoutKind = kind is KnownAttributeKind.StructLayout1 + ? unchecked((ushort)GetInt16(fixedValue)) + : GetInt32(fixedValue); + + TypeAttributes layout = layoutKind switch + { + 0 => TypeAttributes.SequentialLayout, + 1 => TypeAttributes.ExtendedLayout, + 2 => TypeAttributes.ExplicitLayout, + 3 => TypeAttributes.AutoLayout, + _ => (TypeAttributes)(-1), + }; + + if (layout == (TypeAttributes)(-1)) + { + return context.InvalidValue(); + } + + TypeAttributes attributes = (type.Attributes & ~TypeAttributes.LayoutMask) | layout; + + if (FindNamedArgument(arguments, StructLayoutPack) is { } packArgument) + { + uint pack = GetUInt32(packArgument.Value); + if (pack > 128 || (pack & (pack - 1)) != 0) + { + return context.InvalidValue(); + } + + // An explicit .pack directive wins: the native assembler emits the ClassLayout row for + // explicit directives in a later phase than the one that applies this attribute. + type.PackingSize ??= (int)pack; + } + + if (FindNamedArgument(arguments, StructLayoutSize) is { } sizeArgument) + { + uint size = GetUInt32(sizeArgument.Value); + if (size > int.MaxValue) + { + return context.InvalidValue(); + } + + // An explicit .size directive wins, for the same reason as .pack above. + type.ClassSize ??= (int)size; + } + + if (FindNamedArgument(arguments, StructLayoutCharSet) is { } charSetArgument) + { + switch (GetUInt32(charSetArgument.Value)) + { + case 2: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.AnsiClass; + break; + case 3: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.UnicodeClass; + break; + case 4: + attributes = (attributes & ~TypeAttributes.StringFormatMask) | TypeAttributes.AutoClass; + break; + default: + return context.InvalidValue(); + } + } + + type.Attributes = attributes; + return true; + } + + private static bool ApplyDllImport( + in LoweringContext context, + in CustomAttributeValue arguments) + { + var method = (EntityRegistry.MethodDefinitionEntity)context.Owner; + + if (arguments.FixedArguments[0].Value is not string moduleName || moduleName.Length == 0) + { + return context.InvalidValue(); + } + + MethodImportAttributes flags = MethodImportAttributes.None; + + if (FindNamedArgument(arguments, DllImportCallingConvention) is { } callingConventionArgument) + { + flags = GetUInt32(callingConventionArgument.Value) switch + { + 0 => flags, + 1 => flags | MethodImportAttributes.CallingConventionWinApi, + 2 => flags | MethodImportAttributes.CallingConventionCDecl, + 3 => flags | MethodImportAttributes.CallingConventionStdCall, + 4 => flags | MethodImportAttributes.CallingConventionThisCall, + 5 => flags | MethodImportAttributes.CallingConventionFastCall, + _ => flags, + }; + } + else + { + flags |= MethodImportAttributes.CallingConventionWinApi; + } + + if (FindNamedArgument(arguments, DllImportCharSet) is { } charSetArgument) + { + flags = GetUInt32(charSetArgument.Value) switch + { + // 0 means "do nothing" and 1 is "not specified", which is the zero bit pattern. + 0 or 1 => flags, + 2 => flags | MethodImportAttributes.CharSetAnsi, + 3 => flags | MethodImportAttributes.CharSetUnicode, + 4 => flags | MethodImportAttributes.CharSetAuto, + _ => flags, + }; + } + + if (FindNamedArgument(arguments, DllImportExactSpelling) is { } exactSpellingArgument + && GetBoolean(exactSpellingArgument.Value)) + { + flags |= MethodImportAttributes.ExactSpelling; + } + + if (FindNamedArgument(arguments, DllImportSetLastError) is { } setLastErrorArgument + && GetBoolean(setLastErrorArgument.Value)) + { + flags |= MethodImportAttributes.SetLastError; + } + + if (FindNamedArgument(arguments, DllImportBestFitMapping) is { } bestFitMappingArgument) + { + flags |= GetBoolean(bestFitMappingArgument.Value) + ? MethodImportAttributes.BestFitMappingEnable + : MethodImportAttributes.BestFitMappingDisable; + } + + if (FindNamedArgument(arguments, DllImportThrowOnUnmappableChar) is { } throwOnUnmappableCharArgument) + { + flags |= GetBoolean(throwOnUnmappableCharArgument.Value) + ? MethodImportAttributes.ThrowOnUnmappableCharEnable + : MethodImportAttributes.ThrowOnUnmappableCharDisable; + } + + // PreserveSig defaults to set, and is only cleared by an explicit false value. + if (FindNamedArgument(arguments, DllImportPreserveSig) is { } preserveSigArgument + && !GetBoolean(preserveSigArgument.Value)) + { + method.ImplementationAttributes &= ~MethodImplAttributes.PreserveSig; + } + else + { + method.ImplementationAttributes |= MethodImplAttributes.PreserveSig; + } + + string entryPoint = FindNamedArgument(arguments, DllImportEntryPoint) is { } entryPointArgument + ? GetString(entryPointArgument.Value) + : method.Name; + + // The module reference is created even when an explicit pinvokeimpl clause takes precedence, + // because the native emitter resolves it before it discovers the existing ImplMap row. + var moduleReference = context.Registry.GetOrCreateModuleReference(moduleName, _ => { }); + + // An explicit pinvokeimpl clause wins: the native assembler emits the ImplMap row for + // explicit clauses in a later phase than the one that applies this attribute. + method.MethodImportInformation ??= (moduleReference, entryPoint, flags); + + return true; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs new file mode 100644 index 00000000000000..c4a672e136783c --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Decoding.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private readonly record struct EncodedArgumentType( + SerializationTypeCode Type, + SerializationTypeCode ArrayType = SerializationTypeCode.Invalid, + SerializationTypeCode EnumUnderlyingType = SerializationTypeCode.Invalid, + string? EnumName = null); + + private static EncodedArgumentType ReadEncodedArgumentType(ref BlobReader reader) + { + // The native CustomAttributeParser::GetTag method consumes one byte rather than a + // compressed integer, so use BlobReader.ReadByte instead of ReadSerializationTypeCode. + SerializationTypeCode type = (SerializationTypeCode)reader.ReadByte(); + SerializationTypeCode arrayType = SerializationTypeCode.Invalid; + if (type == SerializationTypeCode.SZArray) + { + arrayType = (SerializationTypeCode)reader.ReadByte(); + } + + SerializationTypeCode effectiveType = + type == SerializationTypeCode.SZArray ? arrayType : type; + string? enumName = null; + if (effectiveType == SerializationTypeCode.Enum) + { + enumName = reader.ReadSerializedString(); + if (enumName is null) + { + throw new BadImageFormatException(); + } + } + + return new(type, arrayType, EnumName: enumName); + } + + private static CustomAttributeTypedArgument ReadArgument( + ref BlobReader reader, + SerializationTypeCode type, + SerializationTypeCode enumUnderlyingType = SerializationTypeCode.Invalid) + { + SerializationTypeCode effectiveType = + type == SerializationTypeCode.Enum ? enumUnderlyingType : type; + + object? value = effectiveType switch + { + SerializationTypeCode.Boolean => reader.ReadBoolean(), + SerializationTypeCode.SByte => reader.ReadSByte(), + SerializationTypeCode.Byte => reader.ReadByte(), + SerializationTypeCode.Char => reader.ReadChar(), + SerializationTypeCode.Int16 => reader.ReadInt16(), + SerializationTypeCode.UInt16 => reader.ReadUInt16(), + SerializationTypeCode.Int32 => reader.ReadInt32(), + SerializationTypeCode.UInt32 => reader.ReadUInt32(), + SerializationTypeCode.Int64 => reader.ReadInt64(), + SerializationTypeCode.UInt64 => reader.ReadUInt64(), + SerializationTypeCode.Single => reader.ReadSingle(), + SerializationTypeCode.Double => reader.ReadDouble(), + SerializationTypeCode.String or SerializationTypeCode.Type => reader.ReadSerializedString(), + _ => throw new BadImageFormatException(), + }; + + return new(type, value); + } + + private static unsafe bool TryParseArguments( + in LoweringContext context, + KnownAttribute known, + out CustomAttributeValue arguments) + { + // The native emitter does not look at the blob at all when the attribute has neither + // fixed nor named arguments, so a malformed blob is tolerated for those attributes. + if (known.FixedArguments.Length == 0 && known.NamedArguments.Length == 0) + { + arguments = new( + ImmutableArray>.Empty, + ImmutableArray>.Empty); + return true; + } + + byte[] blob = context.Attribute.Value.ToArray(); + fixed (byte* blobPointer = blob) + { + var reader = new BlobReader(blobPointer, blob.Length); + try + { + if (reader.ReadUInt16() != 0x0001) + { + arguments = default; + return context.InvalidBlob(); + } + + var fixedArguments = + ImmutableArray.CreateBuilder>( + known.FixedArguments.Length); + foreach (SerializationTypeCode type in known.FixedArguments) + { + fixedArguments.Add(ReadArgument(ref reader, type)); + } + + ImmutableArray> namedArguments; + if (known.NamedArguments.Length == 0 && reader.RemainingBytes == 0) + { + namedArguments = ImmutableArray>.Empty; + } + else if (!TryParseNamedArguments(context, known, ref reader, out namedArguments)) + { + arguments = default; + return false; + } + + arguments = new(fixedArguments.MoveToImmutable(), namedArguments); + return true; + } + catch (BadImageFormatException) + { + arguments = default; + return context.InvalidBlob(); + } + } + } + + private static bool TryParseNamedArguments( + in LoweringContext context, + KnownAttribute known, + ref BlobReader reader, + out ImmutableArray> namedArguments) + { + // A missing count is treated as "no named arguments" rather than an error, matching the + // native emitter's documented Everett-compatible behavior. + if (reader.RemainingBytes < sizeof(ushort)) + { + namedArguments = ImmutableArray>.Empty; + return true; + } + + ushort actualCount = reader.ReadUInt16(); + var arguments = + ImmutableArray.CreateBuilder>( + Math.Min(actualCount, (ushort)known.NamedArguments.Length)); + var seenArguments = new bool[known.NamedArguments.Length]; + + // The count is deliberately read as a signed 16-bit value: the native emitter stores it in + // an INT16 and compares against a wider signed loop counter, so a count with the high bit + // set yields no named arguments rather than an error. + for (int i = 0; i < (short)actualCount; i++) + { + var kind = (CustomAttributeNamedArgumentKind)reader.ReadByte(); + if (kind is not (CustomAttributeNamedArgumentKind.Field or CustomAttributeNamedArgumentKind.Property)) + { + namedArguments = default; + return context.InvalidBlob(); + } + + EncodedArgumentType actual = ReadEncodedArgumentType(ref reader); + string? argumentName = reader.ReadSerializedString(); + if (string.IsNullOrEmpty(argumentName)) + { + namedArguments = default; + return context.InvalidBlob(); + } + + int match = -1; + for (int candidate = 0; candidate < known.NamedArguments.Length; candidate++) + { + NamedArgument descriptor = known.NamedArguments[candidate]; + + if (descriptor.Type != SerializationTypeCode.TaggedObject) + { + if (actual.Type != descriptor.Type) + { + continue; + } + + if (actual.Type == SerializationTypeCode.SZArray + && descriptor.ArrayType != SerializationTypeCode.TaggedObject + && actual.ArrayType != descriptor.ArrayType) + { + continue; + } + } + + if (descriptor.Name != argumentName) + { + continue; + } + + if (descriptor.Type == SerializationTypeCode.Enum + || (descriptor.Type == SerializationTypeCode.SZArray && descriptor.ArrayType == SerializationTypeCode.Enum)) + { + if (!EnumNameMatches(descriptor.EnumName, actual.EnumName)) + { + continue; + } + + actual = actual with { EnumUnderlyingType = descriptor.EnumType }; + } + + match = candidate; + break; + } + + if (match < 0) + { + namedArguments = default; + return context.UnknownArgument(argumentName); + } + + if (seenArguments[match]) + { + namedArguments = default; + return context.RepeatedArgument(argumentName); + } + + seenArguments[match] = true; + CustomAttributeTypedArgument value = + ReadArgument(ref reader, actual.Type, actual.EnumUnderlyingType); + arguments.Add(new(argumentName, kind, value.Type, value.Value)); + } + + namedArguments = arguments.ToImmutable(); + return true; + } + + private static CustomAttributeNamedArgument? FindNamedArgument( + in CustomAttributeValue arguments, + string name) + { + foreach (CustomAttributeNamedArgument argument in arguments.NamedArguments) + { + if (argument.Name == name) + { + return argument; + } + } + + return null; + } + + private static short GetInt16(object? value) => value switch + { + short signed => signed, + ushort unsigned => unchecked((short)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static ushort GetUInt16(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => unchecked((ushort)signed), + uint unsigned => unchecked((ushort)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static int GetInt32(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => signed, + uint unsigned => unchecked((int)unsigned), + _ => throw new BadImageFormatException(), + }; + + private static uint GetUInt32(object? value) => value switch + { + short signed => unchecked((ushort)signed), + ushort unsigned => unsigned, + int signed => unchecked((uint)signed), + uint unsigned => unsigned, + _ => throw new BadImageFormatException(), + }; + + private static bool GetBoolean(object? value) => + value is bool boolean ? boolean : throw new BadImageFormatException(); + + private static string GetString(object? value) => value as string ?? ""; + + /// + /// Matches an enum type name against a descriptor name, allowing the blob to carry an + /// assembly-qualified name whose namespace-qualified prefix matches. + /// + private static bool EnumNameMatches(string descriptorName, string? actualName) + { + if (actualName is null || descriptorName.Length > actualName.Length) + { + return false; + } + + if (!actualName.AsSpan(0, descriptorName.Length).SequenceEqual(descriptorName)) + { + return false; + } + + return descriptorName.Length == actualName.Length || actualName[descriptorName.Length] == ','; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs new file mode 100644 index 00000000000000..98332f47f129da --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Identity.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool TryGetAttributeTypeName(EntityRegistry.EntityBase constructor, out string @namespace, out string name) + { + switch (constructor) + { + case EntityRegistry.MemberReferenceEntity memberReference: + switch (memberReference.Parent) + { + case EntityRegistry.TypeReferenceEntity typeReference: + @namespace = typeReference.Namespace; + name = typeReference.Name; + return true; + case EntityRegistry.TypeDefinitionEntity typeDefinition: + @namespace = typeDefinition.Namespace; + name = typeDefinition.Name; + return true; + } + break; + + case EntityRegistry.MethodDefinitionEntity methodDefinition: + @namespace = methodDefinition.ContainingType.Namespace; + name = methodDefinition.ContainingType.Name; + return true; + } + + @namespace = ""; + name = ""; + return false; + } + + private static BlobBuilder? GetConstructorSignature(EntityRegistry.EntityBase constructor) => constructor switch + { + EntityRegistry.MemberReferenceEntity memberReference => memberReference.Signature, + EntityRegistry.MethodDefinitionEntity methodDefinition => methodDefinition.MethodSignature, + _ => null + }; + + /// + /// Resolves a type or member reference that designates an entity defined in this module to that + /// entity. References are otherwise only resolved while metadata rows are written, which happens + /// after this pass runs. Anything that does not designate a local entity is returned unchanged. + /// + private static EntityRegistry.EntityBase ResolveOwner(EntityRegistry registry, EntityRegistry.EntityBase owner) + { + switch (owner) + { + case EntityRegistry.TypeReferenceEntity typeReference: + return registry.FindLocalTypeDefinition(typeReference) ?? owner; + + case EntityRegistry.MemberReferenceEntity memberReference: + if (ResolveDeclaringType(registry, memberReference.Parent) is not { } declaringType) + { + return owner; + } + + byte[] signature = memberReference.Signature.ToArray(); + if (signature.Length == 0) + { + return owner; + } + + switch (new SignatureHeader(signature[0]).Kind) + { + case SignatureKind.Method: + foreach (EntityRegistry.MethodDefinitionEntity method in declaringType.Methods) + { + if (method.Name == memberReference.Name + && method.MethodSignature is { } methodSignature + && methodSignature.ContentEquals(memberReference.Signature)) + { + return method; + } + } + break; + + case SignatureKind.Field: + foreach (EntityRegistry.FieldDefinitionEntity field in declaringType.Fields) + { + if (field.Name == memberReference.Name + && field.Signature.ContentEquals(memberReference.Signature)) + { + return field; + } + } + break; + } + + return owner; + + default: + return owner; + } + } + + private static EntityRegistry.TypeDefinitionEntity? ResolveDeclaringType( + EntityRegistry registry, + EntityRegistry.EntityBase? declaringType) => declaringType switch + { + EntityRegistry.TypeDefinitionEntity typeDefinition => typeDefinition, + EntityRegistry.TypeReferenceEntity typeReference => registry.FindLocalTypeDefinition(typeReference), + _ => null, + }; + + private static KnownAttribute? TryFindKnownAttribute(EntityRegistry.EntityBase constructor, string @namespace, string name) + { + foreach (KnownAttribute candidate in s_knownAttributes) + { + if (candidate.Name != name || candidate.Namespace != @namespace) + { + continue; + } + + if (candidate.MatchBySignature && !SignatureMatches(constructor, candidate)) + { + continue; + } + + return candidate; + } + + return null; + } + + /// + /// Discriminates between overloads of a known attribute by comparing the constructor signature's + /// parameter count and element types against the descriptor's fixed arguments. The serialization + /// type tags of the primitive types used by the known attributes coincide with their + /// ELEMENT_TYPE values, so the tags can be compared directly. + /// + private static unsafe bool SignatureMatches(EntityRegistry.EntityBase constructor, KnownAttribute candidate) + { + BlobBuilder? signature = GetConstructorSignature(constructor); + if (signature is null) + { + return false; + } + + byte[] signatureBytes = signature.ToArray(); + fixed (byte* signaturePointer = signatureBytes) + { + var reader = new BlobReader(signaturePointer, signatureBytes.Length); + try + { + // Calling convention, then parameter count. + if (!reader.TryReadCompressedInteger(out _) + || !reader.TryReadCompressedInteger(out int parameterCount) + || parameterCount != candidate.FixedArguments.Length + // Skip the return type. + || !reader.TryReadCompressedInteger(out _)) + { + return false; + } + + for (int i = 0; i < parameterCount; i++) + { + if (!reader.TryReadCompressedInteger(out int element) + || (SerializationTypeCode)element != candidate.FixedArguments[i]) + { + return false; + } + } + + return true; + } + catch (BadImageFormatException) + { + return false; + } + } + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs new file mode 100644 index 00000000000000..cb900ccd07b1f9 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.KnownAttributes.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection.Metadata; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + [Flags] + private enum CaTargets + { + None = 0, + TypeDef = 1 << 0, + TypeRef = 1 << 1, + MethodDef = 1 << 2, + FieldDef = 1 << 3, + ParamDef = 1 << 4, + Property = 1 << 5, + Event = 1 << 6, + Module = 1 << 7, + Assembly = 1 << 8, + } + + private enum KnownAttributeKind + { + DllImport, + Guid, + ComImport, + InterfaceType, + ClassInterface, + Serializable, + NonSerialized, + MethodImpl1, + MethodImpl2, + MethodImpl3, + MarshalAs1, + MarshalAs2, + PreserveSig, + In, + Out, + Optional, + StructLayout1, + StructLayout2, + FieldOffset, + TypeLibVersion, + ComCompatibleVersion, + SpecialName, + AllowPartiallyTrustedCallers, + WindowsRuntimeImport, + } + + private sealed record NamedArgument( + string Name, + SerializationTypeCode Type, + SerializationTypeCode EnumType = SerializationTypeCode.Invalid, + string EnumName = "", + SerializationTypeCode ArrayType = SerializationTypeCode.Invalid); + + private sealed record KnownAttribute( + KnownAttributeKind Kind, + string Namespace, + string Name, + CaTargets Targets, + bool KeepAttribute = false, + SerializationTypeCode[]? FixedArgumentTypes = null, + NamedArgument[]? NamedArgumentDescriptors = null, + bool MatchBySignature = false) + { + public SerializationTypeCode[] FixedArguments { get; } = FixedArgumentTypes ?? []; + public NamedArgument[] NamedArguments { get; } = NamedArgumentDescriptors ?? []; + } + + private const string InteropNamespace = "System.Runtime.InteropServices"; + private const string CompilerServicesNamespace = "System.Runtime.CompilerServices"; + + private static NamedArgument Enum4(string name, string enumName) => + new(name, SerializationTypeCode.Enum, SerializationTypeCode.Int32, enumName); + + private const string DllImportCallingConvention = "CallingConvention"; + private const string DllImportCharSet = "CharSet"; + private const string DllImportEntryPoint = "EntryPoint"; + private const string DllImportExactSpelling = "ExactSpelling"; + private const string DllImportSetLastError = "SetLastError"; + private const string DllImportPreserveSig = "PreserveSig"; + private const string DllImportBestFitMapping = "BestFitMapping"; + private const string DllImportThrowOnUnmappableChar = "ThrowOnUnmappableChar"; + + private const string MethodImplCodeType = "MethodCodeType"; + + private const string MarshalArraySubType = "ArraySubType"; + private const string MarshalSafeArraySubType = "SafeArraySubType"; + private const string MarshalSafeArrayUserDefinedSubType = "SafeArrayUserDefinedSubType"; + private const string MarshalSizeParamIndex = "SizeParamIndex"; + private const string MarshalSizeConst = "SizeConst"; + private const string MarshalType = "MarshalType"; + private const string MarshalTypeRef = "MarshalTypeRef"; + private const string MarshalCookie = "MarshalCookie"; + private const string MarshalIidParameterIndex = "IidParameterIndex"; + + private const string StructLayoutPack = "Pack"; + private const string StructLayoutSize = "Size"; + private const string StructLayoutCharSet = "CharSet"; + + private static readonly NamedArgument[] s_dllImportNamedArguments = + [ + Enum4(DllImportCallingConvention, InteropNamespace + ".CallingConvention"), + Enum4(DllImportCharSet, InteropNamespace + ".CharSet"), + new(DllImportEntryPoint, SerializationTypeCode.String), + new(DllImportExactSpelling, SerializationTypeCode.Boolean), + new(DllImportSetLastError, SerializationTypeCode.Boolean), + new(DllImportPreserveSig, SerializationTypeCode.Boolean), + new(DllImportBestFitMapping, SerializationTypeCode.Boolean), + new(DllImportThrowOnUnmappableChar, SerializationTypeCode.Boolean), + ]; + + private static readonly NamedArgument[] s_methodImplNamedArguments = + [ + Enum4(MethodImplCodeType, CompilerServicesNamespace + ".MethodCodeType"), + ]; + + private static readonly NamedArgument[] s_marshalAsNamedArguments = + [ + Enum4(MarshalArraySubType, InteropNamespace + ".UnmanagedType"), + Enum4(MarshalSafeArraySubType, InteropNamespace + ".VarEnum"), + new(MarshalSafeArrayUserDefinedSubType, SerializationTypeCode.Type), + new(MarshalSizeParamIndex, SerializationTypeCode.Int16), + new(MarshalSizeConst, SerializationTypeCode.Int32), + new(MarshalType, SerializationTypeCode.String), + new(MarshalTypeRef, SerializationTypeCode.Type), + new(MarshalCookie, SerializationTypeCode.String), + new(MarshalIidParameterIndex, SerializationTypeCode.Int32), + ]; + + private static readonly NamedArgument[] s_structLayoutNamedArguments = + [ + new(StructLayoutPack, SerializationTypeCode.Int32), + new(StructLayoutSize, SerializationTypeCode.Int32), + Enum4(StructLayoutCharSet, InteropNamespace + ".CharSet"), + ]; + + private const CaTargets MarshalTargets = CaTargets.FieldDef | CaTargets.ParamDef | CaTargets.Property; + private const CaTargets InOutTargets = CaTargets.ParamDef; + private const CaTargets SpecialNameTargets = + CaTargets.TypeDef | CaTargets.MethodDef | CaTargets.FieldDef | CaTargets.Property | CaTargets.Event; + + /// + /// The known attributes, in the same order as the native KnownCaList. The order matters: + /// overloads that match by signature are tested before the match-by-name fallback overload. + /// + private static readonly KnownAttribute[] s_knownAttributes = + [ + new(KnownAttributeKind.DllImport, InteropNamespace, "DllImportAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.String], + NamedArgumentDescriptors: s_dllImportNamedArguments), + + new(KnownAttributeKind.Guid, InteropNamespace, "GuidAttribute", + CaTargets.TypeDef | CaTargets.TypeRef | CaTargets.Module | CaTargets.Assembly, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.String]), + + new(KnownAttributeKind.ComImport, InteropNamespace, "ComImportAttribute", CaTargets.TypeDef), + + new(KnownAttributeKind.InterfaceType, InteropNamespace, "InterfaceTypeAttribute", CaTargets.TypeDef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.UInt16]), + + new(KnownAttributeKind.ClassInterface, InteropNamespace, "ClassInterfaceAttribute", + CaTargets.TypeDef | CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.UInt16]), + + new(KnownAttributeKind.Serializable, "System", "SerializableAttribute", CaTargets.TypeDef), + + new(KnownAttributeKind.NonSerialized, "System", "NonSerializedAttribute", CaTargets.FieldDef), + + new(KnownAttributeKind.MethodImpl1, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + NamedArgumentDescriptors: s_methodImplNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MethodImpl2, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_methodImplNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MethodImpl3, CompilerServicesNamespace, "MethodImplAttribute", CaTargets.MethodDef, + FixedArgumentTypes: [SerializationTypeCode.UInt32], + NamedArgumentDescriptors: s_methodImplNamedArguments), + + new(KnownAttributeKind.MarshalAs1, InteropNamespace, "MarshalAsAttribute", MarshalTargets, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_marshalAsNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.MarshalAs2, InteropNamespace, "MarshalAsAttribute", MarshalTargets, + FixedArgumentTypes: [SerializationTypeCode.UInt32], + NamedArgumentDescriptors: s_marshalAsNamedArguments), + + new(KnownAttributeKind.PreserveSig, InteropNamespace, "PreserveSigAttribute", CaTargets.MethodDef), + + new(KnownAttributeKind.In, InteropNamespace, "InAttribute", InOutTargets), + + new(KnownAttributeKind.Out, InteropNamespace, "OutAttribute", InOutTargets), + + new(KnownAttributeKind.Optional, InteropNamespace, "OptionalAttribute", InOutTargets), + + new(KnownAttributeKind.StructLayout1, InteropNamespace, "StructLayoutAttribute", CaTargets.TypeDef, + FixedArgumentTypes: [SerializationTypeCode.Int16], + NamedArgumentDescriptors: s_structLayoutNamedArguments, + MatchBySignature: true), + + new(KnownAttributeKind.StructLayout2, InteropNamespace, "StructLayoutAttribute", CaTargets.TypeDef, + FixedArgumentTypes: [SerializationTypeCode.Int32], + NamedArgumentDescriptors: s_structLayoutNamedArguments), + + new(KnownAttributeKind.FieldOffset, InteropNamespace, "FieldOffsetAttribute", CaTargets.FieldDef, + FixedArgumentTypes: [SerializationTypeCode.UInt32]), + + new(KnownAttributeKind.TypeLibVersion, InteropNamespace, "TypeLibVersionAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: [SerializationTypeCode.Int32, SerializationTypeCode.Int32]), + + new(KnownAttributeKind.ComCompatibleVersion, InteropNamespace, "ComCompatibleVersionAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true, + FixedArgumentTypes: + [ + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + SerializationTypeCode.Int32, + ]), + + new(KnownAttributeKind.SpecialName, CompilerServicesNamespace, "SpecialNameAttribute", SpecialNameTargets), + + new(KnownAttributeKind.AllowPartiallyTrustedCallers, "System.Security", "AllowPartiallyTrustedCallersAttribute", + CaTargets.Assembly | CaTargets.TypeRef, + KeepAttribute: true), + + new(KnownAttributeKind.WindowsRuntimeImport, InteropNamespace + ".WindowsRuntime", + "WindowsRuntimeImportAttribute", CaTargets.TypeDef), + ]; + + private static CaTargets GetTarget(EntityRegistry.EntityBase owner) => owner switch + { + EntityRegistry.TypeDefinitionEntity => CaTargets.TypeDef, + EntityRegistry.TypeReferenceEntity => CaTargets.TypeRef, + EntityRegistry.MethodDefinitionEntity => CaTargets.MethodDef, + EntityRegistry.FieldDefinitionEntity => CaTargets.FieldDef, + EntityRegistry.ParameterEntity => CaTargets.ParamDef, + EntityRegistry.PropertyEntity => CaTargets.Property, + EntityRegistry.EventEntity => CaTargets.Event, + EntityRegistry.ModuleEntity => CaTargets.Module, + EntityRegistry.AssemblyEntity => CaTargets.Assembly, + _ => CaTargets.None, + }; +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs new file mode 100644 index 00000000000000..e070c29af39542 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.MarshalAs.cs @@ -0,0 +1,434 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Text; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private static bool ApplyMarshalAs( + in LoweringContext context, + in CustomAttributeValue arguments) + { + var descriptor = new BlobBuilder(); + if (!TryBuildMarshallingDescriptor(context, arguments, descriptor)) + { + return false; + } + + switch (context.Owner) + { + case EntityRegistry.FieldDefinitionEntity field: + field.MarshallingDescriptor = descriptor; + return true; + + case EntityRegistry.ParameterEntity parameter: + parameter.MarshallingDescriptor = descriptor; + return true; + + case EntityRegistry.PropertyEntity property: + ApplyMarshalAsToProperty(context.Registry, property, descriptor); + return true; + + default: + return context.InvalidTarget(); + } + } + + /// + /// Applies a property's marshalling descriptor to the getter's return parameter and to the + /// setter's last parameter, matching the native emitter. + /// + private static void ApplyMarshalAsToProperty( + EntityRegistry registry, + EntityRegistry.PropertyEntity property, + BlobBuilder descriptor) + { + EntityRegistry.TypeDefinitionEntity? containingType = FindContainingType(registry, property); + + foreach ((MethodSemanticsAttributes semantic, EntityRegistry.EntityBase accessor) in property.Accessors) + { + if (ResolveAccessor(registry, accessor, containingType) is not { } method) + { + continue; + } + + int targetSequence; + if (semantic == MethodSemanticsAttributes.Getter) + { + targetSequence = 0; + } + else if (semantic == MethodSemanticsAttributes.Setter) + { + if (!TryGetSignatureParameterCount(method.MethodSignature, out targetSequence) || targetSequence == 0) + { + continue; + } + } + else + { + continue; + } + + foreach (EntityRegistry.ParameterEntity parameter in method.Parameters) + { + if (parameter.Sequence == targetSequence) + { + // Each target gets its own builder so that the blobs stay independent. + var copy = new BlobBuilder(); + descriptor.WriteContentTo(copy); + parameter.MarshallingDescriptor = copy; + break; + } + } + } + } + + private static EntityRegistry.TypeDefinitionEntity? FindContainingType( + EntityRegistry registry, + EntityRegistry.PropertyEntity property) + { + foreach (var entity in registry.GetSeenEntities(TableIndex.TypeDef)) + { + if (entity is EntityRegistry.TypeDefinitionEntity type && type.Properties.Contains(property)) + { + return type; + } + } + + return null; + } + + /// + /// Property accessors are recorded as member references, so they are resolved to their local + /// method definitions the same way attribute owners are, falling back to a lookup in the + /// declaring type when the reference does not carry one. + /// + private static EntityRegistry.MethodDefinitionEntity? ResolveAccessor( + EntityRegistry registry, + EntityRegistry.EntityBase accessor, + EntityRegistry.TypeDefinitionEntity? containingType) + { + if (ResolveOwner(registry, accessor) is EntityRegistry.MethodDefinitionEntity resolved) + { + return resolved; + } + + if (accessor is not EntityRegistry.MemberReferenceEntity memberReference || containingType is null) + { + return null; + } + + foreach (EntityRegistry.MethodDefinitionEntity candidate in containingType.Methods) + { + if (candidate.Name == memberReference.Name + && candidate.MethodSignature is { } signature + && signature.ContentEquals(memberReference.Signature)) + { + return candidate; + } + } + + return null; + } + + private static unsafe bool TryGetSignatureParameterCount(BlobBuilder? signature, out int count) + { + count = 0; + if (signature is null) + { + return false; + } + + byte[] signatureBytes = signature.ToArray(); + fixed (byte* signaturePointer = signatureBytes) + { + var reader = new BlobReader(signaturePointer, signatureBytes.Length); + try + { + _ = reader.ReadByte(); + return reader.TryReadCompressedInteger(out count); + } + catch (BadImageFormatException) + { + return false; + } + } + } + + private static bool TryBuildMarshallingDescriptor( + in LoweringContext context, + in CustomAttributeValue arguments, + BlobBuilder descriptor) + { + CustomAttributeNamedArgument? arraySubTypeArgument = + FindNamedArgument(arguments, MarshalArraySubType); + CustomAttributeNamedArgument? safeArraySubTypeArgument = + FindNamedArgument(arguments, MarshalSafeArraySubType); + CustomAttributeNamedArgument? safeArrayUserDefinedSubTypeArgument = + FindNamedArgument(arguments, MarshalSafeArrayUserDefinedSubType); + CustomAttributeNamedArgument? sizeParamIndexArgument = + FindNamedArgument(arguments, MarshalSizeParamIndex); + CustomAttributeNamedArgument? sizeConstArgument = + FindNamedArgument(arguments, MarshalSizeConst); + CustomAttributeNamedArgument? marshalTypeArgument = + FindNamedArgument(arguments, MarshalType); + CustomAttributeNamedArgument? marshalTypeRefArgument = + FindNamedArgument(arguments, MarshalTypeRef); + CustomAttributeNamedArgument? marshalCookieArgument = + FindNamedArgument(arguments, MarshalCookie); + CustomAttributeNamedArgument? iidParameterIndexArgument = + FindNamedArgument(arguments, MarshalIidParameterIndex); + + // For the I2 overload the value was read as a 16-bit quantity and is zero-extended here, + // matching the native emitter which widens the I2 into a U4 before building the descriptor. + int nativeType = GetInt32(arguments.FixedArguments[0].Value); + if (!TryWriteCompressed(context, descriptor, nativeType)) + { + return false; + } + + switch (nativeType) + { + case (int)UnmanagedType.Interface: + case (int)UnmanagedType.IUnknown: + case (int)UnmanagedType.IDispatch: + if (iidParameterIndexArgument is { } iidParameterIndex) + { + int iidParameterIndexValue = GetInt32(iidParameterIndex.Value); + if (iidParameterIndexValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, iidParameterIndexValue)) + { + return false; + } + } + break; + + case (int)UnmanagedType.ByValArray: + if (safeArraySubTypeArgument is not null || sizeParamIndexArgument is not null) + { + return context.InvalidValue(); + } + + if (GetTarget(context.Owner) != CaTargets.FieldDef) + { + return context.InvalidTarget(); + } + + if (sizeConstArgument is { } sizeConst) + { + int sizeConstValue = GetInt32(sizeConst.Value); + if (sizeConstValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, sizeConstValue)) + { + return false; + } + } + else if (!TryWriteCompressed(context, descriptor, 1)) + { + return false; + } + + if (arraySubTypeArgument is { } arraySubType + && !TryWriteCompressed(context, descriptor, GetInt32(arraySubType.Value))) + { + return false; + } + break; + + case (int)UnmanagedType.ByValTStr: + if (sizeConstArgument is not { } fixedStringSize) + { + return context.InvalidValue(); + } + + if (arraySubTypeArgument is not null + || sizeParamIndexArgument is not null + || safeArraySubTypeArgument is not null) + { + return context.InvalidValue(); + } + + if (GetTarget(context.Owner) != CaTargets.FieldDef) + { + return context.InvalidTarget(); + } + + if (!TryWriteCompressed(context, descriptor, GetInt32(fixedStringSize.Value))) + { + return false; + } + break; + + case int value when value == (int)UnmanagedType.ByValStr: + if (GetTarget(context.Owner) != CaTargets.ParamDef) + { + return context.InvalidTarget(); + } + break; + + case (int)UnmanagedType.SafeArray: + if (arraySubTypeArgument is not null + || sizeParamIndexArgument is not null + || sizeConstArgument is not null) + { + return context.InvalidValue(); + } + + if (safeArraySubTypeArgument is { } safeArraySubType) + { + int safeArraySubTypeValue = GetInt32(safeArraySubType.Value); + if (!TryWriteCompressed(context, descriptor, safeArraySubTypeValue)) + { + return false; + } + + if (safeArrayUserDefinedSubTypeArgument is { } userDefinedSubType) + { + if (safeArraySubTypeValue != (int)VarEnum.VT_RECORD + && safeArraySubTypeValue != (int)VarEnum.VT_DISPATCH + && safeArraySubTypeValue != (int)VarEnum.VT_UNKNOWN) + { + return context.InvalidValue(); + } + + if (!TryWriteCountedString(context, descriptor, GetString(userDefinedSubType.Value))) + { + return false; + } + } + } + break; + + case (int)UnmanagedType.LPArray: + if (safeArraySubTypeArgument is not null) + { + return context.InvalidValue(); + } + + if (arraySubTypeArgument is { } lpArraySubType) + { + int arraySubTypeValue = GetInt32(lpArraySubType.Value); + if (arraySubTypeValue == (int)UnmanagedType.CustomMarshaler) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, arraySubTypeValue)) + { + return false; + } + } + else if (!TryWriteCompressed(context, descriptor, (int)UnmanagedType.Max)) + { + return false; + } + + if (sizeParamIndexArgument is { } sizeParamIndex) + { + int sizeParamIndexValue = GetInt32(sizeParamIndex.Value); + if (sizeParamIndexValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, GetInt16(sizeParamIndex.Value))) + { + return false; + } + + if (sizeConstArgument is { } indexedSizeConst) + { + int sizeConstValue = GetInt32(indexedSizeConst.Value); + if (sizeConstValue < 0) + { + return context.InvalidValue(); + } + + if (!TryWriteCompressed(context, descriptor, sizeConstValue) + || !TryWriteCompressed(context, descriptor, UnmanagedType.ArraySizeParamIndexSpecified)) + { + return false; + } + } + } + else if (sizeConstArgument is { } unindexedSizeConst) + { + if (!TryWriteCompressed(context, descriptor, 0) + || !TryWriteCompressed(context, descriptor, GetInt32(unindexedSizeConst.Value)) + || !TryWriteCompressed(context, descriptor, 0)) + { + return false; + } + } + break; + + case (int)UnmanagedType.CustomMarshaler: + if (marshalTypeArgument is null && marshalTypeRefArgument is null) + { + return context.InvalidValue(); + } + + // Placeholders for the unmanaged type library GUID and the unmanaged type name. + if (!TryWriteCompressed(context, descriptor, 0) || !TryWriteCompressed(context, descriptor, 0)) + { + return false; + } + + string marshaler = marshalTypeArgument is { } marshalType + ? GetString(marshalType.Value) + : GetString(marshalTypeRefArgument!.Value.Value); + string cookie = marshalCookieArgument is { } marshalCookie + ? GetString(marshalCookie.Value) + : ""; + + if (!TryWriteCountedString(context, descriptor, marshaler) + || !TryWriteCountedString(context, descriptor, cookie)) + { + return false; + } + break; + } + + return true; + } + + private static bool TryWriteCompressed(in LoweringContext context, BlobBuilder builder, int value) + { + if (value < 0 || value > 0x1FFFFFFF) + { + return context.InvalidBlob(); + } + + builder.WriteCompressedInteger(value); + return true; + } + + private static bool TryWriteCountedString(in LoweringContext context, BlobBuilder builder, string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + if (!TryWriteCompressed(context, builder, bytes.Length)) + { + return false; + } + + builder.WriteBytes(bytes); + + return true; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs new file mode 100644 index 00000000000000..bc321fb1f52c07 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.Security.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; + +namespace ILAssembler; + +internal static partial class PseudoCustomAttributes +{ + + private const string DynamicSecurityMethodAttribute = "System.Security.DynamicSecurityMethodAttribute"; + private const string SuppressUnmanagedCodeSecurityAttribute = "System.Security.SuppressUnmanagedCodeSecurityAttribute"; + + private static bool IsSecurityAttribute(string @namespace, string name) => + @namespace == "System.Security" + && name is "DynamicSecurityMethodAttribute" or "SuppressUnmanagedCodeSecurityAttribute"; + + /// + /// Handles the two attributes that the native emitter recognizes by name outside the known + /// attribute table. + /// + private static bool ApplySecurityAttribute(in LoweringContext context, string @namespace, string name, out bool keep) + { + keep = true; + string fullName = @namespace.Length == 0 ? name : @namespace + "." + name; + + if (fullName == DynamicSecurityMethodAttribute) + { + if (context.Owner is not EntityRegistry.MethodDefinitionEntity method) + { + return false; + } + + method.MethodAttributes |= MethodAttributes.RequireSecObject; + keep = false; + return true; + } + + if (fullName == SuppressUnmanagedCodeSecurityAttribute) + { + switch (context.Owner) + { + case EntityRegistry.TypeDefinitionEntity type: + type.Attributes |= TypeAttributes.HasSecurity; + return true; + case EntityRegistry.MethodDefinitionEntity method: + method.MethodAttributes |= MethodAttributes.HasSecurity; + return true; + default: + return false; + } + } + + return false; + } +} diff --git a/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs new file mode 100644 index 00000000000000..f8876dbfc29adc --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/PseudoCustomAttributes.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection.Metadata.Ecma335; + +namespace ILAssembler; + +/// +/// Lowers "pseudo custom attributes" into the metadata flag bits and auxiliary table rows that they +/// represent, and suppresses the CustomAttribute row for the attributes that are not retained. +/// +/// +/// This mirrors RegMeta::DefineCustomAttribute in src/coreclr/md/compiler/custattr_emit.cpp, +/// which the native IL assembler relies on for the same behavior. Attributes are matched on the +/// namespace and name of the declaring type of the constructor only; the assembly the constructor +/// resolves to is deliberately not considered, matching the native implementation. +/// +internal static partial class PseudoCustomAttributes +{ + private static readonly Location s_unknownLocation = new(new SourceSpan(0, 0), new SourceText("", "")); + + public static void Lower(EntityRegistry registry, ImmutableArray.Builder diagnostics) + { + var attributes = new List(); + foreach (var entity in registry.GetSeenEntities(TableIndex.CustomAttribute)) + { + if (entity is EntityRegistry.CustomAttributeEntity attribute) + { + attributes.Add(attribute); + } + } + + if (attributes.Count == 0) + { + return; + } + + var lowered = new List(); + + // Attributes are processed in source order so that a later directive can override an + // earlier one, matching the native emitter which applies each attribute as it is defined. + foreach (var attribute in attributes) + { + if (attribute.Owner is null) + { + continue; + } + + if (!TryGetAttributeTypeName(attribute.Constructor, out string @namespace, out string name)) + { + continue; + } + + KnownAttribute? known = TryFindKnownAttribute(attribute.Constructor, @namespace, name); + if (known is null && !IsSecurityAttribute(@namespace, name)) + { + continue; + } + + var context = new LoweringContext(registry, diagnostics, attribute, @namespace, name); + if (known is not null) + { + // The native emitter abandons the whole DefineCustomAttribute call when a known + // attribute fails validation, so the row is never written regardless of KeepAttribute. + if (!Apply(context, known) || !known.KeepAttribute) + { + lowered.Add(attribute); + } + + continue; + } + + if (ApplySecurityAttribute(context, @namespace, name, out bool keepSecurityAttribute) + && !keepSecurityAttribute) + { + lowered.Add(attribute); + } + } + + registry.RemoveCustomAttributes(lowered); + } + + private readonly struct LoweringContext( + EntityRegistry registry, + ImmutableArray.Builder diagnostics, + EntityRegistry.CustomAttributeEntity attribute, + string @namespace, + string name) + { + public EntityRegistry Registry { get; } = registry; + public EntityRegistry.CustomAttributeEntity Attribute { get; } = attribute; + + /// + /// The attribute target. References to members and types defined in this module are only + /// resolved while metadata rows are written, which happens after this pass runs, so the + /// owner recorded during parsing is resolved to the entity it designates here. + /// + public EntityRegistry.EntityBase Owner { get; } = ResolveOwner(registry, attribute.Owner!); + + public string AttributeName { get; } = @namespace.Length == 0 ? name : @namespace + "." + name; + + public bool Error(string id, string message) + { + diagnostics.Add(new Diagnostic(id, DiagnosticSeverity.Error, message, Attribute.Location ?? s_unknownLocation)); + return false; + } + + public bool InvalidTarget() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidTarget, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidTarget, AttributeName)); + + public bool InvalidValue() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidValue, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidValue, AttributeName)); + + public bool InvalidBlob() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidBlob, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidBlob, AttributeName)); + + public bool InvalidGuid() => Error( + DiagnosticIds.PseudoCustomAttributeInvalidGuid, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeInvalidGuid, AttributeName)); + + public bool UnknownArgument(string argumentName) => Error( + DiagnosticIds.PseudoCustomAttributeUnknownArgument, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeUnknownArgument, AttributeName, argumentName)); + + public bool RepeatedArgument(string argumentName) => Error( + DiagnosticIds.PseudoCustomAttributeRepeatedArgument, + string.Format(DiagnosticMessageTemplates.PseudoCustomAttributeRepeatedArgument, AttributeName, argumentName)); + } + + + + + + +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs index c08a72c8533efc..911accb107fe59 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs @@ -719,5 +719,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 465533cb7357bf..1f82fe3ef8d6de 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -753,5 +753,370 @@ private static void AssertCustomAttributeBlob(MetadataReader reader, CustomAttri reader.GetBlobBytes(attribute.Value)); } + private const TypeAttributes SerializableType = (TypeAttributes)0x00002000; + private const TypeAttributes WindowsRuntimeType = (TypeAttributes)0x00004000; + private const TypeAttributes ExtendedLayoutType = (TypeAttributes)0x00000018; + private const FieldAttributes NotSerializedField = (FieldAttributes)0x0080; + + private static string TypeWithAttribute(string attributeType, string constructor = ".ctor()", string value = "( 01 00 00 00 )") => $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void [mscorlib]{{attributeType}}::{{constructor}} = {{value}} + } + """; + + private static TypeDefinition GetTestType(MetadataReader reader) => + reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test")); + + public static TheoryData PseudoAttributeTypeFlagData => new() + { + { "System.Runtime.InteropServices.ComImportAttribute", TypeAttributes.Import }, + { "System.SerializableAttribute", SerializableType }, + { "System.Runtime.CompilerServices.SpecialNameAttribute", TypeAttributes.SpecialName }, + { "System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeImportAttribute", WindowsRuntimeType }, + }; + + [Theory] + [MemberData(nameof(PseudoAttributeTypeFlagData))] + public void PseudoCustomAttribute_OnType_LowersToFlagAndDropsAttribute(string attributeType, TypeAttributes expected) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(TypeWithAttribute(attributeType), new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(expected, testType.Attributes & expected); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Theory] + [InlineData("System.Runtime.InteropServices.GuidAttribute", ".ctor(string)", "( 01 00 24 30 31 32 33 34 35 36 37 2D 30 31 32 33 2D 30 31 32 33 2D 30 31 32 33 2D 30 30 31 31 32 32 33 33 34 34 35 35 00 00 )")] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 01 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 03 00 00 00 )")] + [InlineData("System.Runtime.InteropServices.ClassInterfaceAttribute", ".ctor(int16)", "( 01 00 02 00 00 00 )")] + public void PseudoCustomAttribute_ValidateOnly_KeepsAttribute(string attributeType, string constructor, string value) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute(attributeType, constructor, value), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Theory] + [InlineData("System.Runtime.InteropServices.GuidAttribute", ".ctor(string)", "( 01 00 04 6E 6F 70 65 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidGuid)] + [InlineData("System.Runtime.InteropServices.InterfaceTypeAttribute", ".ctor(int16)", "( 01 00 07 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidValue)] + [InlineData("System.Runtime.InteropServices.ClassInterfaceAttribute", ".ctor(int16)", "( 01 00 09 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidValue)] + [InlineData("System.SerializableAttribute", ".ctor()", "( 01 00 00 00 )", DiagnosticIds.PseudoCustomAttributeInvalidTarget)] + public void PseudoCustomAttribute_InvalidValueOrTarget_ReportsDiagnostic( + string attributeType, + string constructor, + string value, + string expectedDiagnosticId) + { + // SerializableAttribute is only valid on a type, so applying it to a method is an invalid target. + string source = expectedDiagnosticId == DiagnosticIds.PseudoCustomAttributeInvalidTarget + ? $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]{{attributeType}}::{{constructor}} = {{value}} + ret + } + } + """ + : TypeWithAttribute(attributeType, constructor, value); + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(expectedDiagnosticId, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + + [Theory] + [InlineData("( 01 00 00 00 00 00 00 00 )", TypeAttributes.SequentialLayout)] + [InlineData("( 01 00 01 00 00 00 00 00 )", ExtendedLayoutType)] + [InlineData("( 01 00 02 00 00 00 00 00 )", TypeAttributes.ExplicitLayout)] + [InlineData("( 01 00 03 00 00 00 00 00 )", TypeAttributes.AutoLayout)] + public void PseudoCustomAttribute_StructLayout_SetsLayoutMask(string value, TypeAttributes expected) + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(expected, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_StructLayout_NamedArgumentsSetClassLayoutAndCharSet() + { + // LayoutKind.Explicit with Pack = 4, Size = 16 and CharSet = Unicode (3). + string value = "( 01 00 02 00 00 00 03 00 53 08 04 50 61 63 6B 04 00 00 00 " + + "53 08 04 53 69 7A 65 10 00 00 00 " + + "53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 " + + "07 43 68 61 72 53 65 74 03 00 00 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(TypeAttributes.ExplicitLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Equal(TypeAttributes.UnicodeClass, testType.Attributes & TypeAttributes.StringFormatMask); + + var layout = testType.GetLayout(); + Assert.Equal(4, layout.PackingSize); + Assert.Equal(16, layout.Size); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_DynamicSecurityMethod_SetsRequireSecObjectAndIsDropped() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Security.DynamicSecurityMethodAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodAttributes.RequireSecObject, method.Attributes & MethodAttributes.RequireSecObject); + Assert.Empty(method.GetCustomAttributes()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PseudoCustomAttribute_SuppressUnmanagedCodeSecurity_SetsHasSecurityAndIsKept(bool onType) + { + string attribute = ".custom instance void [mscorlib]System.Security.SuppressUnmanagedCodeSecurityAttribute::.ctor() = ( 01 00 00 00 )"; + string source = onType + ? $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + {{attribute}} + } + """ + : $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + {{attribute}} + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + if (onType) + { + var testType = GetTestType(reader); + Assert.Equal(TypeAttributes.HasSecurity, testType.Attributes & TypeAttributes.HasSecurity); + Assert.Single(testType.GetCustomAttributes()); + } + else + { + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + Assert.Equal(MethodAttributes.HasSecurity, method.Attributes & MethodAttributes.HasSecurity); + Assert.Single(method.GetCustomAttributes()); + } + } + + [Fact] + public void PseudoCustomAttribute_UnknownNamedArgument_ReportsDiagnostic() + { + // StructLayoutAttribute with a named field named "Bogus". + string value = "( 01 00 00 00 00 00 01 00 53 08 05 42 6F 67 75 73 01 00 00 00 )"; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics( + TypeWithAttribute("System.Runtime.InteropServices.StructLayoutAttribute", ".ctor(int32)", value), + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeUnknownArgument, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_StructLayout_ExplicitPackAndSizeDirectivesWin() + { + // LayoutKind.Sequential with Pack = 16, on a type that also declares .pack and .size. + // The native assembler emits the ClassLayout row for the explicit directives in a later + // phase than the one that applies the attribute, so the directives take precedence. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType + { + .custom instance void [mscorlib]System.Runtime.InteropServices.StructLayoutAttribute::.ctor(int32) = ( 01 00 00 00 00 00 01 00 53 08 04 50 61 63 6B 10 00 00 00 ) + .pack 4 + .size 32 + .field public int32 Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var layout = GetTestType(reader).GetLayout(); + + Assert.Equal(4, layout.PackingSize); + Assert.Equal(32, layout.Size); + } + + [Fact] + public void PseudoCustomAttribute_NonPseudoAttribute_IsStillEmitted() + { + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.ObsoleteAttribute"), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_LoweredAttribute_DoesNotShiftRemainingAttributeOwners() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi First extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 ) + .custom instance void [mscorlib]System.SerializableAttribute::.ctor() = ( 01 00 00 00 ) + } + .class public auto ansi Second extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 ) + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + foreach (string typeName in new[] { "First", "Second" }) + { + var typeDefinition = reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == typeName)); + var attribute = reader.GetCustomAttribute(Assert.Single(typeDefinition.GetCustomAttributes())); + var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var declaringType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + Assert.Equal("ObsoleteAttribute", reader.GetString(declaringType.Name)); + } + + var first = reader.GetTypeDefinition(reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "First")); + Assert.Equal(SerializableType, first.Attributes & SerializableType); + } + + [Fact] + public void PseudoCustomAttribute_ZeroArgDescriptor_MalformedBlobSkipped() + { + // SerializableAttribute has zero fixed and zero named-arg descriptors. The native + // emitter does not parse the blob at all for such attributes, so even a completely + // malformed blob (bad prolog or arbitrary bytes) must produce no diagnostics. + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute("System.SerializableAttribute", ".ctor()", "( FF FF DE AD BE EF )"), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(SerializableType, testType.Attributes & SerializableType); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_FixedArgsNoNamedDescriptors_EverettBlobWithNoNamedCountAccepted() + { + // GuidAttribute has one fixed string arg and no named-arg descriptors. When the blob + // ends immediately after the fixed argument with no 2-byte named-arg count, the native + // emitter accepts it as Everett-compatible behavior. Compilation must succeed and the + // attribute row must be retained (GuidAttribute has KeepAttribute = true). + // Blob: 01 00 (prolog) 24 (SerString length = 36) + 36 UTF-8 bytes of GUID -- no trailing 00 00. + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute( + "System.Runtime.InteropServices.GuidAttribute", + ".ctor(string)", + "( 01 00 24 30 31 32 33 34 35 36 37 2D 30 31 32 33 2D 30 31 32 33 2D 30 31 32 33 2D 30 30 31 31 32 32 33 33 34 34 35 35 )"), + new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Single(GetTestType(reader).GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_NamedArgCount0x8000_TreatedAsSignedNegativeAndSkipped() + { + // The named-argument count is stored and compared as a signed INT16 in the native + // emitter. A count of 0x8000 (-32768 when sign-extended) causes the loop to execute + // zero times, so no named arguments are consumed. The fixed layout effect is applied + // and the CA row is dropped. + // Blob: 01 00 (prolog) 02 00 (I2 = LayoutKind.Explicit) 00 80 (count = 0x8000 LE). + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + TypeWithAttribute( + "System.Runtime.InteropServices.StructLayoutAttribute", + ".ctor(int16)", + "( 01 00 02 00 00 80 )"), + new Options()); + var reader = pe.GetMetadataReader(); + var testType = GetTestType(reader); + + Assert.Equal(TypeAttributes.ExplicitLayout, testType.Attributes & TypeAttributes.LayoutMask); + Assert.Empty(testType.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_TruncatedFixedArg_ReportsInvalidBlobDiagnostic() + { + // A blob that is too short to supply all fixed arguments must produce + // PseudoCustomAttributeInvalidBlob and no output image in normal mode. + // Blob: 01 00 (valid prolog) 01 (1 byte; I4 requires 4 bytes) -- truncated. + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText( + TypeWithAttribute( + "System.Runtime.InteropServices.StructLayoutAttribute", + ".ctor(int32)", + "( 01 00 01 )"), + "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidBlob, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + Assert.Null(result); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs index ef47a470cc1596..573a155a39171f 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs @@ -21,6 +21,7 @@ namespace ILAssembler.Tests { public class FieldTests { + private const FieldAttributes NotSerializedField = (FieldAttributes)0x0080; [Fact] public void FieldLayout_ExplicitOffset() @@ -101,6 +102,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 1d5984a1372812..b7f27ba07b6406 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -551,5 +551,297 @@ private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) Assert.Equal("value", reader.GetString(parameter.Name)); return parameter.GetMarshallingDescriptor(); } + + private static MethodDefinition GetMethod(MetadataReader reader, string name) => + reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == name); + + [Fact] + public void PseudoCustomAttribute_PreserveSig_LowersToImplFlagAndIsDropped() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.PreserveSigAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "M"); + + Assert.Equal(MethodImplAttributes.PreserveSig, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + } + + private static string DllImportSource(string blob) => $$""" + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Native() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.DllImportAttribute::.ctor(string) = {{blob}} + ret + } + } + """; + + [Fact] + public void PseudoCustomAttribute_DllImport_CreatesImplMapAndIsDropped() + { + // DllImportAttribute("kernel32.dll") with no named arguments. + string blob = "( 01 00 0C 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(DllImportSource(blob), new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "Native"); + + Assert.Equal(MethodAttributes.PinvokeImpl, method.Attributes & MethodAttributes.PinvokeImpl); + Assert.Equal(MethodImplAttributes.PreserveSig, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + + var import = method.GetImport(); + Assert.Equal("kernel32.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); + // The entry point defaults to the method name. + Assert.Equal("Native", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CallingConventionWinApi, import.Attributes & MethodImportAttributes.CallingConventionMask); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_NamedArgumentsSetImportAttributes() + { + // DllImportAttribute("kernel32.dll") with EntryPoint = "GetLastError", CharSet = Unicode (3), + // CallingConvention = Cdecl (2), SetLastError = true, ExactSpelling = true and PreserveSig = false. + string blob = "( 01 00 0C 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 06 00 " + + "53 0E 0A 45 6E 74 72 79 50 6F 69 6E 74 0C 47 65 74 4C 61 73 74 45 72 72 6F 72 " + + "53 55 26 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 68 61 72 53 65 74 07 43 68 61 72 53 65 74 03 00 00 00 " + + "53 55 30 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E 11 43 61 6C 6C 69 6E 67 43 6F 6E 76 65 6E 74 69 6F 6E 02 00 00 00 " + + "53 02 0C 53 65 74 4C 61 73 74 45 72 72 6F 72 01 " + + "53 02 0D 45 78 61 63 74 53 70 65 6C 6C 69 6E 67 01 " + + "53 02 0B 50 72 65 73 65 72 76 65 53 69 67 00 )"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(DllImportSource(blob), new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "Native"); + var import = method.GetImport(); + + Assert.Equal("GetLastError", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CharSetUnicode, import.Attributes & MethodImportAttributes.CharSetMask); + Assert.Equal(MethodImportAttributes.CallingConventionCDecl, import.Attributes & MethodImportAttributes.CallingConventionMask); + Assert.Equal(MethodImportAttributes.SetLastError, import.Attributes & MethodImportAttributes.SetLastError); + Assert.Equal(MethodImportAttributes.ExactSpelling, import.Attributes & MethodImportAttributes.ExactSpelling); + Assert.Equal(default, method.ImplAttributes & MethodImplAttributes.PreserveSig); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_EmptyModuleName_ReportsInvalidValue() + { + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics( + DllImportSource("( 01 00 00 00 00 )"), + new Options()); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_DllImport_ExplicitPinvokeImplWinsButModuleReferenceIsStillCreated() + { + // The native assembler emits the ImplMap row for an explicit pinvokeimpl clause in a + // later phase than the one that applies the attribute, so the clause takes precedence. + // The attribute's module reference is still resolved, and so is still emitted. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static pinvokeimpl("explicit.dll" as "ExplicitEntry" cdecl) void Native() cil managed + { + .custom instance void [mscorlib]System.Runtime.InteropServices.DllImportAttribute::.ctor(string) = ( 01 00 08 61 74 74 72 2E 64 6C 6C 00 00 ) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var import = GetMethod(reader, "Native").GetImport(); + + Assert.Equal("explicit.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); + Assert.Equal("ExplicitEntry", reader.GetString(import.Name)); + Assert.Equal(MethodImportAttributes.CallingConventionCDecl, import.Attributes & MethodImportAttributes.CallingConventionMask); + + Assert.Contains( + "attr.dll", + Enumerable.Range(1, reader.GetTableRowCount(TableIndex.ModuleRef)) + .Select(rid => reader.GetString( + reader.GetModuleReference(MetadataTokens.ModuleReferenceHandle(rid)).Name))); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnParameter_CreatesFieldMarshalAndIsDropped() + { + // MarshalAsAttribute(UnmanagedType.Bool) applied to the parameter via .param [1]. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32 value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = GetMethod(reader, "M"); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); + + Assert.Equal(ParameterAttributes.HasFieldMarshal, parameter.Attributes & ParameterAttributes.HasFieldMarshal); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(parameter.GetMarshallingDescriptor())); + Assert.Empty(parameter.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_ByValArrayOnField_EncodesSizeAndSubType() + { + // MarshalAsAttribute(UnmanagedType.ByValArray) with SizeConst = 4 and ArraySubType = I4. + string blob = "( 01 00 1E 00 00 00 02 00 " + + "53 08 09 53 69 7A 65 43 6F 6E 73 74 04 00 00 00 " + + "53 55 2C 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 49 6E 74 65 72 6F 70 53 65 72 76 69 63 65 73 2E 55 6E 6D 61 6E 61 67 65 64 54 79 70 65 " + + "0C 41 72 72 61 79 53 75 62 54 79 70 65 07 00 00 00 )"; + + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public sequential ansi sealed Test extends [mscorlib]System.ValueType + { + .field public int32[] Values + .custom (field int32[] Test::Values) instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = {{blob}} + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + + Assert.Equal(FieldAttributes.HasFieldMarshal, field.Attributes & FieldAttributes.HasFieldMarshal); + Assert.Equal( + [(byte)UnmanagedType.ByValArray, 0x04, (byte)UnmanagedType.I4], + reader.GetBlobBytes(field.GetMarshallingDescriptor())); + Assert.Empty(field.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_ByValArrayOnParameter_ReportsInvalidTarget() + { + // UnmanagedType.ByValArray is only valid on fields. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32[] value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = ( 01 00 1E 00 00 00 00 00 ) + ret + } + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidTarget, diagnostic.Id); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_OnProperty_AppliesToAccessorParameters() + { + // MarshalAsAttribute(UnmanagedType.Bool) on a property fans out to the getter's return + // parameter and to the setter's last parameter. + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public instance int32 get_Value() cil managed + { + ldc.i4.0 + ret + } + .method public instance void set_Value(int32 'value') cil managed + { + ret + } + .property instance int32 Value() + { + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + .get instance int32 Test::get_Value() + .set instance void Test::set_Value(int32) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var property = reader.GetPropertyDefinition(Assert.Single(reader.PropertyDefinitions)); + Assert.Empty(property.GetCustomAttributes()); + + var getterReturn = reader.GetParameter(GetMethod(reader, "get_Value").GetParameters().Single()); + Assert.Equal(0, getterReturn.SequenceNumber); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(getterReturn.GetMarshallingDescriptor())); + + var setterValue = reader.GetParameter(GetMethod(reader, "set_Value").GetParameters() + .Single(handle => reader.GetParameter(handle).SequenceNumber == 1)); + Assert.Equal([(byte)UnmanagedType.Bool], reader.GetBlobBytes(setterValue.GetMarshallingDescriptor())); + } + + [Fact] + public void PseudoCustomAttribute_MarshalAs_CustomMarshaler_EncodesMarshalerAndCookie() + { + // MarshalAsAttribute(UnmanagedType.CustomMarshaler) with MarshalType = "M" and MarshalCookie = "C". + string blob = "( 01 00 2C 00 00 00 02 00 " + + "53 0E 0B 4D 61 72 73 68 61 6C 54 79 70 65 01 4D " + + "53 0E 0D 4D 61 72 73 68 61 6C 43 6F 6F 6B 69 65 01 43 )"; + + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(object value) cil managed + { + .param [1] + .custom instance void [mscorlib]System.Runtime.InteropServices.MarshalAsAttribute::.ctor(int32) = {{blob}} + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var parameter = reader.GetParameter(Assert.Single(GetMethod(reader, "M").GetParameters())); + + // Native type, empty GUID placeholder, empty native type name placeholder, marshaler name, cookie. + Assert.Equal( + [(byte)UnmanagedType.CustomMarshaler, 0x00, 0x00, 0x01, (byte)'M', 0x01, (byte)'C'], + reader.GetBlobBytes(parameter.GetMarshallingDescriptor())); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs index 97805fb7ee60fa..bc61c9759ecc6f 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs @@ -159,6 +159,110 @@ .method public static void FlaggedImplMethod() flags(0x1000) { } Assert.Equal((MethodImplAttributes)0x1000, methods["FlaggedImplMethod"].ImplAttributes); } + [Fact] + public void PseudoCustomAttribute_SpecialNameOnMethod_LowersToFlagAndDropsAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.SpecialNameAttribute::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodAttributes.SpecialName, method.Attributes & MethodAttributes.SpecialName); + Assert.Empty(method.GetCustomAttributes()); + } + + [Theory] + [InlineData("08 00", MethodImplAttributes.NoInlining)] + [InlineData("00 20", MethodImplAttributes.Async)] + public void PseudoCustomAttribute_MethodImpl_LowersToImplAttributeAndDropsAttribute( + string valueBytes, + MethodImplAttributes expected) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor(int16) = ( 01 00 {{valueBytes}} 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(expected, method.ImplAttributes & expected); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MethodImplWithMethodCodeTypeNamedArgument_LowersToImplAttributeAndDropsAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor() = ( 01 00 01 00 53 55 2E 53 79 73 74 65 6D 2E 52 75 6E 74 69 6D 65 2E 43 6F 6D 70 69 6C 65 72 53 65 72 76 69 63 65 73 2E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 0E 4D 65 74 68 6F 64 43 6F 64 65 54 79 70 65 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + + Assert.Equal(MethodImplAttributes.Native, method.ImplAttributes & MethodImplAttributes.CodeTypeMask); + Assert.Empty(method.GetCustomAttributes()); + } + + [Fact] + public void PseudoCustomAttribute_MethodImplInvalidValue_ReportsDiagnostic() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.MethodImplAttribute::.ctor(int16) = ( 01 00 02 00 00 00 ) + ret + } + } + """; + + var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.PseudoCustomAttributeInvalidValue, diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + } + [Fact] public void MethodBodyDirectives_EmitRawInstructionLocalsInitializationAndMappedData() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs index 3931daef2d36e1..16671a51242d72 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs @@ -229,6 +229,37 @@ .method public static void M( Assert.Equal((ParameterAttributes)8, parameters["rawFlags"].Attributes); } + [Theory] + [InlineData("System.Runtime.InteropServices.InAttribute", ParameterAttributes.In)] + [InlineData("System.Runtime.InteropServices.OutAttribute", ParameterAttributes.Out)] + [InlineData("System.Runtime.InteropServices.OptionalAttribute", ParameterAttributes.Optional)] + public void PseudoCustomAttribute_OnParameter_LowersToFlagAndDropsAttribute(string attributeType, ParameterAttributes expected) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M(int32 x) cil managed + { + .param [1] + .custom instance void [mscorlib]{{attributeType}}::.ctor() = ( 01 00 00 00 ) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); + + Assert.Equal(expected, parameter.Attributes & expected); + Assert.Empty(parameter.GetCustomAttributes()); + } + [Fact] public void UnnamedInstanceParam_EmitsParamRow() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs index 468c1d7aedd50b..b93199c66b121d 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); + } + } } }