Milos
Milos

Reputation: 63

TDictionary via DataSnap crashes, FComparer is nil

I have a dictionary crash in the DataSnap client because its FComparer is somehow nil.

Server side code:

TColorNames = TDictionary<integer, string>;

function TServerMethods.DictionaryTest: TColorNames;
begin
  result := TColorNames.Create;
  result.Add (1, 'Red');
  result.Add (2, 'Blue');
end;

Client side code:

procedure TformClientMain.FetchColors;
var
  Colors: TColorNames;
begin
  Colors := easServer.DictionaryTest;
  if Colors.Items[1]<>'Red'
     then ShowMessage('Not red');
end;

Colors.Items[1] crashes (as well as other functions that need the FComparer). The crash happens in System.Generics.Collections, when the function tries to access the FComparer.

function TDictionary<TKey,TValue>.Hash(const Key: TKey): Integer;

I do receive all the data in the list, and just looping through it with for color in Colors.Values do ShowMessage(Color); works fine.

watch screenshot on client side

When I create a dictionary instance with TColorNames.Create, on client or server side, the FComparer has a value and these issues do not exist. I placed breakpoints in the dictionary constructor and traced the code during the datasnap call - FComparer always gets a value.

What am I (or Delphi) doing wrong?

Upvotes: 3

Views: 207

Answers (1)

The answer to "What is Delphi doing wrong" is:

DataSnap uses a TJsonMarshal and TJsonUnmarshal from the unit Data.DBXJSONReflect. Upon unmarshalling, an instance of a TDictionary<X,Y> is created by calling the parameterless constructor. The parameterless constructor here is the one inherited straight from TObject.

When you, however, type TDictionary<X, Y>.Create(); you're calling the "correct" constructor with a default parameter (Create(ACapacity: Integer = 0);). The TJsonUnmarshall class, however, does not since it is looking for a constructor with really no parameters. The one you're usually calling has a parameter, even if you don't have to pass it.

I don't know how DataSnap works, but you should probably be able to pass a custom marshal and unmarshaller to whatever does the serialization.

Since Embarcadero closed all the bug reports I know of (example) as "Works as expected", it's probably a safe bet that generic collections should not be marshalled back and forth and you should probably revert to arrays.

Here is the minimal code to reproduce:

unit Unit1;

interface

uses
    System.Generics.Collections,
    System.JSON,
    Data.DBXJSONReflect;

type
    TColorNames = TDictionary<Integer, String>;

procedure p();

implementation

procedure p();
var
    original: TColorNames;
    marshaller: TJSONMarshal;
    unmarshaller: TJSONUnMarshal;
    asJson: TJsonValue;
    marshalledBack: TColorNames;
begin
    original := TColorNames.Create();

    marshaller := TJsonMarshal.Create();
    asJson := marshaller.Marshal(original);

    unmarshaller := TJSONUnMarshal.Create();
    marshalledBack := unmarshaller.Unmarshal(asJson) as TColorNames;

    marshalledBack.Add(0, ''); // << will crash because FComparer is nil
end;

end.

Upvotes: 6

Related Questions