Без рубрикиПрограмування

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 (російською).

Залишити коментар

Вашу електронну адресу не буде оприлюднено. Обов’язкові поля позначено *