Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
378 changes: 378 additions & 0 deletions docs/design/datacontracts/contract_csharp_api_design.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
namespace DataContracts

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.

Is the proposal here to actually provide a stable public API like this? I thought the C# API was just providing a theoretical API against which to write up algorithm contracts, but this iteration makes it seem like it is being proposed as an actual API here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I find it a lot easier to think about things in more concrete fashion. This isn't really a public api, and if its actually what we write the contracts (and possibly implementation of contracts against) thats ok, but it isn't intended to actually be shipped as publicly useable outside of writing the data contracts. As well, the "apis" in the contracts are not public shipped artifacts either. Those are intended to be used to implement the actual public apis like ISOSDacInterface which are public, and useable by customers/partners.

At some point I could see this all turning into a public api once we've used it for a few years, but that's far enough down the line that I don't want to worry too much about it.

{

// Indicate that this type is a DataContractType which should have the DataContractTypeSourceGenerator applied to it
// Also that any types nested in this type with the DataContractLayout define particular versioned layouts for data structures
class DataContractTypeAttribute : System.Attribute {}


// Defined on each specific data layout, the fields of the type are defined by the fields of the class
class DataContractLayoutAttribute : System.Attribute
{
public DataContractLayoutAttribute(uint version) { Version = version; }
public uint Version;
}

// Defined on the class that contains global fields for a contract. The name and version are used to identify the contract
class DataContractGlobalsAttribute : System.Attribute
{
public DataContractGlobalsAttribute(string name, uint version) { Name = name; Version = version; }
public string Name;
public uint Version;
}

// Defined on the class that contains an algorithmic contract. The version, and base type of the associated type are used to identify the contract,
// there must exist a constructor of the type with the following signature (DataContracts.Target target, uint contractVersion)
class DataContractAlgorithmAttribute : System.Attribute
{
public DataContractAlgorithmAttribute(params uint []version) { Name = name; Version = version; }
public uint[] Version;
}

struct TargetPointer
{
public ulong Value;
// Add a full set of operators to support pointer arithmetic
}

struct TargetNInt
{
public long Value;
// Add a full set of operators to support arithmetic as well as casting to/from TargetPointer
}

struct TargetNUInt
{
public ulong Value;
// Add a full set of operators to support arithmetic as well as casting to/from TargetPointer
}

enum FieldType
{
Int8Type,
UInt8Type,
Int16Type,
UInt16Type,
Int32Type,
UInt32Type,
Int64Type,
UInt64Type,
NIntType,
NUIntType,
PointerType,

// Other values are dynamically assigned by the type definition rules
}

struct FieldLayout
{
public int Offset;
public FieldType Type;
}

interface IAlgorithmContract
{
void Init();
}

interface IContract
{
string Name { get; }
uint Version { get; }
}
class Target
{
// Users of the data contract may adjust this number to force re-reading of all data
public int CurrentEpoch = 0;

sbyte ReadInt8(TargetPointer pointer);
byte ReadUInt8(TargetPointer pointer);
short ReadInt16(TargetPointer pointer);
ushort ReadUInt16(TargetPointer pointer);
int ReadInt32(TargetPointer pointer);
uint ReadUInt32(TargetPointer pointer);
long ReadInt64(TargetPointer pointer);
ulong ReadUInt64(TargetPointer pointer);
TargetPointer ReadTargetPointer(TargetPointer pointer);
TargetNInt ReadNInt(TargetPointer pointer);
TargetNUInt ReadNUint(TargetPointer pointer);
byte[] ReadByteArray(TargetPointer pointer, ulong size);
void FillByteArray(TargetPointer pointer, byte[] array, ulong size);

bool TryReadInt8(TargetPointer pointer, out sbyte value);
bool TryReadUInt8(TargetPointer pointer, out byte value);
bool TryReadInt16(TargetPointer pointer, out short value);
bool TryReadUInt16(TargetPointer pointer, out ushort value);
bool TryReadInt32(TargetPointer pointer, out int value);
bool TryReadUInt32(TargetPointer pointer, out uint value);
bool TryReadInt64(TargetPointer pointer, out long value);
bool TryReadUInt64(TargetPointer pointer, out ulong value);
bool TryReadTargetPointer(TargetPointer pointer, out TargetPointer value);
bool TryReadNInt(TargetPointer pointer, out TargetNInt value);
bool TryReadNUInt(TargetPointer pointer, out TargetNUInt value);
bool TryReadByteArray(TargetPointer pointer, ulong size, out byte[] value);
bool TryFillByteArray(TargetPointer pointer, byte[] array, ulong size);

// If pointer is 0, then the return value will be 0
TargetPointer GetTargetPointerForField(TargetPointer pointer, FieldLayout fieldLayout);

sbyte ReadGlobalInt8(string globalName);
byte ReadGlobalUInt8(string globalName);
short ReadGlobalInt16(string globalName);
ushort ReadGlobalUInt16(string globalName);
int ReadGlobalInt32(string globalName);
uint ReadGlobalUInt32(string globalName);
long ReadGlobalInt64(string globalName);
ulong ReadGlobalUInt64(string globalName);
TargetPointer ReadGlobalTargetPointer(string globalName);

bool TryReadGlobalInt8(string globalName, out sbyte value);
bool TryReadGlobalUInt8(string globalName, out byte value);
bool TryReadGlobalInt16(string globalName, out short value);
bool TryReadGlobalUInt16(string globalName, out ushort value);
bool TryReadGlobalInt32(string globalName, out int value);
bool TryReadGlobalUInt32(string globalName, out uint value);
bool TryReadGlobalInt64(string globalName, out long value);
bool TryReadGlobalUInt64(string globalName, out ulong value);
bool TryReadGlobalTargetPointer(string globalName, out TargetPointer value);

Contracts Contract { get; }

partial class Contracts
{
FieldLayout GetFieldLayout(string typeName, string fieldName);
bool TryGetFieldLayout(string typeName, string fieldName, out FieldLayout layout);

object GetContract(string contractName);
bool TryGetContract(string contractName, out object contract);

// Every contract that is defined has a field here. As an example this document defines a MethodTableContract
// If the contract is not supported by the runtime in use, then the implementation of the contract will be the base type which
// is defined to throw if it is ever used.

// List of contracts will be inserted here by source generator
MethodTableContract MethodTableContract;
}
}

// Types defined by contracts live here
namespace ContractDefinitions
{
class CompositeContract
{
List<Tuple<string, uint>> Subcontracts;
}

class DataStructureContract
{
string MethodTableName {get;}
List<Tuple<string, FieldLayout>> FieldData;
}

// Insert Algorithmic Contract definitions here
class MethodTableContract
{
public virtual int DynamicTypeID(TargetPointer methodTablePointer) { throw new NotImplementedException(); }
public virtual int BaseSize(TargetPointer methodTablePointer) { throw new NotImplementedException(); }
}
}

namespace ContractImplementation
{
// Get contract from the predefined contract database
static class PredefinedContracts
{
public static IContract GetContract(string name, uint version, Target target)
{
// Do some lookup and allocate an instance of the contract requested
//
// This lookup can either be reflection based, or we can do it based on a source generator.
}
}

[DataContractGlobals("FeatureFlags", 1)]
public class FeatureFlags_1
{
public const int FeatureComInterop = 0;
}

[DataContractGlobals("FeatureFlags", 2)]
public class FeatureFlags_2
{
public const int FeatureComInterop = 1;
}

[DataContractAlgorithm(1)]
class MethodTableContract_1 : ContractDefinitions.MethodTableContract, IAlgorithmContract

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'm not sure why some contract info is here and other contract info is in markdown files. It seems like there are two different patterns in use. Given a choice between the two I think the markdown approach is easier to read and understand.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm... I think this is a bit misunderstood... the concept of a "MehtodTableContract_1" was intended as an example of what the final code would be. I really ought to put a comment here.

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.

Yeah, all the other content here appeared to be examples of how we'd document contracts so that was my default interpretation of the content here too. Since this is intended to be something else we could just put some comments at the top of the file saying so.

{
DataContracts.Target Target;
readonly uint ContractVersion;
public MethodTableContract_1(DataContracts.Target target, uint contractVersion) { Target = target; ContractVersion = contractVersion; }

public virtual int DynamicTypeID(TargetPointer methodTablePointer) { return new MethodTable(_target, methodTablePointer).dynamicTypeId; }
public virtual int BaseSize(TargetPointer methodTablePointer) { return new MethodTable(_target, methodTablePointer).baseSizeAndFlags & 0x3FFFFFFF; }
}

// This is used for version 2 and 3 of the contract, where the dynamic type id is no longer present, and baseSize has a new limitation in that it can only be a value up to 0x1FFFFFFF in v3
[DataContractAlgorithm(2, 3)]
class MethodTableContract_2 : ContractDefinitions.MethodTableContract, IAlgorithmContract
{
DataContracts.Target Target;
readonly uint ContractVersion;
public MethodTableContract_2(DataContracts.Target target, uint contractVersion) { Target = target; }

public virtual int DynamicTypeID(TargetPointer methodTablePointer)
{
throw new NotImplementedException();
}
public virtual int BaseSize(TargetPointer methodTablePointer)
{
return new MethodTable(_target, methodTablePointer).baseSizeAndFlags & ((ContractVersion == 3) ? 0x1FFFFFFF : 0x3FFFFFFF);
}
}

// We use a source generator to generate the actual runtime properties, and api for working with the fields on this type.
//
// The source generator would fill in most of the apis, and provide a bunch of properties that give a granular failure model where if a particular field isn't defined, it fails at the access point
// This example shows access to a type.
[DataContractType]
partial struct MethodTable
{
partial void Get_dynamicTypeId_optional(ref int value);
partial void Get_baseSizeAndFlags(ref int value);

[DataContractLayout(1)]
public class DataLayout1

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.

It would be nice if the type definitions are inline or linked from the contract markdown files that use them.

{
[FieldOffset(0)]
public int dynamicTypeId;
[FieldOffset(4)]
public int baseSize;
}
[DataContractLayout(2)]
public class DataLayout2
{
[FieldOffset(0)]
public int baseSize;
}

// The rest of this is generated by a source generator

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.

This auto-generated code doesn't seem needed for documentation. I think something like this would be a lot more concise and still convey all the info an implementation author needed:

MethodTable version 1

struct MethodTable
{
    int dynamicTypeId; // offset 0
    int baseSize;      // offset 4
}

MethodTable version 2

struct MethodTable
{
    int baseSize;      // offset 0
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed... but this C# file was intended to express how the contract would be possibly be realized in C#. So, I needed to describe what the source generator might do to effectively discuss the apis available. As a consequence, I wrote out a bunch of boilerplate. I agree this isn't very obvious, and needs to be described more fully.

void Get_dynamicTypeId_optional(ref int value)
{
value = dynamicTypeId;
}
void Get_baseSizeAndFlags(ref int value)
{
value = baseSizeAndFlags;
}

private static int LayoutIndex = DataContracts.Target.RegisterLayout(MethodTableLayout.GetLayoutByTarget);

public readonly TargetPointer Pointer;
private int _epoch;
private readonly MethodTableLayout _layout;

public MethodTable(DataContracts.Target target, TargetPointer pointer)
{
Pointer = pointer;
_epoch = Int32.MinInt;
_layout = target.GetLayoutByIndex(LayoutIndex);
}
class MethodTableLayout
{
public static object GetLayoutByTarget(DataContracts.Target target)
{
return new MethodTableLayout(target);
}

private MethodTableLayout(DataContracts.Target target)
{
Target = target;
if (!_target.Contract.TryGetFieldLayout("MethodTable", "dynamicTypeId", out var dynamicTypeIdField))
{
dynamicTypeId_Offset = -1;
}
else
{
if (dynamicTypeIdField.Type != FieldType.Int32Type)
dynamicTypeId_Offset = -2;
else
dynamicTypeId_Offset = dynamicTypeIdField.Offset;
}
if (!_target.Contract.TryGetFieldLayout("MethodTable", "baseSizeAndFlags", out var baseSizeAndFlagsField))
{
baseSizeAndFlags_Offset = -1;
}
else
{
if (baseSizeAndFlagsField.Type != FieldType.Int32Type)
baseSizeAndFlags_Offset = -2;
else
baseSizeAndFlags_Offset = baseSizeAndFlagsField.Offset;
}
}
public readonly DataContracts.Target Target;

int dynamicTypeId_Offset;
public int dynamicTypeId(TargetPointer pointer)
{
if (dynamicTypeId_Offset == -1)
{
throw new Exception("MethodTable has no field dynamicTypeId");
}
if (dynamicTypeId_Offset == -2)
{
throw new Exception("MethodTable field dynamicTypeId does not have type int32");
}
return _target.ReadInt32(pointer + dynamicTypeId_Offset);
}
public bool Has_dynamicTypeId => dynamicTypeId_Offset >= 0;

int baseSizeAndFlags_Offset;
public int baseSizeAndFlags(TargetPointer pointer)
{
if (baseSizeAndFlags_Offset == -1)
{
throw new Exception("MethodTable has no field baseSizeAndFlags");
}
if (baseSizeAndFlags_Offset == -2)
{
throw new Exception("MethodTable field baseSizeAndFlags does not have type int32");
}
return _target.ReadInt32(pointer + baseSizeAndFlags_Offset);
}
}

private int _dynamicTypeId;
public int dynamicTypeId
{
get
{
int currentEpoch = _layout.Target.CurrentEpoch;
if (_epoch != currentEpoch)
{
_dynamicTypeId = _layout.dynamicTypeId(Pointer);
_epoch = currentEpoch;
}
return _dynamicTypeId;
}
}
public bool Has_dynamicTypeId => layout.Has_dynamicTypeId;

private int _baseSizeAndFlags;
public int baseSizeAndFlags
{
get
{
int currentEpoch = _layout.Target.CurrentEpoch;
if (_epoch != currentEpoch)
{
_baseSizeAndFlags = _layout.baseSizeAndFlags(Pointer);
_epoch = currentEpoch;
}
return _baseSizeAndFlags;
}
}
}
}
}
Loading