Latest Spring4d Collections update breaks our RTTI Code for Serialization. Please Help

Issue #99 wontfix
Todd Flora created an issue

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)

  1. Stefan Glienke 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)

  2. Todd Flora 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?

  3. Stefan Glienke 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

  4. Log in to comment