Unfortunately I don’t have a nice easy reproducible example of this issue but I’ve debugged it enough that I can describe the issue in detail.
Let’s say I have a Delphi RO server project that contains a unit named uFoo.pas with the following class in it
unit uFoo;
interface
type
TFoo = class(TROComplex)
public
Bar : integer;
end;
I use that class in my codefirst API declarations as a remote method argument.
[ROServiceMethod]
function DoubleFoo(AFoo : TFoo) : TFoo;
From my Delphi RO client application I can make a SuperTCP/IP connection to that server and call the DoubleFoo remote method with no problems.
All is well in the world.
When it comes to my unit tests however I have a problem.
My unit test project is a DUnitX project that makes use of the TROLocalServer and TROLocalChannel components so I can run both client and server code inside that test executable. The DPR includes almost every file from both the server and the client including:
- FoobarLibrary_Intf.pas
- FoobarService_Impl.pas
- uFoo.pas
Everything compiles but when the DUnitX tests call the remote DoubleFoo function above the following exception is raised in the server deserialization code.
ROException: Unexpected class found in stream class “TFoo” does not descend from “TFoo”
This exception is raised inside TROStreamSerializer.ReadStruct
I understand the exception message, the deserialization code is confusing Foobar_Library_Intf.TFoo and uFoo.TFoo as both are present in this combined client/server project.
This should not be a problem though since any project that uses TROLocalServer and TROLocalChannel would in theory have both types of TFoo present.
I’ve tried to replicate the issue in a simple example without success. I’ve also tried many combinations of reordering units in the DPR and in uses clauses, also without success.
So I dug into the RemObjects source code to work out what was going on. In my analysis I believe the issue results from the order in which the types are read from the Delphi RTTI system in TRORttiClientCache.RebuildCache().
In that method the main loop is…
for l_type in g_ctx.GetTypes do begin
…where each type is processed for the cache in the order they appear in the array returned by TRttiContext.GetTypes.
That array ordering appears to be somewhat indeterminate though, coming from deep in the bowels of the RTTI subsystem. For a simple TROLocalServer/TROLocalChannel project the array holds them in the order that works just fine. At some point though as project complexity increases that order changes and the RO deserialization receives the two different TFoo types in the opposite order than it expects.
I have made the following code changes to the RemObjects code that works for both my combined client+server project and my separated client and server projects. They’re somewhat hacky changes but hopefully they lead you to finding a better fix.
First up, in uRORTTISupport.pas inside the TRORttiClientCache.DoRegisterROClass() method as well as registering the class by its usual name “TFoo”, I also register it by it’s fully qualified name. Effectively this triples the number of registrations as “TFoo”, “uFoo.TFoo” and “FoobarLibrary_Intf.TFoo” all end up registered in the cache (I told you it was a bit hacky).
Here’s my current copy of that method with the two changes to it surrounded by IFDEFs.
procedure TRORttiClientCache.DoRegisterROClass(const aComplexTypeClass: TROComplexTypeClass; const aNamespace: string; aPersistent: Boolean);
begin
if aPersistent then begin
AddToHolder<TROComplexTypeClass>(fComplexTypes_persistent, aNamespace, aComplexTypeClass.ClassName, aComplexTypeClass);
{$IFDEF LACHLANS_FIX}
AddToHolder<TROComplexTypeClass>(fComplexTypes_persistent, aNamespace, aComplexTypeClass.QualifiedClassName, aComplexTypeClass);
{$ENDIF}
if aComplexTypeClass.InheritsFrom(TROArray) then
AddToHolder<TROArrayClass>(fArrays_persistent, aNamespace, aComplexTypeClass.ClassName, TROArrayClass(aComplexTypeClass));
if not fAllComplexTypes_persistent.Contains(aComplexTypeClass) then fAllComplexTypes_persistent.Add(aComplexTypeClass);
end;
if not (fNeedRebuild and aPersistent) then begin
AddToHolder<TROComplexTypeClass>(fComplexTypes, aNamespace, aComplexTypeClass.ClassName, aComplexTypeClass);
{$IFDEF LACHLANS_FIX}
AddToHolder<TROComplexTypeClass>(fComplexTypes, aNamespace, aComplexTypeClass.QualifiedClassName, aComplexTypeClass);
{$ENDIF}
if aComplexTypeClass.InheritsFrom(TROArray) then
AddToHolder<TROArrayClass>(fArrays, aNamespace, aComplexTypeClass.ClassName, TROArrayClass(aComplexTypeClass));
if not fAllComplexTypes.Contains(aComplexTypeClass) then fAllComplexTypes.Add(aComplexTypeClass);
end;
end;
Next the deserialization code has to go looking for those fully qualified type names that we registered above. Making the below IFDEFed changes to TROStreamSerializer.ReadStruct() was enough for my purposes. I imagine a proper fix will involve modifications to other similar deserialization methods.
function TROStreamSerializer.ReadStruct(const aName: string; aClass: TClass; var Ref; ArrayElementId: Integer): Boolean;
var
l_struct: TROComplexType absolute Ref;
l_className: string;
l_actualClass: TROComplexTypeClass;
begin
Result := IntReadBoolean;
if Result then begin
ReadLegacyString('', l_className, [paAsUTF8String], -1, MAX_ITEM_NAME);
{$IFDEF LACHLANS_FIX}
l_actualClass := FindROClass(l_className, DefaultNamespaces);
if not Assigned(l_actualClass) or not l_actualClass.InheritsFrom(aClass) then begin
l_actualClass := FindROClass(aClass.QualifiedClassName, DefaultNamespaces);
if not Assigned(l_actualClass) then RaiseError(err_UnknownClassInStream, [l_className, aClass.ClassName]);
if not l_actualClass.InheritsFrom(aClass) then RaiseError(err_UnexpectedClassInStream, [l_actualClass.UnitName + '.' + l_className, aClass.UnitName + '.' + aClass.ClassName]);
end;
{$ELSE}
l_actualClass := FindROClass(l_className, DefaultNamespaces);
if not Assigned(l_actualClass) then RaiseError(err_UnknownClassInStream, [l_className, aClass.ClassName]);
if not l_actualClass.InheritsFrom(aClass) then RaiseError(err_UnexpectedClassInStream, [l_className, aClass.ClassName]);
{$ENDIF}
l_struct := l_actualClass.Create;
try
l_struct.ReadComplex(Self);
except
FreeOrDisposeOfAndNil(l_struct);
raise;
end;
end
else begin
l_struct := nil;
end;
end;
My apologies for not being able to provide a simple test case but hopefully you agree with me that the current reliance on the indeterminate order of types as received from the Delphi RTTI system is an issue that needs a resolution of some kind. Best of luck with it.