Skip to content

Commit

Permalink
[cdac] Implement a JSON contract reader (dotnet#100966)
Browse files Browse the repository at this point in the history
Implement a parser for the "compact" JSON contract descriptor format specified in [data_contracts.md](https://github.com/dotnet/runtime/blob/main/docs/design/datacontracts/data_descriptor.md)

The data model is a WIP - it's likely we will want something a bit more abstract (and less mutable).

This is not wired up to consume a real contract descriptor from target process memory at the moment.  It's only exercised by unit tests for now.

---

* compact descriptor format json parser

* suggestions from reviews; remove FieldDescriptor wrong conversion

   we incorrectly allowed `[number]` as a field descriptor conversion. that's not allowed.  removed it.

* Make test project like the nativeoat+linker tests

   Dont' use libraries test infrastructure.  Just normal arcade xunit support.

* add tools.cdacreadertests subset; add to CLR_Tools_Tests test leg

* no duplicate fields/sizes in types

* Make all cdacreader.csproj ProjectReferences use the same AdditionalProperties

   Since we set Configuration and RuntimeIdentifier, if we don't pass the same AdditionalProperties in all ProjectReferences, we bad project.assets.json files

* Don't share the native compilation AdditionalProperties

---------

Co-authored-by: Aaron Robinson <arobins@microsoft.com>
Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
Co-authored-by: Elinor Fung <elfung@microsoft.com>
  • Loading branch information
4 people authored and matouskozak committed Apr 30, 2024
1 parent f9f365a commit eb344fe
Show file tree
Hide file tree
Showing 8 changed files with 584 additions and 12 deletions.
6 changes: 6 additions & 0 deletions eng/Subsets.props
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
<SubsetName Include="Tools.ILLink" Description="The projects that produce illink and analyzer tools for trimming." />
<SubsetName Include="Tools.ILLinkTests" OnDemand="true" Description="Unit tests for the tools.illink subset." />

<SubsetName Include="Tools.CdacReaderTests" OnDemand="true" Description="Units tests for the cDAC reader." />

<!-- Host -->
<SubsetName Include="Host" Description="The .NET hosts, packages, hosting libraries, and tests. Equivalent to: $(DefaultHostSubsets)" />
<SubsetName Include="Host.Native" Description="The .NET hosts." />
Expand Down Expand Up @@ -369,6 +371,10 @@
Test="true" Category="clr" Condition="'$(DotNetBuildSourceOnly)' != 'true' and '$(NativeAotSupported)' == 'true'"/>
</ItemGroup>

<ItemGroup Condition="$(_subset.Contains('+tools.cdacreadertests+'))">
<ProjectToBuild Include="$(SharedNativeRoot)managed\cdacreader\tests\Microsoft.Diagnostics.DataContractReader.Tests.csproj" Test="true" Category="tools"/>
</ItemGroup>

<ItemGroup Condition="$(_subset.Contains('+tools.illink+'))">
<ProjectToBuild Include="$(ToolsProjectRoot)illink\src\linker\Mono.Linker.csproj" Category="tools" />
<ProjectToBuild Include="$(ToolsProjectRoot)illink\src\ILLink.Tasks\ILLink.Tasks.csproj" Category="tools" />
Expand Down
4 changes: 4 additions & 0 deletions eng/pipelines/common/evaluate-default-paths.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ jobs:
- src/tools/illink/*
- global.json

- subset: tools_cdacreader
include:
- src/native/managed/cdacreader/*

- subset: installer
include:
exclude:
Expand Down
3 changes: 2 additions & 1 deletion eng/pipelines/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -713,14 +713,15 @@ extends:
jobParameters:
timeoutInMinutes: 120
nameSuffix: CLR_Tools_Tests
buildArgs: -s clr.aot+clr.iltools+libs.sfx+clr.toolstests -c $(_BuildConfig) -test
buildArgs: -s clr.aot+clr.iltools+libs.sfx+clr.toolstests+tools.cdacreadertests -c $(_BuildConfig) -test
enablePublishTestResults: true
testResultsFormat: 'xunit'
# We want to run AOT tests when illink changes because there's share code and tests from illink which are used by AOT
condition: >-
or(
eq(stageDependencies.EvaluatePaths.evaluate_paths.outputs['SetPathVars_coreclr.containsChange'], true),
eq(stageDependencies.EvaluatePaths.evaluate_paths.outputs['SetPathVars_tools_illink.containsChange'], true),
eq(stageDependencies.EvaluatePaths.evaluate_paths.outputs['SetPathVars_tools_cdacreader.containsChange'], true),
eq(variables['isRollingBuild'], true))
#
# Build CrossDacs
Expand Down
327 changes: 327 additions & 0 deletions src/native/managed/cdacreader/src/ContractDescriptorParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// 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.Diagnostics.Contracts;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.Diagnostics.DataContractReader;

/// <summary>
/// A parser for the JSON representation of a contract descriptor.
/// </summary>
/// <remarks>
/// <see href="https://github.com/dotnet/runtime/blob/main/docs/design/datacontracts/data_descriptor.md">See design doc</see> for the format.
/// </remarks>
public partial class ContractDescriptorParser
{
// data_descriptor.md uses a distinguished property name to indicate the size of a type
public const string TypeDescriptorSizeSigil = "!";

/// <summary>
/// Parses the "compact" representation of a contract descriptor.
/// </summary>
public static ContractDescriptor? ParseCompact(ReadOnlySpan<byte> json)
{
return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor);
}

[JsonSerializable(typeof(ContractDescriptor))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Dictionary<string, int>))]
[JsonSerializable(typeof(Dictionary<string, TypeDescriptor>))]
[JsonSerializable(typeof(Dictionary<string, FieldDescriptor>))]
[JsonSerializable(typeof(Dictionary<string, GlobalDescriptor>))]
[JsonSerializable(typeof(TypeDescriptor))]
[JsonSerializable(typeof(FieldDescriptor))]
[JsonSerializable(typeof(GlobalDescriptor))]
[JsonSourceGenerationOptions(AllowTrailingCommas = true,
DictionaryKeyPolicy = JsonKnownNamingPolicy.Unspecified, // contracts, types and globals are case sensitive
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
ReadCommentHandling = JsonCommentHandling.Skip)]
internal sealed partial class ContractDescriptorContext : JsonSerializerContext
{
}

public class ContractDescriptor
{
public int? Version { get; set; }
public string? Baseline { get; set; }
public Dictionary<string, int>? Contracts { get; set; }

public Dictionary<string, TypeDescriptor>? Types { get; set; }

public Dictionary<string, GlobalDescriptor>? Globals { get; set; }

[JsonExtensionData]
public Dictionary<string, object?>? Extras { get; set; }
}

[JsonConverter(typeof(TypeDescriptorConverter))]
public class TypeDescriptor
{
public uint? Size { get; set; }
public Dictionary<string, FieldDescriptor>? Fields { get; set; }
}

[JsonConverter(typeof(FieldDescriptorConverter))]
public class FieldDescriptor
{
public string? Type { get; set; }
public int Offset { get; set; }
}

[JsonConverter(typeof(GlobalDescriptorConverter))]
public class GlobalDescriptor
{
public string? Type { get; set; }
public ulong Value { get; set; }
public bool Indirect { get; set; }
}

internal sealed class TypeDescriptorConverter : JsonConverter<TypeDescriptor>
{
// Almost a normal dictionary converter except:
// 1. looks for a special key "!" to set the Size property
// 2. field names are property names, but treated case-sensitively
public override TypeDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
uint? size = null;
Dictionary<string, FieldDescriptor>? fields = new();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.EndObject:
return new TypeDescriptor { Size = size, Fields = fields };
case JsonTokenType.PropertyName:
string? fieldNameOrSizeSigil = reader.GetString();
reader.Read(); // read the next value: either a number or a field descriptor
if (fieldNameOrSizeSigil == TypeDescriptorSizeSigil)
{
uint newSize = reader.GetUInt32();
if (size is not null)
{
throw new JsonException($"Size specified multiple times: {size} and {newSize}");
}
size = newSize;
}
else
{
string? fieldName = fieldNameOrSizeSigil;
var field = JsonSerializer.Deserialize(ref reader, ContractDescriptorContext.Default.FieldDescriptor);
if (fieldName is null || field is null)
throw new JsonException();
if (!fields.TryAdd(fieldName, field))
{
throw new JsonException($"Duplicate field name: {fieldName}");
}
}
break;
case JsonTokenType.Comment:
// unexpected - we specified to skip comments. but let's ignore anyway
break;
default:
throw new JsonException();
}
}
throw new JsonException();
}

public override void Write(Utf8JsonWriter writer, TypeDescriptor value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

internal sealed class FieldDescriptorConverter : JsonConverter<FieldDescriptor>
{
// Compact Field descriptors are either a number or a two element array
// 1. number - no type, offset is given as the number
// 2. [number, string] - offset is given as the number, type name is given as the string
public override FieldDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (TryGetInt32FromToken(ref reader, out int offset))
return new FieldDescriptor { Offset = offset };
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException();
reader.Read();
// [number, string]
// ^ we're here
if (!TryGetInt32FromToken(ref reader, out offset))
throw new JsonException();
reader.Read(); // string
if (reader.TokenType != JsonTokenType.String)
throw new JsonException();
string? type = reader.GetString();
reader.Read(); // end of array
if (reader.TokenType != JsonTokenType.EndArray)
throw new JsonException();
return new FieldDescriptor { Type = type, Offset = offset };
}

public override void Write(Utf8JsonWriter writer, FieldDescriptor value, JsonSerializerOptions options)
{
throw new JsonException();
}
}

internal sealed class GlobalDescriptorConverter : JsonConverter<GlobalDescriptor>
{
public override GlobalDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// four cases:
// 1. number - no type, direct value, given value
// 2. [number] - no type, indirect value, given aux data ptr
// 3. [number, string] - type, direct value, given value
// 4. [[number], string] - type, indirect value, given aux data ptr

// Case 1: number
if (TryGetUInt64FromToken(ref reader, out ulong valueCase1))
return new GlobalDescriptor { Value = valueCase1 };
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException();
reader.Read();
// we're in case 2 or 3 or 4:
// case 2: [number]
// ^ we're here
// case 3: [number, string]
// ^ we're here
// case 4: [[number], string]
// ^ we're here
if (TryGetUInt64FromToken(ref reader, out ulong valueCase2or3))
{
// case 2 or 3
// case 2: [number]
// ^ we're here
// case 3: [number, string]
// ^ we're here
reader.Read(); // end of array (case 2) or string (case 3)
if (reader.TokenType == JsonTokenType.EndArray) // it was case 2
{
return new GlobalDescriptor { Value = valueCase2or3, Indirect = true };
}
if (reader.TokenType == JsonTokenType.String) // it was case 3
{
string? type = reader.GetString();
reader.Read(); // end of array for case 3
if (reader.TokenType != JsonTokenType.EndArray)
throw new JsonException();
return new GlobalDescriptor { Type = type, Value = valueCase2or3 };
}
throw new JsonException();
}
if (reader.TokenType == JsonTokenType.StartArray)
{
// case 4: [[number], string]
// ^ we're here
reader.Read(); // number
if (!TryGetUInt64FromToken(ref reader, out ulong value))
throw new JsonException();
reader.Read(); // end of inner array
if (reader.TokenType != JsonTokenType.EndArray)
throw new JsonException();
reader.Read(); // string
if (reader.TokenType != JsonTokenType.String)
throw new JsonException();
string? type = reader.GetString();
reader.Read(); // end of outer array
if (reader.TokenType != JsonTokenType.EndArray)
throw new JsonException();
return new GlobalDescriptor { Type = type, Value = value, Indirect = true };
}
throw new JsonException();
}

public override void Write(Utf8JsonWriter writer, GlobalDescriptor value, JsonSerializerOptions options)
{
throw new JsonException();
}
}

// Somewhat flexible parsing of numbers, allowing json number tokens or strings as decimal or hex, possibly negatated.
private static bool TryGetUInt64FromToken(ref Utf8JsonReader reader, out ulong value)
{
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetUInt64(out value))
return true;
if (reader.TryGetInt64(out long signedValue))
{
value = (ulong)signedValue;
return true;
}
}
if (reader.TokenType == JsonTokenType.String)
{
var s = reader.GetString();
if (s == null)
{
value = 0u;
return false;
}
if (ulong.TryParse(s, out value))
return true;
if (long.TryParse(s, out long signedValue))
{
value = (ulong)signedValue;
return true;
}
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) &&
ulong.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value))
{
return true;
}
if (s.StartsWith("-0x", StringComparison.OrdinalIgnoreCase) &&
ulong.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out ulong negValue))
{
value = ~negValue + 1; // two's complement
return true;
}
}
value = 0;
return false;
}

// Somewhat flexible parsing of numbers, allowing json number tokens or strings as either decimal or hex, possibly negated
private static bool TryGetInt32FromToken(ref Utf8JsonReader reader, out int value)
{
if (reader.TokenType == JsonTokenType.Number)
{
value = reader.GetInt32();
return true;
}
if (reader.TokenType == JsonTokenType.String)
{
var s = reader.GetString();
if (s == null)
{
value = 0;
return false;
}
if (int.TryParse(s, out value))
{
return true;
}
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out value))
{
return true;
}
if (s.StartsWith("-0x", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(s.AsSpan(3), System.Globalization.NumberStyles.HexNumber, null, out int negValue))
{
value = -negValue;
return true;
}
}
value = 0;
return false;
}
}
Loading

0 comments on commit eb344fe

Please sign in to comment.