DataRecord

This commit is contained in:
Michael Schimmel
2025-07-20 20:21:55 +02:00
parent 6010f61953
commit dd50049b06
8 changed files with 363 additions and 60 deletions
+105 -18
View File
@@ -8,7 +8,7 @@ uses
System.TypInfo;
type
// A record that provides field-level access to data, stored in a packed byte buffer.
// A record that provides field-level access to managed data, stored in a byte buffer.
TDataRecord = record
public
type
@@ -32,42 +32,47 @@ type
TypeInfo: PTypeInfo;
end;
IBuilder = interface
procedure AddField(const Name: String; const Value: TValue); overload;
procedure SetupRecord(out Layout: TArray<TFieldLayout>; out Buffer: TBytes);
end;
// The interface helper record that provides the generic AddField<T> method.
TBuilder = record
type
IBuilder = interface
procedure AddField(const Name: String; const Value: TValue); overload;
procedure SetupRecord(out Layout: TArray<TFieldLayout>; out Buffer: TBytes);
end;
private
FBuilder: IBuilder;
public
constructor Create(const ABuilder: IBuilder);
// This generic method is the reason for the helper's existence.
procedure AddField<T>(const Name: String; const Value: T); inline;
// Wrapper for methods on the underlying interface.
function CreateRec: TDataRecord; inline;
// Implicit operators for seamless casting between the helper and the interface.
class operator Implicit(const AValue: IBuilder): TBuilder; overload;
class operator Implicit(const AValue: TBuilder): IBuilder; overload;
procedure AddField<T>(const Name: String); overload; inline;
procedure AddField<T>(const Name: String; const Value: T); overload; inline;
function CreateRec: TDataRecord; inline;
end;
private
FLayout: TArray<TFieldLayout>;
FBuffer: TBytes;
public
class operator Initialize(out Dest: TDataRecord);
class operator Finalize(var Dest: TDataRecord);
class operator Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord);
// Factory method now returns the helper record instance.
class function CreateBuilder: TBuilder; static;
class function CreateFrom<T>(const Rec: T): TDataRecord; static;
procedure AssignTo<T>(var Rec: T);
function FitsRecord<T>: Boolean;
function IndexOf(const Name: String): Integer;
function GetField(const Name: String): IField; overload;
function GetField<T>(const Name: String): IField<T>; overload;
property Layout: TArray<TFieldLayout> read FLayout;
end;
@@ -90,6 +95,11 @@ begin
FBuilder.AddField(Name, TValue.From<T>(Value));
end;
procedure TDataRecord.TBuilder.AddField<T>(const Name: String);
begin
AddField<T>(Name, Default(T));
end;
function TDataRecord.TBuilder.CreateRec: TDataRecord;
begin
FBuilder.SetupRecord(Result.FLayout, Result.FBuffer);
@@ -125,6 +135,76 @@ begin
Result := TDataRecord.TBuilder.Create(TDataRecordBuilder.Create);
end;
class function TDataRecord.CreateFrom<T>(const Rec: T): TDataRecord;
var
ctx: TRttiContext;
rttiType: TRttiType;
builder: TBuilder.IBuilder;
begin
// Create a builder to construct the TDataRecord
builder := CreateBuilder;
// Use RTTI to iterate over the fields of the source record/object
ctx := TRttiContext.Create;
rttiType := ctx.GetType(TypeInfo(T));
// Add each field and its value to the builder
for var field in rttiType.GetFields do
begin
builder.AddField(field.Name, field.GetValue(Pointer(@Rec)));
end;
// Create the final TDataRecord from the builder
builder.SetupRecord(Result.FLayout, Result.FBuffer);
end;
procedure TDataRecord.AssignTo<T>(var Rec: T);
var
ctx: TRttiContext;
rttiType: TRttiType;
fieldValue: TValue;
fieldIndex: Integer;
layout: TFieldLayout;
begin
Assert(FitsRecord<T>);
// Use RTTI to iterate over the fields of the destination record/object
ctx := TRttiContext.Create;
rttiType := ctx.GetType(TypeInfo(T));
for var rttiField in rttiType.GetFields do
begin
// Find the corresponding field in the TDataRecord by name
fieldIndex := IndexOf(rttiField.Name);
if fieldIndex >= 0 then
begin
layout := FLayout[fieldIndex];
// Create a TValue from the data in our buffer
TValue.Make(@FBuffer[layout.Offset], layout.TypeInfo, fieldValue);
// Assign the value to the destination record's field
rttiField.SetValue(Pointer(@Rec), fieldValue);
end;
end;
end;
function TDataRecord.FitsRecord<T>: Boolean;
begin
var ctx := TRttiContext.Create;
var rttiType := ctx.GetType(TypeInfo(T));
for var rttiField in rttiType.GetFields do
begin
// Find the corresponding field in the TDataRecord by name
var fieldIndex := IndexOf(rttiField.Name);
if fieldIndex < 0 then
exit(false);
if rttiField.DataType.Handle <> FLayout[fieldIndex].TypeInfo then
exit(false);
end;
exit(true);
end;
function TDataRecord.GetField(const Name: String): IField;
var
idx: Integer;
@@ -162,12 +242,18 @@ begin
Dest.FLayout := Src.FLayout;
SetLength(Dest.FBuffer, Length(Src.FBuffer));
for var layout in Dest.FLayout do
for var i := 0 to High(Dest.FLayout) do
begin
var P: PByte := @Dest.FBuffer[layout.Offset];
var Q: PByte := @Src.FBuffer[layout.Offset];
Assert(Dest.FLayout[i].Name = Src.Layout[i].Name);
Assert(Dest.FLayout[i].Offset = Src.Layout[i].Offset);
Assert(Dest.FLayout[i].Size = Src.Layout[i].Size);
Assert(Dest.FLayout[i].TypeInfo = Src.Layout[i].TypeInfo);
var ofs := Dest.FLayout[i].Offset;
var P: PByte := @Dest.FBuffer[ofs];
var Q: PByte := @Src.FBuffer[ofs];
var Val: TValue;
TValue.Make(Q, layout.TypeInfo, val);
TValue.Make(Q, Dest.FLayout[i].TypeInfo, val);
val.ExtractRawData(P);
end;
end;
@@ -182,6 +268,7 @@ class operator TDataRecord.Finalize(var Dest: TDataRecord);
begin
if (Dest.FBuffer = nil) or (Dest.FLayout = nil) then
Exit;
for var layout in Dest.FLayout do
begin
var P: PByte := @Dest.FBuffer[layout.Offset];