12
votes

I was talking to a co-worker the other day about how you can leak a string in Delphi if you really mess things up. By default strings are reference counted and automatically allocated, so they typically just work without any thought - no need for manual allocation, size calculations, or memory management.

But I remember reading once that there is a way to leak a string directly (without including it in an object that gets leaked). It seems like it had something to do with passing a string by reference and then accessing it from a larger scope from within the routine it was passed to. Yeah, I know that is vague, which is why I am asking the question here.

5

5 Answers

8
votes

I don't know about the issue in your second paragraph, but I was bitten once by leaked strings in a record.

If you call FillChar() on a record that contains strings you overwrite the ref count and the address of the dynamically allocated memory with zeroes. Unless the string is empty this will leak the memory. The way around this is to call Finalize() on the record before clearing the memory it occupies.

Unfortunately calling Finalize() when there are no record members that need finalizing causes a compiler hint. It happened to me that I commented out the Finalize() call to silence the hint, but later when I added a string member to the record I missed uncommenting the call, so a leak was introduced. Luckily I'm generally using the FastMM memory manager in the most verbose and paranoid setting in debug mode, so the leak didn't go unnoticed.

The compiler hint is probably not such a good thing, silently omitting the Finalize() call if it's not needed would be much better IMHO.

4
votes

No, I don't think such a thing can happen. It's possible for a string variable to obtain a value that you didn't expect, but it won't leak memory. Consider this:

var
  Global: string;

procedure One(const Arg: string);
begin
  Global := '';

  // Oops. This is an invalid reference now. Arg points to
  // what Global used to refer to, which isn't there anymore.
  writeln(Arg);
end;

procedure Two;
begin
  Global := 'foo';
  UniqueString(Global);
  One(Global);
  Assert(Global = 'foo', 'Uh-oh. The argument isn''t really const?');
end;

Here One's argument is declared const, so supposedly, it won't change. But then One circumvents that by changing the actual parameter instead of the formal parameter. Procedure Two "knows" that One's argument is const, so it expects the actual parameter to retain its original value. The assertion fails.

The string hasn't leaked, but this code does demonstrate how you can get a dangling reference for a string. Arg is a local alias of Global. Although we've changed Global, Arg's value remains untouched, and because it was declared const, the string's reference count was not incremented upon entry to the function. Reassigning Global dropped the reference count to zero, and the string was destroyed. Declaring Arg as var would have the same problem; passing it by value would fix this problem. (The call to UniqueString is just to ensure the string is reference-counted. Otherwise, it may be a non-reference-counted string literal.) All compiler-managed types are susceptible to this problem; simple types are immune.

The only way to leak a string is to treat it as something other than a string, or to use non-type-aware memory-management functions. Mghie's answer describes how to treat a string as something other than a string by using FillChar to clobber a string variable. Non-type-aware memory functions include GetMem and FreeMem. For example:

type
  PRec = ^TRec;
  TRec = record
    field: string;
  end;

var
  Rec: PRec;
begin
  GetMem(Rec, SizeOf(Rec^));
  // Oops. Rec^ is uninitialized. This assignment isn't safe.
  Rec^.field := IntToStr(4);
  // Even if the assignment were OK, FreeMem would leak the string.
  FreeMem(Rec);
end;

There are two ways to fix it. One is to call Initialize and Finalize:

GetMem(Rec, SizeOf(Rec^));
Initialize(Rec^);
Rec^.field := IntToStr(4);
Finalize(Rec^);
FreeMem(Rec);

The other is to use type-aware functions:

New(Rec);
Rec^.field := IntToStr(4);
Dispose(Rec);
2
votes

Actually, passing string as CONST or non const are the same in term of reference count in Delphi 2007 and 2009. There was a case that causing access violation when string is passed as CONST. Here is the problem one

type
  TFoo = class
    S: string;
    procedure Foo(const S1: string);
  end;

procedure TFoo.Foo(const S1: string);
begin
  S:= S1; //access violation
end;

var
  F: TFoo;
begin
  F:= TFoo.create;
  try
    F.S := 'S';
    F.Foo(F.S);
  finally
    F.Free;
  end;
end.
1
votes

Another way to leak a string is to declare it as a threadvar variable. See my question for details. And for the solution, see the solution on how to tidy it.

0
votes

I think this might have been similar to what I was thinking of. It is the reverse of a string leak, a string that gets collected early:

var
  p : ^String;

procedure InitString;
var
  s, x : String;
begin
  s := 'A cool string!';
  x := s + '. Append something to make a copy in' +
             'memory and generate a new string.';

  p := @x;
end;

begin
  { Call a function that will generate a string }
  InitString();

  { Write the value of the string (pointed to by p) }
  WriteLn(p^); // Runtime error 105!


  { Wait for a key press }
  ReadLn;
end.