Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -2003,7 +2003,13 @@ Determining if a method supports multiple code versions:
if (md.IsEligibleForTieredCompilation)
return true;
// MethodDesc::IsEligibleForReJIT
if (_target.Contracts.ReJIT.IsEnabled())
// Targets without profiling support do not advertise ReJIT.
// An invalid advertised contract is still an error.
if (!_target.Contracts.TryGetContract(out IReJIT reJit))
{
return false;
}
if (reJit.IsEnabled())
{
if (!md.IsIL)
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,32 @@ public TContract GetContract<TContract>() where TContract : IContract
return contract;
}

/// <summary>
/// Attempts to get the requested contract.
/// </summary>
/// <typeparam name="TContract">The contract type to retrieve.</typeparam>
/// <param name="contract">
/// When this method returns, contains the contract if found; otherwise, its default value.
/// </param>
/// <returns>
/// <see langword="true"/> if the contract was retrieved; <see langword="false"/> if it is not
/// advertised and no default implementation is registered.
/// </returns>
/// <exception cref="ContractUnsupportedException">
/// The target advertises a contract version that this cDAC cannot provide.
/// </exception>
/// <remarks>
/// Contract-creation errors propagate. Use the two-output overload to inspect availability failures.
/// </remarks>
public bool TryGetContract<TContract>([NotNullWhen(true)] out TContract contract) where TContract : IContract
{
return TryGetContract(out contract, out _);
if (!TryGetContract(out contract, out System.Exception? failureException))
{
if (failureException is ContractMissingException)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect TryGetContract(out contract, out System.Exception? failureException) to always return false when the requested contract is missing. Is that not the case? If there is a bug, should it be fixed inside TryGetContract(out contract, out System.Exception? failureException) implementation instead?

return false;
throw failureException;
}
return true;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,11 @@ bool IRuntimeTypeSystem.IsVersionable(MethodDescHandle methodDesc)
if (md.IsEligibleForTieredCompilation)
return true;
// MethodDesc::IsEligibleForReJIT
if (_target.Contracts.ReJIT.IsEnabled())
if (!_target.Contracts.TryGetContract(out IReJIT reJit))
{
return false;
}
if (reJit.IsEnabled())
{
if (!md.IsIL)
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,16 @@ public static void Register(ContractRegistry registry)
}

/// <summary>
/// Eagerly validates that every contract required by the cDAC data-access interfaces can be
/// provided for the target. Contract availability is checked without instantiating the
/// contracts; <see cref="IRuntimeInfo"/> is read to determine the target operating system so
/// that OS-specific contracts are validated only when the target platform actually uses them.
/// In-box (main-descriptor) contracts are required unconditionally. Contracts published by a
/// sub-descriptor are version-checked always, but their absence is tolerated while their
/// sub-descriptor is still pending.
/// Validates the contracts required by the cDAC data-access interfaces without instantiating them,
/// except for <see cref="IRuntimeInfo"/>, which determines the target operating system.
/// ReJIT may be absent, but its advertised version must be supported. Sub-descriptor contracts
/// may be absent while their provider is pending.
/// </summary>
/// <param name="target">The target being validated (source of the contract registry and
/// sub-descriptor resolution state).</param>
/// <exception cref="ContractNotAvailableException">
/// Thrown for the first required contract that cannot be provided. The concrete exception type
/// and its <see cref="System.Exception.HResult"/> identify the failure:
/// A required contract is missing or a checked contract version is unsupported. The concrete
/// exception type and its <see cref="System.Exception.HResult"/> identify the failure:
/// <see cref="ContractMissingException"/> / <see cref="CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED"/>
/// if the target does not advertise a required contract,
/// <see cref="ContractUnrecognizedException"/> / <see cref="CdacHResults.CDAC_E_CONTRACT_UNRECOGNIZED"/>
Expand All @@ -105,11 +102,8 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null)
using Lock.Scope scope = apiLock is null ? default : apiLock.EnterScope();
ContractRegistry registry = target.Contracts;

// In-box (main-descriptor) contract accesses across the ISOSDac* and IXCLRData* surface that
// SOSDacImpl exposes. These live in the main descriptor, present as soon as the runtime module
// is loaded, so they are required eagerly and unconditionally - a genuinely-missing one is a
// serviceability failure even at early attach. IObjectiveCMarshal is intentionally omitted:
// SOS reaches it through TryGetContract so its absence degrades gracefully rather than faulting.
// Main-descriptor contracts are already published at early attach.
// IObjectiveCMarshal is omitted because its callers handle absence through TryGetContract.
Validate<IAuxiliarySymbols>(registry);
Validate<ICodeNotifications>(registry);
Validate<ICodeVersions>(registry);
Expand All @@ -125,7 +119,7 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null)
Validate<INotifications>(registry);
Validate<IObject>(registry);
Validate<IPrecodeStubs>(registry);
Validate<IReJIT>(registry);
Validate<IReJIT>(registry, allowMissing: true); // Not advertised without PROFILING_SUPPORTED.
Validate<IRuntimeInfo>(registry);
Validate<IRuntimeTypeSystem>(registry);
Validate<ISignature>(registry);
Expand Down Expand Up @@ -170,9 +164,10 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null)
// sub-descriptor is resolved. Defer and let the tool APIs see a degradation to E_NOTIMPL.
ValidateSubDescriptorContract<IGC>(target);

static void Validate<TContract>(ContractRegistry registry) where TContract : IContract
static void Validate<TContract>(ContractRegistry registry, bool allowMissing = false) where TContract : IContract
{
if (registry.TryValidate<TContract>(out System.Exception? failure))
if (registry.TryValidate<TContract>(out System.Exception? failure) ||
(allowMissing && failure is ContractMissingException))
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,40 @@ public void AdvertisedVersion_UsesVersionedRegistration_NotDefault(MockTarget.Ar
Assert.Equal("v1", contract.Tag);
}

public static IEnumerable<object[]> UnsupportedVersionData()
{
foreach (object[] data in new MockTarget.StdArch())
{
yield return [data[0], false];
yield return [data[0], true];
}
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void AdvertisedVersion_NoMatchingRegistration_DoesNotFallBackToDefault(MockTarget.Architecture arch)
[MemberData(nameof(UnsupportedVersionData))]
public void AdvertisedVersion_NoMatchingRegistration_DoesNotFallBackToDefault(MockTarget.Architecture arch, bool obsolete)
{
// Target advertises FakeContract (version "c1"), but only a default ("")
// registration exists. This is a version-skew failure and must NOT
// silently use the default registration.
ContractDescriptorTarget target = CreateTarget(
arch,
advertisedContracts: ["FakeContract"],
registerFake: static r => r.Register<IFakeContract>(string.Empty, static t => new FakeContract("default")));
registerFake: r =>
{
r.Register<IFakeContract>(string.Empty, static t => new FakeContract("default"));
if (obsolete)
r.RegisterUnsupported<IFakeContract>("c1");
});

Assert.False(target.Contracts.TryGetContract<IFakeContract>(out IFakeContract? contract));
Assert.False(target.Contracts.TryGetContract<IFakeContract>(out IFakeContract? contract, out System.Exception? failure));
Assert.Null(contract);
if (obsolete)
{
Assert.IsType<ContractObsoleteException>(failure);
Assert.Throws<ContractObsoleteException>(() => target.Contracts.TryGetContract<IFakeContract>(out _));
}
else
{
Assert.IsType<ContractUnrecognizedException>(failure);
Assert.Throws<ContractUnrecognizedException>(() => target.Contracts.TryGetContract<IFakeContract>(out _));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ public void TryGetContract_UnrecognizedVersion_ReturnsContractUnrecognizedExcept
Assert.Equal("unsupported-version", ex.ContractVersion);
}

// The contracts required by the data-access interfaces, advertised at the versions
// CoreCLRContracts registers. Mirrors CoreCLRContracts.ValidateForDataAccess.
// Contracts used by the data-access interfaces, including optional ReJIT.
// Versions match CoreCLRContracts.Register.
private static readonly IReadOnlyDictionary<string, string> s_requiredDataAccessContracts =
new Dictionary<string, string>
{
Expand Down Expand Up @@ -448,19 +448,19 @@ public void TryValidate_RegisteredVersion_ReturnsTrue(MockTarget.Architecture ar
[ClassData(typeof(MockTarget.StdArch))]
public void TryValidate_DoesNotInstantiateContract(MockTarget.Architecture arch)
{
// GCInfo's creator reads RuntimeInfo from target memory. Advertise GCInfo but omit RuntimeInfo:
// TryValidate must succeed (a creator is registered) without invoking it, whereas a real
// GetContract would chain into RuntimeInfo and fail.
TargetTestHelpers targetTestHelpers = new(arch);
ContractDescriptorBuilder builder = new(targetTestHelpers);
ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
// GCInfo's factory requires RuntimeInfo; presence-only validation must not invoke it.
descriptorBuilder.SetContracts(new Dictionary<string, string> { ["GCInfo"] = "c1" });

Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));

Assert.True(target.Contracts.TryValidate<Contracts.IGCInfo>(out System.Exception? failure));
Assert.Null(failure);
Assert.Throws<ContractMissingException>(() => target.Contracts.GCInfo);
ContractMissingException exception = Assert.Throws<ContractMissingException>(
() => target.Contracts.TryGetContract<Contracts.IGCInfo>(out _));
Assert.Equal("RuntimeInfo", exception.ContractName);
}

[Theory]
Expand Down Expand Up @@ -492,6 +492,48 @@ public void ValidateForDataAccess_AllRequiredPresent_DoesNotThrow(MockTarget.Arc
Contracts.CoreCLRContracts.ValidateForDataAccess(target);
}

public static IEnumerable<object?[]> ReJITValidationData()
{
foreach (object[] data in new MockTarget.StdArch())
{
yield return [data[0], null, null];
yield return [data[0], "c999", typeof(ContractUnrecognizedException)];
yield return [data[0], "obsolete-version", typeof(ContractObsoleteException)];
}
}

[Theory]
[MemberData(nameof(ReJITValidationData))]
public void ValidateForDataAccess_ReJITIsOptional(
MockTarget.Architecture arch, string? reJitVersion, Type? expectedExceptionType)
{
TargetTestHelpers targetTestHelpers = new(arch);
ContractDescriptorBuilder builder = new(targetTestHelpers);
ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
Dictionary<string, string> contracts = new(s_requiredDataAccessContracts);
contracts.Remove("ReJIT");
if (reJitVersion is not null)
contracts["ReJIT"] = reJitVersion;

descriptorBuilder.SetContracts(contracts);
Assert.True(builder.TryCreateTarget(
descriptorBuilder,
out ContractDescriptorTarget? target,
registry => registry.RegisterUnsupported<Contracts.IReJIT>("obsolete-version")));

if (expectedExceptionType is not null)
{
System.Exception exception = Assert.Throws(expectedExceptionType, () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
ContractUnsupportedException failure = Assert.IsAssignableFrom<ContractUnsupportedException>(exception);
Assert.Equal("ReJIT", failure.ContractName);
Assert.Equal(reJitVersion, failure.ContractVersion);
}
else
{
Contracts.CoreCLRContracts.ValidateForDataAccess(target);
}
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void ValidateForDataAccess_Net11Target_DoesNotRequireExternalMemoryHandles(MockTarget.Architecture arch)
Expand Down Expand Up @@ -547,7 +589,7 @@ public void ValidateForDataAccess_MissingRequiredContract_ThrowsNotAdvertised(Mo
ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
descriptorBuilder.SetContracts(
s_requiredDataAccessContracts
.Where(static pair => pair.Key != "RuntimeInfo")
.Where(static pair => pair.Key is not ("ReJIT" or "RuntimeInfo"))
.ToDictionary(static pair => pair.Key, static pair => pair.Value));

Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
Expand Down
85 changes: 84 additions & 1 deletion src/native/managed/cdac/tests/UnitTests/MethodDescTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,16 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract(
MockTarget.Architecture arch,
Action<MockDescriptors.MockMethodDescriptorsBuilder> configure,
Mock<IExecutionManager>? mockExecutionManager = null,
Mock<IPrecodeStubs>? mockPrecodeStubs = null)
Mock<IPrecodeStubs>? mockPrecodeStubs = null,
Action<TestPlaceholderTarget.Builder>? configureTarget = null)
{
var targetBuilder = new TestPlaceholderTarget.Builder(arch);
MockDescriptors.RuntimeTypeSystem rtsBuilder = new(targetBuilder.MemoryBuilder);
MockLoaderBuilder loaderBuilder = new(targetBuilder.MemoryBuilder);
MockDescriptors.MockMethodDescriptorsBuilder methodDescBuilder = new(rtsBuilder, loaderBuilder);

configure(methodDescBuilder);
configureTarget?.Invoke(targetBuilder);

mockExecutionManager ??= new Mock<IExecutionManager>();
mockPrecodeStubs ??= new Mock<IPrecodeStubs>();
Expand All @@ -101,6 +103,87 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract(
return target.Contracts.RuntimeTypeSystem;
}

private static (IRuntimeTypeSystem Contract, MethodDescHandle Method) CreateMethodForVersioning(
MockTarget.Architecture arch,
bool tiered,
Action<TestPlaceholderTarget.Builder> configureTarget)
{
TargetPointer address = TargetPointer.Null;
IRuntimeTypeSystem contract = CreateRuntimeTypeSystemContract(arch, builder =>
{
byte size = (byte)(builder.MethodDescLayout.Size / builder.MethodDescAlignment);
MockMethodDescChunk chunk = builder.AddMethodDescChunk("versioning", size);
chunk.MethodTable = builder.RTSBuilder.SystemObjectMethodTable.Address;
chunk.Size = size;
chunk.Count = 1;
MockMethodDesc method = chunk.GetMethodDescAtChunkIndex(0, builder.MethodDescLayout);
method.Flags3AndTokenRemainder = tiered
? (ushort)MethodDescFlags_1.MethodDescFlags3.IsEligibleForTieredCompilation
: (ushort)0;
address = new TargetPointer(method.Address);
}, configureTarget: configureTarget);
return (contract, contract.GetMethodDescHandle(address));
}

public static IEnumerable<object?[]> IsVersionableData()
{
foreach (object[] data in new MockTarget.StdArch())
{
MockTarget.Architecture arch = (MockTarget.Architecture)data[0];
yield return [arch, false, null, false, false];
yield return [arch, true, null, false, true];
yield return [arch, false, false, false, false];
yield return [arch, false, true, false, false];
yield return [arch, false, true, true, true];
yield return [arch, true, true, false, true];
}
}

[Theory]
[MemberData(nameof(IsVersionableData))]
public void IsVersionable_RespectsReJITAvailability(
MockTarget.Architecture arch, bool tiered, bool? reJitEnabled, bool supportsVersions, bool expected)
{
Mock<IReJIT> reJit = new(MockBehavior.Strict);
Mock<ICodeVersions> codeVersions = new(MockBehavior.Strict);
codeVersions.Setup(c => c.CodeVersionManagerSupportsMethod(It.IsAny<TargetPointer>())).Returns(supportsVersions);
if (reJitEnabled is bool enabled)
reJit.Setup(r => r.IsEnabled()).Returns(enabled);

(IRuntimeTypeSystem contract, MethodDescHandle method) = CreateMethodForVersioning(arch, tiered, builder =>
{
builder.AddMockContract(codeVersions);
if (reJitEnabled is not null)
builder.AddMockContract(reJit);
});

Assert.Equal(expected, contract.IsVersionable(method));
reJit.Verify(r => r.IsEnabled(), Times.Exactly(!tiered && reJitEnabled is not null ? 1 : 0));
codeVersions.Verify(c => c.CodeVersionManagerSupportsMethod(method.Address),
Times.Exactly(!tiered && reJitEnabled == true ? 1 : 0));
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void IsVersionable_AdvertisedReJITFailuresPropagate(MockTarget.Architecture arch)
{
(IRuntimeTypeSystem contract, MethodDescHandle method) = CreateMethodForVersioning(
arch, tiered: false, builder => builder.AddContract<IReJIT>("c999"));
Assert.Throws<ContractUnrecognizedException>(() => contract.IsVersionable(method));

// A partial dump can advertise ReJIT without capturing the profiler's data.
const ulong ProfilerControlBlockAddress = 0x3000_0000;
(contract, method) = CreateMethodForVersioning(
arch, tiered: false, builder => builder
.AddTypes(new Dictionary<DataType, Target.TypeInfo>
{
[DataType.ProfControlBlock] = TargetTestHelpers.CreateTypeInfo(MockProfControlBlock.CreateLayout(arch)),
})
.AddGlobals((nameof(Constants.Globals.ProfilerControlBlock), ProfilerControlBlockAddress))
.AddContract<IReJIT>("c1"));
Assert.Throws<VirtualReadException>(() => contract.IsVersionable(method));
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void GetMethodDescHandle_ILMethod_GetBasicData(MockTarget.Architecture arch)
Expand Down
Loading