File: Emitter.cs
Web Access
Project: src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.DataGenerator\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.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
    // TypeHandle (in ...Contracts) explicitly.
    private const string Target = "Target";
    private const string TargetPointer = "TargetPointer";
    private const string TypeHandleType = "TypeHandle";
    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 hasWritableFields = HasWritableFields(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)
        {
            // LayoutPair 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));
        sb.AppendLine($"partial class {model.ClassName}");
        sb.AppendLine("{");

        // Emit a static _typeNames array for LayoutPair.Resolve and TypeHandle 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 (TypeHandle) when HasTypeHandle is set.
        if (model.HasTypeHandle)
        {
            sb.AppendLine($"    public static {TypeHandleType} TypeHandle({Target} target)");
            sb.AppendLine($"        => TypeNameResolver.GetTypeHandle(target, _typeNames);");
            sb.AppendLine();
        }

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

        // Writable types capture the target so Write{Name} methods don't
        // need a target parameter on every call.
        if (hasWritableFields)
        {
            sb.AppendLine($"    private readonly {Target} _target;");
            sb.AppendLine();
        }

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

        EmitWriteBackMethods(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();
    }

    /// <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.HasSetter)
                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)!;

        sb.AppendLine($"    public void Write{member.Name}({propType} value)");
        sb.AppendLine("    {");
        sb.AppendLine($"        LayoutPair layouts = LayoutPair.Resolve(_target, _typeNames);");
        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 needsDescriptor, bool hasWritableFields)
    {
        // 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 (hasWritableFields)
        {
            sb.AppendLine("        _target = target;");
        }

        if (needsDescriptor)
        {
            sb.AppendLine();
            sb.AppendLine($"        LayoutPair layouts = LayoutPair.Resolve(target, _typeNames);");
        }
        sb.AppendLine();

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

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

    private static void EmitMemberAssignment(StringBuilder sb, MemberModel member)
    {
        switch (member.Kind)
        {
            case MemberKind.Field:
            {
                // RawOffset fields bypass the descriptor entirely.
                if (member.RawOffset is int offset)
                {
                    sb.AppendLine($"        {member.Name} = {RawOffsetReadExpression(member, offset)};");
                    break;
                }

                string? typeArg = Shorten(member.DataTypeArgumentFqn);
                string propType = Shorten(member.PropertyOrReturnTypeFqn)!;
                string readExpr = ReadExpression(member, "b", "t", "n", typeArg);
                if (member.IsOptional)
                {
                    sb.AppendLine($"        {{");
                    sb.AppendLine($"            if (layouts.TrySelect(address, out var t, out var b, out var n, {NameArgs(member)}))");
                    sb.AppendLine($"                {member.Name} = {readExpr};");
                    sb.AppendLine($"            else");
                    sb.AppendLine($"                {member.Name} = default({propType});");
                    sb.AppendLine($"        }}");
                }
                else
                {
                    sb.AppendLine($"        {{");
                    sb.AppendLine($"            layouts.Select(address, out var t, out var b, out var n, {NameArgs(member)});");
                    sb.AppendLine($"            {member.Name} = {readExpr};");
                    sb.AppendLine($"        }}");
                }
                break;
            }
            case MemberKind.FieldAddress:
                sb.AppendLine($"        {{");
                sb.AppendLine($"            layouts.Select(address, out var t, out var b, out var n, {NameArgs(member)});");
                sb.AppendLine($"            {member.Name} = b + (ulong)t.Fields[n].Offset;");
                sb.AppendLine($"        }}");
                break;
            case MemberKind.InstanceDataStart:
                sb.AppendLine($"        {member.Name} = address + layouts.InstanceSize;");
                break;
        }
    }

    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.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 HasWritableFields(CdacTypeModel model)
    {
        foreach (MemberModel member in model.Members)
        {
            if (member.Kind == MemberKind.Field && member.HasSetter && member.RawOffset is null)
                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.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;
    }
}