File: Emitter.cs
Web Access
Project: src\runtime\src\native\managed\cdac\gen\Microsoft.Diagnostics.DataContractReader.DataGenerator.csproj (Microsoft.Diagnostics.DataContractReader.DataGenerator)
// 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.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace Microsoft.Diagnostics.DataContractReader.DataGenerator;

internal static class Emitter
{
    // Generated files declare a file-scoped namespace inside
    // Microsoft.Diagnostics.DataContractReader.* so these short names resolve
    // via parent-namespace lookup. The using directives below cover
    // ITypeHandle (in ...Contracts) explicitly.
    private const string Target = "Target";
    private const string TargetPointer = "TargetPointer";
    private const string ITypeHandleType = "ITypeHandle";
    private const string IDataInterface = "IData";

    private const string RootNamespace = "Microsoft.Diagnostics.DataContractReader";

    public static string Emit(CdacTypeModel model)
    {
        // Does any member need a descriptor lookup at runtime?
        // RawOffset / Static* members bypass the descriptor entirely.
        bool needsDescriptor = HasInstanceDescriptorMembers(model);
        bool needsGeneratedUsing = needsDescriptor || model.Names.Count > 0;
        bool hasInstanceMembers = HasInstanceMembers(model);

        StringBuilder sb = new();
        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine();
        sb.AppendLine("using Microsoft.Diagnostics.DataContractReader;");
        sb.AppendLine("using Microsoft.Diagnostics.DataContractReader.Contracts;");
        if (needsGeneratedUsing)
        {
            // LayoutSet and TypeNameResolver are emitted by the generator
            // via RegisterPostInitializationOutput into this namespace.
            sb.AppendLine("using Microsoft.Diagnostics.DataContractReader.Generated;");
        }
        sb.AppendLine();
        if (!string.IsNullOrEmpty(model.Namespace))
        {
            sb.AppendLine($"namespace {model.Namespace};");
            sb.AppendLine();
        }

        sb.AppendLine(BuildClassDoc(model));
        // A partial class may add interfaces from any declaration. Lazily-read
        // types implement IReadableData so callers can force a full read.
        if (hasInstanceMembers)
            sb.AppendLine($"partial class {model.ClassName} : global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData");
        else
            sb.AppendLine($"partial class {model.ClassName}");
        sb.AppendLine("{");

        // Emit a static _typeNames array for LayoutSet.Resolve and ITypeHandle resolution.
        if (model.Names.Count > 0)
        {
            string namesLiteral = NamesArrayLiteral(model.Names);
            sb.AppendLine($"    private static readonly string[] _typeNames = {namesLiteral};");
            sb.AppendLine();
        }

        // The class advertises a managed identity (ITypeHandle) when HasTypeHandle is set.
        if (model.HasTypeHandle)
        {
            sb.AppendLine($"    public static {ITypeHandleType} TypeHandle({Target} target)");
            sb.AppendLine($"        => TypeNameResolver.GetTypeHandle(target, _typeNames);");
            sb.AppendLine();
        }

        if (model.Names.Count > 0)
        {
            sb.AppendLine("    [UsesDataDescriptorTypeSize]");
            sb.AppendLine($"    public static uint GetSize({Target} target)");
            sb.AppendLine("        => checked((uint)LayoutSet.Resolve(target, _typeNames).InstanceSize);");
            sb.AppendLine();
        }

        EmitFieldOffsetMethods(sb, model);

        sb.AppendLine($"    public {TargetPointer} Address {{ get; }}");
        sb.AppendLine();

        // Instance members read from the target lazily on first property
        // access, so the target is captured for the lifetime of the object.
        if (hasInstanceMembers)
        {
            sb.AppendLine($"    private readonly {Target} _target;");
            sb.AppendLine();
        }

        // The type layout is resolved once at construction. Resolution never
        // reads a field or fails for a missing one (it only looks up the type
        // and wires an on-demand managed-metadata source), so resolving it
        // eagerly does not defeat the lazy per-field reads: the failure sites
        // (field lookup + read) still happen on first property access.
        if (needsDescriptor)
        {
            sb.AppendLine("    private readonly LayoutSet _layouts;");
            sb.AppendLine();
        }

        EmitConstructor(sb, model, hasInstanceMembers, needsDescriptor);
        EmitCreate(sb, model);

        EmitWriteBackMethods(sb, model);

        foreach (MemberModel member in model.Members)
            EmitMemberProperty(sb, member);

        if (hasInstanceMembers)
            EmitEnsureAllFieldsRead(sb, model);

        foreach (MemberModel member in model.Members)
        {
            switch (member.Kind)
            {
                case MemberKind.StaticAddress:
                    EmitStaticAddressMethod(sb, member);
                    break;
                case MemberKind.StaticReference:
                    EmitStaticReferenceMethod(sb, member);
                    break;
                case MemberKind.ThreadStaticAddress:
                    EmitThreadStaticAddressMethod(sb, member);
                    break;
            }
        }

        sb.AppendLine("}");
        return sb.ToString();
    }

    private static void EmitFieldOffsetMethods(StringBuilder sb, CdacTypeModel model)
    {
        foreach (MemberModel member in model.Members)
        {
            if (member.Kind is not (MemberKind.Field or MemberKind.FieldAddress))
                continue;

            if (member.RawOffset is null)
                EmitDataDescriptorDependencyAttribute(sb, member);
            sb.AppendLine($"    public static int Get{member.Name}Offset({Target} target)");
            if (member.RawOffset is int offset)
            {
                sb.AppendLine($"        => {offset};");
            }
            else
            {
                sb.AppendLine("    {");
                sb.AppendLine("        LayoutSet layouts = LayoutSet.Resolve(target, _typeNames);");
                sb.AppendLine($"        layouts.Select(default, out var type, out _, out var name, {NameArgs(member)});");
                sb.AppendLine("        return type.Fields[name].Offset;");
                sb.AppendLine("    }");
            }
            sb.AppendLine();
        }
    }

    /// <summary>
    /// Emits a <c>Write{Name}(T value)</c> method for each settable
    /// <c>[Field]</c> property. Uses the captured <c>_target</c> field.
    /// Only supported for primitive integer, bool, and NUInt read kinds.
    /// </summary>
    private static void EmitWriteBackMethods(StringBuilder sb, CdacTypeModel model)
    {
        foreach (MemberModel member in model.Members)
        {
            if (member.Kind != MemberKind.Field || member.Setter != SetterKind.Writable)
                continue;

            if (member.ReadKind != FieldReadKind.Primitive
                && member.ReadKind != FieldReadKind.Bool
                && member.ReadKind != FieldReadKind.NUInt)
                continue;

            // RawOffset-based fields don't have a descriptor entry to write through.
            if (member.RawOffset is not null)
                continue;

            EmitWriteBackMethod(sb, member);
        }
    }

    private static void EmitWriteBackMethod(StringBuilder sb, MemberModel member)
    {
        string propType = Shorten(member.PropertyOrReturnTypeFqn)!;

        EmitDataDescriptorDependencyAttribute(sb, member);
        sb.AppendLine($"    public void Write{member.Name}({propType} value)");
        sb.AppendLine("    {");
        sb.AppendLine($"        _layouts.Select(Address, out var t, out var b, out var n, {NameArgs(member)});");
        if (member.ReadKind == FieldReadKind.Bool)
        {
            sb.AppendLine($"        _target.WriteField<byte>(b, t, n, (byte)(value ? 1 : 0));");
        }
        else if (member.ReadKind == FieldReadKind.NUInt)
        {
            sb.AppendLine($"        _target.WriteNUIntField(b, t, n, value);");
        }
        else
        {
            string? typeArg = Shorten(member.DataTypeArgumentFqn);
            sb.AppendLine($"        _target.WriteField<{typeArg}>(b, t, n, value);");
        }
        sb.AppendLine($"        {member.Name} = value;");
        sb.AppendLine("    }");
        sb.AppendLine();
    }

    private static void EmitConstructor(StringBuilder sb, CdacTypeModel model, bool hasInstanceMembers, bool needsDescriptor)
    {
        // Hook: a `partial void OnInit(Target, TargetPointer)` the user may
        // implement to perform reads that don't fit the declarative attribute
        // surface (e.g. variable-count loops, raw-offset reads, or values
        // computed from other fields). If no implementation is provided the
        // C# compiler elides both the call site and the signature.
        sb.AppendLine($"    partial void OnInit({Target} target, {TargetPointer} address);");
        sb.AppendLine();

        sb.AppendLine($"    public {model.ClassName}({Target} target, {TargetPointer} address)");
        sb.AppendLine("    {");
        sb.AppendLine("        Address = address;");

        if (hasInstanceMembers)
        {
            sb.AppendLine("        _target = target;");
        }

        if (needsDescriptor)
        {
            sb.AppendLine("        _layouts = LayoutSet.Resolve(target, _typeNames);");
        }

        sb.AppendLine();
        sb.AppendLine("        OnInit(target, address);");
        sb.AppendLine("    }");
        sb.AppendLine();
    }

    private static string ValueField(MemberModel member) => $"_{member.Name}__value";
    private static string ReadFlag(MemberModel member) => $"_{member.Name}__read";

    /// <summary>
    /// Emits the lazily-read partial property implementation for an instance
    /// member. The field is read from the target on first access and memoized.
    /// The getter throws exactly what the eager constructor would have: a field
    /// missing from the descriptor throws <c>InvalidOperationException</c> from
    /// the layout lookup, and a present-but-unreadable field throws
    /// <c>VirtualReadException</c> from the read -- just deferred to first
    /// access instead of construction.
    /// </summary>
    private static void EmitMemberProperty(StringBuilder sb, MemberModel member)
    {
        string valueField = ValueField(member);
        List<string> read = new();
        switch (member.Kind)
        {
            case MemberKind.Field:
                if (member.RawOffset is int offset)
                    read.Add($"                {valueField} = {RawOffsetReadExpression(member, offset)};");
                else
                    EmitLayoutRead(read, member, valueField,
                        ReadExpression(member, "b", "t", "n", Shorten(member.DataTypeArgumentFqn)),
                        $"default({Shorten(member.PropertyOrReturnTypeFqn)})");
                EmitLazyProperty(sb, member, read, emitSetter: member.Setter != SetterKind.None);
                break;
            case MemberKind.FieldAddress:
                EmitLayoutRead(read, member, valueField, "b + (ulong)t.Fields[n].Offset", "null");
                EmitLazyProperty(sb, member, read, emitSetter: false);
                break;
            case MemberKind.InstanceDataStart:
                read.Add($"                {valueField} = Address + _layouts.InstanceSize;");
                EmitLazyProperty(sb, member, read, emitSetter: false);
                break;
        }
    }

    /// <summary>
    /// Appends the descriptor lookup + assignment for a member that reads
    /// through <c>_layouts</c>. Optional members use <c>TrySelect</c> (absent =>
    /// <paramref name="optionalElse"/>); required members use <c>Select</c>,
    /// which throws when the field is missing.
    /// </summary>
    private static void EmitLayoutRead(List<string> lines, MemberModel member, string valueField, string assignExpr, string optionalElse)
    {
        if (member.IsOptional)
        {
            lines.Add($"                if (_layouts.TrySelect(Address, out var t, out var b, out var n, {NameArgs(member)}))");
            lines.Add($"                    {valueField} = {assignExpr};");
            lines.Add("                else");
            lines.Add($"                    {valueField} = {optionalElse};");
        }
        else
        {
            lines.Add($"                _layouts.Select(Address, out var t, out var b, out var n, {NameArgs(member)});");
            lines.Add($"                {valueField} = {assignExpr};");
        }
    }

    /// <summary>
    /// Emits the common lazy partial-property scaffold: a backing field, a read
    /// flag, and a getter that runs <paramref name="readBody"/> once on first
    /// access. A writable/settable member also gets a private setter that stores
    /// the value and marks it read.
    /// </summary>
    private static void EmitLazyProperty(StringBuilder sb, MemberModel member, List<string> readBody, bool emitSetter)
    {
        string propType = Shorten(member.PropertyOrReturnTypeFqn)!;
        string valueField = ValueField(member);
        string readFlag = ReadFlag(member);

        sb.AppendLine($"    private {propType} {valueField} = default!;");
        sb.AppendLine($"    private bool {readFlag};");
        EmitDataDescriptorDependencyAttribute(sb, member);
        sb.AppendLine($"    public partial {propType} {member.Name}");
        sb.AppendLine("    {");
        sb.AppendLine("        get");
        sb.AppendLine("        {");
        sb.AppendLine($"            if (!{readFlag})");
        sb.AppendLine("            {");
        foreach (string line in readBody)
            sb.AppendLine(line);
        sb.AppendLine($"                {readFlag} = true;");
        sb.AppendLine("            }");
        sb.AppendLine($"            return {valueField};");
        sb.AppendLine("        }");
        if (emitSetter)
        {
            sb.AppendLine("        private set");
            sb.AppendLine("        {");
            sb.AppendLine($"            {valueField} = value;");
            sb.AppendLine($"            {readFlag} = true;");
            sb.AppendLine("        }");
        }
        sb.AppendLine("    }");
        sb.AppendLine();
    }

    private static void EmitDataDescriptorDependencyAttribute(StringBuilder sb, MemberModel member)
    {
        if (member.Kind == MemberKind.InstanceDataStart)
        {
            sb.AppendLine("    [UsesDataDescriptorTypeSize]");
        }
        else if (member.RawOffset is null)
        {
            Debug.Assert(member.DescriptorNativeType is not null);
            sb.AppendLine(
                $"    [DataDescriptorDependency(\"{member.DescriptorOrFieldName}\", \"{member.DescriptorNativeType}\")]");
        }
    }

    /// <summary>
    /// Emits the <see cref="Data.IReadableData.EnsureAllFieldsRead"/> implementation,
    /// which touches every lazily-read member so a caller can eagerly force a full
    /// read of the structure (used to validate that the whole struct is readable).
    /// </summary>
    private static void EmitEnsureAllFieldsRead(StringBuilder sb, CdacTypeModel model)
    {
        EmitEnsureAllFieldsReadDependencyAttributes(sb, model);
        sb.AppendLine("    void global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData.EnsureAllFieldsRead()");
        sb.AppendLine("    {");
        foreach (MemberModel member in model.Members)
        {
            if (member.Kind == MemberKind.Field
                || member.Kind == MemberKind.FieldAddress
                || member.Kind == MemberKind.InstanceDataStart)
            {
                sb.AppendLine($"        _ = {member.Name};");
            }
        }
        sb.AppendLine("    }");
        sb.AppendLine();
    }

    private static void EmitEnsureAllFieldsReadDependencyAttributes(StringBuilder sb, CdacTypeModel model)
    {
        (string FieldName, string NativeType)[] fields = model.Members
            .Where(member =>
                member.Kind is MemberKind.Field or MemberKind.FieldAddress &&
                member.RawOffset is null)
            .Select(DescriptorDependency)
            .Distinct()
            .ToArray();
        bool usesTypeSize = model.Members.Any(member => member.Kind == MemberKind.InstanceDataStart);

        foreach ((string fieldName, string nativeType) in fields)
        {
            sb.AppendLine(
                $"    [DataDescriptorDependency(\"{fieldName}\", \"{nativeType}\")]");
        }
        if (usesTypeSize)
            sb.AppendLine("    [UsesDataDescriptorTypeSize]");
    }

    private static (string FieldName, string NativeType) DescriptorDependency(MemberModel member)
    {
        Debug.Assert(member.DescriptorNativeType is not null);
        return (member.DescriptorOrFieldName, member.DescriptorNativeType!);
    }

    private static string ReadExpression(MemberModel member, string baseVar, string typeVar, string nameVar, string? typeArg)
        => member.ReadKind switch
        {
            FieldReadKind.Primitive   => $"_target.ReadField<{typeArg}>({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.Bool        => $"_target.ReadField<{member.BoolUnderlyingType ?? "byte"}>({baseVar}, {typeVar}, {nameVar}) != 0",
            FieldReadKind.Pointer     => $"_target.ReadPointerField({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.NUInt       => $"_target.ReadNUIntField({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.NInt        => $"_target.ReadNIntField({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.CodePointer => $"_target.ReadCodePointerField({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.DataInPlace => $"_target.ReadDataField<{typeArg}>({baseVar}, {typeVar}, {nameVar})",
            FieldReadKind.DataPointer => $"_target.ProcessedData.GetOrAdd<{typeArg}>(_target.ReadPointerField({baseVar}, {typeVar}, {nameVar}))",
            _ => $"default({Shorten(member.PropertyOrReturnTypeFqn)})",
        };

    private static string NameArgs(MemberModel member)
        => string.Join(", ", Enumerate(member.Names).Select(n => $"\"{n}\""));

    private static IEnumerable<string> Enumerate(EquatableArray<string> array)
    {
        for (int i = 0; i < array.Count; i++)
            yield return array[i];
    }

    private static string NamesArrayLiteral(EquatableArray<string> names)
    {
        if (names.Count == 1)
            return $"new[] {{ \"{names[0]}\" }}";
        var parts = new List<string>();
        for (int i = 0; i < names.Count; i++)
            parts.Add($"\"{names[i]}\"");
        return "new[] { " + string.Join(", ", parts) + " }";
    }

    private static string BuildClassDoc(CdacTypeModel model)
    {
        if (model.Names.Count == 0)
            return "/// <summary>Generated IData implementation.</summary>";

        var parts = new List<string>();
        for (int i = 0; i < model.Names.Count; i++)
            parts.Add($"<c>{model.Names[i]}</c>");
        string nameList = string.Join(" / ", parts);
        return $"/// <summary>Wraps the {nameList} type.</summary>";
    }

    private static void EmitCreate(StringBuilder sb, CdacTypeModel model)
    {
        sb.AppendLine($"    static {model.ClassName} {IDataInterface}<{model.ClassName}>.Create({Target} target, {TargetPointer} address)");
        sb.AppendLine($"        => new {model.ClassName}(target, address);");
        sb.AppendLine();
    }

    private static bool HasInstanceMembers(CdacTypeModel model)
    {
        foreach (MemberModel member in model.Members)
        {
            if (member.Kind == MemberKind.Field
                || member.Kind == MemberKind.FieldAddress
                || member.Kind == MemberKind.InstanceDataStart)
                return true;
        }

        return false;
    }

    private static bool HasInstanceDescriptorMembers(CdacTypeModel model)
    {
        foreach (MemberModel member in model.Members)
        {
            // Members with a hardcoded RawOffset don't need the descriptor.
            if (member.RawOffset is not null)
                continue;

            if (member.Kind == MemberKind.Field
                || member.Kind == MemberKind.FieldAddress
                || member.Kind == MemberKind.InstanceDataStart)
                return true;
        }

        return false;
    }

    private static string RawOffsetReadExpression(MemberModel member, int offset)
    {
        string? typeArg = Shorten(member.DataTypeArgumentFqn);
        string addr = $"Address + {offset}";
        string readMethod = member.LittleEndian ? "ReadLittleEndian" : "Read";
        return member.ReadKind switch
        {
            FieldReadKind.Primitive => $"_target.{readMethod}<{typeArg}>({addr})",
            FieldReadKind.Bool => $"_target.{readMethod}<byte>({addr}) != 0",
            FieldReadKind.Pointer => $"_target.ReadPointer({addr})",
            FieldReadKind.NUInt => $"_target.ReadNUInt({addr})",
            FieldReadKind.NInt => $"_target.ReadNInt({addr})",
            FieldReadKind.CodePointer => $"_target.ReadCodePointer({addr})",
            FieldReadKind.DataInPlace => $"_target.ProcessedData.GetOrAdd<{typeArg}>({addr})",
            _ => $"default({Shorten(member.PropertyOrReturnTypeFqn)})",
        };
    }

    private static void EmitStaticAddressMethod(StringBuilder sb, MemberModel member)
    {
        sb.AppendLine($"    public static partial {TargetPointer} {member.Name}({Target} target)");
        sb.AppendLine($"        => TypeNameResolver.GetStaticFieldAddress(target, _typeNames, \"{member.DescriptorOrFieldName}\");");
        sb.AppendLine();
    }

    private static void EmitStaticReferenceMethod(StringBuilder sb, MemberModel member)
    {
        sb.AppendLine($"    public static partial {TargetPointer}? {member.Name}({Target} target)");
        sb.AppendLine("    {");
        sb.AppendLine($"        if (TypeNameResolver.TryGetStaticFieldAddress(target, _typeNames, \"{member.DescriptorOrFieldName}\", out {TargetPointer} address))");
        sb.AppendLine($"            return target.ReadPointer(address);");
        sb.AppendLine($"        return null;");
        sb.AppendLine("    }");
        sb.AppendLine();
    }

    private static void EmitThreadStaticAddressMethod(StringBuilder sb, MemberModel member)
    {
        sb.AppendLine($"    public static partial {TargetPointer} {member.Name}({Target} target, {TargetPointer} thread)");
        sb.AppendLine($"        => TypeNameResolver.GetThreadStaticFieldAddress(target, _typeNames, \"{member.DescriptorOrFieldName}\", thread);");
        sb.AppendLine();
    }

    public static string HintNameFor(CdacTypeModel model)
    {
        string ns = string.IsNullOrEmpty(model.Namespace) ? "Global" : model.Namespace.Replace('.', '_');
        return $"{ns}.{model.ClassName}.g.cs";
    }

    /// <summary>
    /// Strips redundant <c>global::</c> and <c>Microsoft.Diagnostics.DataContractReader.</c>
    /// prefixes from a fully-qualified type name. The generated file's namespace
    /// and the using directives at the top make these prefixes redundant.
    /// </summary>
    private static string? Shorten(string? fqn)
    {
        if (fqn is null)
            return null;

        const string GlobalPrefix = "global::";
        if (fqn.StartsWith(GlobalPrefix, System.StringComparison.Ordinal))
            fqn = fqn.Substring(GlobalPrefix.Length);

        const string RootPrefix = RootNamespace + ".";
        if (fqn.StartsWith(RootPrefix, System.StringComparison.Ordinal))
            fqn = fqn.Substring(RootPrefix.Length);

        return fqn;
    }
}