Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cdac] CodeVersions contract and tests #109021

Merged
merged 15 commits into from
Oct 18, 2024
Merged
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
256 changes: 256 additions & 0 deletions docs/design/datacontracts/CodeVersions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Contract CodeVersions

This contract encapsulates support for [code versioning](../features/code-versioning.md) in the runtime.

## APIs of contract

```csharp
internal struct NativeCodeVersionHandle
{
// no public constructors
internal readonly TargetPointer MethodDescAddress;
internal readonly TargetPointer CodeVersionNodeAddress;
internal NativeCodeVersionHandle(TargetPointer methodDescAddress, TargetPointer codeVersionNodeAddress)
{
if (methodDescAddress != TargetPointer.Null && codeVersionNodeAddress != TargetPointer.Null)
{
throw new ArgumentException("Only one of methodDescAddress and codeVersionNodeAddress can be non-null");
}
MethodDescAddress = methodDescAddress;
CodeVersionNodeAddress = codeVersionNodeAddress;
}

internal static NativeCodeVersionHandle Invalid => new(TargetPointer.Null, TargetPointer.Null);
public bool Valid => MethodDescAddress != TargetPointer.Null || CodeVersionNodeAddress != TargetPointer.Null;
}
```

```csharp
// Return a handle to the version of the native code that includes the given instruction pointer
public virtual NativeCodeVersionHandle GetNativeCodeVersionForIP(TargetCodePointer ip);
// Return a handle to the active version of the native code for a given method descriptor
public virtual NativeCodeVersionHandle GetActiveNativeCodeVersion(TargetPointer methodDesc);

// returns true if the given method descriptor supports multiple code versions
public virtual bool CodeVersionManagerSupportsMethod(TargetPointer methodDesc);

// Return the instruction pointer corresponding to the start of the given native code version
public virtual TargetCodePointer GetNativeCode(NativeCodeVersionHandle codeVersionHandle);
```

## Version 1

See [code versioning](../features/code-versioning.md) for a general overview and the definitions of *synthetic* and *explicit* nodes.

Data descriptors used:
| Data Descriptor Name | Field | Meaning |
| --- | --- | --- |
| MethodDescVersioningState | Flags | `MethodDescVersioningStateFlags` flags, see below |
| MethodDescVersioningState | NativeCodeVersionNode | code version node of this method desc, if active |
| NativeCodeVersionNode | Next | pointer to the next native code version |
| NativeCodeVersionNode | MethodDesc | indicates a synthetic native code version node |
| NativeCodeVersionNode | NativeCode | indicates an explicit native code version node |
| ILCodeVersioningState | ActiveVersionKind | an `ILCodeVersionKind` value indicating which fields of the active version are value |
| ILCodeVersioningState | ActiveVersionNode | if the active version is explicit, the NativeCodeVersionNode for the active version |
| ILCodeVersioningState | ActiveVersionModule | if the active version is synthetic or unknown, the pointer to the Module that defines the method |
| ILCodeVersioningState | ActiveVersionMethodDef | if the active version is synthetic or unknown, the MethodDef token for the method |

The flag indicates that the default version of the code for a method desc is active:
```csharp
internal enum MethodDescVersioningStateFlags : byte
{
IsDefaultVersionActiveChildFlag = 0x4
};
```

The value of the `ILCodeVersioningState::ActiveVersionKind` field is one of:
```csharp
private enum ILCodeVersionKind
{
Unknown = 0,
Explicit = 1, // means Node is set
Synthetic = 2, // means Module and Token are set
}
```

Global variables used: *none*

Contracts used:
| Contract Name |
| --- |
| ExecutionManager |
| Loader |
| RuntimeTypeSystem |

### Finding the start of a specific native code version

```csharp
NativeCodeVersionHandle ICodeVersions.GetNativeCodeVersionForIP(TargetCodePointer ip)
{
Contracts.IExecutionManager executionManager = _target.Contracts.ExecutionManager;
EECodeInfoHandle? info = executionManager.GetEECodeInfoHandle(ip);
if (!info.HasValue)
{
return NativeCodeVersionHandle.Invalid;
}
TargetPointer methodDescAddress = executionManager.GetMethodDesc(info.Value);
if (methodDescAddress == TargetPointer.Null)
{
return NativeCodeVersionHandle.Invalid;
}
IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
MethodDescHandle md = rts.GetMethodDescHandle(methodDescAddress);
if (!rts.IsVersionable(md))
{
return new NativeCodeVersionHandle(methodDescAddress, codeVersionNodeAddress: TargetPointer.Null);
}
else
{
TargetCodePointer startAddress = executionManager.GetStartAddress(info.Value);
return GetSpecificNativeCodeVersion(md, startAddress);
}
}

NativeCodeVersionHandle GetSpecificNativeCodeVersion(MethodDescHandle md, TargetCodePointer startAddress)
{
TargetPointer methodDescVersioningStateAddress = target.Contracts.RuntimeTypeSystem.GetMethodDescVersioningState(md);
if (methodDescVersioningStateAddress == TargetPointer.Null)
{
return NativeCodeVersionHandle.Invalid;
}
Data.MethodDescVersioningState methodDescVersioningStateData = _target.ProcessedData.GetOrAdd<Data.MethodDescVersioningState>(methodDescVersioningStateAddress);
return FindFirstCodeVersion(methodDescVersioningStateData, (codeVersion) =>
{
return codeVersion.MethodDesc == md.Address && codeVersion.NativeCode == startAddress;
});
}

NativeCodeVersionHandle FindFirstCodeVersion(Data.MethodDescVersioningState versioningState, Func<Data.NativeCodeVersionNode, bool> predicate)
{
TargetPointer currentAddress = versioningState.NativeCodeVersionNode;
while (currentAddress != TargetPointer.Null)
{
Data.NativeCodeVersionNode current = _target.ProcessedData.GetOrAdd<Data.NativeCodeVersionNode>(currentAddress);
if (predicate(current))
{
return new NativeCodeVersionHandle(methodDescAddress: TargetPointer.Null, currentAddress);
}
currentAddress = current.Next;
}
return NativeCodeVersionHandle.Invalid;
}
```

### Finding the active native code version of a method descriptor

```csharp
NativeCodeVersionHandle ICodeVersions.GetActiveNativeCodeVersion(TargetPointer methodDesc)
{
IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
MethodDescHandle md = rts.GetMethodDescHandle(methodDesc);
TargetPointer mtAddr = rts.GetMethodTable(md);
TypeHandle typeHandle = rts.GetTypeHandle(mtAddr);
TargetPointer module = rts.GetModule(typeHandle);
uint methodDefToken = rts.GetMethodToken(md);
ILCodeVersionHandle methodDefActiveVersion = FindActiveILCodeVersion(module, methodDefToken);
if (!methodDefActiveVersion.IsValid)
{
return NativeCodeVersionHandle.Invalid;
}
return FindActiveNativeCodeVersion(methodDefActiveVersion, methodDesc);
}

ILCodeVersionHandle ILCodeVersionHandleFromState(Data.ILCodeVersioningState ilState)
{
switch ((ILCodeVersionKind)ilState.ActiveVersionKind)
{
case ILCodeVersionKind.Explicit:
return new ILCodeVersionHandle(module: TargetPointer.Null, methodDef: 0, ilState.ActiveVersionNode);
case ILCodeVersionKind.Synthetic:
case ILCodeVersionKind.Unknown:
return new ILCodeVersionHandle(ilState.ActiveVersionModule, ilState.ActiveVersionMethodDef, TargetPointer.Null);
default:
throw new InvalidOperationException($"Unknown ILCodeVersionKind {ilState.ActiveVersionKind}");
}
}

ILCodeVersionHandle FindActiveILCodeVersion(TargetPointer module, uint methodDefinition)
{
ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandle(module);
TargetPointer ilCodeVersionTable = _target.Contracts.Loader.GetLookupTables(moduleHandle).MethodDefToILCodeVersioningState;
TargetPointer ilVersionStateAddress = _target.Contracts.Loader.GetModuleLookupMapElement(ilCodeVersionTable, methodDefinition, out var _);
if (ilVersionStateAddress == TargetPointer.Null)
{
return new ILCodeVersionHandle(module, methodDefinition, TargetPointer.Null);
}
Data.ILCodeVersioningState ilState = _target.ProcessedData.GetOrAdd<Data.ILCodeVersioningState>(ilVersionStateAddress);
return ILCodeVersionHandleFromState(ilState);
}

bool IsActiveNativeCodeVersion(NativeCodeVersionHandle nativeCodeVersion)
{
if (nativeCodeVersion.MethodDescAddress != TargetPointer.Null)
{
MethodDescHandle md = _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(nativeCodeVersion.MethodDescAddress);
TargetPointer versioningStateAddress = _target.Contracts.RuntimeTypeSystem.GetMethodDescVersioningState(md);
if (versioningStateAddress == TargetPointer.Null)
{
return true;
}
Data.MethodDescVersioningState versioningState = _target.ProcessedData.GetOrAdd<Data.MethodDescVersioningState>(versioningStateAddress);
MethodDescVersioningStateFlags flags = (MethodDescVersioningStateFlags)versioningState.Flags;
return flags.HasFlag(MethodDescVersioningStateFlags.IsDefaultVersionActiveChildFlag);
}
else if (nativeCodeVersion.CodeVersionNodeAddress != TargetPointer.Null)
{
throw new NotImplementedException(); // TODO[cdac]: IsActiveNativeCodeVersion - explicit
}
else
{
throw new ArgumentException("Invalid NativeCodeVersionHandle");
}
}

NativeCodeVersionHandle FindActiveNativeCodeVersion(ILCodeVersionHandle methodDefActiveVersion, TargetPointer methodDescAddress)
{
if (methodDefActiveVersion.Module != TargetPointer.Null)
{
NativeCodeVersionHandle provisionalHandle = new NativeCodeVersionHandle(methodDescAddress: methodDescAddress, codeVersionNodeAddress: TargetPointer.Null);
if (IsActiveNativeCodeVersion(provisionalHandle))
{
return provisionalHandle;
}
else
{
throw new NotImplementedException(); // TODO[cdac]: iterate through versioning state nodes
}
}
else
{
throw new NotImplementedException(); // TODO: [cdac] find explicit il code version
}
}
```

### Determining whether a method descriptor supports code versioning

```csharp
bool ICodeVersions.CodeVersionManagerSupportsMethod(TargetPointer methodDescAddress)
{
IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
MethodDescHandle md = rts.GetMethodDescHandle(methodDescAddress);
if (rts.IsDynamicMethod(md))
return false;
if (rts.IsCollectibleMethod(md))
return false;
TargetPointer mtAddr = rts.GetMethodTable(md);
TypeHandle mt = rts.GetTypeHandle(mtAddr);
TargetPointer modAddr = rts.GetModule(mt);
ILoader loader = _target.Contracts.Loader;
ModuleHandle mod = loader.GetModuleHandle(modAddr);
ModuleFlags modFlags = loader.GetFlags(mod);
if (modFlags.HasFlag(ModuleFlags.EditAndContinue))
return false;
return true;
}
```
32 changes: 32 additions & 0 deletions docs/design/datacontracts/Loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ TargetPointer GetLoaderAllocator(ModuleHandle handle);
TargetPointer GetThunkHeap(ModuleHandle handle);
TargetPointer GetILBase(ModuleHandle handle);
ModuleLookupTables GetLookupTables(ModuleHandle handle);
TargetPointer GetModuleLookupMapElement(TargetPointer table, uint token, out TargetNUInt flags);
```

## Version 1
Expand All @@ -58,6 +59,9 @@ Data descriptors used:
| `Module` | `TypeDefToMethodTableMap` | Mapping table |
| `Module` | `TypeRefToMethodTableMap` | Mapping table |
| `ModuleLookupMap` | `TableData` | Start of the mapping table's data |
| `ModuleLookupMap` | `SupportedFlagsMask` | Mask for flag bits on lookup map entries |
| `ModuleLookupMap` | `Count` | Number of TargetPointer sized entries in this section of the map |
| `ModuleLookupMap` | `Next` | Pointer to next ModuleLookupMap segment for this map

``` csharp
ModuleHandle GetModuleHandle(TargetPointer modulePointer)
Expand Down Expand Up @@ -109,4 +113,32 @@ ModuleLookupTables GetLookupTables(ModuleHandle handle)
MethodDefToILCodeVersioningState: target.ReadPointer(handle.Address + /*
Module::MethodDefToILCodeVersioningState */));
}

TargetPointer GetModuleLookupMapElement(TargetPointer table, uint token, out TargetNUInt flags);
{
uint rid = /* get row id from token*/ (token);
flags = new TargetNUInt(0);
if (table == TargetPointer.Null)
return TargetPointer.Null;
uint index = rid;
// have to read lookupMap an extra time upfront because only the first map
// has valid supportedFlagsMask
TargetNUInt supportedFlagsMask = _target.ReadNUInt(table + /* ModuleLookupMap::SupportedFlagsMask */);
do
{
if (index < _target.Read<uint>(table + /*ModuleLookupMap::Count*/))
{
TargetPointer entryAddress = _target.ReadPointer(lookupMap + /*ModuleLookupMap::TableData*/) + (ulong)(index * _target.PointerSize);
TargetPointer rawValue = _target.ReadPointer(entryAddress);
flags = rawValue & supportedFlagsMask;
return rawValue & ~(supportedFlagsMask.Value);
}
else
{
table = _target.ReadPointer(lookupMap + /*ModuleLookupMap::Next*/);
index -= _target.Read<uint>(lookupMap + /*ModuleLookupMap::Count*/);
}
} while (table != TargetPointer.Null);
return TargetPointer.Null;
}
```
46 changes: 45 additions & 1 deletion docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ partial interface IRuntimeTypeSystem : IContract
// Return true if a MethodDesc represents an IL Stub dynamically generated by the runtime
// A IL Stub method is also a StoredSigMethodDesc, and a NoMetadataMethod
public virtual bool IsILStub(MethodDescHandle methodDesc);

// Return true if a MethodDesc is in a collectible module
public virtual bool IsCollectibleMethod(MethodDescHandle methodDesc);

// Return true if a MethodDesc supports mulitiple code versions
public virtual bool IsVersionable(MethodDescHandle methodDesc);

// Return a pointer to the IL versioning state of the MethodDesc
public virtual TargetPointer GetMethodDescVersioningState(MethodDescHandle methodDesc);

// Get an instruction pointer that can be called to cause the MethodDesc to be executed
public virtual TargetCodePointer GetNativeCode(MethodDescHandle methodDesc);

}
```

Expand Down Expand Up @@ -607,6 +620,7 @@ The version 1 `MethodDesc` APIs depend on the `MethodDescAlignment` global and t
| `MethodDescAlignment` | `MethodDescChunk` trailing data is allocated in multiples of this constant. The size (in bytes) of each `MethodDesc` (or subclass) instance is a multiple of this constant. |
| `MethodDescTokenRemainderBitCount` | Number of bits in the token remainder in `MethodDesc` |

**TODO** MethodDesc code pointers additions

In the runtime a `MethodDesc` implicitly belongs to a single `MethodDescChunk` and some common data is shared between method descriptors that belong to the same chunk. A single method table
will typically have multiple chunks. There are subkinds of MethodDescs at runtime of varying sizes (but the sizes must be mutliples of `MethodDescAlignment`) and each chunk contains method descriptors of the same size.
Expand All @@ -631,6 +645,15 @@ We depend on the following data descriptors:
| `StoredSigMethodDesc` | `ExtendedFlags` | Flags field for the `StoredSigMethodDesc` |
| `DynamicMethodDesc` | `MethodName` | Pointer to Null-terminated UTF8 string describing the Method desc |

**TODO** MethodDesc code pointers additions

The contract depends on the following other contracts

| Contract |
| --- |
| Loader |
| ReJIT |
| CodeVersions |

And the following enumeration definitions

Expand Down Expand Up @@ -821,4 +844,25 @@ And the various apis are implemented with the following algorithms
return ((DynamicMethodDescExtendedFlags)ExtendedFlags).HasFlag(DynamicMethodDescExtendedFlags.IsILStub);
}
```
**TODO(cdac)**

Determining if a method is in a collectible module:

```csharp
bool IRuntimeTypeSystem.IsCollectibleMethod(MethodDescHandle methodDesc) => // TODO[cdac]: finish this
```

Determining if a method supports multiple code versions:

```csharp
bool IRuntimeTypeSystem.IsVersionable(MethodDescHandle methodDesc) => // TODO[cdac]: finish this
```

Extracting a pointer to the `MethodDescVersioningState` data for a given method
```csharp
TargetPointer IRuntimeTypeSystem.GetMethodDescVersioningState(MethodDescHandle methodDesc) => // TODO[cdac]: finish this
```

Getting the native code pointer for methods with a NativeCodeSlot or a stable entry point
```csharp
public virtual TargetCodePointer GetNativeCode(MethodDescHandle methodDesc) => // TODO[cdac]: finish this
```
1 change: 1 addition & 0 deletions src/coreclr/debug/runtimeinfo/contracts.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// cdac-build-tool can take multiple "-c contract_file" arguments
// so to conditionally include contracts, put additional contracts in a separate file
{
"CodeVersions": 1,
"DacStreams": 1,
"EcmaMetadata" : 1,
"Exception": 1,
Expand Down
Loading
Loading