unit Myc.DataRecord; interface uses System.Rtti, System.TypInfo, System.SysUtils, System.Generics.Collections, System.Generics.Defaults; type // Provides generic access to a value of type T. IAccess = interface ['{1D422E3A-25C8-459B-A480-2E246942CF56}'] function GetValue: T; procedure SetValue(const Value: T); property Value: T read GetValue write SetValue; end; // Describes the memory layout of a field in the data buffer. TFieldLayout = record Key: string; Offset: Integer; Size: Integer; TypeInfo: PTypeInfo; end; // A record that provides field-level access to data, stored in a packed byte buffer. TDataRecord = record public type // Builder for dynamically creating a TDataRecord. TBuilder = record private FStagedValues: TList>; class operator Initialize(out Dest: TBuilder); class operator Finalize(var Dest: TBuilder); public procedure AddField(const Name: String; const Value: T); overload; procedure AddField(const Name: String; const Value: TValue); overload; function CreateRec: TDataRecord; end; private FLayout: TArray; FBuffer: TBytes; class operator Initialize(out Dest: TDataRecord); class operator Finalize(var Dest: TDataRecord); public function GetAccess(const Name: String): IAccess; class function CreateFromRec(const Rec: T): TDataRecord; static; class function CreateBuilder: TDataRecord.TBuilder; static; class operator Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord); end; // Accessor for reading/writing data directly from/to the TBytes buffer. TByteAccessor = class(TInterfacedObject, IAccess) private FBuffer: TBytes; FLayout: TFieldLayout; public constructor Create(ABuffer: TBytes; const ALayout: TFieldLayout); function GetValue: T; procedure SetValue(const Value: T); end; // Comparer for sorting and searching field layouts by their string key (Singleton). TFieldLayoutComparer = class(TComparer) strict private class var FDefault: IComparer; class constructor CreateClass; class destructor DestroyClass; public function Compare(const Left, Right: TFieldLayout): Integer; override; class property Default: IComparer read FDefault; end; procedure Testfunc; implementation uses System.JSON, System.Classes; {$region 'TFieldLayoutComparer'} class constructor TFieldLayoutComparer.CreateClass; begin FDefault := TFieldLayoutComparer.Create; end; class destructor TFieldLayoutComparer.DestroyClass; begin FDefault := nil; end; function TFieldLayoutComparer.Compare(const Left, Right: TFieldLayout): Integer; begin Result := TComparer.Default.Compare(Left.Key, Right.Key); end; {$endregion} {$region 'TByteAccessor'} constructor TByteAccessor.Create(ABuffer: TBytes; const ALayout: TFieldLayout); begin inherited Create; FBuffer := ABuffer; FLayout := ALayout; end; function TByteAccessor.GetValue: T; type PT = ^T; begin Result := PT(@FBuffer[FLayout.Offset])^; end; procedure TByteAccessor.SetValue(const Value: T); type PT = ^T; begin PT(@FBuffer[FLayout.Offset])^ := Value; end; {$endregion} {$region 'TDataRecord'} function TDataRecord.GetAccess(const Name: String): IAccess; var dummyLayout: TFieldLayout; index: Integer; begin Result := nil; dummyLayout.Key := Name; if TArray.BinarySearch(FLayout, dummyLayout, index, TFieldLayoutComparer.Default) then begin if (FLayout[index].TypeInfo <> nil) and (FLayout[index].TypeInfo = System.TypeInfo(T)) then begin Result := TByteAccessor.Create(FBuffer, FLayout[index]); end; end; end; class function TDataRecord.CreateBuilder: TDataRecord.TBuilder; begin Result := Default(TDataRecord.TBuilder); end; class function TDataRecord.CreateFromRec(const Rec: T): TDataRecord; var ctx: TRttiContext; recType: TRttiRecordType; field: TRttiField; recValue: TValue; builder: TBuilder; begin builder := CreateBuilder; ctx := TRttiContext.Create; recType := ctx.GetType(System.TypeInfo(T)) as TRttiRecordType; recValue := TValue.From(Rec); for field in recType.GetFields do begin builder.AddField(field.Name, field.GetValue(recValue.GetReferenceToRawData)); end; Result := builder.CreateRec; end; class operator TDataRecord.Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord); begin Finalize(Dest); Dest.FLayout := Src.FLayout; SetLength(Dest.FBuffer, Length(Src.FBuffer)); for var layout in Dest.FLayout do begin var P: PByte := @Dest.FBuffer[layout.Offset]; var Q: PByte := @Src.FBuffer[layout.Offset]; var Val: TValue; TValue.Make(Q, layout.TypeInfo, val); val.ExtractRawData(P); end; end; class operator TDataRecord.Initialize(out Dest: TDataRecord); begin Dest.FLayout := nil; Dest.FBuffer := nil; end; 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]; var Val: TValue; // Move instance from buffer into a managed TValue. It will be destroyed, when it gets out of scope. TValue.MakeWithoutCopy(P, layout.TypeInfo, val, false); end; Dest.FLayout := nil; Dest.FBuffer := nil; end; {$endregion} {$region 'TDataRecord.TBuilder'} procedure TDataRecord.TBuilder.AddField(const Name: String; const Value: T); begin FStagedValues.Add(TPair.Create(Name, TValue.From(Value))); end; procedure TDataRecord.TBuilder.AddField(const Name: String; const Value: TValue); begin // Overload to accept a TValue directly. FStagedValues.Add(TPair.Create(Name, Value)); end; function TDataRecord.TBuilder.CreateRec: TDataRecord; var layoutList: TList; pair: TPair; layout: TFieldLayout; begin layoutList := TList.Create; try layoutList.Capacity := FStagedValues.Count; var ofs: NativeUInt := 0; for pair in FStagedValues do begin layout.Key := pair.Key; layout.Size := pair.Value.DataSize; ofs := (ofs + 15) and not 15; layout.Offset := ofs; inc(ofs, layout.Size); layout.TypeInfo := pair.Value.TypeInfo; layoutList.Add(layout); end; SetLength(Result.FBuffer, ofs); for var i := 0 to layoutList.Count - 1 do begin var P: PByte := @Result.FBuffer[layoutList[i].Offset]; FStagedValues[i].Value.ExtractRawData(P); end; layoutList.Sort(TFieldLayoutComparer.Default); Result.FLayout := layoutList.ToArray; finally layoutList.Free; end; FStagedValues.Clear; end; class operator TDataRecord.TBuilder.Initialize(out Dest: TBuilder); begin Dest.FStagedValues := TList>.Create; end; class operator TDataRecord.TBuilder.Finalize(var Dest: TBuilder); begin Dest.FStagedValues.Free; end; {$endregion} type TTestRec = record A, B, C: Integer; Name: String; Val: Double; end; procedure Testfunc; var testRec: TTestRec; R: TDataRecord; nameAccess: IAccess; bAccess: IAccess; const JSON_DATA = '{"IntValue": 123, "StringValue": "Hello JSON", "FloatValue": 99.9, "BoolValue": true}'; begin testRec.A := 1; testRec.B := 5; testRec.C := 10; testRec.Name := 'Hi'; testRec.Val := 3.14; R := TDataRecord.CreateFromRec(testRec); // Test reading a string value nameAccess := R.GetAccess('Name'); Assert(Assigned(nameAccess)); Assert(nameAccess.Value = 'Hi'); // Test reading an integer value bAccess := R.GetAccess('B'); Assert(Assigned(bAccess)); Assert(bAccess.Value = 5); // Test writing a value bAccess.Value := 99; Assert(bAccess.Value = 99); // Verify it was written back to the buffer var bAccess2 := R.GetAccess('B'); Assert(bAccess2.Value = 99); // Verify that a request for a wrong type returns nil var aAccess := R.GetAccess('A'); Assert(not Assigned(aAccess)); // Test the builder pattern var RecBuilder: TDataRecord.TBuilder := TDataRecord.CreateBuilder; RecBuilder.AddField('ZValue', 222.0); RecBuilder.AddField('AValue', 'Builder Test'); var S := RecBuilder.CreateRec; var T := S; var dblAccess := T.GetAccess('ZValue'); Assert(Assigned(dblAccess)); Assert(dblAccess.Value = 222.0); var strAccess := T.GetAccess('AValue'); Assert(Assigned(strAccess)); Assert(strAccess.Value = 'Builder Test'); { // Test JSON parsing var jsonRec := TDataRecord.CreateFromJSON(JSON_DATA); var intAccess := jsonRec.GetAccess('IntValue'); Assert(Assigned(intAccess)); Assert(intAccess.Value = 123); strAccess := jsonRec.GetAccess('StringValue'); Assert(Assigned(strAccess)); Assert(strAccess.Value = 'Hello JSON'); dblAccess := jsonRec.GetAccess('FloatValue'); Assert(Assigned(dblAccess)); Assert(dblAccess.Value = 99.9); var boolAccess := jsonRec.GetAccess('BoolValue'); Assert(Assigned(boolAccess)); Assert(boolAccess.Value = true); } end; end.