|
// 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;
}
}
|