Bug in Delphi XE RegularExpressions Unit
- by Jan Goyvaerts
Using the new RegularExpressions unit in Delphi XE, you can iterate over all the matches that a regex finds in a string like this:
procedure TForm1.Button1Click(Sender: TObject);
var
RegEx: TRegEx;
Match: TMatch;
begin
RegEx := TRegex.Create('\w+');
Match := RegEx.Match('One two three four');
while Match.Success do begin
Memo1.Lines.Add(Match.Value);
Match := Match.NextMatch;
end
end;
Or you could save yourself two lines of code by using the static TRegEx.Match call:
procedure TForm1.Button2Click(Sender: TObject);
var
Match: TMatch;
begin
Match := TRegEx.Match('One two three four', '\w+');
while Match.Success do begin
Memo1.Lines.Add(Match.Value);
Match := Match.NextMatch;
end
end;
Unfortunately, due to a bug in the RegularExpressions unit, the static call doesn’t work. Depending on your exact code, you may get fewer matches or blank matches than you should, or your application may crash with an access violation.
The RegularExpressions unit defines TRegEx and TMatch as records. That way you don’t have to explicitly create and destroy them. Internally, TRegEx uses TPerlRegEx to do the heavy lifting. TPerlRegEx is a class that needs to be created and destroyed like any other class. If you look at the TRegEx source code, you’ll notice that it uses an interface to destroy the TPerlRegEx instance when TRegEx goes out of scope. Interfaces are reference counted in Delphi, making them usable for automatic memory management.
The bug is that TMatch and TGroupCollection also need the TPerlRegEx instance to do their work. TRegEx passes its TPerlRegEx instance to TMatch and TGroupCollection, but it does not pass the instance of the interface that is responsible for destroying TPerlRegEx.
This is not a problem in our first code sample. TRegEx stays in scope until we’re done with TMatch. The interface is destroyed when Button1Click exits.
In the second code sample, the static TRegEx.Match call creates a local variable of type TRegEx. This local variable goes out of scope when TRegEx.Match returns. Thus the reference count on the interface reaches zero and TPerlRegEx is destroyed when TRegEx.Match returns. When we call MatchAgain the TMatch record tries to use a TPerlRegEx instance that has already been destroyed.
To fix this bug, delete or rename the two RegularExpressions.dcu files and copy RegularExpressions.pas into your source code folder. Make these changes to both the TMatch and TGroupCollection records in this unit:
Declare FNotifier: IInterface; in the private section.
Add the parameter ANotifier: IInterface; to the Create constructor.
Assign FNotifier := ANotifier; in the constructor’s implementation.
You also need to add the ANotifier: IInterface; parameter to the TMatchCollection.Create constructor.
Now try to compile some code that uses the RegularExpressions unit. The compiler will flag all calls to TMatch.Create, TGroupCollection.Create and TMatchCollection.Create. Fix them by adding the ANotifier or FNotifier parameter, depending on whether ARegEx or FRegEx is being passed.
With these fixes, the TPerlRegEx instance won’t be destroyed until the last TRegEx, TMatch, or TGroupCollection that uses it goes out of scope or is used with a different regular expression.