Search code examples
delphidelphi-xe5

Unable to free IXMLDocument, IXMLNodeList or maybe other (omnixml's) interface type objects


The function below takes in XML input, parses it, and returns an ordinary string which is simply displayed in the caller function. So objects in the context are internal to the function below.

But this function has a strange problem that, it remembers the input which means that the objects are not released properly. Output string has a part resulting from the previous call even if the input is checked to be different.

Before assigning to nil each of XMLDoc, IXMLNodeList, I also tried to release, in a loop, each IXMLNode directly referred from the Item array by assigning nil to it but that assignment statement resulted in a syntax error so settled for the below.

function tform1.nodeToSentence(nodeXml: string): string ;
var
  tempXmlDoc : IXMLDocument;
  resultWordPuncNodes : IXMLNodeList;
  i: Integer;

begin
  tempXmlDoc := CreateXMLDoc;
  tempXmlDoc.LoadXML(nodeXml);
  resultWordPuncNodes := XPathSelect(tempXmlDoc.DocumentElement,'//*');

  for i:= 0 to resultWordPuncNodes.Length-1   do
  begin

    if resultWordPuncNodes.Item[i].NodeName = 'name' then
     begin
       if resultWordPuncNodes.Item[i].Attributes['attrib'] = 'v_attrib' then
       begin
          Result := TrimRight(Result);
          Result := Result + resultWordPuncNodes.Item[i].Text + ' ';
       end
       else Result := Result + resultWordPuncNodes.Item[i].Text + ' ';
     end;
  end;


  resultWordPuncNodes:=nil;
  tempXmlDoc := nil; //interface based objects are freed this way
end;

calling code

//iterating over i   
          nodeXML := mugList.Strings[i];
          readableSentence := nodeToSentence(mugList.Strings[i]);
          //examplesList.Append(readableSentence);
          //for debugging
          showMessage(readableSentence);
 //iteration ends

Solution

  • It does not have to do with interfaces. It only has with string implementation in Delphi.

    You have to clear the Result variable as the first line of your function.

    Here below is your function fixed:

    function tform1.nodeToSentence(nodeXml: string): string ;
    var
      tempXmlDoc : IXMLDocument;
      resultWordPuncNodes : IXMLNodeList;
      i: Integer;
    
    begin
    
    // Change #1 - added the line
      Result := ''; 
    // Variable Result is shared here before by both the function and the caller.
    // You DO HAVE to clear the shared variable to make the function FORGET the previous result.
    // You may do it by the 'function' or by the calling site, but it should have be done.
    // Usually it is better to do it inside the function.
    
      tempXmlDoc := CreateXMLDoc;
      tempXmlDoc.LoadXML(nodeXml);
      resultWordPuncNodes := XPathSelect(tempXmlDoc.DocumentElement,'//*');
    
      for i:= 0 to resultWordPuncNodes.Length-1   do
      begin
    
        if resultWordPuncNodes.Item[i].NodeName = 'name' then
         begin
           if resultWordPuncNodes.Item[i].Attributes['attrib'] = 'v_attrib' then
           begin
    
    /// REMEMBER this line, it is important, I would explain later.
              Result := TrimRight(Result);
              Result := Result + resultWordPuncNodes.Item[i].Text + ' ';
           end
           else Result := Result + resultWordPuncNodes.Item[i].Text + ' ';
         end;
      end;
    
      resultWordPuncNodes:=nil;
    
    // Change #2 - removed the line - it is redundant
     (* tempXmlDoc := nil; //interface based objects are freed this way *)
    // Yes, they are. But Delphi automatically clears local ARC-variables 
    //   when destroying them where the function exits.
    // Variable should both be local and should be one of ARC types foe that.
    end;
    

    1) about auto-clearing local and ARC-kind variables see http://docwiki.embarcadero.com/Libraries/XE8/en/System.Finalize

    2) Your real function is not a function at all.

    // function tform1.nodeToSentence(nodeXml: string): string ;   // only an illusion  
    procedure tform1.nodeToSentence(nodeXml: string; var Result: string);  // real thing
    

    You may say that you was writing a function, not the procedure. However that was merely a syntactic sugar. It is like TDateTime and double are different terms, but those terms are synonyms for the same implementation;

    All the functions returning String/AnsiString/UnicodeString variables in Delphi are procedures. They are merely disguised as function, and the disguise is thin.

    3) there is an old Delphi kōan about it. Without OmniXML and all the complex stuff that only blurs the truth. Run this code and see how it behaves.

    Function Impossible: String;
    begin
      ShowMessage( 'Empty string is equal to: ' + Result );
    end;
    
    Procedure ShowMe; Var spell: string;
    begin
      spell := Impossible();
    
      spell := 'ABCDE';
      spell := Impossible();
    
      spell := '12345';
      spell := Impossible();
    end;
    

    4) Now, could you know it ? Yes you could, it only takes a little bit of attention. Let us make a little change.

    Function ImpossibleS: String;
    begin
      ShowMessage( 'Unknown string today is equal to: ' + Result );
    end;
    
    Function ImpossibleI: Integer;
    begin
      ShowMessage( 'Unknown integer today is equal to: ' + IntToStr(Result) );
    end;
    
    
    Procedure ShowMe; 
    Var spell: string; chant: integer;
    begin
      spell := ImpossibleS();
    
      spell := 'ABCDE';
      spell := ImpossibleS();
    
      spell := '12345';
      spell := ImpossibleS();
    
      chant := ImpossibleI();
    
      chant := 100;
      chant := ImpossibleI();
    
      chant := 54321;
      chant := ImpossibleI();
    end;
    

    Be attentive and read compilation warnings. You did fixed your original code that it compiles with NO COMPILER WARNINGS there, didn't you ?

    Now compile my second kōan. Read the warnings. Integer-function does generate "Non-initialized variable" warning. String-function does not. Is it so ?

    Why the difference? Because string is ARC-type. That means string-function is really a procedure, not a function. And that means the Result variable is initialized, by the caller, outside of the looks-like-function. You may also observe that chant variables does change after the call, because the integer-function is a real function and the assignment-after-call is a real one. Conversely a string-function is an illusionary one and so is the illusionary assignment-after-call. It looks as being assigned, but it is not.

    5) Last thing. Why did I put that mark in your code.

    /// REMEMBER this line, it is important, I would explain later.
              Result := TrimRight(Result);
    

    Exactly because of that kōan above. You must have been reading the garbage here. You must have been using "Result" variable that you did not initialized nowhere before. You must have expected your compiler giving warning to you at this line. Like it does in ImpossibleI real function above. But it does not. Just like it does not with ImpossibleS illusionary function. This lack of the warning is a dead giveaway of the illusion Delphi creates here. Would you notice that there SHOULD be a 'non-initialized var' warning but it got missing you would ask yourself "who initialized variable if it was not my function" and you would understand what had happened yourself. ;-)

    6) Okay, one more last thing.

    procedure StringVar( const S1: string; var S2: string );
    begin
      ShowMessage ( S1 + S2 );
    end;
    
    procedure StringOut( const S1: string; out S2: string );
    begin
      ShowMessage ( S1 + S2 );
    end;
    

    Those two procedures looks similar. The difference is S2 kind. It should be an OUT-parameter, not the VAR (IN+OUT) parameter. But Delphi fails here just with your function. Delphi can not do string-type OUT-parameters. To compare, FreePascal(Lazarus,CodeTyphon) knows the difference and would show the legit 'non-initialized variable' warning for the StringOut procedure.