A while ago I blogged about the various possibilities for auto completion in TEdits. I created several units that supply auto completion based on
- a StringList
- files
- directories
- path (basically also directories)
They all followed a basic principle: Implement IAutoComplete and IAutoComplete2 and provide different implementations for the IEnumStrings interface. They worked fine in Delphi 2007 but I never tested them in later versions.
Today I wanted to add auto completion for files to a tool written in Delphi 10.2 and got an Access Violation in my implementation of the IEnumStrings.Next method. I could not figure out the problem, so I wrote a test program using the simplest implementation I had, the one using a StringList, to be sure there wasn’t any other part of the tool that wreaked havoc with the auto completion. The bug was still there and I still could not figure out what the problem was:
function TEnumStringStringList.Next(celt: Integer; out elt; pceltFetched: PLongint): HResult; var i: Integer; wStr: WideString; begin i := 0; while (i < celt) and (FCurrIndex < FStrings.Count) do begin wStr := FStrings[FCurrIndex]; TPointerList(elt)[I] := Pointer(wStr); // <= AV here Pointer(wStr) := nil; Inc(i); Inc(FCurrIndex); end; if pceltFetched <> nil then pceltFetched^ := i; if i = celt then Result := S_OK else Result := S_FALSE; end;
The Access violation happens in the line that assigns something to the first item in the elt TPointerList. As I said: It works fine in Delphi 2007.
Googling turned up a slightly different implementation for this:
TPointerList(elt)[i] := CoTaskMemAlloc(2 * (Length(wStr) + 1)); StringToWideChar(wStr, TPointerList(elt)[i], 2 * (Length(wStr) + 1));
Which unfortunately still caused an access violation. Finally I happened about this answer from Remy Lebau on StackOverflow that contained a type defintion with a comment:
type TPointerList = array[0..0] of Pointer; //avoid bug of Classes.pas declaration TPointerList = array of Pointer;
Guess what? It solved the problem. The code now works in both, Delphi 2007 and 10.2.
So, what causes the Access Violation? And why not in Delphi 2007?
The declaration of TPointerList in Delphi 2007 looks like this:
TPointerList = array[0..MaxListSize - 1] of Pointer;
while the one in Delphi 10.2 is this:
TPointerList = array of Pointer;
The first one is a static array the second one is a dynamic array. A Static array is a block of memory which is allocated automatically by the compiler for the given size, while a dynamic array is a pointer to an array descriptor (initially it’s NIL). Once the dynamic array gets initialized using the SetLength procedure the array descriptor contains the size of the array and enough memory to store the entries. The dynamic array variable points to the first entry.
So the difference between typecasting the elt parameter to a static array of pointers or an dynamic array of pointers causes the Access Violation. The change happened between Delphi XE and Delphi XE2. (I reported this as a bug in the Delphi RTL: RSP-24616. because apparently nobody had.).
elt is declared as an untyped out parameter. That means it is a pointer to some memory that the caller provides and which is to be written to by the Next method.
Typecasting it to a static array of pointers means that we assume that elt points to some memory which is to contain pointers to OleStrings. Typecasting it to a dynamic array of pointers means that we assume that elt points to a pointer to some memory that is to contain pointers, so the first (usually uninitialized) pointer in the array is referenced and since it points to somewhere which should not be written to, writing to it causes an access violation.
Adding the type definition given above fixed the problem.
The difference is the same as between a PInteger and a PPInteger Parameter:
type PInteger = ^Integer; PPInteger = ^PInteger; procedure bla(param: PInteger); begin param^ := 5; end; proceduer blub(param: PPInteger); begin Param^^ := 5; end; var InvValue: integer; begin bla(@IntValue); blub(@IntValue); end;
If no type checking took place passing a pointer to an Integer to blub would compile but cause an Access Violation because it tries to use the value of IntValue as a pointer. Since it usually isn’t, the memory that “pointer” references would not be accessible.
Later I found another post on StackOverflow that actually was about that very Access Violation I was trying to find with an answer from Rudy Velthuis.