- changed status to wontfix
Latest Spring4d Collections update breaks our RTTI Code for Serialization. Please Help
We have a generic serializer from NG that we have enhanced to work with Spring collections. We wrote the following routine:
rType := ctx.GetType(PTypeInfo(Enumerable.ClassInfo));
{If we did not find the GetEnumerator method then this is not an enumerable type}
if not IsEnumerableType(rType, EnumMethod) then
raise ESerialize.Create(FInfo.Name + 'is not an Enumerable type');
Enumerator := EnumMethod.Invoke(Enumerable,[]);
eType := Ctx.GetType(Enumerator.TypeInfo);
{Current Property Handle gives us the contained class type, IF this property is missing then
we cannot continue}
CurrentProperty := eType.GetProperty('Current');
if CurrentProperty = nil then
begin
CurrentMethod := eType.GetMethod('GetCurrent');
if CurrentMethod = nil then
raise ESerialize.Create('Missing Property/Method: Enumerable type must have current property or method');
end;
if CurrentProperty <> nil then
cType := Ctx.GetType(CurrentProperty.PropertyType.Handle)
else
cType := Ctx.GetType(CurrentMethod.ReturnType.Handle);
{Find parameterless constructor for contained type. Throw an exception if there is not one}
CurrentConstructor := nil;
for m in cType.GetMethods do
begin
if (m.IsConstructor) and (Length(m.GetParameters) = 0) then
begin
CurrentConstructor := m;
break;
end;
end;
{The contained type must have a parameterless constructor, so we can create instances, if
not then we cannot continue}
if CurrentConstructor = nil then
raise ESerialize.Create('Missing Constructor: Contained class type must have parameterless constructor');
cInstance := cType.AsInstance;
Previous to our pull of last nights code in the Marshmallow branch the call to cType := Ctx.GetType(CurrentMethod.ReturnType.Handle); returned a PTypeInfo for the contained class in the list. But now with the new FoldedObjectList wrapper this method returns TObject for all types as the handle of the generic type.
We need a way using RTTI to determine what the generic type is so that we can create instances of it during deserialization. Any suggestions of how we can correct the above code to work wit the new FoldedObjectList wrapper would be greatly appreciated.
Comments (5)
-
repo owner -
repo owner There is no property RTTI for interfaces, use the getter.
Anyway your code implies you are working on an object rather than an instance. Why don't you just use Supports(Enumerable, Spring.Collections.IEnumerable)
-
reporter Finally got back to this integration today.
In order to access the GetElementType method using RTTI I had to add the following directive to the top of TFoldedObjectList as the RTTI information was not available for protected methods.
{$RTTI EXPLICIT METHODS([vcPublished, vcPublic, vcProtected])} TFoldedObjectList<T{: class}> = class(TObjectList<TObject>) protected function GetElementType: PTypeInfo; override; end;
Maybe I am not holding this right but this was the only way I could see to access this method.
Here is my code.
procedure TMetadata.TEnumerableType.Read(D: TDeserializer; var V); var Ctx : TRttiContext; EnumMethod : TRttiMethod; CurrentProperty: TRttiProperty; Method : TRttiMethod; CurrentConstructor : TRttiMethod; Current : TObject; rType : TRttiType; eType : TRttiType; cType : TRttiType; cInstance : TRttiInstanceType; Enumerator: TValue; Enumerable : TObject; AddMethod : TRttiMethod; m : TRttiMethod; S : String; tmp : TValue; p : PTypeInfo; begin {Spring Collections come in as interfaces, so cast to TObject to get the underlying implementation class} if (Info.Kind = tkInterface) then Enumerable := TObject(IInterface(V)) else Enumerable := TObject(V); { Must pass in the type using fill read mode, we cannot create this class ourselves because of the explicit list definition requirement of generics, T cannot be determined at runtime } if (Enumerable = nil) then raise ESerialize.Create('Enumerable types can only be deserialized using Fill Read Mode'); Ctx := TRTTIContext.Create; try rType := ctx.GetType(PTypeInfo(Enumerable.ClassInfo)); {If we did not find the GetEnumerator method then this is not an enumerable type} if not IsEnumerableType(rType, EnumMethod) then raise ESerialize.Create(FInfo.Name + 'is not an Enumerable type'); Enumerator := EnumMethod.Invoke(Enumerable,[]); eType := Ctx.GetType(Enumerator.TypeInfo); {Latest version of Spring collections are now wrapped in class called TFoldedObjectList, Therefore the handle of the current method no longer returns the actual contained type but rather TObject, So now we need to call the GetElementType method of this class to get the PTypeInfo of the Contained Type. Had to modify Spring.Collections.Lists to add the RTTI directive to expose this method via RTTI} Method := rType.GetMethod('GetElementType'); if (Method = nil) then begin {Current Property Handle gives us the contained class type, IF this property is missing then we cannot continue} CurrentProperty := eType.GetProperty('Current'); if CurrentProperty = nil then begin Method := eType.GetMethod('GetCurrent'); if Method = nil then raise ESerialize.Create('Missing Property/Method: Enumerable type must have current property or method'); end; if CurrentProperty <> nil then cType := Ctx.GetType(CurrentProperty.PropertyType.Handle) else cType := Ctx.GetType(Method.ReturnType.Handle); end else begin tmp := Method.Invoke(Enumerable, []); {Since the TValue returned by this method is a Pointer we need to just dereference the raw data and cast it to a ponter} cType := Ctx.GetType(Pointer(tmp.GetReferenceToRawData^)); end; AddMethod := rType.GetMethod('Add'); {Finally the Enumerable type must have an add method otherwise we cannot add new contained types to the container} if AddMethod = nil then raise ESerialize.Create('Missing Method: Enumerable type must have add method'); if (cType.TypeKind = tkClass) then begin {Find parameterless constructor for contained type. Throw an exception if there is not one} CurrentConstructor := nil; for m in cType.GetMethods do begin if (m.IsConstructor) and (Length(m.GetParameters) = 0) then begin CurrentConstructor := m; break; end; end; {The contained type must have a parameterless constructor, so we can create instances, if not then we cannot continue} if CurrentConstructor = nil then raise ESerialize.Create('Missing Constructor: Contained class type must have parameterless constructor'); cInstance := cType.AsInstance; end; D.BeginArray; while D.HasNext do begin if (cType.TypeKind = tkClass) then begin {Create contained class} Current := CurrentConstructor.Invoke(cInstance.MetaclassType, []).AsObject; {Fills the Object with the deserializer content} D.Value(Current); AddMethod.Invoke(Enumerable, [current]); end else begin S := D.Value<string>; if not S.IsEmpty then begin case cType.TypeKind of tkInteger : AddMethod.Invoke(Enumerable, [StrToInt(S)]); tkFloat : AddMethod.Invoke(Enumerable, [StrToFloat(S)]); tkInt64 : AddMethod.Invoke(Enumerable, [StrToInt64(S)]); else AddMethod.Invoke(Enumerable, [S]); end; AddMethod.Invoke(Enumerable, [S]); end; end; end; D.EndArray; finally Ctx.Free; end; end;
If I am doing this correctly can you please consider turning on RTTI for protected methods for this or maybe make this a public method?
-
repo owner You should not reach into the class behind an interface but operate on the interface. Doing so will make your code break every time we change an implementation detail. In fact we could even decide to turn off the RTTI for the classes entirely :)
If you are just using lists where T is a class then use the IObjectList interface. Otherwise use the AsList method to return an IList which wraps the original generic list and uses TValue on its API.
{Since the TValue returned by this method is a Pointer we need to just dereference the raw data and cast it to a ponter} cType := Ctx.GetType(Pointer(tmp.GetReferenceToRawData^));
May I suggest a bit better code?
cType := Ctx.GetType(tmp.AsType<PTypeInfo>);
And for the entire ctor finding and creating object code I suggest using TActivator from Spring.pas
-
reporter Awesome thanks for the tip. That works great.
Todd.
--
- Log in to comment
Look at the ElementType property - that's what it's for.