Delphi 7 и потеря старшего разряда в футтере DevExpress. Виновник оказался в RTL!

Столкнулся с неправильной суммой в футтере DevExpress показывал 1,00 вместо 11,00. Ошибка появлялась лишь на части данных и напоминала проблему отрисовки: будто у числа исчезла первая цифра. И это не первый раз, периодически уже натыкался, несколько лет назад, но обновления под древнюю Delphi-7 не нашел, в результате даже писал обход бага ручным суммированием и выводом в виде текста. Про это я уже писал ранее в  Шпаргалка по DevExpress cxGrid и Delphi 7 Теперь же решил вдруг ИИ найдет корень проблемы. Все кто использует DevExpress знает насколько монументальное это сооружение — исходников многие тысячи файлов и копаться в них можно очень долго. Однако на мое удивление ИИ порывшись в них пришел к выводу что ошибка глубже — в базовых компонентах Delphi 7, а именно в установленной RTL Delphi 7, где исправление довольно исправили две строки и проверили результат как отдельно от интерфейса, так и на настоящих формах.

Минимальный пример без базы и компонентов

Для воспроизведения достаточно консольного проекта Delphi 7 и стандартного модуля FMTBcd. Дробные нули в строках оставлены намеренно: они задают масштаб, на котором обнаружился дефект.

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.

С дефектной библиотекой второй результат — 1; после исправления — 11.

Где терялся разряд

Цепочка вычисления оказалась такой:

штатное суммирование DevExpress
  → сложение BCD Variant
  → TFMTBcdData.DoAdd
  → BcdAdd
  → NormalizeBcdPair

BCD хранит десятичные цифры. Precision описывает общую точность представления, а масштаб — количество дробных разрядов. Для 9.00000 при точности 6 и масштабе 5 остаётся один разряд целой части. Для 11.00000 нужен уже второй. Перед сложением BcdAdd вызывал:

NormalizeBcdPair(bcdIn1, bcdIn2, bcd1, bcd2, 1);

Последний аргумент, ExtraDigits, запрашивал запас для переноса. Однако в исследованной версии NormalizeBcdPair этот аргумент не учитывался при выборе необходимой точности. Операнды помещались в выбранное представление, а результат с переносом — уже нет. В нашем примере старший разряд терялся. Это также объясняет, почему ошибка казалась случайной. Она зависела от масштаба, доступной точности и появления переноса при конкретной последовательности сложений.

Патч: две строки

В NormalizeBcdPair заменили условие увеличения точности. Весь остальной алгоритм остался прежним:

   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);

Теперь проверка учитывает и фактически необходимые цифры операндов, и дополнительный разряд, запрошенный вызывающей функцией. NumberOfDigits здесь важен: если на каждом сложении прибавлять запас к уже выделенной точности, она может расти даже без роста самого числа. Итог из тысяч строк требует проверки и этого сценария. Мы сохранили BCD-арифметику. Переводить значения в Double, менять SQL или подменять штатное суммирование пользовательским обработчиком не понадобилось. DevExpress также остался без изменений. Точный патч FMTBcd.patch и скрипт его применения Apply-FmtBcdFix.ps1 сохранены отдельно. Пересборка должна обновить два файла:

Delphi7/Lib/FMTBcd.dcu
Delphi7/Lib/Debug/FMTBcd.dcu

Так решлась проблемма которая досаждала много лет. Из интереса я поискал есть ли у кого то этот патч, но именно такого исправления нигде не нашел, зато посмотрел что ещё выпускают энтузиасты для старой Delphi.

Проект Назначение и способ подключения
VCL Fix Pack 1.4, Andreas Hausladen Исправления VCL и RTL при запуске приложения. Поддерживает Delphi 7; среди исправлений — фокус MDI-окон, контекстные меню и отрисовка некоторых контролов. Подключается одним модулем в проект.
Synopse Enhanced RTL, Arnaud Bouchez Исправления и оптимизации исходников для Delphi 7 и 2007, включая базовые модули RTL и некоторые VCL-модули. Более широкое вмешательство в библиотеку, чем отдельный подключаемый модуль.
FastCode Оптимизированные замены отдельных функций RTL: обработки строк, поиска, сравнения и работы с памятью. Основная задача — производительность.
DelphiSpeedUp 3.1 Ускорение и исправления среды разработки, в том числе для Delphi 7. Это отдельный уровень: улучшения IDE не следует считать исправлениями арифметики собранного приложения.

т.е. как то апгрейдить старые библиотеки можно но «наши ряды рядеют» — эти патчи тоже уже довольно давно не обновлялись. Из них я себе поставил DelphiSpeedUp 3.1, и FastMM4.

FastMM4 как улучшение стандартной среды выполнения

FastMM4 — сторонний менеджер памяти Pierre le Riche. Формально это замена менеджера памяти, а не пакет исправлений VCL. Но для старого проекта он выполняет похожую практическую роль: улучшает базовый механизм среды выполнения, не требуя переписывать прикладной код. Он обслуживает выделение, освобождение и перераспределение памяти, рассчитан на многопоточную работу и уменьшение фрагментации. Доступны средства диагностики утечек и ошибок памяти, поведение которых зависит от конфигурации. Автор указывает поддержку Delphi 4 и новее, включая Delphi 7. Описание проекта. Последнее обновление от 10 августа 2021 года. Про этот ускоритель я уже как то упоминал в Шпаргалка по DevExpress cxGrid и Delphi 7

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *