diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h index 31f9f945e7f2cb..21f9bfd08fc80f 100644 --- a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h @@ -25,3 +25,10 @@ #include #include + +// ILC emits a ContractDescriptor named "DotNetManagedContractDescriptor" with +// managed type layouts. We take its address so datadescriptor.inc can reference +// it as a sub-descriptor via CDAC_GLOBAL_SUB_DESCRIPTOR. +struct ContractDescriptor; +extern "C" ContractDescriptor DotNetManagedContractDescriptor; +static const void* g_pManagedContractDescriptor = &DotNetManagedContractDescriptor; diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc index fa71924c6120f0..7f477cebe2debd 100644 --- a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc @@ -133,6 +133,12 @@ CDAC_GLOBAL_CONTRACT(Thread, 1001) CDAC_GLOBAL_CONTRACT(Exception, 1) CDAC_GLOBAL_CONTRACT(RuntimeTypeSystem, 1001) +// Managed type sub-descriptor: ILC emits a ContractDescriptor with managed type layouts +// that the cDAC reader merges as a sub-descriptor. This provides field offsets for managed +// types (e.g., ConditionalWeakTable internals, IdDispenser) that are not exposed through +// native C++ data descriptors. +CDAC_GLOBAL_SUB_DESCRIPTOR(ManagedTypes, &g_pManagedContractDescriptor) + // GC sub-descriptor: the GC populates gc_descriptor during GC_Initialize. // It is important for subdescriptor pointers to be the last pointers. CDAC_GLOBAL_SUB_DESCRIPTOR(GC, &(g_gc_dac_vars.gc_descriptor)) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index d120f10da497ac..bd0999730238a7 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -195,6 +195,8 @@ + + diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacFieldAttribute.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacFieldAttribute.cs new file mode 100644 index 00000000000000..bb1e3e033227c5 --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacFieldAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// When applied to an instance field of a type annotated with , + /// indicates that ILC should include this field in the managed cDAC data descriptor. + /// + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + internal sealed class CdacFieldAttribute : Attribute + { + public CdacFieldAttribute() + { + } + + public CdacFieldAttribute(string name) + { + Name = name; + } + + /// + /// The cDAC descriptor field name. If not specified, the field's declared name is used. + /// + public string? Name { get; } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacTypeAttribute.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacTypeAttribute.cs new file mode 100644 index 00000000000000..f8a73e89c7eb9a --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/CdacTypeAttribute.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// When applied to a type, indicates that ILC should include its field layout in the + /// managed cDAC data descriptor. The cDAC reader merges this information as a + /// sub-descriptor so diagnostic tools can inspect managed type instances without + /// runtime metadata (critical for NativeAOT where metadata may be stripped). + /// + /// + /// Fields to include must be individually annotated with . + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] + internal sealed class CdacTypeAttribute : Attribute + { + public CdacTypeAttribute(string name) + { + Name = name; + } + + /// + /// The cDAC descriptor type name (e.g., "ManagedThread"). + /// + public string Name { get; } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index 3f7aa4ffc429df..ea5d063f73bc3e 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -13,6 +13,7 @@ namespace System.Threading { + [CdacType("ManagedThread")] public sealed partial class Thread { // Extra bits used in _threadState @@ -29,8 +30,11 @@ public sealed partial class Thread private volatile int _threadState = (int)ThreadState.Unstarted; private ThreadPriority _priority; + [CdacField("ManagedThreadId")] private ManagedThreadId _managedThreadId; + [CdacField("Name")] private string? _name; + [CdacField("StartHelper")] private StartHelper? _startHelper; private Exception? _startException; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs new file mode 100644 index 00000000000000..38ec3d580eede8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs @@ -0,0 +1,228 @@ +// 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.Text; + +using Internal.Text; +using Internal.TypeSystem; + +namespace ILCompiler.DependencyAnalysis +{ + /// + /// Emits a ContractDescriptor for managed type layouts that the cDAC reader + /// can consume as a sub-descriptor. ILC knows managed type layouts at compile time, + /// so it can emit field offsets that would otherwise require runtime metadata resolution. + /// + /// The NativeAOT runtime C++ code declares an extern pointer to this symbol and references + /// it via CDAC_GLOBAL_SUB_DESCRIPTOR in datadescriptor.inc, enabling the cDAC reader to + /// merge managed type information into its unified type map. + /// + /// + /// The emitted structure matches the ContractDescriptor format: + /// + /// struct ContractDescriptor { + /// uint64_t magic; // 0x0043414443434e44 "DNCCDAC\0" + /// uint32_t flags; // Platform flags + /// uint32_t descriptor_size; // JSON blob size + /// char* descriptor; // Pointer to JSON string + /// uint32_t pointer_data_count; + /// uint32_t pad0; + /// void** pointer_data; // Pointer to auxiliary data array + /// }; + /// + /// The JSON descriptor follows the cDAC contract descriptor schema: + /// + /// { "version": 0, "types": { "TypeName": [size, { "Field": offset }] }, "globals": {} } + /// + /// + public class ManagedDataDescriptorNode : ObjectNode, ISymbolDefinitionNode + { + public const string SymbolName = "DotNetManagedContractDescriptor"; + + private readonly List _typeDescriptors = new List(); + + public override ObjectNodeSection GetSection(NodeFactory factory) => + factory.Target.IsWindows ? ObjectNodeSection.ReadOnlyDataSection : ObjectNodeSection.DataSection; + + public override bool StaticDependenciesAreComputed => true; + public override bool IsShareable => false; + + public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) + { + sb.Append(nameMangler.NodeMangler.ExternVariable(new Utf8String(SymbolName))); + } + + public int Offset => 0; + + protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler); + + /// + /// Register a managed type to be included in the descriptor. + /// + /// The cDAC type name (e.g., "ManagedIdDispenser") + /// The resolved managed type from ILC's type system + /// Optional field name remapping: cDAC field name → managed field name. + /// If null, all instance fields are included with their original names. + public void AddType(string descriptorTypeName, MetadataType type, Dictionary fieldMappings = null) + { + _typeDescriptors.Add(new ManagedTypeDescriptor(descriptorTypeName, type, fieldMappings)); + } + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) + { + if (relocsOnly) + return new ObjectData(Array.Empty(), Array.Empty(), 1, new ISymbolDefinitionNode[] { this }); + + ObjectDataBuilder builder = new ObjectDataBuilder(factory, relocsOnly); + builder.RequireInitialPointerAlignment(); + builder.AddSymbol(this); + + // uint64_t magic + builder.EmitLong(0x0043414443434e44L); // "DNCCDAC\0" + + // uint32_t flags (bit 0 must be set; bit 1 indicates 32-bit pointers) + uint flags = (uint)(0x01 | (factory.Target.PointerSize == 4 ? 0x02 : 0x00)); + builder.EmitUInt(flags); + + // uint32_t descriptor_size + builder.EmitInt(_jsonBytesLength); + + // char* descriptor — pointer to JSON blob (separate compilation root) + builder.EmitPointerReloc(_jsonBlobNode); + + // uint32_t pointer_data_count = 0 + builder.EmitInt(0); + + // uint32_t pad0 + builder.EmitInt(0); + + // void** pointer_data = null + builder.EmitZeroPointer(); + + return builder.ToObjectData(); + } + + /// + /// Build the JSON and create the blob node. Must be called before the node + /// is added to the dependency graph. + /// + public void FinalizeDescriptor() + { + string jsonDescriptor = BuildJsonDescriptor(); + byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonDescriptor); + _jsonBytesLength = jsonBytes.Length; + + byte[] nullTerminated = new byte[jsonBytes.Length + 1]; + Array.Copy(jsonBytes, nullTerminated, jsonBytes.Length); + _jsonBlobNode = new BlobNode( + new Utf8String("__ManagedContractDescriptorJsonBlob"), + ObjectNodeSection.ReadOnlyDataSection, + nullTerminated, + alignment: 1); + } + + /// + /// The blob node containing the JSON data. Add this as a separate compilation root. + /// + public BlobNode JsonBlobNode => _jsonBlobNode; + + private BlobNode _jsonBlobNode; + private int _jsonBytesLength; + + private string BuildJsonDescriptor() + { + var sb = new StringBuilder(); + sb.Append("{\"version\":0,\"types\":{"); + + bool firstType = true; + foreach (var desc in _typeDescriptors) + { + if (!firstType) + sb.Append(','); + firstType = false; + + EmitTypeJson(sb, desc); + } + + sb.Append("},\"globals\":{}}"); + return sb.ToString(); + } + + private static void EmitTypeJson(StringBuilder sb, ManagedTypeDescriptor desc) + { + MetadataType type = desc.Type; + + // Use 0 (indeterminate) for reference types — their "size" from cDAC perspective + // is not meaningful since they're GC-managed objects. + int typeSize = type.IsValueType ? type.InstanceFieldSize.AsInt : 0; + + // JSON format: "TypeName": [size, { "Field1": offset, "Field2": offset }] + sb.Append('"').Append(desc.DescriptorName).Append("\":["); + sb.Append(typeSize); + sb.Append(",{"); + + bool firstField = true; + foreach (FieldDesc field in type.GetFields()) + { + if (field.IsStatic) + continue; + + string fieldName = field.GetName(); + string cdacFieldName; + if (desc.FieldMappings is not null) + { + // Check if any cDAC name maps to this managed field name + cdacFieldName = null; + foreach (var kvp in desc.FieldMappings) + { + if (kvp.Value == fieldName) + { + cdacFieldName = kvp.Key; + break; + } + } + if (cdacFieldName is null) + continue; + } + else + { + cdacFieldName = fieldName; + } + + if (!firstField) + sb.Append(','); + firstField = false; + + sb.Append('"').Append(cdacFieldName).Append("\":"); + sb.Append(field.Offset.AsInt); + } + + sb.Append("}]"); + } + +#if !SUPPORT_JIT + public override int ClassCode => 0x4d444e01; + + public override int CompareToImpl(ISortableNode other, CompilerComparer comparer) + { + return 0; // Singleton + } +#endif + + private readonly struct ManagedTypeDescriptor + { + public readonly string DescriptorName; + public readonly MetadataType Type; + public readonly Dictionary FieldMappings; + + public ManagedTypeDescriptor(string descriptorName, MetadataType type, Dictionary fieldMappings) + { + DescriptorName = descriptorName; + Type = type; + FieldMappings = fieldMappings; + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs new file mode 100644 index 00000000000000..215dde5cab4dfc --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs @@ -0,0 +1,102 @@ +// 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.Reflection.Metadata; + +using ILCompiler.DependencyAnalysis; + +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; + +namespace ILCompiler +{ + /// + /// Compilation root provider that emits managed type layout descriptors for the cDAC. + /// Types annotated with [CdacType] will have their [CdacField]-annotated field offsets + /// computed at compile time and embedded in the final binary as a ContractDescriptor + /// that the cDAC reader merges as a sub-descriptor. + /// + /// + /// The descriptor is emitted as a symbol named "DotNetManagedContractDescriptor" + /// that the NativeAOT runtime's C++ code references via extern and stores as a + /// sub-descriptor pointer in the main contract descriptor. + /// + public class ManagedDataDescriptorProvider : ICompilationRootProvider + { + private const string CdacTypeAttributeNamespace = "System.Runtime.CompilerServices"; + private const string CdacTypeAttributeName = "CdacTypeAttribute"; + private const string CdacFieldAttributeName = "CdacFieldAttribute"; + + private readonly CompilerTypeSystemContext _context; + + public ManagedDataDescriptorProvider(CompilerTypeSystemContext context) + { + _context = context; + } + + void ICompilationRootProvider.AddCompilationRoots(IRootingServiceProvider rootProvider) + { + var descriptorNode = new ManagedDataDescriptorNode(); + + DiscoverAnnotatedTypes(descriptorNode); + descriptorNode.FinalizeDescriptor(); + + rootProvider.AddCompilationRoot(descriptorNode, "Managed type descriptors for cDAC"); + rootProvider.AddCompilationRoot(descriptorNode.JsonBlobNode, "Managed descriptor JSON data"); + } + + private void DiscoverAnnotatedTypes(ManagedDataDescriptorNode descriptorNode) + { + if (_context.SystemModule is not EcmaModule systemModule) + return; + + var seenDescriptorNames = new HashSet(); + MetadataReader reader = systemModule.MetadataReader; + + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + EcmaType ecmaType = (EcmaType)systemModule.GetType(typeDefHandle); + var typeAttr = ecmaType.GetDecodedCustomAttribute(CdacTypeAttributeNamespace, CdacTypeAttributeName); + if (typeAttr is null) + continue; + + string descriptorTypeName = (string)typeAttr.Value.FixedArguments[0].Value; + + if (string.IsNullOrEmpty(descriptorTypeName)) + throw new InvalidOperationException($"[CdacType] on '{ecmaType}' has a null or empty descriptor name."); + + if (ecmaType.HasInstantiation) + throw new InvalidOperationException($"[CdacType] is not supported on generic type '{ecmaType}'."); + + if (!seenDescriptorNames.Add(descriptorTypeName)) + throw new InvalidOperationException($"Duplicate [CdacType] descriptor name '{descriptorTypeName}' on '{ecmaType}'."); + + var fieldMappings = new Dictionary(); + foreach (FieldDesc field in ecmaType.GetFields()) + { + if (field.IsStatic || field is not EcmaField ecmaField) + continue; + + var fieldAttr = ecmaField.GetDecodedCustomAttribute(CdacTypeAttributeNamespace, CdacFieldAttributeName); + if (fieldAttr is null) + continue; + + string cdacFieldName = fieldAttr.Value.FixedArguments.Length > 0 + && fieldAttr.Value.FixedArguments[0].Value is string name + ? name + : field.GetName(); + + if (!fieldMappings.TryAdd(cdacFieldName, field.GetName())) + throw new InvalidOperationException($"Duplicate [CdacField] name '{cdacFieldName}' on type '{ecmaType}'."); + } + + if (fieldMappings.Count > 0) + { + descriptorNode.AddType(descriptorTypeName, (MetadataType)ecmaType, fieldMappings); + } + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 4592ac765a86a7..2c6ba5abf09322 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -595,6 +595,7 @@ + @@ -688,6 +689,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 9786ffc489b473..e59a518e4a8965 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -263,6 +263,7 @@ public int Run() compilationRoots.Add(new RuntimeConfigurationRootProvider(settingsBlobName, runtimeOptions)); compilationRoots.Add(new RuntimeConfigurationRootProvider(knobsBlobName, runtimeKnobs)); compilationRoots.Add(new ExpectedIsaFeaturesRootProvider(instructionSetSupport)); + compilationRoots.Add(new ManagedDataDescriptorProvider(typeSystemContext)); if (SplitExeInitialization) { compilationRoots.Add(new MainMethodRootProvider(entrypointModule, CreateInitializerList(typeSystemContext), generateLibraryAndModuleInitializers: false)); @@ -274,6 +275,7 @@ public int Run() compilationRoots.Add(new RuntimeConfigurationRootProvider(settingsBlobName, runtimeOptions)); compilationRoots.Add(new RuntimeConfigurationRootProvider(knobsBlobName, runtimeKnobs)); compilationRoots.Add(new ExpectedIsaFeaturesRootProvider(instructionSetSupport)); + compilationRoots.Add(new ManagedDataDescriptorProvider(typeSystemContext)); if (SplitExeInitialization) { compilationRoots.Add(new NativeLibraryInitializerRootProvider(typeSystemContext.GeneratedAssembly, CreateInitializerList(typeSystemContext)));