diff --git a/eng/Subsets.props b/eng/Subsets.props index 499e9c3cb645f..9de503dd5002f 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -173,6 +173,8 @@ + + @@ -369,6 +371,10 @@ Test="true" Category="clr" Condition="'$(DotNetBuildSourceOnly)' != 'true' and '$(NativeAotSupported)' == 'true'"/> + + + + diff --git a/eng/pipelines/common/evaluate-default-paths.yml b/eng/pipelines/common/evaluate-default-paths.yml index d954a1ddacbb5..975c18eb69d46 100644 --- a/eng/pipelines/common/evaluate-default-paths.yml +++ b/eng/pipelines/common/evaluate-default-paths.yml @@ -164,6 +164,10 @@ jobs: - src/tools/illink/* - global.json + - subset: tools_cdacreader + include: + - src/native/managed/cdacreader/* + - subset: installer include: exclude: diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index b4ed2afd252d2..f0591ff810c37 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -713,7 +713,7 @@ 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 @@ -721,6 +721,7 @@ extends: 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 diff --git a/src/native/managed/cdacreader/src/ContractDescriptorParser.cs b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs new file mode 100644 index 0000000000000..fbf76cd4e8d43 --- /dev/null +++ b/src/native/managed/cdacreader/src/ContractDescriptorParser.cs @@ -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; + +/// +/// A parser for the JSON representation of a contract descriptor. +/// +/// +/// See design doc for the format. +/// +public partial class ContractDescriptorParser +{ + // data_descriptor.md uses a distinguished property name to indicate the size of a type + public const string TypeDescriptorSizeSigil = "!"; + + /// + /// Parses the "compact" representation of a contract descriptor. + /// + public static ContractDescriptor? ParseCompact(ReadOnlySpan json) + { + return JsonSerializer.Deserialize(json, ContractDescriptorContext.Default.ContractDescriptor); + } + + [JsonSerializable(typeof(ContractDescriptor))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [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? Contracts { get; set; } + + public Dictionary? Types { get; set; } + + public Dictionary? Globals { get; set; } + + [JsonExtensionData] + public Dictionary? Extras { get; set; } + } + + [JsonConverter(typeof(TypeDescriptorConverter))] + public class TypeDescriptor + { + public uint? Size { get; set; } + public Dictionary? 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 + { + // 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? 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 + { + // 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 + { + 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; + } +} diff --git a/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs new file mode 100644 index 0000000000000..eca740870727b --- /dev/null +++ b/src/native/managed/cdacreader/tests/ContractDescriptorParserTests.cs @@ -0,0 +1,213 @@ +// 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.Text.Unicode; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.UnitTests; + +public class ContractDescriptorParserTests +{ + [Fact] + public void ParsesEmptyContract() + { + ReadOnlySpan json = "{}"u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Null(descriptor.Version); + Assert.Null(descriptor.Baseline); + Assert.Null(descriptor.Contracts); + Assert.Null(descriptor.Types); + Assert.Null(descriptor.Extras); + } + [Fact] + public void ParsesTrivialContract() + { + ReadOnlySpan json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": {}, + "globals": {} + } + """u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Types); + Assert.Empty(descriptor.Globals); + Assert.Null(descriptor.Extras); + } + + [Fact] + public void ParseSizedTypes() + { + ReadOnlySpan json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": + { + "pointer": { "!" : 8}, + "int": { "!" : 4}, + "Point": { + "x": [ 4, "int"], + "y": 8, + "!": 12 + }, + "Point3D": { // no size + "r": [ 0, "double"], + "phi": 8, + "rho": 16 + } + }, + "globals": {} + } + """u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Globals); + Assert.Equal(4, descriptor.Types.Count); + Assert.Equal(8u, descriptor.Types["pointer"].Size); + Assert.Equal(4u, descriptor.Types["int"].Size); + Assert.Equal(2, descriptor.Types["Point"].Fields.Count); + Assert.Equal(4, descriptor.Types["Point"].Fields["x"].Offset); + Assert.Equal(8, descriptor.Types["Point"].Fields["y"].Offset); + Assert.Equal("int", descriptor.Types["Point"].Fields["x"].Type); + Assert.Null(descriptor.Types["Point"].Fields["y"].Type); + Assert.Equal(12u, descriptor.Types["Point"].Size); + Assert.Equal(3, descriptor.Types["Point3D"].Fields.Count); + Assert.Equal(0, descriptor.Types["Point3D"].Fields["r"].Offset); + Assert.Equal(8, descriptor.Types["Point3D"].Fields["phi"].Offset); + Assert.Equal(16, descriptor.Types["Point3D"].Fields["rho"].Offset); + Assert.Equal("double", descriptor.Types["Point3D"].Fields["r"].Type); + Assert.Null(descriptor.Types["Point3D"].Fields["phi"].Type); + Assert.Null(descriptor.Types["Point3D"].Size); + } + + [Fact] + public void ParseContractsCaseSensitive() + { + ReadOnlySpan json = """ + { + "version": 0, + "baseline": "empty", + "contracts": { + "foo": 1, + "Foo": 2 + }, + "types": {}, + "globals": {} + } + """u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Equal(2, descriptor.Contracts.Count); + Assert.Equal(1, descriptor.Contracts["foo"]); + Assert.Equal(2, descriptor.Contracts["Foo"]); + } + + [Fact] + public void ParsesGlobals() + { + ReadOnlySpan json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": {}, + "globals": { + "globalInt": 1, + "globalPtr": [2], + "globalTypedInt": [3, "uint8"], + "globalTypedPtr": [[4], "uintptr"], + "globalHex": "0x1234", + "globalNegative": -2, + "globalStringyInt": "17", + "globalStringyNegative": "-2", + "globalNegativeHex": "-0xff", + "globalBigStringyInt": "0x123456789abcdef", + "globalStringyPtr": ["0x1234"], + "globalTypedStringyInt": ["0x1234", "int"], + "globalTypedStringyPtr": [["0x1234"], "int"] + } + } + """u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Types); + Assert.Equal(13, descriptor.Globals.Count); + Assert.Equal((ulong)1, descriptor.Globals["globalInt"].Value); + Assert.False(descriptor.Globals["globalInt"].Indirect); + Assert.Equal((ulong)2, descriptor.Globals["globalPtr"].Value); + Assert.True(descriptor.Globals["globalPtr"].Indirect); + Assert.Equal((ulong)3, descriptor.Globals["globalTypedInt"].Value); + Assert.False(descriptor.Globals["globalTypedInt"].Indirect); + Assert.Equal("uint8", descriptor.Globals["globalTypedInt"].Type); + Assert.Equal((ulong)4, descriptor.Globals["globalTypedPtr"].Value); + Assert.True(descriptor.Globals["globalTypedPtr"].Indirect); + Assert.Equal("uintptr", descriptor.Globals["globalTypedPtr"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalHex"].Value); + Assert.False(descriptor.Globals["globalHex"].Indirect); + Assert.Equal((ulong)0xfffffffffffffffe, descriptor.Globals["globalNegative"].Value); + Assert.False(descriptor.Globals["globalNegative"].Indirect); + Assert.Equal((ulong)17, descriptor.Globals["globalStringyInt"].Value); + Assert.False(descriptor.Globals["globalStringyInt"].Indirect); + Assert.Equal((ulong)0xfffffffffffffffe, descriptor.Globals["globalStringyNegative"].Value); + Assert.False(descriptor.Globals["globalStringyNegative"].Indirect); + Assert.Equal((ulong)0xffffffffffffff01, descriptor.Globals["globalNegativeHex"].Value); + Assert.False(descriptor.Globals["globalNegativeHex"].Indirect); + Assert.Equal((ulong)0x123456789abcdef, descriptor.Globals["globalBigStringyInt"].Value); + Assert.False(descriptor.Globals["globalBigStringyInt"].Indirect); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalStringyPtr"].Value); + Assert.True(descriptor.Globals["globalStringyPtr"].Indirect); + Assert.Equal("int", descriptor.Globals["globalTypedStringyInt"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalTypedStringyInt"].Value); + Assert.False(descriptor.Globals["globalTypedStringyInt"].Indirect); + Assert.Equal("int", descriptor.Globals["globalTypedStringyPtr"].Type); + Assert.Equal((ulong)0x1234, descriptor.Globals["globalTypedStringyPtr"].Value); + Assert.True(descriptor.Globals["globalTypedStringyPtr"].Indirect); + } + + [Fact] + void ParsesExoticOffsets() + { + ReadOnlySpan json = """ + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": { + "OddStruct": { + "a": -12, + "b": "0x12", + "c": "-0x12", + "d": ["0x100", "int"] + } + }, + "globals": { + } + } + """u8; + ContractDescriptorParser.ContractDescriptor descriptor = ContractDescriptorParser.ParseCompact(json); + Assert.Equal(0, descriptor.Version); + Assert.Equal("empty", descriptor.Baseline); + Assert.Empty(descriptor.Contracts); + Assert.Empty(descriptor.Globals); + Assert.Equal(1, descriptor.Types.Count); + Assert.Equal(4, descriptor.Types["OddStruct"].Fields.Count); + Assert.Equal(-12, descriptor.Types["OddStruct"].Fields["a"].Offset); + Assert.Equal(0x12, descriptor.Types["OddStruct"].Fields["b"].Offset); + Assert.Equal(-0x12, descriptor.Types["OddStruct"].Fields["c"].Offset); + Assert.Equal(0x100, descriptor.Types["OddStruct"].Fields["d"].Offset); + Assert.Equal("int", descriptor.Types["OddStruct"].Fields["d"].Type); + } +} diff --git a/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj new file mode 100644 index 0000000000000..22e8d256e01df --- /dev/null +++ b/src/native/managed/cdacreader/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -0,0 +1,15 @@ + + + true + $(NetCoreAppToolCurrent) + + + + + + + + + + diff --git a/src/native/managed/compile-native.proj b/src/native/managed/compile-native.proj index d227466bfcebd..bcda8c5d6b57b 100644 --- a/src/native/managed/compile-native.proj +++ b/src/native/managed/compile-native.proj @@ -35,24 +35,17 @@ --gcc-toolchain=$(ROOTFS_DIR)/usr - - - - - - - + + - - @(SubprojectProps->'%(Identity)=%(Value)', ';') - - + + + + + + + + + + + + @(SubprojectProps->'%(Identity)=%(Value)', ';') + +