diff --git a/src/tools/ilasm/src/ILAssembler/CIL.g4 b/src/tools/ilasm/src/ILAssembler/CIL.g4 index 41fe4f88c5e820..dc982483940f17 100644 --- a/src/tools/ilasm/src/ILAssembler/CIL.g4 +++ b/src/tools/ilasm/src/ILAssembler/CIL.g4 @@ -558,12 +558,12 @@ classAttr: | 'private' | VALUE | ENUM - | 'interface' + | INTERFACE | 'sealed' | 'abstract' | 'auto' | 'sequential' - | 'explicit' + | EXPLICIT | 'extended' | ANSI | 'unicode' diff --git a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs index 39f3561e8b60f8..9b803fa23ad7a1 100644 --- a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs +++ b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs @@ -50,6 +50,7 @@ public static class DiagnosticIds public const string UnknownGenericParameter = "ILA0028"; public const string ParameterIndexOutOfRange = "ILA0029"; public const string DuplicateMethod = "ILA0030"; + public const string MissingExportedTypeImplementation = "ILA0031"; } internal static class DiagnosticMessageTemplates @@ -84,4 +85,5 @@ internal static class DiagnosticMessageTemplates public const string UnknownGenericParameter = "Unknown generic parameter '{0}'"; public const string ParameterIndexOutOfRange = "Parameter index {0} is out of range"; public const string DuplicateMethod = "Duplicate method definition"; + public const string MissingExportedTypeImplementation = "Undefined implementation in ExportedType '{0}' -- ExportedType not emitted"; } diff --git a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs index 04e80a6e69444d..5d1a40abff3a38 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -94,6 +94,14 @@ public IReadOnlyList GetSeenEntities(TableIndex table) public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadOnlyDictionary mappedFieldDataNames) { + // Set the assembly handle early since DeclarativeSecurityAttribute needs it + // The assembly definition handle is always row 1 (there's only ever one assembly per module) + // Assembly table token = 0x20000001 + if (Assembly is not null) + { + ((IHasHandle)Assembly).SetHandle(MetadataTokens.EntityHandle(0x20000001)); + } + // Now that we've seen all of the entities, we can write them out in the correct order. // Record the entities in the correct order so they are assigned handles. // After this, we'll write out the content of the entities in the correct order. @@ -109,11 +117,21 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO // or other rows that would refer to it. if (param.Name is not null || param.MarshallingDescriptor.Count != 0 - || param.HasCustomAttributes) + || param.HasCustomAttributes + || param.HasConstant) { RecordEntityInTable(TableIndex.Param, param); } } + // Record generic parameters for methods + foreach (var genericParam in method.GenericParameters) + { + RecordEntityInTable(TableIndex.GenericParam, genericParam); + } + foreach (var constraint in method.GenericParameterConstraints) + { + RecordEntityInTable(TableIndex.GenericParamConstraint, constraint); + } } foreach (var field in type.Fields) { @@ -269,6 +287,11 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO { builder.AddMarshallingDescriptor(param.Handle, builder.GetOrAddBlob(param.MarshallingDescriptor)); } + + if (param.HasConstant) + { + builder.AddConstant(param.Handle, param.ConstantValue); + } } foreach (InterfaceImplementationEntity impl in GetSeenEntities(TableIndex.InterfaceImpl)) @@ -324,6 +347,11 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO { builder.AddMethodSemantics(prop.Handle, accessor.Semantic, (MethodDefinitionHandle)accessor.Method.Handle); } + + if (prop.HasConstant) + { + builder.AddConstant(prop.Handle, prop.ConstantValue); + } } foreach (ModuleReferenceEntity moduleRef in GetSeenEntities(TableIndex.ModuleRef)) @@ -349,12 +377,14 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO if (Assembly is not null) { + // Combine the base flags with the architecture bits + var assemblyFlags = Assembly.Flags | (AssemblyFlags)((int)Assembly.ProcessorArchitecture << 4); builder.AddAssembly( builder.GetOrAddString(Assembly.Name), Assembly.Version ?? new Version(), Assembly.Culture is null ? default : builder.GetOrAddString(Assembly.Culture), Assembly.PublicKeyOrToken is null ? default : builder.GetOrAddBlob(Assembly.PublicKeyOrToken), - Assembly.Flags, + assemblyFlags, Assembly.HashAlgorithm); } @@ -368,11 +398,17 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO foreach (ExportedTypeEntity exportedType in GetSeenEntities(TableIndex.ExportedType)) { + // Implementation must be a valid handle type: AssemblyFileHandle, AssemblyReferenceHandle, or ExportedTypeHandle + // COMPAT: If implementation is null, skip emitting this exported type + if (exportedType.Implementation is null) + { + continue; + } builder.AddExportedType( exportedType.Attributes, builder.GetOrAddString(exportedType.Namespace), builder.GetOrAddString(exportedType.Name), - exportedType.Implementation?.Handle ?? default, + exportedType.Implementation.Handle, exportedType.TypeDefinitionId); } @@ -390,6 +426,22 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO builder.AddMethodSpecification(methodSpec.Parent.Handle, builder.GetOrAddBlob(methodSpec.Signature)); } + foreach (GenericParameterEntity genericParam in GetSeenEntities(TableIndex.GenericParam)) + { + builder.AddGenericParameter( + genericParam.Owner!.Handle, + genericParam.Attributes, + builder.GetOrAddString(genericParam.Name), + genericParam.Index); + } + + foreach (GenericParameterConstraintEntity constraint in GetSeenEntities(TableIndex.GenericParamConstraint)) + { + builder.AddGenericParameterConstraint( + (GenericParameterHandle)constraint.Owner!.Handle, + constraint.BaseType.Handle); + } + static FieldDefinitionHandle GetFieldHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) => (FieldDefinitionHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.Field); @@ -407,20 +459,26 @@ static ParameterHandle GetParameterHandleForList(IReadOnlyList list, static EntityHandle GetHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex, TableIndex tokenType) { - // Return the first entry in the list. - // If the list is empty, return the start of the next list. + // Return the first entry in the list that has a handle. + // If no item has a handle, return the start of the next list. // If there is no next list, return one past the end of the previous list. - if (list.Count != 0 && !list[0].Handle.IsNil) + foreach (var item in list) { - return list[0].Handle; + if (!item.Handle.IsNil) + { + return item.Handle; + } } - for (int i = 0; i < listOwner.Count; i++) + for (int i = ownerIndex + 1; i < listOwner.Count; i++) { var otherList = getList(listOwner[i]); - if (otherList.Count != 0 && !otherList[0].Handle.IsNil) + foreach (var item in otherList) { - return otherList[0].Handle; + if (!item.Handle.IsNil) + { + return item.Handle; + } } } @@ -1020,8 +1078,10 @@ public ManifestResourceEntity CreateManifestResource(string name, uint offset) public ExportedTypeEntity GetOrCreateExportedType(EntityBase? implementation, string @namespace, string name, Action onCreateType) { - // We only key on the implementation if the type is nested. - return GetOrCreateEntity((implementation as ExportedTypeEntity, @namespace, name), TableIndex.ExportedType, _seenExportedTypes, (key) => new(key.Item3, key.Item2, key.Item1), onCreateType); + // We only key on the implementation if the type is nested (ExportedTypeEntity). + // For forwarders, implementation is AssemblyReferenceEntity which is not used in the key. + // However, we need to pass the actual implementation to the entity constructor. + return GetOrCreateEntity((implementation as ExportedTypeEntity, @namespace, name), TableIndex.ExportedType, _seenExportedTypes, (key) => new(key.Item3, key.Item2, implementation), onCreateType); } public ExportedTypeEntity? FindExportedType(ExportedTypeEntity? containingType, string @namespace, string @name) @@ -1232,6 +1292,26 @@ public sealed class MethodDefinitionEntity(TypeDefinitionEntity containingType, /// Debug information for this method (sequence points, document). /// public MethodDebugInfo DebugInfo { get; } = new(); + + /// + /// Export ordinal for this method (from .export directive). -1 means not exported. + /// + public int ExportOrdinal { get; set; } = -1; + + /// + /// Export alias name (from .export [n] as alias). Null means use method name. + /// + public string? ExportAlias { get; set; } + + /// + /// 1-based VTable entry index (from .vtentry directive). 0 means not in vtable. + /// + public int VTableEntry { get; set; } + + /// + /// 1-based slot within the VTable entry (from .vtentry directive). 0 means not in vtable. + /// + public int VTableSlot { get; set; } } public sealed class ParameterEntity(ParameterAttributes attributes, string? name, BlobBuilder marshallingDescriptor, int sequence) : EntityBase @@ -1241,6 +1321,8 @@ public sealed class ParameterEntity(ParameterAttributes attributes, string? name public BlobBuilder MarshallingDescriptor { get; set; } = marshallingDescriptor; public bool HasCustomAttributes { get; set; } public int Sequence { get; } = sequence; + public bool HasConstant { get; set; } + public object? ConstantValue { get; set; } } public sealed class MemberReferenceEntity(EntityBase parent, string name, BlobBuilder signature) : EntityBase @@ -1342,9 +1424,11 @@ public sealed class EventEntity(EventAttributes attributes, TypeEntity type, str public sealed class PropertyEntity(PropertyAttributes attributes, BlobBuilder type, string name) : EntityBase { - public PropertyAttributes Attributes { get; } = attributes; + public PropertyAttributes Attributes { get; set; } = attributes; public BlobBuilder Type { get; } = type; public string Name { get; } = name; + public bool HasConstant { get; set; } + public object? ConstantValue { get; set; } public List<(MethodSemanticsAttributes Semantic, EntityBase Method)> Accessors { get; } = new(); } diff --git a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs index 069a2b494dc2c6..eba265d9614afe 100644 --- a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs +++ b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -97,13 +97,15 @@ internal sealed class GrammarVisitor : ICILVisitor private readonly Dictionary _documentHandles = new(); private readonly MetadataBuilder _pdbBuilder = new(); + // VTable fixup tracking - uses types from VTableFixupSupport + private readonly List _vtableFixups = new(); + public GrammarVisitor(IReadOnlyDictionary documents, Options options, Func resourceLocator) { _documents = documents; _options = options; _resourceLocator = resourceLocator; } - /// /// Represents a typedef alias entry. /// @@ -138,9 +140,30 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC return (_diagnostics.ToImmutable(), null); } + // Check for vtable fixups and exports - collect export info + var exports = ImmutableArray.CreateBuilder(); + foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + { + if (entity is EntityRegistry.MethodDefinitionEntity method && method.ExportOrdinal >= 0) + { + exports.Add(new VTableExportPEBuilder.ExportInfo( + method.ExportOrdinal, + method.ExportAlias ?? method.Name, + MetadataTokens.GetToken(method.Handle), + method.VTableEntry, + method.VTableSlot)); + } + } + BlobBuilder ilStream = new(); _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames); MetadataRootBuilder rootBuilder = new(_metadataBuilder); + + // Compute metadata size from the MetadataSizes + // We need this for data label fixup RVA calculations + var sizes = rootBuilder.Sizes; + int metadataSize = ComputeMetadataSize(sizes); + PEHeaderBuilder header = new( fileAlignment: _alignment, imageBase: (ulong)_imageBase, @@ -153,9 +176,33 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC } // Build debug directory if we have any debug info - DebugDirectoryBuilder? debugDirectoryBuilder = BuildDebugDirectory(entryPoint); + DebugDirectoryBuilder? debugDirectoryBuilder = BuildDebugDirectory(entryPoint, out int debugDataSize); + + // Use custom PE builder if we have vtable fixups, exports, or data label reference fixups + if (_vtableFixups.Count > 0 || exports.Count > 0 || _mappedFieldDataReferenceFixups.Count > 0) + { + var vtableFixupInfos = BuildVTableFixupInfos(); + + VTableExportPEBuilder peBuilder = new( + header, + rootBuilder, + ilStream, + _mappedFieldData, + _manifestResources, + debugDirectoryBuilder: debugDirectoryBuilder, + entryPoint: entryPoint, + flags: CorFlags.ILOnly, + vtableFixups: vtableFixupInfos, + exports: exports.ToImmutable(), + mappedFieldDataOffsets: _mappedFieldDataNames, + dataLabelFixups: _mappedFieldDataReferenceFixups, + metadataSize: metadataSize, + debugDataSize: debugDataSize); + + return (_diagnostics.ToImmutable(), peBuilder); + } - ManagedPEBuilder peBuilder = new( + ManagedPEBuilder standardBuilder = new( header, rootBuilder, ilStream, @@ -165,11 +212,53 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC entryPoint: entryPoint, debugDirectoryBuilder: debugDirectoryBuilder); - return (_diagnostics.ToImmutable(), peBuilder); + return (_diagnostics.ToImmutable(), standardBuilder); } - private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint) + private ImmutableArray BuildVTableFixupInfos() { + if (_vtableFixups.Count == 0) + return ImmutableArray.Empty; + + var builder = ImmutableArray.CreateBuilder(_vtableFixups.Count); + + for (int entryIndex = 0; entryIndex < _vtableFixups.Count; entryIndex++) + { + var vtf = _vtableFixups[entryIndex]; + var methodTokens = ImmutableArray.CreateBuilder(vtf.SlotCount); + + // Initialize with zeros + for (int i = 0; i < vtf.SlotCount; i++) + { + methodTokens.Add(0); + } + + // Find methods that reference this vtable entry + foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + { + if (entity is EntityRegistry.MethodDefinitionEntity method && + method.VTableEntry == entryIndex + 1 && // 1-based + method.VTableSlot > 0 && + method.VTableSlot <= vtf.SlotCount) + { + methodTokens[method.VTableSlot - 1] = MetadataTokens.GetToken(method.Handle); + } + } + + builder.Add(new VTableExportPEBuilder.VTableFixupInfo( + vtf.DataLabel, + vtf.SlotCount, + vtf.Flags, + methodTokens.ToImmutable())); + } + + return builder.ToImmutable(); + } + + private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint, out int debugDataSize) + { + debugDataSize = 0; + // Check if we have any methods with debug info bool hasDebugInfo = false; foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) @@ -211,6 +300,17 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC pdbBuilder.FormatVersion); debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(pdbBlob, pdbBuilder.FormatVersion); + // Calculate debug data size: + // 2 debug directory entries (28 bytes each) + CodeView data (~24 bytes) + Embedded PDB data (compressed pdbBlob + 8 header) + // CodeView entry: signature (4) + guid (16) + age (4) + path (variable, ~12 for "assembly.pdb\0") + const int debugDirEntrySize = 28; + int codeViewDataSize = 4 + 16 + 4 + "assembly.pdb".Length + 1; // signature + guid + age + path + null + int embeddedPdbHeaderSize = 8; // MPDB signature (4) + uncompressed size (4) + // The embedded PDB is compressed, estimate conservatively as same size + int embeddedPdbDataSize = embeddedPdbHeaderSize + pdbBlob.Count; + + debugDataSize = (2 * debugDirEntrySize) + codeViewDataSize + embeddedPdbDataSize; + return debugDirectoryBuilder; } @@ -693,6 +793,15 @@ public GrammarResult VisitChildren(IRuleNode node) // COMPAT: ilasm implies the Sealed flag when using the 'value' keyword in a type declaration return new((new(TypeAttributes.Sealed), EntityRegistry.WellKnownBaseType.System_ValueType, true)); } + else if (context.EXPLICIT() is not null) + { + return new((new(TypeAttributes.ExplicitLayout), null, false)); + } + else if (context.INTERFACE() is not null) + { + // COMPAT: interface implies abstract + return new((new(TypeAttributes.Interface | TypeAttributes.Abstract), null, false)); + } switch (context.GetText()) { @@ -706,8 +815,6 @@ public GrammarResult VisitChildren(IRuleNode node) return new((new(TypeAttributes.AutoLayout), null, false)); case "sequential": return new((new(TypeAttributes.SequentialLayout), null, false)); - case "explicit": - return new((new(TypeAttributes.ExplicitLayout), null, false)); case "extended": return new((new(TypeAttributes.ExtendedLayout), null, false)); default: @@ -782,6 +889,34 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) } } } + else if (context.propHead() is CILParser.PropHeadContext propHead) + { + var property = VisitPropHead(propHead).Value; + var currentType = _currentTypeDefinition.PeekOrDefault(); + if (currentType is not null) + { + currentType.Properties.Add(property); + var accessors = VisitPropDecls(context.propDecls()).Value; + foreach (var accessor in accessors) + { + property.Accessors.Add(accessor); + } + } + } + else if (context.eventHead() is CILParser.EventHeadContext eventHead) + { + var evt = VisitEventHead(eventHead).Value; + var currentType = _currentTypeDefinition.PeekOrDefault(); + if (currentType is not null) + { + currentType.Events.Add(evt); + var accessors = VisitEventDecls(context.eventDecls()).Value; + foreach (var accessor in accessors) + { + evt.Accessors.Add(accessor); + } + } + } return GrammarResult.SentinelValue.Result; } @@ -830,7 +965,8 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) isNewType = true; EntityRegistry.WellKnownBaseType? fallbackBase = _options.NoAutoInherit ? null : EntityRegistry.WellKnownBaseType.System_Object; bool requireSealed = false; - newTypeDef.Attributes = context.classAttr().Select(VisitClassAttr).Aggregate( + var classAttrs = context.classAttr(); + newTypeDef.Attributes = classAttrs.Select(VisitClassAttr).Aggregate( (TypeAttributes)0, (acc, result) => { @@ -850,11 +986,13 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) return attribute.Value; } requireSealed |= attrRequireSealed; - if (TypeAttributes.LayoutMask.HasFlag(attribute.Value)) + // Note: We check attribute.Value != 0 because HasFlag(0) always returns true, + // but AutoLayout (0) and AnsiClass (0) should not clear other flags. + if (attribute.Value != 0 && TypeAttributes.LayoutMask.HasFlag(attribute.Value)) { return (acc & ~TypeAttributes.LayoutMask) | attribute.Value; } - if (TypeAttributes.StringFormatMask.HasFlag(attribute.Value)) + if (attribute.Value != 0 && TypeAttributes.StringFormatMask.HasFlag(attribute.Value)) { return (acc & ~TypeAttributes.StringFormatMask) | attribute.Value; } @@ -867,7 +1005,7 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) // COMPAT: ILASM ignores the rtspecialname directive on a type. return acc; } - if (attribute.Value == TypeAttributes.Interface) + if ((attribute.Value & TypeAttributes.Interface) != 0) { // COMPAT: interface implies abstract return acc | TypeAttributes.Interface | TypeAttributes.Abstract; @@ -1330,13 +1468,15 @@ public GrammarResult VisitDdItem(CILParser.DdItemContext context) } else if (context.id() is CILParser.IdContext id) { + // Reference to another data label - this will be patched with the target's RVA + // during PE serialization by VTableExportPEBuilder.ApplyDataLabelFixups() string name = VisitId(id).Value; if (!_mappedFieldDataReferenceFixups.TryGetValue(name, out var fixups)) { _mappedFieldDataReferenceFixups[name] = fixups = new(); } - // TODO: Figure out how to handle relocs correctly + // Reserve 4 bytes for the RVA that will be patched later fixups.Add(_mappedFieldData.ReserveBytes(4)); return GrammarResult.SentinelValue.Result; } @@ -1478,6 +1618,14 @@ public GrammarResult VisitDecl(CILParser.DeclContext context) var (attrs, dottedName) = VisitExptypeHead(exptypeHead).Value; (string typeNamespace, string name) = NameHelpers.SplitDottedNameToNamespaceAndName(dottedName); var (impl, typeDefId, customAttrs) = VisitExptypeDecls(context.exptypeDecls()).Value; + if (impl is null) + { + // COMPAT: Like native ilasm, warn and skip the exported type when implementation is not specified + ReportWarning(DiagnosticIds.MissingExportedTypeImplementation, + string.Format(DiagnosticMessageTemplates.MissingExportedTypeImplementation, dottedName), + exptypeHead); + return GrammarResult.SentinelValue.Result; + } var exp = _entityRegistry.GetOrCreateExportedType(impl, typeNamespace, name, exp => { exp.Attributes = attrs; @@ -1831,7 +1979,7 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt }; } - // TODO: Implement multimodule type exports and fowarders + // Type exports and forwarders are implemented via VisitExptypeDecls public GrammarResult VisitExptypeDecl(CILParser.ExptypeDeclContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); GrammarResult ICILVisitor.VisitExptypeDecls(CILParser.ExptypeDeclsContext context) => VisitExptypeDecls(context); @@ -2204,11 +2352,65 @@ public GrammarResult VisitFieldDecl(CILParser.FieldDeclContext context) SerializationTypeCode.Single => valueBytes.Length >= 4 ? BitConverter.ToSingle(valueBytes) : 0f, SerializationTypeCode.Double => valueBytes.Length >= 8 ? BitConverter.ToDouble(valueBytes) : 0d, SerializationTypeCode.String => Encoding.Unicode.GetString(valueBytes), - // TODO: Support arbitrary byte blobs that don't correspond to any currently-valid format. - _ => null + // Type is encoded as a SerString (compressed length followed by UTF-8 type name) + SerializationTypeCode.Type => ExtractSerString(valueBytes), + // SZArray: element type followed by element count followed by elements + // Return the raw bytes for arrays since we can't easily represent them + SerializationTypeCode.SZArray => valueBytes.ToArray(), + // TaggedObject: type tag followed by value - return raw bytes + SerializationTypeCode.TaggedObject => valueBytes.ToArray(), + // Enum: type name (SerString) followed by underlying value - return raw bytes + SerializationTypeCode.Enum => valueBytes.ToArray(), + // For unknown/future type codes, return the raw bytes to preserve the data + _ => bytes.AsSpan().ToArray() }; } + /// + /// Extracts a SerString (compressed length + UTF-8 string) from the given bytes. + /// Returns null if the first byte is 0xFF (null string marker). + /// + private static string? ExtractSerString(ReadOnlySpan bytes) + { + if (bytes.Length == 0) + { + return null; + } + // 0xFF indicates null string + if (bytes[0] == 0xFF) + { + return null; + } + // Decode compressed length + int length; + int bytesRead; + if ((bytes[0] & 0x80) == 0) + { + // 1-byte length + length = bytes[0]; + bytesRead = 1; + } + else if ((bytes[0] & 0xC0) == 0x80) + { + // 2-byte length + if (bytes.Length < 2) return null; + length = ((bytes[0] & 0x3F) << 8) | bytes[1]; + bytesRead = 2; + } + else + { + // 4-byte length + if (bytes.Length < 4) return null; + length = ((bytes[0] & 0x1F) << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + bytesRead = 4; + } + if (bytes.Length < bytesRead + length) + { + return null; + } + return Encoding.UTF8.GetString(bytes.Slice(bytesRead, length)); + } + public GrammarResult VisitFieldOrProp(CILParser.FieldOrPropContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); GrammarResult ICILVisitor.VisitFieldRef(CILParser.FieldRefContext context) => VisitFieldRef(context); @@ -2279,11 +2481,11 @@ public GrammarResult.FormattedBlob VisitFieldSerInit(CILParser.FieldSerInitConte case CILParser.UINT16: builder.WriteInt16((short)VisitInt32(context.int32()).Value); break; - case CILParser.INT32: + case CILParser.INT32_: case CILParser.UINT32: builder.WriteInt32(VisitInt32(context.int32()).Value); break; - case CILParser.INT64: + case CILParser.INT64_: case CILParser.UINT64: builder.WriteInt64(VisitInt64(context.int64()).Value); break; @@ -2413,13 +2615,15 @@ public GrammarResult.Literal VisitGenArity(CILParser.GenArityContext contex _currentMethod.Definition.MethodBody.MarkLabel(end); return new((start, end)); } - if (context.id() is CILParser.IdContext[] ids) + var ids = context.id(); + if (ids.Length == 2) { var start = _currentMethod!.Labels.TryGetValue(VisitId(ids[0]).Value, out LabelHandle startLabel) ? startLabel : _currentMethod.Labels[VisitId(ids[0]).Value] = _currentMethod.Definition.MethodBody.DefineLabel(); var end = _currentMethod!.Labels.TryGetValue(VisitId(ids[1]).Value, out LabelHandle endLabel) ? endLabel : _currentMethod.Labels[VisitId(ids[1]).Value] = _currentMethod.Definition.MethodBody.DefineLabel(); return new((start, end)); } - if (context.int32() is CILParser.Int32Context[] offsets) + var offsets = context.int32(); + if (offsets.Length == 2) { var start = _currentMethod!.Definition.MethodBody.DefineLabel(); var end = _currentMethod.Definition.MethodBody.DefineLabel(); @@ -3289,19 +3493,31 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) { var labelId = labelDecl.id(); string labelName = VisitId(labelId).Value; + currentMethod.DeclaredLabels.Add(labelName); if (!currentMethod.Labels.TryGetValue(labelName, out var label)) { label = currentMethod.Definition.MethodBody.DefineLabel(); + currentMethod.Labels[labelName] = label; } currentMethod.Definition.MethodBody.MarkLabel(label); } else if (context.EXPORT() is not null) { - // TODO: Need custom ManagedPEBuilder subclass to write the exports directory. + // .export [ordinal] or .export [ordinal] as alias + int ordinal = VisitInt32(context.int32()[0]).Value; + string? alias = context.id() is { } aliasId ? VisitId(aliasId).Value : null; + + currentMethod.Definition.ExportOrdinal = ordinal; + currentMethod.Definition.ExportAlias = alias; } else if (context.VTENTRY() is not null) { - // TODO: Need custom ManagedPEBuilder subclass to write the exports directory. + // .vtentry vtableIndex : slotIndex + int vtableEntry = VisitInt32(context.int32()[0]).Value; + int vtableSlot = VisitInt32(context.int32()[1]).Value; + + currentMethod.Definition.VTableEntry = vtableEntry; + currentMethod.Definition.VTableSlot = vtableSlot; } else if (context.OVERRIDE() is not null) { @@ -3443,7 +3659,7 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) } else { - // Adding attibutes to parameters. + // Adding attributes to parameters. int index = VisitInt32(context.int32()[0]).Value; if (index < 0 || index >= currentMethod.Definition.Parameters.Count) { @@ -3453,8 +3669,14 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) return GrammarResult.SentinelValue.Result; } - // TODO: Visit initOpt to get the Constant table entry if a constant value is provided. + // Handle initOpt to get the Constant table entry if a constant value is provided. + var constantValue = VisitInitOpt(context.initOpt()).Value; var param = currentMethod.Definition.Parameters[index]; + if (constantValue is not NoConstantSentinel) + { + param.ConstantValue = constantValue; + param.HasConstant = true; + } foreach (var attr in customAttrDeclarations ?? Array.Empty()) { var customAttrDecl = VisitCustomAttrDecl(attr).Value; @@ -4178,9 +4400,16 @@ public static GrammarResult.Flag VisitPropAttr(CILParser.Pro arg.SignatureBlob.WriteContentTo(signature); } - // TODO: Handle initOpt - _ = VisitInitOpt(context.initOpt()); - return new(new(propAttrs, signature, name)); + // Handle initOpt to set the Constant table entry if a constant value is provided. + var constantValue = VisitInitOpt(context.initOpt()).Value; + var property = new EntityRegistry.PropertyEntity(propAttrs, signature, name); + if (constantValue is not NoConstantSentinel) + { + property.ConstantValue = constantValue; + property.HasConstant = true; + property.Attributes |= PropertyAttributes.HasDefault; + } + return new(property); } GrammarResult ICILVisitor.VisitRepeatOpt(CILParser.RepeatOptContext context) => VisitRepeatOpt(context); @@ -4584,13 +4813,15 @@ public static GrammarResult.Literal VisitTruefalse(CILParser.TruefalseCont _currentMethod.Definition.MethodBody.MarkLabel(end); return new((start, end)); } - if (context.id() is CILParser.IdContext[] ids) + var ids = context.id(); + if (ids.Length == 2) { var start = _currentMethod!.Labels.TryGetValue(VisitId(ids[0]).Value, out LabelHandle startLabel) ? startLabel : _currentMethod.Labels[VisitId(ids[0]).Value] = _currentMethod.Definition.MethodBody.DefineLabel(); var end = _currentMethod!.Labels.TryGetValue(VisitId(ids[1]).Value, out LabelHandle endLabel) ? endLabel : _currentMethod.Labels[VisitId(ids[1]).Value] = _currentMethod.Definition.MethodBody.DefineLabel(); return new((start, end)); } - if (context.int32() is CILParser.Int32Context[] offsets) + var offsets = context.int32(); + if (offsets.Length == 2) { var start = _currentMethod!.Definition.MethodBody.DefineLabel(); var end = _currentMethod.Definition.MethodBody.DefineLabel(); @@ -5018,19 +5249,133 @@ public GrammarResult.Literal VisitVariantTypeElement(CILParser.VariantT public GrammarResult VisitVtableDecl(CILParser.VtableDeclContext context) { - // TODO: Need custom ManagedPEBuilder subclass to write the exports directory. - throw new NotImplementedException("raw vtable fixups blob not supported"); + // Raw .vtable directive with bytes - not commonly used + // For now, we don't support this legacy syntax + throw new NotImplementedException("raw vtable fixups blob (.vtable) not supported - use .vtfixup instead"); } - public GrammarResult VisitVtfixupAttr(CILParser.VtfixupAttrContext context) + GrammarResult ICILVisitor.VisitVtfixupAttr(CILParser.VtfixupAttrContext context) => VisitVtfixupAttr(context); + public GrammarResult.Literal VisitVtfixupAttr(CILParser.VtfixupAttrContext context) { - // TODO: Need custom ManagedPEBuilder subclass to write the exports directory. - throw new NotImplementedException("vtable fixups not supported"); + // vtfixupAttr: | vtfixupAttr INT32_ | vtfixupAttr INT64_ | vtfixupAttr 'fromunmanaged' | vtfixupAttr 'callmostderived' | vtfixupAttr 'retainappdomain' + ushort flags = 0; + foreach (var child in context.children ?? []) + { + string text = child.GetText(); + flags |= text switch + { + "int32" => VTableFixupSupport.COR_VTABLE_32BIT, + "int64" => VTableFixupSupport.COR_VTABLE_64BIT, + "fromunmanaged" => VTableFixupSupport.COR_VTABLE_FROM_UNMANAGED, + "callmostderived" => VTableFixupSupport.COR_VTABLE_CALL_MOST_DERIVED, + "retainappdomain" => VTableFixupSupport.COR_VTABLE_FROM_UNMANAGED_RETAIN_APPDOMAIN, + _ => 0 + }; + } + + // Default to 32-bit if neither 32 nor 64 is specified + if ((flags & (VTableFixupSupport.COR_VTABLE_32BIT | VTableFixupSupport.COR_VTABLE_64BIT)) == 0) + { + flags |= VTableFixupSupport.COR_VTABLE_32BIT; + } + + return new(flags); } + + GrammarResult ICILVisitor.VisitVtfixupDecl(CILParser.VtfixupDeclContext context) => VisitVtfixupDecl(context); public GrammarResult VisitVtfixupDecl(CILParser.VtfixupDeclContext context) { - // TODO: Need custom ManagedPEBuilder subclass to write the exports directory. - throw new NotImplementedException("raw vtable fixups blob not supported"); + // vtfixupDecl: '.vtfixup' '[' int32 ']' vtfixupAttr 'at' id; + int slotCount = VisitInt32(context.int32()).Value; + ushort flags = VisitVtfixupAttr(context.vtfixupAttr()).Value; + string dataLabel = VisitId(context.id()).Value; + + _vtableFixups.Add(new VTableFixupSupport.VTableFixupEntry(slotCount, flags, dataLabel)); + + return GrammarResult.SentinelValue.Result; + } + + /// + /// Computes the total metadata size from MetadataSizes. + /// This replicates the internal MetadataSizes.MetadataSize calculation. + /// + private static int ComputeMetadataSize(MetadataSizes sizes) + { + // Metadata header size (fixed structure): + // - signature (4) + // - major/minor version (4) + // - reserved (4) + // - version string length (4) + // - version string padded to 4 bytes ("v4.0.30319" = 12 bytes padded) + // - storage header (4) + // - 5 stream headers (#~, #Strings, #US, #GUID, #Blob) = 76 bytes + // Total header: ~108 bytes + const int metadataHeaderSize = 108; + + // Stream storage: heaps (#Strings, #US, #GUID, #Blob) - we can get aligned sizes + int heapStorageSize = 0; + heapStorageSize += sizes.GetAlignedHeapSize(HeapIndex.String); + heapStorageSize += sizes.GetAlignedHeapSize(HeapIndex.UserString); + heapStorageSize += sizes.GetAlignedHeapSize(HeapIndex.Guid); + heapStorageSize += sizes.GetAlignedHeapSize(HeapIndex.Blob); + + // Table stream (#~): header + table data + // Header: Reserved(4) + Version(2) + HeapSizes(1) + RowIdBitWidth(1) + ValidMask(8) + SortedMask(8) + // + 4 bytes per present table for row counts + int tableStreamSize = 24; // base header + var rowCounts = sizes.RowCounts; + + // Count present tables and add 4 bytes each for row count + for (int i = 0; i < rowCounts.Length; i++) + { + if (rowCounts[i] > 0) + { + tableStreamSize += 4; + } + } + + // Add table data size with estimated row sizes + // Row sizes depend on index sizes (2 or 4 bytes) which we don't have access to + // For small assemblies, all indexes are 2 bytes + tableStreamSize += rowCounts[(int)TableIndex.Module] * 10; // 2+2+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.TypeRef] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.TypeDef] * 14; // 4+2+2+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.Field] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.MethodDef] * 14; // 4+2+2+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.Param] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.InterfaceImpl] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.MemberRef] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.Constant] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.CustomAttribute] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.FieldMarshal] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.DeclSecurity] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.ClassLayout] * 8; // 2+4+2 + tableStreamSize += rowCounts[(int)TableIndex.FieldLayout] * 6; // 4+2 + tableStreamSize += rowCounts[(int)TableIndex.StandAloneSig] * 2; // 2 + tableStreamSize += rowCounts[(int)TableIndex.EventMap] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.Event] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.PropertyMap] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.Property] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.MethodSemantics] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.MethodImpl] * 6; // 2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.ModuleRef] * 2; // 2 + tableStreamSize += rowCounts[(int)TableIndex.TypeSpec] * 2; // 2 + tableStreamSize += rowCounts[(int)TableIndex.ImplMap] * 8; // 2+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.FieldRva] * 6; // 4+2 + tableStreamSize += rowCounts[(int)TableIndex.Assembly] * 22; // 16+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.AssemblyRef] * 20; // 12+2+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.File] * 8; // 4+2+2 + tableStreamSize += rowCounts[(int)TableIndex.ExportedType] * 14; // 8+2+2+2 + tableStreamSize += rowCounts[(int)TableIndex.ManifestResource] * 12; // 8+2+2 + tableStreamSize += rowCounts[(int)TableIndex.NestedClass] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.GenericParam] * 8; // 4+2+2 + tableStreamSize += rowCounts[(int)TableIndex.MethodSpec] * 4; // 2+2 + tableStreamSize += rowCounts[(int)TableIndex.GenericParamConstraint] * 4; // 2+2 + + // Align table stream to 4 bytes (includes +1 for terminating 0 byte) + tableStreamSize = ((tableStreamSize + 1) + 3) & ~3; + + return metadataHeaderSize + heapStorageSize + tableStreamSize; } GrammarResult ICILVisitor.VisitOptionalModifier(CILParser.OptionalModifierContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); diff --git a/src/tools/ilasm/src/ILAssembler/ILCompilation.cs b/src/tools/ilasm/src/ILAssembler/ILCompilation.cs deleted file mode 100644 index 70597169cae7f8..00000000000000 --- a/src/tools/ilasm/src/ILAssembler/ILCompilation.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; - -namespace ILAssembler; - -public class ILCompilation -{ - public ILCompilation(SourceText source) - { - } - - public ImmutableArray Diagnostics { get; } - - public ImmutableArray Emit() - { - return ImmutableArray.Empty; - } -} diff --git a/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs b/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs new file mode 100644 index 00000000000000..b59c2c7f44eb37 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs @@ -0,0 +1,628 @@ +// 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; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; + +namespace ILAssembler; + +/// +/// A PE builder that extends ManagedPEBuilder to support VTable fixups, unmanaged exports, +/// and data label reference fixups. +/// +/// +/// This builder extends ManagedPEBuilder with additional features: +/// 1. VTable fixups - adds an .sdata section containing VTableFixups directory and slot data +/// 2. Export stubs - jump thunks that indirect through vtable slots +/// 3. PE Export Directory - for native callers to find exported methods +/// 4. Data label fixups - patches references from one .data label to another with correct RVAs +/// +/// For VTable fixups: +/// - The runtime patches the token slots with actual method addresses at load time +/// +/// For exports: +/// - Export stubs are small pieces of machine code that jump through vtable slots +/// - The PE Export Directory lists the exports by name and ordinal +/// - Native code calls the export stubs, which redirect through the vtable +/// +/// For data label fixups: +/// - When .data contains a reference like `&Label`, the reference is patched with the +/// correct RVA of the target label in the mapped field data section. +/// +internal sealed class VTableExportPEBuilder : ManagedPEBuilder +{ + private const string TextSectionName = ".text"; + private const string SDataSectionName = ".sdata"; + + private readonly ImmutableArray _vtableFixups; + private readonly ImmutableArray _exports; + private readonly Dictionary _mappedFieldDataOffsets; + private readonly BlobBuilder? _mappedFieldData; + private readonly IReadOnlyDictionary>? _dataLabelFixups; + private readonly string _dllName; + + // Sizes needed to calculate mapped field data RVA + private readonly int _ilStreamSize; + private readonly int _metadataSize; + private readonly int _managedResourcesSize; + private readonly int _strongNameSignatureSize; + private readonly int _debugDataSize; + + // Calculated during serialization + private int _sdataRva; + private int _sdataSize; + private BlobBuilder? _textSectionBuilder; + private int _textSectionRva; + + // Export-related state + private int _exportDirectoryRva; + private int _exportDirectorySize; + + /// + /// Information about a VTable fixup entry. + /// + public readonly record struct VTableFixupInfo( + string DataLabel, + int SlotCount, + ushort Flags, + ImmutableArray MethodTokens); + + /// + /// Information about an unmanaged export. + /// + public readonly record struct ExportInfo( + int Ordinal, + string Name, + int MethodToken, + int VTableEntryIndex, // 1-based + int VTableSlotIndex); // 1-based + + public VTableExportPEBuilder( + PEHeaderBuilder header, + MetadataRootBuilder metadataRootBuilder, + BlobBuilder ilStream, + BlobBuilder? mappedFieldData = null, + BlobBuilder? managedResources = null, + ResourceSectionBuilder? nativeResources = null, + DebugDirectoryBuilder? debugDirectoryBuilder = null, + int strongNameSignatureSize = 128, + MethodDefinitionHandle entryPoint = default, + CorFlags flags = CorFlags.ILOnly, + Func, BlobContentId>? deterministicIdProvider = null, + ImmutableArray vtableFixups = default, + ImmutableArray exports = default, + Dictionary? mappedFieldDataOffsets = null, + IReadOnlyDictionary>? dataLabelFixups = null, + int metadataSize = 0, + int debugDataSize = 0, + string? dllName = null) + : base(header, metadataRootBuilder, ilStream, mappedFieldData, managedResources, + nativeResources, debugDirectoryBuilder, strongNameSignatureSize, entryPoint, + // Clear ILOnly flag if we have vtable fixups - mixed mode assembly + vtableFixups.IsDefaultOrEmpty ? flags : (flags & ~CorFlags.ILOnly), + deterministicIdProvider) + { + _vtableFixups = vtableFixups.IsDefault ? ImmutableArray.Empty : vtableFixups; + _exports = exports.IsDefault ? ImmutableArray.Empty : exports; + _mappedFieldDataOffsets = mappedFieldDataOffsets ?? new Dictionary(); + _mappedFieldData = mappedFieldData; + _dataLabelFixups = dataLabelFixups; + _dllName = dllName ?? "output.dll"; + + // Store sizes needed for RVA calculation + _ilStreamSize = ilStream.Count; + _metadataSize = metadataSize; + _managedResourcesSize = managedResources?.Count ?? 0; + _strongNameSignatureSize = strongNameSignatureSize; + _debugDataSize = debugDataSize; + } + + protected override ImmutableArray
CreateSections() + { + var baseSections = base.CreateSections(); + + // If we have vtable fixups, add .sdata section + if (_vtableFixups.Length > 0) + { + var builder = ImmutableArray.CreateBuilder
(baseSections.Length + 1); + + // Add .text section first + builder.Add(baseSections[0]); + + // Add .sdata section for VTable fixup data (must be read/write for runtime patching) + builder.Add(new Section(SDataSectionName, + SectionCharacteristics.MemRead | + SectionCharacteristics.MemWrite | + SectionCharacteristics.ContainsInitializedData)); + + // Add remaining sections + for (int i = 1; i < baseSections.Length; i++) + { + builder.Add(baseSections[i]); + } + + return builder.ToImmutable(); + } + + return baseSections; + } + + protected override BlobBuilder SerializeSection(string name, SectionLocation location) + { + if (name == TextSectionName) + { + // Apply data label fixups before serializing the text section + ApplyDataLabelFixups(location); + + // Serialize the text section + var builder = base.SerializeSection(name, location); + + // Store for later patching + _textSectionBuilder = builder; + _textSectionRva = location.RelativeVirtualAddress; + + return builder; + } + + if (name == SDataSectionName) + { + var builder = SerializeSDataSection(location); + + // Now that we have the .sdata RVA, patch the COR header's VTableFixups directory + if (_textSectionBuilder is not null && _vtableFixups.Length > 0) + { + PatchCorHeaderVTableFixups(_textSectionBuilder, _textSectionRva); + } + + return builder; + } + + return base.SerializeSection(name, location); + } + + /// + /// Patches the COR header's VTableFixups directory entry in the already-serialized text section. + /// + private void PatchCorHeaderVTableFixups(BlobBuilder textSection, int _) + { + // The COR header is at offset SizeOfImportAddressTable in the text section + // VTableFixups directory is at offset 52 within the COR header (after CodeManagerTable at 44) + bool is32Bit = Header.Machine == Machine.I386 || Header.Machine == 0; + int sizeOfImportAddressTable = (is32Bit || Header.Machine == 0) ? 8 : 0; + + // COR header offset in text section + int corHeaderOffset = sizeOfImportAddressTable; + + // VTableFixups directory entry is at offset 52 within COR header + const int vtableFixupsOffset = 52; + int patchOffset = corHeaderOffset + vtableFixupsOffset; + + // Find the blob containing this offset and patch it + int currentOffset = 0; + foreach (var blob in textSection.GetBlobs()) + { + int blobEnd = currentOffset + blob.Length; + if (patchOffset >= currentOffset && patchOffset + 8 <= blobEnd) + { + // Patch within this blob + var bytes = blob.GetBytes(); + int relativeOffset = patchOffset - currentOffset; + + // Write VTableFixups RVA (4 bytes) + bytes.Array![bytes.Offset + relativeOffset + 0] = (byte)(_sdataRva & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 1] = (byte)((_sdataRva >> 8) & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 2] = (byte)((_sdataRva >> 16) & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 3] = (byte)((_sdataRva >> 24) & 0xFF); + + // Write VTableFixups size (4 bytes) + bytes.Array[bytes.Offset + relativeOffset + 4] = (byte)(_sdataSize & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 5] = (byte)((_sdataSize >> 8) & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 6] = (byte)((_sdataSize >> 16) & 0xFF); + bytes.Array[bytes.Offset + relativeOffset + 7] = (byte)((_sdataSize >> 24) & 0xFF); + + return; + } + currentOffset = blobEnd; + } + } + + /// + /// Override to add export directory entry if we have exports. + /// + protected override PEDirectoriesBuilder GetDirectories() + { + var directories = base.GetDirectories(); + + // Add export directory if we have exports + if (_exportDirectoryRva != 0 && _exportDirectorySize != 0) + { + directories.ExportTable = new DirectoryEntry(_exportDirectoryRva, _exportDirectorySize); + } + + return directories; + } + + /// + /// Applies fixups for data label references (e.g., .data Ptr = &Label). + /// + private void ApplyDataLabelFixups(SectionLocation textSectionLocation) + { + if (_dataLabelFixups is null || _dataLabelFixups.Count == 0 || _mappedFieldData is null) + { + return; + } + + // Calculate the RVA of the mapped field data within the text section + int mappedFieldDataOffset = CalculateMappedFieldDataOffset(); + int mappedFieldDataRva = textSectionLocation.RelativeVirtualAddress + mappedFieldDataOffset; + + // Apply each fixup + foreach (var (labelName, fixupBlobs) in _dataLabelFixups) + { + if (!_mappedFieldDataOffsets.TryGetValue(labelName, out int labelOffset)) + { + // Label not found - skip (should have been caught during parsing) + continue; + } + + int targetRva = mappedFieldDataRva + labelOffset; + + foreach (var fixupBlob in fixupBlobs) + { + // Write the target RVA to the reserved fixup location + var writer = new BlobWriter(fixupBlob); + writer.WriteInt32(targetRva); + } + } + } + + /// + /// Calculates the offset to mapped field data within the text section. + /// + /// + /// The text section layout is: + /// - Import Address Table (8 bytes for 32-bit, 16 for 64-bit, or 0 if not needed) + /// - COR Header (72 bytes) + /// - IL Stream (aligned to 4) + /// - Metadata + /// - Managed Resources + /// - Strong Name Signature + /// - Debug Data + /// - Import Table + Name Table + Runtime Startup Stub (if needed) + /// - Mapped Field Data (aligned to 8) + /// + private int CalculateMappedFieldDataOffset() + { + bool is32Bit = Header.Machine == Machine.I386 || Header.Machine == 0; + bool requiresStartupStub = is32Bit || Header.Machine == 0; + + // Import Address Table size + int sizeOfImportAddressTable = requiresStartupStub ? (is32Bit ? 8 : 16) : 0; + + // COR Header size (fixed at 72 bytes) + const int corHeaderSize = 72; + + // Offset to IL stream + int offset = sizeOfImportAddressTable + corHeaderSize; + + // IL stream (aligned to 4) + offset += Align(_ilStreamSize, 4); + + // Metadata + offset += _metadataSize; + + // Managed resources + offset += _managedResourcesSize; + + // Strong name signature + offset += _strongNameSignatureSize; + + // Debug data + offset += _debugDataSize; + + // Import table, name table, and startup stub (if needed) + if (requiresStartupStub) + { + // Import table size (matches ManagedTextSection.SizeOfImportTable) + // 32-bit: 4+4+4+4+4+20+12+2+11+1 = 66 + // 64-bit: 4+4+4+4+4+20+16+2+11+1 = 70 + int sizeOfImportTable = is32Bit ? 66 : 70; + + // Name table size: "mscoree.dll" + NUL + hint = 11+1+2 = 14 bytes + const int sizeOfNameTable = 14; + + offset += sizeOfImportTable + sizeOfNameTable; + + // Align for startup stub + offset = Align(offset, is32Bit ? 4 : 8); + + // Startup stub size + int startupStubSize = is32Bit ? 8 : 16; + offset += startupStubSize; + } + + // Align for mapped field data (if present) + if (_mappedFieldData is not null && _mappedFieldData.Count > 0) + { + offset = Align(offset, 8); + } + + return offset; + } + + private static int Align(int value, int alignment) + { + return (value + alignment - 1) & ~(alignment - 1); + } + + private BlobBuilder SerializeSDataSection(SectionLocation location) + { + var builder = new BlobBuilder(); + + if (_vtableFixups.IsEmpty) + { + return builder; + } + + _sdataRva = location.RelativeVirtualAddress; + + // Calculate sizes for VTableFixups directory + int vtfDirSize = _vtableFixups.Length * 8; // 8 bytes per IMAGE_COR_VTABLEFIXUP entry + + // Calculate slot data size and build slot offset map + var slotOffsets = new Dictionary<(int EntryIndex, int SlotIndex), int>(); + int slotDataOffset = vtfDirSize; + + for (int entryIndex = 0; entryIndex < _vtableFixups.Length; entryIndex++) + { + var vtf = _vtableFixups[entryIndex]; + bool is64Bit = (vtf.Flags & VTableFixupSupport.COR_VTABLE_64BIT) != 0; + int slotSize = is64Bit ? 8 : 4; + + for (int slotIndex = 0; slotIndex < vtf.SlotCount; slotIndex++) + { + slotOffsets[(entryIndex + 1, slotIndex + 1)] = slotDataOffset + slotIndex * slotSize; + } + slotDataOffset += vtf.SlotCount * slotSize; + } + + int slotDataEndOffset = slotDataOffset; + + // Calculate export-related sizes + int exportStubsOffset = slotDataEndOffset; + int numExports = _exports.Length; + int exportStubSize = GetExportStubSize(); + int exportStubsTotalSize = numExports * exportStubSize; + + // Export directory comes after export stubs + int exportDirOffset = Align(exportStubsOffset + exportStubsTotalSize, 4); + + // Export directory structure: + // - IMAGE_EXPORT_DIRECTORY (40 bytes) + // - Export Address Table (4 bytes per export) + // - Export Name Pointer Table (4 bytes per export) + // - Export Ordinal Table (2 bytes per export) + // - Export names (null-terminated strings) + // - DLL name (null-terminated string) + int exportAddrTableOffset = exportDirOffset + 40; + int exportNamePtrTableOffset = exportAddrTableOffset + numExports * 4; + int exportOrdinalTableOffset = exportNamePtrTableOffset + numExports * 4; + + // Calculate name table size + int nameTableSize = 0; + foreach (var export in _exports) + { + nameTableSize += Encoding.ASCII.GetByteCount(export.Name) + 1; + } + int dllNameSize = Encoding.ASCII.GetByteCount(_dllName) + 1; + + int exportNamesOffset = exportOrdinalTableOffset + numExports * 2; + int dllNameOffset = exportNamesOffset + nameTableSize; + int exportDirTotalSize = numExports > 0 ? (dllNameOffset + dllNameSize - exportDirOffset) : 0; + + // Store total size for COR header patching (only vtfixup directory, not stubs/exports) + _sdataSize = vtfDirSize; + + // Write VTableFixups directory (array of IMAGE_COR_VTABLEFIXUP structures) + int currentSlotDataOffset = vtfDirSize; + foreach (var vtf in _vtableFixups) + { + int slotDataRva = location.RelativeVirtualAddress + currentSlotDataOffset; + builder.WriteInt32(slotDataRva); // RVA to slot data + builder.WriteUInt16((ushort)vtf.SlotCount); // Count + builder.WriteUInt16(vtf.Flags); // Type/Flags + + bool is64Bit = (vtf.Flags & VTableFixupSupport.COR_VTABLE_64BIT) != 0; + int slotSize = is64Bit ? 8 : 4; + currentSlotDataOffset += vtf.SlotCount * slotSize; + } + + // Write slot data (method tokens that get patched by the runtime) + foreach (var vtf in _vtableFixups) + { + bool is64Bit = (vtf.Flags & VTableFixupSupport.COR_VTABLE_64BIT) != 0; + + for (int i = 0; i < vtf.SlotCount; i++) + { + int token = i < vtf.MethodTokens.Length ? vtf.MethodTokens[i] : 0; + if (is64Bit) + { + builder.WriteInt64(token); + } + else + { + builder.WriteInt32(token); + } + } + } + + // Write export stubs if we have exports + if (numExports > 0) + { + var exportStubRvas = new int[numExports]; + + for (int i = 0; i < numExports; i++) + { + var export = _exports[i]; + + // Find the vtable slot address for this export + if (!slotOffsets.TryGetValue((export.VTableEntryIndex, export.VTableSlotIndex), out int slotOffset)) + { + // Export doesn't have a valid vtable reference, skip + continue; + } + + int slotRva = location.RelativeVirtualAddress + slotOffset; + exportStubRvas[i] = location.RelativeVirtualAddress + exportStubsOffset + i * exportStubSize; + + // Write the export stub + WriteExportStub(builder, slotRva); + } + + // Align for export directory + builder.Align(4); + + // Record export directory location + _exportDirectoryRva = location.RelativeVirtualAddress + builder.Count; + + // Write IMAGE_EXPORT_DIRECTORY + int baseOrdinal = int.MaxValue; + int maxOrdinal = 0; + foreach (var export in _exports) + { + if (export.Ordinal < baseOrdinal) baseOrdinal = export.Ordinal; + if (export.Ordinal > maxOrdinal) maxOrdinal = export.Ordinal; + } + if (baseOrdinal == int.MaxValue) baseOrdinal = 1; + int numFunctions = maxOrdinal - baseOrdinal + 1; + + int exportDirStart = builder.Count; + + builder.WriteUInt32(0); // Characteristics + builder.WriteUInt32(0); // TimeDateStamp (filled later or 0) + builder.WriteUInt16(0); // MajorVersion + builder.WriteUInt16(0); // MinorVersion + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + + numExports * 4 + numExports * 4 + numExports * 2 + nameTableSize); // Name RVA (DLL name) + builder.WriteInt32(baseOrdinal); // Base + builder.WriteInt32(numExports); // NumberOfFunctions + builder.WriteInt32(numExports); // NumberOfNames + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40); // AddressOfFunctions + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numExports * 4); // AddressOfNames + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numExports * 4 * 2); // AddressOfNameOrdinals + + // Sort exports by name for binary search + var sortedExports = _exports.AsSpan().ToArray(); + Array.Sort(sortedExports, (a, b) => string.CompareOrdinal(a.Name, b.Name)); + + // Write Export Address Table (RVAs to stubs) + var exportsArray = _exports.AsSpan().ToArray(); + foreach (var export in sortedExports) + { + int stubIndex = Array.FindIndex(exportsArray, e => e.Ordinal == export.Ordinal); + builder.WriteInt32(exportStubRvas[stubIndex]); + } + + // Write Export Name Pointer Table (RVAs to names) + int nameOffset = location.RelativeVirtualAddress + exportDirStart + 40 + + numExports * 4 + numExports * 4 + numExports * 2; + foreach (var export in sortedExports) + { + builder.WriteInt32(nameOffset); + nameOffset += Encoding.ASCII.GetByteCount(export.Name) + 1; + } + + // Write Export Ordinal Table + for (int i = 0; i < numExports; i++) + { + builder.WriteUInt16((ushort)(sortedExports[i].Ordinal - baseOrdinal)); + } + + // Write export names + foreach (var export in sortedExports) + { + byte[] nameBytes = Encoding.ASCII.GetBytes(export.Name); + builder.WriteBytes(nameBytes); + builder.WriteByte(0); // null terminator + } + + // Write DLL name + byte[] dllNameBytes = Encoding.ASCII.GetBytes(_dllName); + builder.WriteBytes(dllNameBytes); + builder.WriteByte(0); // null terminator + + _exportDirectorySize = builder.Count - exportDirStart; + } + + return builder; + } + + /// + /// Gets the size of an export stub for the current machine type. + /// + private int GetExportStubSize() + { + var machine = Header.Machine == 0 ? Machine.I386 : Header.Machine; + int size = VTableFixupSupport.GetExportStubSize(machine); + // VTableFixupSupport returns 12 for ARM64 but we need 16 for our implementation + return machine == Machine.Arm64 ? 16 : (size == 0 ? 6 : size); + } + + /// + /// Writes an export stub that jumps through a vtable slot. + /// + private void WriteExportStub(BlobBuilder builder, int vtableSlotRva) + { + // Calculate absolute address (RVA + ImageBase) + long absoluteAddress = (long)Header.ImageBase + vtableSlotRva; + + switch (Header.Machine) + { + case Machine.Amd64: + VTableFixupSupport.WriteExportStubAmd64(builder, absoluteAddress); + break; + + case Machine.I386: + case 0: // Default to x86 + VTableFixupSupport.WriteExportStubX86(builder, (int)absoluteAddress); + break; + + case Machine.Arm: + VTableFixupSupport.WriteExportStubArm(builder, (int)absoluteAddress); + break; + + case Machine.Arm64: + // ARM64 is more complex - use inline implementation + // ldr x16, [literal]; br x16 + builder.WriteUInt32(0x58000050); // ldr x16, #8 + builder.WriteUInt32(0xD61F0200); // br x16 + builder.WriteInt64(absoluteAddress); + break; + } + } + + /// + /// Gets the RVA of the VTableFixups directory after serialization. + /// + public int VTableFixupsRva => _sdataRva; + + /// + /// Gets the size of the VTableFixups directory. + /// + public int VTableFixupsSize => _sdataSize; + + /// + /// Gets the RVA of the export directory after serialization. + /// + public int ExportDirectoryRva => _exportDirectoryRva; + + /// + /// Gets the size of the export directory. + /// + public int ExportDirectorySize => _exportDirectorySize; +} diff --git a/src/tools/ilasm/src/ILAssembler/VTableFixupSupport.cs b/src/tools/ilasm/src/ILAssembler/VTableFixupSupport.cs new file mode 100644 index 00000000000000..9c7ab236b58d62 --- /dev/null +++ b/src/tools/ilasm/src/ILAssembler/VTableFixupSupport.cs @@ -0,0 +1,239 @@ +// 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; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace ILAssembler; + +/// +/// Support for VTable fixups and native exports in IL assembly. +/// +/// +/// VTable fixups allow managed methods to be exported as unmanaged entry points +/// that can be called from native code. This is used for: +/// - DllExport functionality (exporting managed methods from a DLL) +/// - COM interop with custom vtables +/// - Reverse P/Invoke scenarios +/// +/// The implementation requires: +/// 1. VTableFixups directory in the CLR header - points to an array of VTableFixup entries +/// 2. Each VTableFixup entry contains: RVA to slot data, slot count, and flags +/// 3. The slot data contains method tokens that the runtime patches with method addresses +/// 4. For exports, jump stubs in the text section that indirect through the vtable slots +/// 5. PE Export directory pointing to the jump stubs +/// +internal static class VTableFixupSupport +{ + // COR_VTABLE_* flags from corhdr.h + public const ushort COR_VTABLE_32BIT = 0x01; + public const ushort COR_VTABLE_64BIT = 0x02; + public const ushort COR_VTABLE_FROM_UNMANAGED = 0x04; + public const ushort COR_VTABLE_FROM_UNMANAGED_RETAIN_APPDOMAIN = 0x08; + public const ushort COR_VTABLE_CALL_MOST_DERIVED = 0x10; + + /// + /// Represents a VTable fixup entry parsed from .vtfixup directive. + /// + /// Number of slots in this VTable. + /// COR_VTABLE_* flags. + /// Label name in the data section where method tokens are stored. + public readonly record struct VTableFixupEntry(int SlotCount, ushort Flags, string DataLabel); + + /// + /// Represents a method export from .export directive. + /// + /// Export ordinal number. + /// Export name (method name or alias). + /// Method definition token. + /// 1-based index of the VTable fixup entry. + /// 1-based slot index within the VTable entry. + public readonly record struct MethodExport(int Ordinal, string Name, int MethodToken, int VTableEntryIndex, int VTableSlotIndex); + + /// + /// Checks if any VTable fixups or exports are defined. + /// + public static bool HasVTableFixupsOrExports( + ImmutableArray vtableFixups, + ImmutableArray exports) + { + return !vtableFixups.IsDefaultOrEmpty || !exports.IsDefaultOrEmpty; + } + + /// + /// Validates that exports have corresponding vtable entries. + /// + public static void ValidateExports( + ImmutableArray vtableFixups, + ImmutableArray exports, + Action reportError) + { + if (exports.IsDefaultOrEmpty) + return; + + foreach (var export in exports) + { + if (export.VTableEntryIndex <= 0 || export.VTableEntryIndex > vtableFixups.Length) + { + reportError($"Export '{export.Name}' references invalid VTable entry index {export.VTableEntryIndex}"); + continue; + } + + var vtfEntry = vtableFixups[export.VTableEntryIndex - 1]; + if (export.VTableSlotIndex <= 0 || export.VTableSlotIndex > vtfEntry.SlotCount) + { + reportError($"Export '{export.Name}' references invalid VTable slot {export.VTableSlotIndex} (entry has {vtfEntry.SlotCount} slots)"); + } + } + } + + /// + /// Calculates the size of the VTableFixups directory data. + /// + /// + /// The VTableFixups directory is an array of IMAGE_COR_VTABLEFIXUP structures: + /// struct IMAGE_COR_VTABLEFIXUP { + /// DWORD RVA; // RVA of the vtable slot data + /// WORD Count; // Number of entries + /// WORD Type; // COR_VTABLE_* flags + /// }; + /// Total: 8 bytes per entry + /// + public static int CalculateVTableFixupsDirectorySize(ImmutableArray vtableFixups) + { + if (vtableFixups.IsDefaultOrEmpty) + return 0; + + return vtableFixups.Length * 8; // 8 bytes per entry + } + + /// + /// Calculates the size of the vtable slot data (method tokens). + /// + public static int CalculateVTableSlotDataSize(ImmutableArray vtableFixups) + { + if (vtableFixups.IsDefaultOrEmpty) + return 0; + + int totalSize = 0; + foreach (var entry in vtableFixups) + { + int slotSize = (entry.Flags & COR_VTABLE_64BIT) != 0 ? 8 : 4; + totalSize += entry.SlotCount * slotSize; + } + + return totalSize; + } + + /// + /// Gets the size of an export stub for the given machine type. + /// + public static int GetExportStubSize(Machine machine) + { + return machine switch + { + Machine.Amd64 => 12, // mov rax, [addr]; jmp rax + Machine.I386 => 6, // jmp [addr] + Machine.Arm => 8, // ldr pc, [pc, #0]; addr + Machine.Arm64 => 12, // adrp x16, addr; ldr x16, [x16]; br x16 + _ => 0 + }; + } + + /// + /// Writes the VTableFixups directory entries to a blob. + /// + /// The blob builder to write to. + /// The vtable fixup entries. + /// RVAs of the slot data for each entry. + public static void WriteVTableFixupsDirectory( + BlobBuilder builder, + ImmutableArray vtableFixups, + ReadOnlySpan slotDataRvas) + { + if (vtableFixups.IsDefaultOrEmpty) + return; + + for (int i = 0; i < vtableFixups.Length; i++) + { + var entry = vtableFixups[i]; + builder.WriteInt32(slotDataRvas[i]); // RVA + builder.WriteUInt16((ushort)entry.SlotCount); // Count + builder.WriteUInt16(entry.Flags); // Type + } + } + + /// + /// Writes the vtable slot data (method tokens) to a blob. + /// + /// The blob builder to write to. + /// The vtable fixup entries. + /// Function to get method token for a given vtable entry and slot. + public static void WriteVTableSlotData( + BlobBuilder builder, + ImmutableArray vtableFixups, + Func getMethodToken) + { + if (vtableFixups.IsDefaultOrEmpty) + return; + + for (int entryIndex = 0; entryIndex < vtableFixups.Length; entryIndex++) + { + var entry = vtableFixups[entryIndex]; + bool is64Bit = (entry.Flags & COR_VTABLE_64BIT) != 0; + + for (int slotIndex = 0; slotIndex < entry.SlotCount; slotIndex++) + { + int token = getMethodToken(entryIndex + 1, slotIndex + 1); + if (is64Bit) + { + builder.WriteInt64(token); + } + else + { + builder.WriteInt32(token); + } + } + } + } + + /// + /// Writes an export stub for AMD64. + /// + public static void WriteExportStubAmd64(BlobBuilder builder, long vtableSlotAddress) + { + // mov rax, [vtableSlotAddress] + builder.WriteByte(0x48); // REX.W + builder.WriteByte(0xA1); // mov rax, moffs64 + builder.WriteInt64(vtableSlotAddress); + // jmp rax + builder.WriteByte(0xFF); + builder.WriteByte(0xE0); + } + + /// + /// Writes an export stub for x86. + /// + public static void WriteExportStubX86(BlobBuilder builder, int vtableSlotAddress) + { + // jmp [vtableSlotAddress] + builder.WriteByte(0xFF); + builder.WriteByte(0x25); + builder.WriteInt32(vtableSlotAddress); + } + + /// + /// Writes an export stub for ARM (Thumb-2). + /// + public static void WriteExportStubArm(BlobBuilder builder, int vtableSlotAddress) + { + // ldr pc, [pc, #0] + builder.WriteUInt16(0xF8DF); + builder.WriteUInt16(0xF000); + // address + builder.WriteInt32(vtableSlotAddress); + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs index 725d4b99cb645d..1c8871130fe13a 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs @@ -362,7 +362,9 @@ .file NonExistentFile.dll """; var diagnostics = CompileAndGetDiagnostics(source, new Options()); - var error = Assert.Single(diagnostics); + // Expect FileNotFound error + MissingExportedTypeImplementation warning + Assert.Equal(2, diagnostics.Length); + var error = diagnostics.First(d => d.Severity == DiagnosticSeverity.Error); Assert.Equal(DiagnosticIds.FileNotFound, error.Id); Assert.Equal(DiagnosticSeverity.Error, error.Severity); } @@ -381,7 +383,9 @@ .assembly extern NonExistentAssembly """; var diagnostics = CompileAndGetDiagnostics(source, new Options()); - var error = Assert.Single(diagnostics); + // Expect AssemblyNotFound error + MissingExportedTypeImplementation warning + Assert.Equal(2, diagnostics.Length); + var error = diagnostics.First(d => d.Severity == DiagnosticSeverity.Error); Assert.Equal(DiagnosticIds.AssemblyNotFound, error.Id); Assert.Equal(DiagnosticSeverity.Error, error.Severity); } @@ -407,8 +411,10 @@ .class extern NonExistentParent var diagnostics = CompileAndGetDiagnostics(source, new Options()); Assert.NotEmpty(diagnostics); - Assert.All(diagnostics, d => Assert.Equal(DiagnosticIds.ExportedTypeNotFound, d.Id)); - Assert.All(diagnostics, d => Assert.Equal(DiagnosticSeverity.Error, d.Severity)); + // Check only error diagnostics (warnings are also expected for missing implementations) + var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.NotEmpty(errors); + Assert.All(errors, d => Assert.Equal(DiagnosticIds.ExportedTypeNotFound, d.Id)); } [Fact] @@ -555,7 +561,13 @@ .assembly extern System.Runtime { } var typeHandle = reader.TypeDefinitions .First(h => reader.GetString(reader.GetTypeDefinition(h).Name) == "UnionStruct"); - var fields = reader.GetTypeDefinition(typeHandle).GetFields() + var typeDef = reader.GetTypeDefinition(typeHandle); + + // Verify ExplicitLayout is set (this was a regression bug - EXPLICIT token wasn't being parsed) + Assert.True(typeDef.Attributes.HasFlag(System.Reflection.TypeAttributes.ExplicitLayout), + $"Expected ExplicitLayout, got {typeDef.Attributes} (0x{(int)typeDef.Attributes:X8})"); + + var fields = typeDef.GetFields() .Select(reader.GetFieldDefinition).ToArray(); Assert.Equal(3, fields.Length); @@ -1044,6 +1056,294 @@ .method public static void TestMethod() cil managed Assert.Empty(diagnostics); } + [Fact] + public void VtfixupDecl_CompilesSuccessfully() + { + // .vtfixup directive should compile successfully with VTable fixup support + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data VT = int32(0) + .vtfixup [1] int32 fromunmanaged at VT + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void ExportedMethod() cil managed + { + .vtentry 1 : 1 + .export [1] + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Verify COR flags don't have ILOnly (mixed mode for vtfixups) + var corHeader = pe.PEHeaders.CorHeader; + Assert.NotNull(corHeader); + Assert.False(corHeader!.Flags.HasFlag(CorFlags.ILOnly), + "VTable fixups require mixed-mode assembly (ILOnly should be cleared)"); + + // Verify .sdata section exists + var sdataSection = pe.PEHeaders.SectionHeaders.FirstOrDefault(s => s.Name == ".sdata"); + Assert.False(sdataSection.Equals(default), "Expected .sdata section for vtable fixups"); + + // Read and verify the .sdata section contains valid VTableFixup directory structure + // Structure: IMAGE_COR_VTABLEFIXUP { DWORD RVA, WORD Count, WORD Type } + var sdataBytes = pe.GetSectionData(sdataSection.VirtualAddress).GetContent(); + Assert.True(sdataBytes.Length >= 8, "VTableFixup directory should be at least 8 bytes"); + + // Read the first VTableFixup entry + int slotDataRva = BitConverter.ToInt32(sdataBytes.AsSpan(0, 4)); + ushort slotCount = BitConverter.ToUInt16(sdataBytes.AsSpan(4, 2)); + ushort flags = BitConverter.ToUInt16(sdataBytes.AsSpan(6, 2)); + + Assert.Equal(1, slotCount); + Assert.True((flags & 0x01) != 0, "Expected COR_VTABLE_32BIT flag"); + Assert.True((flags & 0x04) != 0, "Expected COR_VTABLE_FROM_UNMANAGED flag"); + + // The slot data RVA should point within the .sdata section (after the directory) + Assert.True(slotDataRva >= sdataSection.VirtualAddress, + $"Slot data RVA {slotDataRva} should be >= section start {sdataSection.VirtualAddress}"); + + // Verify the method token in the slot data (should be a valid MethodDef token) + int slotDataOffset = slotDataRva - sdataSection.VirtualAddress; + int methodToken = BitConverter.ToInt32(sdataBytes.AsSpan(slotDataOffset, 4)); + Assert.True((methodToken & 0xFF000000) == 0x06000000, + $"Expected MethodDef token (0x06xxxxxx), got 0x{methodToken:X8}"); + } + + [Fact] + public void ExportDirective_WithoutVtfixup_CompilesSuccessfully() + { + // .export directive without .vtfixup records export info but doesn't create vtable + // This is valid IL - the export ordinal/name is stored for potential use by tools + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void ExportedMethod() cil managed + { + .export [1] as MyExport + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Without vtfixup, ILOnly flag should remain set (no vtable slot data needed) + var corHeader = pe.PEHeaders.CorHeader; + Assert.NotNull(corHeader); + Assert.True(corHeader!.Flags.HasFlag(CorFlags.ILOnly), + "Without vtfixup, assembly should remain IL-only"); + + // Verify the method exists in metadata + var reader = pe.GetMetadataReader(); + var exportedMethod = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .FirstOrDefault(m => reader.GetString(m.Name) == "ExportedMethod"); + Assert.False(exportedMethod.Equals(default), "ExportedMethod should exist in metadata"); + } + + [Fact] + public void VtfixupDecl_64bit_CompilesSuccessfully() + { + // .vtfixup with int64 (64-bit slots) - used for 64-bit platforms + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data VT = int64(0) + .vtfixup [1] int64 fromunmanaged at VT + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void ExportedMethod() cil managed + { + .vtentry 1 : 1 + .export [1] + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Verify .sdata section exists + var sdataSection = pe.PEHeaders.SectionHeaders.FirstOrDefault(s => s.Name == ".sdata"); + Assert.False(sdataSection.Equals(default), "Expected .sdata section for vtable fixups"); + + // Read VTableFixup directory entry and verify 64-bit flag + var sdataBytes = pe.GetSectionData(sdataSection.VirtualAddress).GetContent(); + ushort flags = BitConverter.ToUInt16(sdataBytes.AsSpan(6, 2)); + Assert.True((flags & 0x02) != 0, "Expected COR_VTABLE_64BIT flag (0x02)"); + + // Verify slot data is 8 bytes (64-bit token) + int slotDataRva = BitConverter.ToInt32(sdataBytes.AsSpan(0, 4)); + int slotDataOffset = slotDataRva - sdataSection.VirtualAddress; + + // 64-bit slot should have method token in lower 32 bits, zeros in upper 32 bits + long slotValue = BitConverter.ToInt64(sdataBytes.AsSpan(slotDataOffset, 8)); + int methodToken = (int)(slotValue & 0xFFFFFFFF); + Assert.True((methodToken & 0xFF000000) == 0x06000000, + $"Expected MethodDef token (0x06xxxxxx), got 0x{methodToken:X8}"); + } + + [Fact] + public void VtfixupDecl_MultipleSlots_CompilesSuccessfully() + { + // .vtfixup with multiple slots - each method gets its own slot + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data VT = int32(0) int32(0) + .vtfixup [2] int32 fromunmanaged at VT + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Method1() cil managed + { + .vtentry 1 : 1 + .export [1] as Export1 + ret + } + .method public static void Method2() cil managed + { + .vtentry 1 : 2 + .export [2] as Export2 + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Verify .sdata section exists + var sdataSection = pe.PEHeaders.SectionHeaders.FirstOrDefault(s => s.Name == ".sdata"); + Assert.False(sdataSection.Equals(default), "Expected .sdata section for vtable fixups"); + + var sdataBytes = pe.GetSectionData(sdataSection.VirtualAddress).GetContent(); + + // Verify VTableFixup directory entry has count of 2 + ushort slotCount = BitConverter.ToUInt16(sdataBytes.AsSpan(4, 2)); + Assert.Equal(2, slotCount); + + // Read both method tokens from slot data + int slotDataRva = BitConverter.ToInt32(sdataBytes.AsSpan(0, 4)); + int slotDataOffset = slotDataRva - sdataSection.VirtualAddress; + + int token1 = BitConverter.ToInt32(sdataBytes.AsSpan(slotDataOffset, 4)); + int token2 = BitConverter.ToInt32(sdataBytes.AsSpan(slotDataOffset + 4, 4)); + + // Both should be valid MethodDef tokens + Assert.True((token1 & 0xFF000000) == 0x06000000, + $"Slot 1: Expected MethodDef token, got 0x{token1:X8}"); + Assert.True((token2 & 0xFF000000) == 0x06000000, + $"Slot 2: Expected MethodDef token, got 0x{token2:X8}"); + + // Tokens should be different (different methods) + Assert.NotEqual(token1, token2); + + // Verify the methods exist in metadata with expected names + var reader = pe.GetMetadataReader(); + var methodNames = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Select(m => reader.GetString(m.Name)) + .ToHashSet(); + Assert.Contains("Method1", methodNames); + Assert.Contains("Method2", methodNames); + } + + [Fact] + public void DataLabelReference_FixedUpCorrectly() + { + // Test that .data with a reference to another label (&Label) is patched with the correct RVA + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data TargetData = int32(0x12345678) + .data PointerData = &TargetData + .class public explicit ansi sealed beforefieldinit DataHolder extends [mscorlib]System.ValueType + { + .size 8 + .field [0] public static int32 Target at TargetData + .field [4] public static int32 Pointer at PointerData + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var testType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(t => reader.GetString(t.Name) == "DataHolder"); + + var fields = testType.GetFields() + .Select(reader.GetFieldDefinition) + .ToDictionary(f => reader.GetString(f.Name)); + + // Both fields should have RVAs + int targetRva = fields["Target"].GetRelativeVirtualAddress(); + int pointerRva = fields["Pointer"].GetRelativeVirtualAddress(); + Assert.NotEqual(0, targetRva); + Assert.NotEqual(0, pointerRva); + + // The pointer field should contain the RVA of the target data + // Read the actual data from the PE at the pointer location + var pointerSection = pe.GetSectionData(pointerRva); + int storedRva = BitConverter.ToInt32(pointerSection.GetContent().AsSpan(0, 4)); + + // The stored RVA should equal the target's RVA + Assert.Equal(targetRva, storedRva); + + // Verify the target data contains the expected value + var targetSection = pe.GetSectionData(targetRva); + int targetValue = BitConverter.ToInt32(targetSection.GetContent().AsSpan(0, 4)); + Assert.Equal(0x12345678, targetValue); + } + + [Fact] + public void DataLabelReference_MultipleReferences_AllFixedUp() + { + // Test multiple references to the same label + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data Target = int32(42) + .data Ptr1 = &Target + .data Ptr2 = &Target + .class public explicit ansi sealed beforefieldinit DataHolder extends [mscorlib]System.ValueType + { + .size 12 + .field [0] public static int32 TargetField at Target + .field [4] public static int32 Ptr1Field at Ptr1 + .field [8] public static int32 Ptr2Field at Ptr2 + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var testType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(t => reader.GetString(t.Name) == "DataHolder"); + + var fields = testType.GetFields() + .Select(reader.GetFieldDefinition) + .ToDictionary(f => reader.GetString(f.Name)); + + int targetRva = fields["TargetField"].GetRelativeVirtualAddress(); + int ptr1Rva = fields["Ptr1Field"].GetRelativeVirtualAddress(); + int ptr2Rva = fields["Ptr2Field"].GetRelativeVirtualAddress(); + + // Read both pointer values + int storedRva1 = BitConverter.ToInt32(pe.GetSectionData(ptr1Rva).GetContent().AsSpan(0, 4)); + int storedRva2 = BitConverter.ToInt32(pe.GetSectionData(ptr2Rva).GetContent().AsSpan(0, 4)); + + // Both should point to the target + Assert.Equal(targetRva, storedRva1); + Assert.Equal(targetRva, storedRva2); + } + private static PEReader CompileAndGetReader(string source, Options options) { var sourceText = new SourceText(source, "test.il"); @@ -1142,5 +1442,948 @@ .method public static void TestMethod() cil managed var embeddedPdbEntry = debugDirectory.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); Assert.Equal(default, embeddedPdbEntry); } + + [Fact] + public void ParamInitOpt_Int32Constant_CreatesConstantEntry() + { + // Test that .param with int32 initOpt creates a constant entry + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod(int32 x) cil managed + { + .param [1] = int32(42) + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Find the method + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(m => reader.GetString(m.Name) == "TestMethod"); + + // Get parameters + var parameters = method.GetParameters().ToArray(); + Assert.True(parameters.Length >= 1, $"Expected at least 1 parameter, got {parameters.Length}"); + + // Find the parameter by sequence number + var param1 = parameters.Select(reader.GetParameter).FirstOrDefault(p => p.SequenceNumber == 1); + var param1Handle = parameters.FirstOrDefault(h => reader.GetParameter(h).SequenceNumber == 1); + Assert.False(param1Handle.IsNil, "Parameter with sequence 1 not found"); + + // Check constant for first param (int32) + var intConstantHandle = param1.GetDefaultValue(); + Assert.False(intConstantHandle.IsNil, "No constant for parameter 1"); + var intConstant = reader.GetConstant(intConstantHandle); + Assert.Equal(ConstantTypeCode.Int32, intConstant.TypeCode); + var intValue = reader.GetBlobReader(intConstant.Value).ReadInt32(); + Assert.Equal(42, intValue); + } + + [Fact] + public void ParamInitOpt_ReturnParam_CreatesConstantEntry() + { + // Test that .param [0] (return value) with initOpt works + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .method public static int32 GetValue() cil managed + { + .param [0] = int32(100) + ldc.i4 100 + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Find the method + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(m => reader.GetString(m.Name) == "GetValue"); + + // Get parameters - param [0] is the return value + var parameters = method.GetParameters().ToArray(); + Assert.Single(parameters); + + var param = reader.GetParameter(parameters[0]); + Assert.Equal(0, param.SequenceNumber); // Return value has sequence 0 + + var constantHandle = param.GetDefaultValue(); + Assert.False(constantHandle.IsNil); + var constant = reader.GetConstant(constantHandle); + Assert.Equal(ConstantTypeCode.Int32, constant.TypeCode); + var value = reader.GetBlobReader(constant.Value).ReadInt32(); + Assert.Equal(100, value); + } + + [Fact] + public void Property_BasicProperty_IsEmitted() + { + // First check if properties work at all without initOpt + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .field private int32 _value + + .property int32 Value() + { + .get instance int32 Test::get_Value() + } + + .method public hidebysig specialname instance int32 get_Value() cil managed + { + ldarg.0 + ldfld int32 Test::_value + ret + } + } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + // Check for diagnostics + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + // Check how many properties are in the table + var propCount = reader.GetTableRowCount(TableIndex.Property); + Assert.True(propCount > 0, $"Expected at least 1 property, got {propCount}"); + } + + [Fact] + public void PropertyInitOpt_WithConstantValue_CreatesConstantEntry() + { + // Test that .property with initOpt creates a constant entry + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .field private int32 _value + + .property int32 Value() = int32(42) + { + .get instance int32 Test::get_Value() + } + + .method public hidebysig specialname instance int32 get_Value() cil managed + { + ldarg.0 + ldfld int32 Test::_value + ret + } + } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + // Find the property + var propertyHandle = reader.PropertyDefinitions.First(); + var property = reader.GetPropertyDefinition(propertyHandle); + + // Check attributes include HasDefault + Assert.True((property.Attributes & System.Reflection.PropertyAttributes.HasDefault) != 0, + $"Expected HasDefault attribute, got {property.Attributes}"); + + // Check for constant + var constantHandle = property.GetDefaultValue(); + Assert.False(constantHandle.IsNil, "No constant for property"); + var constant = reader.GetConstant(constantHandle); + Assert.Equal(ConstantTypeCode.Int32, constant.TypeCode); + var value = reader.GetBlobReader(constant.Value).ReadInt32(); + Assert.Equal(42, value); + } + + [Fact] + public void PropertyInitOpt_WithStringConstant_CreatesConstantEntry() + { + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .field private string _name + + .property string Name() = "DefaultName" + { + .get instance string Test::get_Name() + } + + .method public hidebysig specialname instance string get_Name() cil managed + { + ldarg.0 + ldfld string Test::_name + ret + } + } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + // Find the property + var propertyHandle = reader.PropertyDefinitions.First(); + var property = reader.GetPropertyDefinition(propertyHandle); + + // Check attributes include HasDefault + Assert.True((property.Attributes & System.Reflection.PropertyAttributes.HasDefault) != 0, + $"Expected HasDefault attribute, got {property.Attributes}"); + + // Check for constant + var constantHandle = property.GetDefaultValue(); + Assert.False(constantHandle.IsNil, "No constant for property"); + var constant = reader.GetConstant(constantHandle); + Assert.Equal(ConstantTypeCode.String, constant.TypeCode); + // String constants are stored as UTF-16 + var blobReader = reader.GetBlobReader(constant.Value); + var stringBytes = blobReader.ReadBytes(blobReader.Length); + var value = System.Text.Encoding.Unicode.GetString(stringBytes); + Assert.Equal("DefaultName", value); + } + + [Fact] + public void ExplicitLayout_SetsTypeLayoutFlags() + { + // Test that ExplicitLayout (0x10) is set for structs with field offsets + string source = """ + .class public explicit ansi sealed beforefieldinit Test + extends [System.Runtime]System.ValueType + { + .field [0] public int32 x + .field [4] public int32 y + } + .assembly extern System.Runtime { } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + var typeDef = reader.TypeDefinitions + .Select(h => reader.GetTypeDefinition(h)) + .First(t => reader.GetString(t.Name) == "Test"); + + Assert.True(typeDef.Attributes.HasFlag(System.Reflection.TypeAttributes.ExplicitLayout), + $"Expected ExplicitLayout, got {typeDef.Attributes}"); + } + + [Fact] + public void AssemblyNoPlatform_SetsNoPlatformFlag() + { + string source = """ + .assembly test + { + .hash algorithm 0x00008004 + .ver 1:0:0:0 + .custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = (01 00 18 2E 4E 45 54 46 72 61 6D 65 77 6F 72 6B 2C 56 65 72 73 69 6F 6E 3D 76 38 2E 30 01 00 54 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79 4E 61 6D 65 08 2E 4E 45 54 20 38 2E 30) + } + .assembly extern mscorlib + { + .publickeytoken = (B7 7A 5C 56 19 34 E0 89) + } + .class public auto ansi Test { } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + } + + [Fact] + public void TryBlock_WithLabeledBlocks_GeneratesExceptionHandlers() + { + // This tests exception handler generation with labeled try blocks + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi Test + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .locals init (int32 V_0) + + .try + { + ldc.i4.0 + stloc.0 + leave.s END + } + catch [mscorlib]System.Exception + { + pop + ldc.i4.1 + stloc.0 + leave.s END + } + END: ret + } + } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + // Verify method exists and has body + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "TestMethod"); + + Assert.True(methodDef.RelativeVirtualAddress != 0, "Method should have IL body"); + } + + [Fact] + public void ScopeBlock_WithLabeledInstructions_UsesMarkLabelForBranches() + { + // Tests that labeled instructions properly work with branches + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi Test + { + .method public static int32 TestBranches(int32 x) cil managed + { + .maxstack 2 + + ldarg.0 + ldc.i4.0 + bgt.s POSITIVE + ldc.i4.m1 + br.s DONE + + POSITIVE: ldc.i4.1 + + DONE: ret + } + } + """; + + var sourceText = new ILAssembler.SourceText(source, "test.il"); + var compiler = new ILAssembler.DocumentCompiler(); + var (diagnostics, result) = compiler.Compile(sourceText, _ => default!, _ => default!, new Options()); + + foreach (var d in diagnostics) + { + throw new Exception($"Unexpected diagnostic: {d.Id} - {d.Message}"); + } + Assert.NotNull(result); + + var blobBuilder = new System.Reflection.Metadata.BlobBuilder(); + result.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + + // Verify the PE is valid and method has code + Assert.True(pe.HasMetadata); + var reader = pe.GetMetadataReader(); + + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "TestBranches"); + + Assert.True(methodDef.RelativeVirtualAddress != 0); + } + + [Fact] + public void TryBlock_WithOffsetLabels_UsesInstructionEncoderExtensions() + { + // Test offset-based labels in try/catch blocks (exercises InstructionEncoderExtensions.MarkLabel) + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try 0 to 5 catch [mscorlib]System.Exception handler 5 to 9 + nop // 0: 1 byte + nop // 1: 1 byte + nop // 2: 1 byte + leave.s IL_9 // 3-4: 2 bytes (opcode + offset) + IL_5: + pop // 5: 1 byte + leave.s IL_9 // 6-8: 2 bytes + IL_9: + ret // 9: 1 byte + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "TestMethod"); + Assert.True(methodDef.RelativeVirtualAddress != 0); + } + + [Fact] + public void ScopeBlock_WithOffsetLabels_UsesInstructionEncoderExtensions() + { + // Test offset-based scope blocks (exercises InstructionEncoderExtensions.MarkLabel) + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try 0 to 3 finally handler 3 to 5 + nop // 0 + leave.s IL_5 // 1-2 + IL_3: + endfinally // 3 + IL_5: + ret // 5 + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "TestMethod"); + Assert.True(methodDef.RelativeVirtualAddress != 0); + } + + [Fact] + public void ExtendedLayout_SetsExtendedLayoutFlag() + { + // Test the 'extended' class attribute (exercises MetadataExtensions.ExtendedLayout) + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public extended ansi sealed beforefieldinit Test + extends [mscorlib]System.ValueType + { + .field public int32 x + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var typeDef = reader.TypeDefinitions + .Select(h => reader.GetTypeDefinition(h)) + .First(t => reader.GetString(t.Name) == "Test"); + + // ExtendedLayout = 0x18 + Assert.Equal((System.Reflection.TypeAttributes)0x18, typeDef.Attributes & (System.Reflection.TypeAttributes)0x18); + } + + [Fact] + public void AssemblyNoPlatform_WithKeyword_SetsNoPlatformFlag() + { + // Test the 'noplatform' assembly attribute + string source = """ + .assembly noplatform test + { + .ver 1:0:0:0 + } + .class public auto ansi Test { } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var assembly = reader.GetAssemblyDefinition(); + + // NoPlatform = 0x70 (stored in architecture bits of AssemblyFlags) + Assert.Equal((System.Reflection.AssemblyFlags)0x70, assembly.Flags & (System.Reflection.AssemblyFlags)0xF0); + } + + [Fact] + public void AssemblyArchitecture_SetsArchitectureFlags() + { + // Test the x86 architecture assembly attribute + string source = """ + .assembly x86 test + { + .ver 1:0:0:0 + } + .class public auto ansi Test { } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var assembly = reader.GetAssemblyDefinition(); + + // x86 = ProcessorArchitecture.X86 (2) << 4 = 0x20 + Assert.Equal((System.Reflection.AssemblyFlags)0x20, assembly.Flags & (System.Reflection.AssemblyFlags)0xF0); + } + + [Fact] + public void PermissionSet_WithDemand_UsesSecurityAction() + { + // Test permission set with bytearray (exercises security action handling) + string source = """ + .assembly test + { + .permissionset demand = (2E) + .ver 1:0:0:0 + } + .class public auto ansi Test { } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Verify assembly compiled successfully with the permission set + Assert.True(reader.GetTableRowCount(TableIndex.DeclSecurity) >= 1); + } + + [Fact] + public void VarargMethod_Definition_Compiles() + { + // Test vararg method definition + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static vararg void VarargMethod(int32 x) cil managed + { + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "VarargMethod"); + + // VarArgs calling convention = 0x5 + var signature = reader.GetBlobReader(methodDef.Signature); + var header = signature.ReadByte(); + Assert.Equal(0x5, header & 0x0F); + } + + [Fact] + public void GenericType_UsesNamedElementList() + { + // Test generic types (exercises NamedElementList for generic parameters) + // Note: Generic parameter handling may require type being in the module context + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Test`2 extends [mscorlib]System.Object + { + .field public !0 fieldT + .field public !1 fieldU + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var typeDef = reader.TypeDefinitions + .Select(h => reader.GetTypeDefinition(h)) + .First(t => reader.GetString(t.Name) == "Test`2"); + + var genericParams = typeDef.GetGenericParameters(); + Assert.Equal(2, genericParams.Count); + } + + [Fact] + public void GenericMethod_UsesNamedElementList() + { + // Test generic methods (exercises NamedElementList for method generic parameters) + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static !!0 GenericMethod(!!0 arg) cil managed + { + .maxstack 1 + ldarg.0 + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var methodDef = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "GenericMethod"); + + var genericParams = methodDef.GetGenericParameters(); + Assert.Single(genericParams); + } + + [Fact] + public void FieldConstant_WithCharValue_UsesBlobBuilderExtensions() + { + // Test field with char constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal char CharField = char(0x0041) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "CharField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Char, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithDoubleValue_UsesBlobBuilderExtensions() + { + // Test field with double constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal float64 DoubleField = float64(3.14159265358979) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "DoubleField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Double, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithInt16Value_UsesBlobBuilderExtensions() + { + // Test field with int16 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal int16 ShortField = int16(12345) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "ShortField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Int16, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithInt64Value_UsesBlobBuilderExtensions() + { + // Test field with int64 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal int64 LongField = int64(9223372036854775807) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "LongField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Int64, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithInt8Value_UsesBlobBuilderExtensions() + { + // Test field with int8 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + // Use hex 0xD6 which is -42 in signed byte representation + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal int8 SByteField = int8(42) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "SByteField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.SByte, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithFloat32Value_UsesBlobBuilderExtensions() + { + // Test field with float32 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal float32 FloatField = float32(3.14) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "FloatField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Single, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithUInt16Value_UsesBlobBuilderExtensions() + { + // Test field with uint16 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal uint16 UShortField = uint16(65535) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "UShortField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.UInt16, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithUInt32Value_UsesBlobBuilderExtensions() + { + // Test field with uint32 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal uint32 UIntField = uint32(4294967295) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "UIntField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.UInt32, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithUInt64Value_UsesBlobBuilderExtensions() + { + // Test field with uint64 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + // Use smaller value that fits in int64 range + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal uint64 ULongField = uint64(9223372036854775807) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "ULongField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.UInt64, constant.TypeCode); + } + + [Fact] + public void FieldConstant_WithUInt8Value_UsesBlobBuilderExtensions() + { + // Test field with uint8 constant (exercises BlobBuilderExtensions.WriteSerializedValue) + string source = """ + .assembly test { } + .assembly extern System.Runtime { } + .class public auto ansi Test extends [System.Runtime]System.Object + { + .field public static literal uint8 ByteField = uint8(255) + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var fieldDef = reader.FieldDefinitions + .Select(h => reader.GetFieldDefinition(h)) + .First(f => reader.GetString(f.Name) == "ByteField"); + + var constant = reader.GetConstant(fieldDef.GetDefaultValue()); + Assert.Equal(ConstantTypeCode.Byte, constant.TypeCode); + } + + [Fact] + public void TypeForwarder_EmitsExportedType() + { + // Test type forwarder (exercises ExportedType with assembly reference implementation) + string source = """ + .assembly extern mscorlib { } + .assembly extern ForwardedAssembly { } + .assembly test { } + .class extern forwarder System.ForwardedType + { + .assembly extern ForwardedAssembly + } + """; + + // First check diagnostics to see if there are any errors + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)); + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Verify the ExportedType table has an entry + Assert.Equal(1, reader.GetTableRowCount(TableIndex.ExportedType)); + + var exportedType = reader.ExportedTypes.Select(h => reader.GetExportedType(h)).First(); + Assert.Equal("ForwardedType", reader.GetString(exportedType.Name)); + Assert.Equal("System", reader.GetString(exportedType.Namespace)); + // Forwarder flag is TypeAttributes.Forwarder (0x00200000) + Assert.True(exportedType.Attributes.HasFlag((System.Reflection.TypeAttributes)0x00200000)); + } + + [Fact] + public void TypeForwarder_WithMissingImplementation_EmitsWarning() + { + // Test that missing implementation emits a warning and doesn't emit the ExportedType + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class extern forwarder System.ForwardedType + { + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + + // Should have a warning about missing implementation + var warning = diagnostics.FirstOrDefault(d => d.Id == DiagnosticIds.MissingExportedTypeImplementation); + Assert.NotNull(warning); + Assert.Equal(DiagnosticSeverity.Warning, warning.Severity); + } } }