I ran into an incorrect total in a DevExpress footer: it displayed 1.00 instead of 11.00. The error only appeared with certain data and looked like a rendering problem, as if the first digit had disappeared. This was not the first time I had seen it. I had come across it occasionally several years ago, but could not find an update for ancient Delphi 7. I even ended up working around the bug by calculating the total manually and displaying it as text. I wrote about that earlier in my DevExpress cxGrid and Delphi 7 cheat sheet.
This time I decided to see whether AI could find the root cause. Anyone who uses DevExpress knows what a monumental piece of software it is: many thousands of source files, which can take a very long time to dig through. To my surprise, after going through them, AI concluded that the problem was deeper—in Delphi 7’s core components, specifically the installed RTL. The fix came down to changing two lines, and we checked the result both independently of the interface and on actual application forms.
A minimal example without a database or components
A Delphi 7 console project and the standard FMTBcd unit are enough to reproduce the problem. The trailing fractional zeros in the strings are intentional: they set the scale at which the defect appeared.
program BcdFooterRepro;
{$APPTYPE CONSOLE}
uses
SysUtils, Variants, FMTBcd;
function DecimalValue(const S: string): Variant;
begin
Result := VarFMTBcdCreate(
StrToBcd(StringReplace(S, '.', DecimalSeparator, [])));
end;
var
Total: Variant;
begin
Total := DecimalValue('4.00000');
Total := Total + DecimalValue('5.00000');
Writeln('4 + 5 = ', VarToStr(Total));
Total := Total + DecimalValue('2.00000');
Writeln('4 + 5 + 2 = ', VarToStr(Total));
end.
With the defective library, the second result is 1; after the fix, it is 11.
Where the leading digit was lost
The calculation followed this chain:
built-in DevExpress summation → BCD Variant addition → TFMTBcdData.DoAdd → BcdAdd → NormalizeBcdPair
BCD stores decimal digits. Precision describes the overall precision of the representation, while the scale is the number of fractional digits. For 9.00000, with a precision of 6 and a scale of 5, there is room for one integer digit. 11.00000 needs a second one. Before adding the values, BcdAdd called:
NormalizeBcdPair(bcdIn1, bcdIn2, bcd1, bcd2, 1);
The last argument, ExtraDigits, requested extra room for a carry. However, in the version of NormalizeBcdPair we examined, this argument was not taken into account when choosing the required precision. The operands fit into the chosen representation, but the result with a carry did not. In our example, the leading digit was lost.
This also explains why the error seemed random. It depended on the scale, the available precision, and whether a carry occurred in a particular sequence of additions.
The patch: two lines
We changed the condition that increases precision in NormalizeBcdPair. The rest of the algorithm remained the same:
while (MaxDigits < (SizeOf(Bcd1.Fraction) * 2)) and
- ((MaxDigits - MaxScale < Bcd1.Precision - BcdScale(Bcd1)) or
- (MaxDigits - MaxScale < Bcd2.Precision - BcdScale(Bcd2))) do
+ ((MaxDigits - MaxScale < NumberOfDigits(Bcd1) - BcdScale(Bcd1) + ExtraDigits) or
+ (MaxDigits - MaxScale < NumberOfDigits(Bcd2) - BcdScale(Bcd2) + ExtraDigits)) do
Inc(MaxDigits, 2);
The check now takes into account both the digits actually needed by the operands and the extra digit requested by the calling function. NumberOfDigits matters here: if you add extra room to the precision already allocated on every addition, it can keep growing even when the number itself does not. A total calculated over thousands of rows needs this scenario checked too.
We kept BCD arithmetic. There was no need to convert the values to Double, change the SQL, or replace the built-in summation with a custom handler. DevExpress itself also remained unchanged. The exact patch, FMTBcd.patch, and the script that applies it, Apply-FmtBcdFix.ps1, are available in a separate archive. Rebuilding should update two files:
Delphi7/Lib/FMTBcd.dcu Delphi7/Lib/Debug/FMTBcd.dcu
That finally solved a problem that had been bothering me for years. Out of curiosity, I looked to see whether anyone else had this patch. I could not find this particular fix, but I did take a look at what enthusiasts are still releasing for old Delphi versions.
| Project | Purpose and integration |
|---|---|
| VCL Fix Pack 1.4, Andreas Hausladen | VCL and RTL fixes applied when the application starts. Supports Delphi 7; fixes include MDI window focus, context menus, and the rendering of some controls. Added to a project as a single unit. |
| Synopse Enhanced RTL, Arnaud Bouchez | Source fixes and optimizations for Delphi 7 and 2007, including core RTL units and some VCL units. Changes the library more broadly than a single unit added to a project. |
| FastCode | Optimized replacements for individual RTL functions: string processing, searching, comparison, and memory operations. Primarily focused on performance. |
| DelphiSpeedUp 3.1 | Development environment speedups and fixes, including support for Delphi 7. This works at a different level: IDE improvements should not be treated as fixes to arithmetic in the compiled application. |
So there are still ways to upgrade the old libraries, but our ranks are thinning—these patches have not been updated for quite a while either. Of these tools, I installed DelphiSpeedUp 3.1 and FastMM4.
FastMM4 as an improvement to the standard runtime
FastMM4 is a third-party memory manager by Pierre le Riche. Strictly speaking, it replaces the memory manager rather than providing a set of VCL fixes. For an old project, though, it serves a similar practical purpose: it improves a basic part of the runtime without requiring you to rewrite application code.
It handles memory allocation, deallocation, and reallocation, and is designed for multithreaded use and reduced fragmentation. Tools for diagnosing memory leaks and memory errors are available, with their behavior depending on the configuration. The author lists support for Delphi 4 and later, including Delphi 7. See the project description. The last update was on August 10, 2021. I also mentioned this performance improvement earlier in my DevExpress cxGrid and Delphi 7 cheat sheet.
