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);
+ }
+ }
}
}