views:

377

answers:

8

I have defined a record which has lots of fields with different types (integer, real , string, ... plus dynamic arrays in terms of "array of ..."). I want to save it as a whole to a file and then be able to load it back to my program. I don't want to go through saving each field's value individually. The file type (binary or ascii or ...) is not important as long Delphi could read it back to a record.

Do you have any suggestions?

+7  A: 

You can load and save the memory of a record directly to and from a stream, as long as you don't use dynamic arrays. So if you use strings, you need to make them fixed:

type TTestRecord = record 
  FMyString : string[20]; 
end; 

var 
  rTestRecord: TTestRecord;
  strm : TMemoryStream; 

strm.Write(rTestRecord, Sizeof(TTestRecord) );

You can even load or save an array of record at once!

type TRecordArray = array of TTestRecord;

var ra : TRecordArray; 

strm.Write(ra[0], SizeOf(TTestRecord) * Length(ra));

In case you want to write dynamic content:

iCount   := Length(aArray);
strm.Write(iCount, Sizeof(iCount) );      //first write our length
strm.Write(aArray[0], SizeOf * iCount);   //then write content

After that, you can read it back:

strm.Read(iCount, Sizeof(iCount) );       //first read the length
SetLength(aArray, iCount);                //then alloc mem
strm.Read(aArray[0], SizeOf * iCount);    //then read content
André
+1 for mentioning not using dynamic arrays/strings
Imre L
Good solution, but sadly I have dynamic arrays, too. What can I do? Isn't there any way that I could save the "current" state of the record to the stream?
Flom Enol
You can manual write the state to the stream, see my edited version...
André
There is enough RTTI info for records/dyn.arrays/strings to save ANY dynamic record/array/strings/etc. with a one function. Tested in D2009. Records that contains for example dynamic array of dynamic record ... is not a problem as well. Analyze System.pas:_FinalizeArray
kibab
A: 

Codes from delphibasics :

 type
   TCustomer = Record
     name : string[20];
     age  : Integer;
     male : Boolean;
   end;

 var
   myFile   : File of TCustomer;  // A file of customer records
   customer : TCustomer;          // A customer record variable

 begin
   // Try to open the Test.cus binary file for writing to
   AssignFile(myFile, 'Test.cus');
   ReWrite(myFile);

   // Write a couple of customer records to the file
   customer.name := 'Fred Bloggs';
   customer.age  := 21;
   customer.male := true;
   Write(myFile, customer);

   customer.name := 'Jane Turner';
   customer.age  := 45;
   customer.male := false;
   Write(myFile, customer);

   // Close the file
   CloseFile(myFile);

   // Reopen the file in read only mode
   FileMode := fmOpenRead;
   Reset(myFile);

   // Display the file contents
   while not Eof(myFile) do
   begin
     Read(myFile, customer);
     if customer.male
     then ShowMessage('Man with name '+customer.name+
                      ' is '+IntToStr(customer.age))
     else ShowMessage('Lady with name '+customer.name+
                      ' is '+IntToStr(customer.age));
   end;

   // Close the file for the last time
   CloseFile(myFile);
 end;
SimaWB
As I mentioned above: I have dynamic arrays in my record. That doesn't work with "file of". Any other solutions?
Flom Enol
+2  A: 

You could also define an object instead of a record, so you can use RTTI to save your object to XML or whatever. If you have D2010 or XE, you can use DeHL to serialize it: http://stackoverflow.com/questions/2559297/delphi-2010-dehl-serialization-xml-and-custom-attribute-how-it-work

But if you "google" you can find other libs with RTTI and serialization (with D2007 etc)

André
A: 

The problem with saving a record containing dynamic array or real strings (or other "managed" types for that matter) is, it's not an big blob of memory containing everything, it's more like a tree. Someone or some thing needs to go over everything and save it to storage, somehow. Other languages (Python for example) include all sorts of facilities to transform most objects to text (serialize it), save it to disk and reload it (deserialize it).

Even those a Embarcadero-provided solution doesn't exist for Delphi, one can be implemented using the extended RTII available in Delphi 2010. A ready-made implementation is available in the DeHL library (here's a blog post about it) - but I can't say much about the implementation, I never uses DeHL.

An other option is the one you want to avoid: Manually serialize the record to an TStream; It's actually not half difficult. Here's the kind of code I usually use to read/write objects to a file stream:

procedure SaveToFile(FileName:string);
var F:TFileStream;
    W:TWriter;
    i:Integer;
begin
  F := TFileStream.Create(FileName, fmCreate);
  try
    W := TWriter.Create(F, 128);
    try
      // For every field that needs saving:
      W.WriteString(SomeStr);
      W.WriteInteger(TheNumber);
      // Dynamic arrays? Save the length first, then save
      // every item. The length is needed when reading.
      W.WriteInteger(Length(DArray));              
      for i:=0 to High(DArray) do
        W.WriteString(DArray[i]);
    finally W.Free;
    end;
  finally F.Free;
  end;
end;

procedure ReadFromFile(FileName:string);
var F:TFileStream;
    R:TReader;
    i,n:Integer;
begin
  F := TFileStream.Create(FileName, fmOpenRead);
  try
    R := TReader.Create(F, 128);
    try
      SomeStr := R.ReadString;
      TheNumber := R.ReadInteger;
      // Reading the dynamic-array. We first get the length:
      n := R.ReadInteger;
      SetLength(DArray, n);
      // And item-by-item
      for i:=0 to n-1 do
        DArray[i] := R.ReadString;
    finally R.Free;
    end;    
  finally F.Free;
  end;
end;
Cosmin Prund
"... solution doesn't exist for Delphi ..." but you can make yourself one, and not only for D2010, but for any Delphi (win-native) version (didn't tried .NET). Read my comments from this 'topic'
kibab
@kibab, it looks like you're trying to sell something, but you forgot to provide the link to your 1k-wander; And when you quote me, would you mind quoting the whole phrase, not just the 5 words that suite your purpose? The whole phrase is: ` Even those a Embarcadero-provided solution doesn't exist for Delphi, one can be implemented using the extended RTTI available in Delphi 2010 `. And did you noticed I posted a link to DeHL, right? And you do actually know that not everything gets RTTI, not even with Delphi 2010? Example: There's no RTTI for the elements in an enumeration type.
Cosmin Prund
+1  A: 

If you have dynamic strings or array you can't write the record "as a whole". Instead of using old style-25 characters max strings, I would add methods to the record to be able to "stream" itself to a stream, or better using a TFiler descendant:

TMyRec = record
  A: string;
  B: Integer;
  procedure Read(AReader: TReader);
  procedure Writer(AWriter: TWriter);
end;

procedure TMyrec.Read(AReader: TReader);
begin
  A := AReader.ReadString;
  B := AReader.ReadInteger;
end;
ldsandon
Don't lie ;) you CAN write WHOLE dynamic record, but you need to write/find function that do this for ANY record/dyn.array/string (not just specific one like above). Delphi RTL doesn't come with one, don't know why... I've build one by analysing how System.pas:_Finalize works. With about 1k lines of code I've made functions that works for ANY dynamic array/string/record like this: SizeOfDynamic(const ADynamicType; ATypeInfo: PTypeInfo), WriteDynamicToStream, ReadDynamicFromStream, CompareDynamic. Usage is simply WriteDynamicToStream/ReadDynamicFromStream(lFileStream, lMyRec, TypeInfo(TMyRec)).
kibab
You can write whole dynamic data, but you can't write a record whose fields are dynamic just writing the record memory image. You have anyway to read them separately and store them separately. What path to take depens on you needs. You can use an ad hoc solution, or a generic one - which could take far more time to develop. How RTTI, dynamic arrays and strings are stored is documented, you would have not needed that much reading of system.pas.Just "1k" lines of code? There are whole applications written with less lines. Anyway show your answer, instead of just criticising others...
ldsandon
+1, because even those the OP specifically requested a method that doesn't `go through saving each field's value individually`, this method is worth considering: you get space and time efficiency over any RTTI-based method, and you get a lot of flexibility in dealing with future variations in record structure.
Cosmin Prund
Wait for this weekend, and you will get link for this unit (need to write another one, because that previous, I've wrote at work, so you should understand why I didn't post 'link' to that before). This will be of course free (MPL/GPL/LGPL licensed). And also you will get an answer why I did wrote previous comments in such 'criticism style' (not only to you, nothing personally). And this unit will fit perfectly for this SO question. PS. It will be a binary storage.
kibab
Handle this one too: PMyRec = ^TMyRec; TMyRec = packed record Size: Cardinal; Buffer: array[0..0] of byte; end; ... GetMem(MyRec, SizeOf(Cardinal) + 1024); MyRec^.Size := 1024; ....This kind of records is often used in some API calls.
ldsandon
+1 Definitely agree with Cosmin. This method makes me say "Why haven't I thought of it before!". Absolutely worth considering: much easier to implement with much less complexity. It may not save the "whole" record with one call, but seems an efficient solution. Thanks again.
Flom Enol
+1  A: 

In addition to the answers that indicate how you do this, please also be aware of these:

  1. You must be aware that writing records out to a file will be Delphi version specific (usually: specific to a series of Delphi versions that share the same memory layout for the underlying data types).

  2. You can only do that if your record does not contain fields of a managed type. Which means that fields cannot be of these managed types: strings, dynamic arrays, variants, and reference types (like pointers, procedural types, method references, interfaces or classes) and file types, or types that contain those manages types. Which basically limits to to these unmanaged types:

    • A: Simple types (including bytes, integers, floats, enumerations, chars and such)
    • B: Short strings
    • C: Sets
    • D: Static arrays of A, B, C, D and E
    • E: Records of A, B, C, D and E

In stead of writing out records to/from a file, it might be better to go with class instances and convert them to/from JSON, and them write the JSON string equivalent to a file and read it back in.

You can use this unit to do the JSON conversion for you (should work with Delphi 2010 and up; works for sure with Delphi XE and up) from this location.

unit BaseObject;

interface

uses DBXJSON, DBXJSONReflect;

type
  TBaseObject = class
  public
    { public declarations }
    class function ObjectToJSON<T : class>(myObject: T): TJSONValue;
    class function JSONToObject<T : class>(json: TJSONValue): T;
  end;

implementation

{ TBaseObject }

class function TBaseObject.JSONToObject<T>(json: TJSONValue): T;
var
  unm: TJSONUnMarshal;
begin
  if json is TJSONNull then
    exit(nil);
  unm := TJSONUnMarshal.Create;
  try
    exit(T(unm.Unmarshal(json)))
  finally
    unm.Free;
  end;

end;

class function TBaseObject.ObjectToJSON<T>(myObject: T): TJSONValue;
var
  m: TJSONMarshal;
begin

  if Assigned(myObject) then
  begin
    m := TJSONMarshal.Create(TJSONConverter.Create);
    try
      exit(m.Marshal(myObject));
    finally
      m.Free;
    end;
  end
  else
    exit(TJSONNull.Create);

end;

end.

I hope this helps you getting an overview of things.

--jeroen

Jeroen Pluimers
+3  A: 

Another option which works very well for records (Delphi 2010+) is to use the SuperObject library. For example:

type
  TData = record
    str: string;
    int: Integer;
    bool: Boolean;
    flt: Double;
  end;
var
  ctx: TSuperRttiContext;
  data: TData;
  obj: ISuperObject;
  sValue : string;
begin
  ctx := TSuperRttiContext.Create;
  try
    sValue := '{str: "foo", int: 123, bool: true, flt: 1.23}';
    data := ctx.AsType<TData>(SO(sValue));
    obj := ctx.AsJson<TData>(data);
    sValue := Obj.AsJson;
  finally
    ctx.Free;
  end;
end;

I also tested this briefly with a simple TArray<Integer> dynamic array and it did not have a problem storing and loading the array elements.

skamradt
+1 for what seems like a nice solution (although as a noob, it is a little hard to understand!). But I have 3 questions: 1) How do you make the sValue from a big existing record to pass it to "SO()"? 2) What is the object here to be stored to the file? The sValue or the obj? 3) How do you load it back to the record? Thanks again for your answer.
Flom Enol
The sValue is a representation of the record as a JSON packet. You could easily save this single string to a text file. In the above example the two lines "Obj := ctx.AsJson<TData>(data)" and "sValue := Obj.AsJson" are what perform the magic translation from record to string. Store the sValue. The previous line "data := ctx.AsType<tData>(so(sValue));" is what parses this and populates the record Data with the proper values.
skamradt
+1  A: 

As promised here it is: http://kblib.googlecode.com

When you defined for example record as:

TTestRecord = record
  I: Integer;
  D: Double;
  U: UnicodeString;
  W: WideString;
  A: AnsiString;
  Options: TKBDynamicOptions;

  IA: array[0..2] of Integer;

  AI: TIntegerDynArray;
  AD: TDoubleDynArray;
  AU: array of UnicodeString;
  AW: TWideStringDynArray;
  AA: array of AnsiString;

  R: array of TTestRecord; // record contain dynamic array of itself (D2009+)
end;

You can save whole dynamic record to stream (as binary data) by :

TKBDynamic.WriteTo(lStream, lTestRecord, TypeInfo(TTestRecord));

To load it back:

TKBDynamic.ReadFrom(lStream, lTestRecord, TypeInfo(TTestRecord));

It not need to be a record, you can do same for any dynamic type like:

TKBDynamic.WriteTo(lStream, lStr, TypeInfo(UnicodeString));
TKBDynamic.WriteTo(lStream, lInts, TypeInfo(TIntegerDynArray));
TKBDynamic.WriteTo(lStream, lArrayOfTestRecord, TypeInfo(TArrayOfTestRecord)); // TArrayOfTestRecord = array of TTestRecord;

Tested on Delphi 2006/2009/XE. License: MPL 1.1/GPL 2.0/LGPL 3.0 See readme for information.

kibab