Implement sizeof completely (#119)

This commit is contained in:
Patrick Stevens
2025-08-28 00:46:50 +01:00
committed by GitHub
parent 655ba4400a
commit 07fabfff65
15 changed files with 438 additions and 60 deletions

View File

@@ -33,6 +33,20 @@ type ConcreteType<'typeGeneric> =
_Generics : ImmutableArray<'typeGeneric>
}
override this.ToString () : string =
let basic = $"%s{this.Assembly.Name}.%s{this.Namespace}.%s{this.Name}"
let generics =
if this.Generics.IsEmpty then
""
else
this.Generics
|> Seq.map string
|> String.concat ", "
|> fun x -> "<" + x + ">"
basic + generics
member this.Assembly : AssemblyName = this._AssemblyName
member this.Definition : ComparableTypeDefinitionHandle = this._Definition
member this.Generics : ImmutableArray<'typeGeneric> = this._Generics

View File

@@ -57,11 +57,10 @@ module FieldInfo =
let fieldSig = def.DecodeSignature (TypeDefn.typeProvider assembly, ())
let declaringType = def.GetDeclaringType ()
let typeGenerics =
mr.GetTypeDefinition(declaringType).GetGenericParameters ()
|> GenericParameter.readAll mr
let decType = mr.GetTypeDefinition declaringType
let typeGenerics = decType.GetGenericParameters () |> GenericParameter.readAll mr
let declaringTypeNamespace = mr.GetString decType.Namespace
let declaringTypeName = mr.GetString decType.Name

View File

@@ -29,6 +29,10 @@ type InterfaceImplementation =
RelativeToAssembly : AssemblyName
}
type Layout =
| Default
| Custom of size : int * packingSize : int
/// <summary>
/// Represents detailed information about a type definition in a .NET assembly.
/// This is a strongly-typed representation of TypeDefinition from System.Reflection.Metadata.
@@ -93,6 +97,8 @@ type TypeInfo<'generic, 'fieldGeneric> =
Events : EventDefn ImmutableArray
ImplementedInterfaces : InterfaceImplementation ImmutableArray
Layout : Layout
}
member this.IsInterface = this.TypeAttributes.HasFlag TypeAttributes.Interface
@@ -213,6 +219,7 @@ module TypeInfo =
Generics = gen
Events = t.Events
ImplementedInterfaces = t.ImplementedInterfaces
Layout = t.Layout
}
let mapGeneric<'a, 'b, 'field> (f : 'a -> 'b) (t : TypeInfo<'a, 'field>) : TypeInfo<'b, 'field> =
@@ -308,6 +315,14 @@ module TypeInfo =
result.ToImmutable ()
let layout =
let l = typeDef.GetLayout ()
if l.IsDefault then
Layout.Default
else
Layout.Custom (size = l.Size, packingSize = l.PackingSize)
{
Namespace = ns
Name = name
@@ -323,6 +338,7 @@ module TypeInfo =
Events = events
ImplementedInterfaces = interfaces
DeclaringType = declaringType
Layout = layout
}
let isBaseType<'corelib>

View File

@@ -51,11 +51,6 @@ module TestPureCases =
ExpectedReturnCode = 114
NativeImpls = MockEnv.make ()
}
{
FileName = "Sizeof.cs"
ExpectedReturnCode = 0
NativeImpls = MockEnv.make ()
}
{
FileName = "LdtokenField.cs"
ExpectedReturnCode = 0
@@ -80,6 +75,16 @@ module TestPureCases =
ExpectedReturnCode = 1
NativeImpls = MockEnv.make ()
}
{
FileName = "Sizeof.cs"
ExpectedReturnCode = 0
NativeImpls = MockEnv.make ()
}
{
FileName = "Sizeof2.cs"
ExpectedReturnCode = 0
NativeImpls = MockEnv.make ()
}
{
FileName = "Initobj.cs"
ExpectedReturnCode = 0

View File

@@ -10,16 +10,16 @@ unsafe public class Program
public struct MediumStruct
{
public int Value1;
public int Value2;
public int MediumValue1;
public int MediumValue2;
}
public struct LargeStruct
{
public long Value1;
public long Value2;
public long Value3;
public long Value4;
public long LongValue1;
public long LongValue2;
public long LongValue3;
public long LongValue4;
}
public struct NestedStruct

View File

@@ -0,0 +1,235 @@
using System;
using System.Runtime.InteropServices;
unsafe public class Program
{
// Test for empty struct (should be 1 byte, not 0)
public struct EmptyStruct
{
}
// Test for char alignment (should align to 2, not 1)
public struct CharStruct
{
public byte B;
public char C; // Should be at offset 2, not 1
}
// Test for end padding
public struct NeedsEndPadding
{
public int X;
public byte Y;
// Should pad to 8 bytes total (multiple of 4)
}
// Test Pack=1 (no padding)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedStruct
{
public byte B;
public int I; // At offset 1, not 4
public byte B2;
// Total 6 bytes, no padding
}
// Test Pack=2
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct Pack2Struct
{
public byte B;
public int I; // At offset 2 (2-byte aligned, not 4)
public byte B2;
// Should pad to 8 bytes (multiple of 2)
}
// Test custom size smaller than natural size
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct CustomSizeSmaller
{
public long L1;
public long L2;
// Natural size is 16, but Size=12 is ignored (12 < 16)
}
// Test custom size larger than natural size
[StructLayout(LayoutKind.Sequential, Size = 20)]
public struct CustomSizeLarger
{
public long L;
// Natural size is 8, custom size 20 should win
}
// Test custom size not multiple of alignment
[StructLayout(LayoutKind.Sequential, Size = 15)]
public struct CustomSizeOdd
{
public long L;
// Size=15 should be honored even though not multiple of 8
}
// Test Pack=0 (means default, not 0)
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct Pack0Struct
{
public byte B;
public int I; // Should be at offset 4 (default packing)
}
// Test both Pack and Size
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 10)]
public struct PackAndSize
{
public byte B;
public int I;
// Natural packed size is 5, custom size 10 should win
}
// Test explicit with custom Size
[StructLayout(LayoutKind.Explicit, Size = 10)]
public struct ExplicitWithSize
{
[FieldOffset(0)]
public int I;
[FieldOffset(2)]
public short S;
// Max offset+size is 4, but Size=10 should win
}
public struct SmallStruct
{
public byte Value;
}
public struct MediumStruct
{
public int MediumValue1;
public int MediumValue2;
}
public struct LargeStruct
{
public long LongValue1;
public long LongValue2;
public long LongValue3;
public long LongValue4;
}
public struct NestedStruct
{
public SmallStruct Small;
public MediumStruct Medium;
public int Extra;
}
[StructLayout(LayoutKind.Explicit)]
public struct UnionStruct
{
[FieldOffset(0)]
public int AsInt;
[FieldOffset(0)]
public float AsFloat;
}
public static int Main(string[] args)
{
// Test 1: Basic primitive types
if (sizeof(byte) != 1) return 1;
if (sizeof(sbyte) != 1) return 2;
if (sizeof(short) != 2) return 3;
if (sizeof(ushort) != 2) return 4;
if (sizeof(int) != 4) return 5;
if (sizeof(uint) != 4) return 6;
if (sizeof(long) != 8) return 7;
if (sizeof(ulong) != 8) return 8;
if (sizeof(float) != 4) return 9;
if (sizeof(double) != 8) return 10;
if (sizeof(char) != 2) return 11;
if (sizeof(bool) != 1) return 12;
// Test 2: Struct sizes
if (sizeof(SmallStruct) != 1) return 13;
if (sizeof(MediumStruct) != 8) return 14;
if (sizeof(LargeStruct) != 32) return 15;
// Test 3: Nested struct size
// SmallStruct (1) + padding (3) + MediumStruct (8) + int (4) = 16
if (sizeof(NestedStruct) != 16) return 16;
// Test 4: Union struct size
if (sizeof(UnionStruct) != 4) return 17;
// Test 5: Enum size (underlying type is int)
if (sizeof(DayOfWeek) != 4) return 18;
// Test 6: Empty struct (should be 1, not 0)
if (sizeof(EmptyStruct) != 1) return 19;
// Test 7: Char alignment
// byte (1) + padding (1) + char (2) = 4
if (sizeof(CharStruct) != 4) return 20;
// Test 8: End padding
// int (4) + byte (1) + padding (3) = 8
if (sizeof(NeedsEndPadding) != 8) return 21;
// Test 9: Pack=1 removes all padding
// byte (1) + int (4) + byte (1) = 6
if (sizeof(PackedStruct) != 6) return 22;
// Test 10: Pack=2
// byte (1) + padding (1) + int (4) + byte (1) + padding (1) = 8
if (sizeof(Pack2Struct) != 8) return 23;
// Test 11: Custom size smaller than natural (ignored)
if (sizeof(CustomSizeSmaller) != 16) return 24;
// Test 12: Custom size larger than natural (honored)
if (sizeof(CustomSizeLarger) != 20) return 25;
// Test 13: Custom size not multiple of alignment (honored)
if (sizeof(CustomSizeOdd) != 15) return 26;
// Test 14: Pack=0 means default packing
// byte (1) + padding (3) + int (4) = 8
if (sizeof(Pack0Struct) != 8) return 27;
// Test 15: Pack and Size together
// Natural packed: byte (1) + int (4) = 5, but Size=10
if (sizeof(PackAndSize) != 10) return 28;
// Test 16: Explicit with Size
// Max used is 4, but Size=10
if (sizeof(ExplicitWithSize) != 10) return 29;
// Test 17: Pointer types
unsafe
{
if (sizeof(IntPtr) != sizeof(void*)) return 30;
if (sizeof(UIntPtr) != sizeof(void*)) return 31;
}
// Test 18: Using sizeof in expressions
int totalSize = sizeof(int) + sizeof(long) + sizeof(byte);
if (totalSize != 13) return 32;
// Test 19: Array element size calculation
int arrayElementSize = sizeof(MediumStruct);
int arraySize = arrayElementSize * 3;
if (arraySize != 24) return 33;
// Test 20: Complex nested struct with Pack
// byte (1) + CharStruct (4) + byte (1) = 6
if (sizeof(PackedNested) != 6) return 34;
return 0;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct PackedNested
{
public byte B;
public CharStruct C;
public byte B2;
}
}

View File

@@ -134,6 +134,12 @@ type CliRuntimePointer =
| Unmanaged of int64
| Managed of ManagedPointerSource
type SizeofResult =
{
Alignment : int
Size : int
}
/// This is the kind of type that can be stored in arguments, local variables, statics, array elements, fields.
type CliType =
/// III.1.1.1
@@ -150,13 +156,35 @@ type CliType =
/// as a concatenated list of its fields.
| ValueType of CliValueType
static member SizeOf (t : CliType) : int =
static member SizeOf (t : CliType) : SizeofResult =
match t with
| CliType.Numeric ty -> CliNumericType.SizeOf ty
| CliType.Bool _ -> 1
| CliType.Char _ -> 2
| CliType.ObjectRef _ -> 8
| CliType.RuntimePointer _ -> 8
| CliType.Numeric ty ->
let size = CliNumericType.SizeOf ty
{
Size = size
Alignment = size
}
| CliType.Bool _ ->
{
Size = 1
Alignment = 1
}
| CliType.Char _ ->
{
Size = 2
Alignment = 2
}
| CliType.ObjectRef _ ->
{
Size = 8
Alignment = 8
}
| CliType.RuntimePointer _ ->
{
Size = 8
Alignment = 8
}
| CliType.ValueType vt -> CliValueType.SizeOf vt
and CliField =
@@ -171,33 +199,95 @@ and CliValueType =
private
{
_Fields : CliField list
Layout : Layout
}
static member OfFields (f : CliField list) =
static member OfFields (layout : Layout) (f : CliField list) =
{
_Fields = f
Layout = layout
}
static member AddField (f : CliField) (vt : CliValueType) =
{
_Fields = f :: vt._Fields
Layout = vt.Layout
}
static member DereferenceField (name : string) (f : CliValueType) : CliType =
// TODO: this is wrong, it doesn't account for overlapping fields
f._Fields |> List.find (fun f -> f.Name = name) |> _.Contents
static member SizeOf (vt : CliValueType) : int =
match vt._Fields with
| [] -> failwith "is it even possible to instantiate a value type with no fields"
| [ field ] -> CliType.SizeOf field.Contents
| fields ->
// TODO: consider struct layout (there's an `Explicit` test that will exercise that)
fields |> List.map (_.Contents >> CliType.SizeOf) |> List.sum
static member SizeOf (vt : CliValueType) : SizeofResult =
let minimumSize, packingSize =
match vt.Layout with
| Layout.Custom (size = size ; packingSize = packing) ->
size, if packing = 0 then DEFAULT_STRUCT_ALIGNMENT else packing
| Layout.Default -> 0, DEFAULT_STRUCT_ALIGNMENT
if vt._Fields.IsEmpty then
{
Size = minimumSize
Alignment = 1
}
else
let seqFields, nonSeqFields =
vt._Fields |> List.partition (fun field -> field.Offset.IsNone)
let finalOffset, alignment =
match seqFields, nonSeqFields with
| [], [] -> (1, packingSize)
| _ :: _, [] ->
((0, 0), seqFields)
||> List.fold (fun (currentOffset, maxAlignmentCap) field ->
let size = CliType.SizeOf field.Contents
let alignmentCap = min size.Alignment packingSize
let error = currentOffset % alignmentCap
let currentOffset =
if error > 0 then
alignmentCap - error + currentOffset
else
currentOffset
currentOffset + size.Size, max maxAlignmentCap alignmentCap
)
| [], _ :: _ ->
nonSeqFields
|> List.map (fun field ->
let offset = field.Offset.Value
let size = CliType.SizeOf field.Contents
let alignmentCap = min size.Alignment packingSize
offset + size.Size, alignmentCap
)
|> List.fold
(fun (finalOffset, alignment) (newFinalOffset, newAlignment) ->
max finalOffset newFinalOffset, max alignment newAlignment
)
(0, 0)
| _ :: _, _ :: _ -> failwith "unexpectedly mixed explicit and automatic layout of fields"
let error = finalOffset % alignment
let size =
if error = 0 then
finalOffset
else
finalOffset + (alignment - error)
{
Size = max size minimumSize
Alignment = alignment
}
static member WithFieldSet (field : string) (value : CliType) (cvt : CliValueType) : CliValueType =
// TODO: this doesn't account for overlapping fields
{
Layout = cvt.Layout
_Fields =
cvt._Fields
|> List.replaceWhere (fun f ->
@@ -241,7 +331,7 @@ module CliType =
let ofManagedObject (ptr : ManagedHeapAddress) : CliType = CliType.ObjectRef (Some ptr)
let sizeOf (ty : CliType) : int = CliType.SizeOf ty
let sizeOf (ty : CliType) : int = CliType.SizeOf(ty).Size
let zeroOfPrimitive (primitiveType : PrimitiveType) : CliType =
match primitiveType with
@@ -267,19 +357,19 @@ module CliType =
{
Name = "_value"
Contents = CliType.RuntimePointer (CliRuntimePointer.Managed ManagedPointerSource.Null)
Offset = Some 0
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
|> CliType.ValueType
| PrimitiveType.UIntPtr ->
{
Name = "_value"
Contents = CliType.RuntimePointer (CliRuntimePointer.Managed ManagedPointerSource.Null)
Offset = Some 0
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
|> CliType.ValueType
| PrimitiveType.Object -> CliType.ObjectRef None
@@ -441,7 +531,7 @@ module CliType =
Offset = field.Offset
}
)
|> CliValueType.OfFields
|> CliValueType.OfFields typeDef.Layout
CliType.ValueType vt, currentConcreteTypes
else

View File

@@ -8,3 +8,6 @@ module Constants =
[<Literal>]
let SIZEOF_OBJ = 8
[<Literal>]
let DEFAULT_STRUCT_ALIGNMENT = 8

View File

@@ -36,6 +36,7 @@ and EvalStackValueField =
and EvalStackValueUserType =
{
Fields : EvalStackValueField list
Layout : Layout
}
static member DereferenceField (name : string) (this : EvalStackValueUserType) =
@@ -48,9 +49,10 @@ and EvalStackValueUserType =
None
)
static member OfFields (fields : EvalStackValueField list) =
static member OfFields (layout : Layout) (fields : EvalStackValueField list) =
{
Fields = fields
Layout = layout
}
static member TrySequentialFields (cvt : EvalStackValueUserType) : EvalStackValueField list option =
@@ -263,8 +265,8 @@ module EvalStackValue =
| popped -> failwith $"Unexpectedly wanted a char from {popped}"
| CliType.ValueType vt ->
match popped with
| EvalStackValue.UserDefinedValueType popped ->
match CliValueType.TrySequentialFields vt, EvalStackValueUserType.TrySequentialFields popped with
| EvalStackValue.UserDefinedValueType popped' ->
match CliValueType.TrySequentialFields vt, EvalStackValueUserType.TrySequentialFields popped' with
| Some vt, Some popped ->
if vt.Length <> popped.Length then
failwith
@@ -286,7 +288,7 @@ module EvalStackValue =
Offset = field1.Offset
}
)
|> CliValueType.OfFields
|> CliValueType.OfFields popped'.Layout
|> CliType.ValueType
| _, _ -> failwith "TODO: overlapping fields going onto eval stack"
| popped ->
@@ -322,7 +324,6 @@ module EvalStackValue =
| CliRuntimePointer.Unmanaged ptrInt -> NativeIntSource.Verbatim ptrInt |> EvalStackValue.NativeInt
| CliRuntimePointer.Managed ptr -> ptr |> EvalStackValue.ManagedPointer
| CliType.ValueType fields ->
// TODO: this is a bit dubious; we're being a bit sloppy with possibly-overlapping fields here
// The only allowable use of _Fields
fields._Fields
|> List.map (fun field ->
@@ -334,7 +335,7 @@ module EvalStackValue =
ContentsEval = contents
}
)
|> EvalStackValueUserType.OfFields
|> EvalStackValueUserType.OfFields fields.Layout
|> EvalStackValue.UserDefinedValueType
type EvalStack =

View File

@@ -59,7 +59,7 @@ module FieldHandleRegistry =
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
|> CliType.ValueType
let handle =
@@ -92,7 +92,7 @@ module FieldHandleRegistry =
Offset = None // no struct layout was specified
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
|> CliType.ValueType
// https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L1074
@@ -103,36 +103,36 @@ module FieldHandleRegistry =
{
Name = "m_keepalive"
Contents = CliType.ObjectRef None
Offset = Some 0
Offset = None
}
{
Name = "m_c"
Contents = CliType.ObjectRef None
Offset = Some SIZEOF_OBJ
Offset = None
}
{
Name = "m_d"
Contents = CliType.ObjectRef None
Offset = Some (SIZEOF_OBJ * 2)
Offset = None
}
{
Name = "m_b"
Contents = CliType.Numeric (CliNumericType.Int32 0)
Offset = Some (SIZEOF_OBJ * 3)
Offset = None
}
{
Name = "m_e"
Contents = CliType.ObjectRef None
Offset = Some (SIZEOF_OBJ * 3 + SIZEOF_INT)
Offset = None
}
// RuntimeFieldHandleInternal: https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L1048
{
Name = "m_fieldHandle"
Contents = runtimeFieldHandleInternal
Offset = Some (SIZEOF_OBJ * 4 + SIZEOF_INT)
Offset = None
}
]
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default // explicitly sequential but no custom packing size
let alloc, state = allocate runtimeFieldInfoStub allocState

View File

@@ -1652,7 +1652,21 @@ module IlMachineState =
match obj with
| CliType.ValueType vt -> vt |> CliValueType.DereferenceField name
| v -> failwith $"could not find field {name} on object {v}"
| ManagedPointerSource.InterpretedAsType (src, ty) -> failwith "TODO"
| ManagedPointerSource.InterpretedAsType (src, ty) ->
let src = dereferencePointer state src
let concrete =
match
AllConcreteTypes.findExistingConcreteType
state.ConcreteTypes
(ty.Assembly, ty.Namespace, ty.Name, ty.Generics)
with
| Some ty -> ty
| None -> failwith "not concretised type"
match concrete with
| ConcreteUInt32 state.ConcreteTypes -> failwith "TODO: cast"
| _ -> failwith "TODO"
let lookupTypeDefn
(baseClassTypes : BaseClassTypes<DumpedAssembly>)

View File

@@ -173,10 +173,10 @@ module Intrinsics =
{
Name = "m_type"
Contents = CliType.ObjectRef arg
Offset = Some 0
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
IlMachineState.pushToEvalStack (CliType.ValueType vt) currentThread state
|> IlMachineState.advanceProgramCounter currentThread

View File

@@ -58,7 +58,7 @@ module TypeHandleRegistry =
Offset = None
}
]
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
let alloc, state = allocate fields allocState

View File

@@ -316,7 +316,7 @@ module internal UnaryMetadataIlOp =
state, field :: zeros
)
let fields = List.rev fieldZeros |> CliValueType.OfFields
let fields = List.rev fieldZeros |> CliValueType.OfFields ctorType.Layout
// Note: this is a bit unorthodox for value types, which *aren't* heap-allocated.
// We'll perform their construction on the heap, though, to keep the interface
@@ -498,7 +498,8 @@ module internal UnaryMetadataIlOp =
if v.ConcreteType = targetConcreteType then
actualObj
else
failwith $"TODO: is {v.ConcreteType} an instance of {targetType} ({targetConcreteType})"
failwith
$"TODO: is {AllConcreteTypes.lookup v.ConcreteType state.ConcreteTypes |> Option.get} an instance of {AllConcreteTypes.lookup targetConcreteType state.ConcreteTypes |> Option.get}"
| false, _ ->
match state.ManagedHeap.Arrays.TryGetValue addr with
@@ -1240,7 +1241,7 @@ module internal UnaryMetadataIlOp =
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
IlMachineState.pushToEvalStack (CliType.ValueType vt) thread state
@@ -1299,7 +1300,7 @@ module internal UnaryMetadataIlOp =
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
IlMachineState.pushToEvalStack (CliType.ValueType vt) thread state
| MetadataToken.TypeReference h ->
@@ -1336,7 +1337,7 @@ module internal UnaryMetadataIlOp =
Offset = None
}
|> List.singleton
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
IlMachineState.pushToEvalStack (CliType.ValueType vt) thread state
| MetadataToken.TypeDefinition h ->

View File

@@ -63,7 +63,7 @@ module internal UnaryStringTokenIlOp =
Offset = None
}
]
|> CliValueType.OfFields
|> CliValueType.OfFields Layout.Default
let state, stringType =
DumpedAssembly.typeInfoToTypeDefn' baseClassTypes state._LoadedAssemblies baseClassTypes.String