مقاله فنی

باگ‌های Delphi مخصوص Win64 هنگام سخت‌سازی HotPDF

کد Delphi در Win64 می‌تواند جایی شکست بخورد که همان سورس روی Win32 تمیز اجرا می‌شود، و کامپوننت HotPDF Delphi PDF در یک گذر سخت‌سازی اخیر پنج چنین موردی را خورد: ‏Power(10, N) که به سربارگذاری Single bind می‌شد، یک حلقهٔ while که یک TList.Count کهنه می‌خواند، یک کران High(Int64) که به 2^63 گرد می‌شود، متن float پانزده‌رقمی روی FPC، و assertهای تست که کامپایل را متوقف می‌کردند

اگر فقط Win32 را بیلد و تست کنی هیچ‌کدام پیدایش نمی‌شود، و دقیقاً همین‌طور داخل شدند. موارد پایین از importerهای SVG و XPS یعنی HotPDF و renderer صفحه‌اش و خوانندهٔ JSON کارهایش می‌آیند و نتایج عددی نقل‌شده با برنامه‌های اکتشافی کوچک بیلدشده برای Win32 و Win64 بازتولید شدند. اگر در حال بردن یک کدبیس Delphi به 64 بیتی هستی، هر کدام یک grep لایق است

چرا Power(10, 100) فقط روی Win64 سرریز می‌کند؟

روی Win64، ‏System.Math.Power(10, N) با آرگومان‌های عدد صحیح به سربارگذاری Single حل می‌شود، پس نتیجه در دقت تکی محاسبه و برگردانده می‌شود و هر چیز بالای حدود 3.4E38 سرریز می‌کند. روی Win32 همان فراخوانی به سربارگذاری Extended bind می‌شود و روی FPU یعنی x87 با دقت 80 بیتی اجرا می‌شود، پس Power(10, 100) به‌سادگی 1E100 است

System.Math ‏Power را برای Extended و Double و Single declare می‌کند به‌علاوهٔ یک خانوادهٔ منطبق IntPower که Power وقتی توان یک عدد صحیح کامل است صدا می‌زند. روی Win64 ‏Extended فقط یک نام مستعار برای Double است (‏SizeOf(Extended) = 8) و برای دو آرگومان عدد صحیح کامپایلر نسخهٔ Single را انتخاب می‌کند. نشانه دقت است نه فقط سرریز: روی Win64 ‏Power(10, 20) مقدار 1.0000000200408773E20 برمی‌گرداند که دقیقاً Single(1E20) است. یک نتیجهٔ Double به‌شکل 1E20 چاپ می‌شد. همان bind را با هر کامپایلر Win64 ای که امتحان کردیم دیدیم، از Delphi 10.3 تا نسخهٔ کامپایلر 37.0

بعدش چه می‌شود به ماسک استثناهای ممیز شناور وابسته است. ‏Delphi 12 و بعدتر به‌طور پیش‌فرض همهٔ استثناهای ممیز شناور را ماسک می‌کنند، پس سرریز بی‌صداست: ‏Power(10, 100) مقدار +Inf و Power(10, -100) مقدار 0 برمی‌گرداند. ‏Delphi 11 و قبل‌تر ‏exOverflow را ماسک‌نشده می‌گذارند و همان فراخوانی ‏EOverflow بالا می‌دهد. اپ‌هایی که خودشان ماسک را ست می‌کنند و DLLهایی که داخل چنین میزبان‌هایی load می‌شوند هر رفتاری را که میزبان انتخاب کرده می‌گیرند، و برای همین یک کتابخانه نمی‌تواند هیچ‌کدام از دو نتیجه را فرض کند

تلهٔ عددی Win64 در HotPDF که در آن Power یعنی System.Math با آرگومان‌های عدد صحیح به سربارگذاری Single bind می‌شود، پس Power از 10 به توان بیستم مقدار 1.0000000200408773E20 را به‌جای 1E20 برمی‌گرداند و Power از 10 به توان صدم وقتی استثناها ماسک‌اند بی‌نهایت مثبت و وقتی ماسک نیستند EOverflow می‌دهد
از دست دادن دقت نشانه است: اگر یک توان ده با نویز Single برگردد سربارگذاری اشتباه برده است — مقیاس را خودت بساز
uses
  System.SysUtils, System.Math;

procedure ShowPowerOverload;
var
  N: Integer;
  OldMask: TArithmeticExceptionMask;
begin
  N := 20;
  // ‏Win32 مقدار 1E20 را چاپ می‌کند؛ ‏Win64 مقدار 1.0000000200408773E20 (سربارگذاری Single)
  Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));

  // بازتولید رفتار Delphi 11 یا میزبانی با تنظیمات FP سخت‌گیرانه
  OldMask := GetExceptionMask;
  SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
  try
    N := 100;
    Writeln(Power(10, N));       // ‏Win64: ‏EOverflow؛ ‏Win32: ‏1E100
  finally
    SetExceptionMask(OldMask);
  end;
end;

ماسک‌برداشتن از exOverflow و exInvalidOp برای مدت یک تست ارزان‌ترین راه دیدن چیزی است که یک کامپایلر قدیمی‌تر یا میزبان سخت‌گیر می‌بیند. روی کامپایلر مدرن با تنظیمات پیش‌فرض باگ crash نمی‌کند، بی‌نهایت‌ها و صفرها تولید می‌کند و آن‌ها در یک لاگ تست به‌مراتب سخت‌تر دیده می‌شوند. ماسک قبلی را در finally برگردان: ماسک state به‌ازای هر thread است و بقیهٔ اجرای تست هرچه را که جا گذاشته‌ای به ارث می‌برد

سربارگذاری چطور به import یعنی SVG و XPS در HotPDF رسید

خواننده‌های مسیر یعنی SVG و XPS در HotPDF یک اسکنر عدد مشترک دارند و آن اسکنر وقتی توانی خوانده بود مانتیس را با Power(10, Exponent) مقیاس می‌کرد. پس هر SVG ای که به THotPDF.ImportSVGFormXObject داده می‌شد (نقطهٔ ورود پشت import کردن SVG به PDF به‌شکل form XObjectهای قابل‌استفادهٔ دوباره) و هر هندسهٔ مسیری که حین تبدیل XPS و OpenXPS به PDF هندل می‌شد می‌توانست مختصاتی مثل 1e100 یا 5e99 به آن فراخوانی بدهد

‏v2.770.91 از قبل توان را روی 100 کپ کرده بود و مقادیری را که از 1E300 رد می‌شدند رد می‌کرد، که به اندازهٔ کافی به نظر می‌رسید: ‏1E100 به هیچ‌جای نزدیک کران Double یعنی حدود 1.8E308 نیست. روی Win64 همچنان سرریز می‌شد، چون محاسبه اصلاً در Double اتفاق نمی‌افتاد. از v2.770.155 اسکنر توان ده را خودش می‌سازد و عددهایی مثل 1e-100 یا مانتیسی بلند با توانی منفی بزرگ به‌شکل مقدار واقعی‌شان خوانده می‌شوند به‌جای فروپاشی به 0

یک توان ده امن برای توان‌های کپ‌شده

وقتی توان کپ شده، امن‌ترین توان ده آنی است که خودت با ضرب در Double بسازی. یک حلقهٔ حداکثر 100 ضربی کنار اسکن کردن متن اطرافش هیچ هزینه‌ای ندارد، هرگز واسطی بزرگ‌تر از مقیاس نهایی تولید نمی‌کند و روی Win32 و Win64 و Free Pascal یکسان رفتار می‌کند

const
  MaxDecimalExponent = 100;

function TryScaleByPowerOf10(const Value: Double; Exponent: Integer;
  out Scaled: Double): Boolean;
var
  Scale: Double;
  I: Integer;
begin
  Scaled := 0;
  Result := False;
  if (Exponent < -MaxDecimalExponent) or (Exponent > MaxDecimalExponent) then
    Exit;
  // نتایجی که از بازهٔ Double بیرون می‌زنند را رد کن
  if (Exponent > 0) and (Value <> 0) and
     (Log10(Abs(Value)) + Exponent > 300) then
    Exit;
  Scale := 1.0;
  for I := 1 to Abs(Exponent) do
    Scale := Scale * 10.0;       // هرگز از 1E100 عبور نمی‌کند
  if Exponent >= 0 then
    Scaled := Value * Scale
  else
    Scaled := Value / Scale;     // تقسیم: 1E-100 هیچ Double دقیقی ندارد
  Result := True;
end;

سه جزئیات بار را حمل می‌کنند. چک بازه از دو مقایسه استفاده می‌کند نه Abs(Exponent) <= 100، چون Abs(Low(Integer)) همچنان منفی است و مستقیم از آن رد می‌شد. توان‌های منفی بر مقیاس تقسیم می‌کنند به‌جای ضرب در یک 1E-100 از پیش محاسبه‌شده، که هیچ Double دقیقی ندارد و یک گام گرد کردن اضافه می‌گذاشت. و پیش‌چک یعنی Log10 نتایج بیرون بازهٔ Double را قبل از اینکه ضرب فرصت سرریز پیدا کند رد می‌کند

روشن باش که حلقه چه چیزی را قربانی می‌کند. توان‌های ده تا 1E22 در Double دقیق‌اند؛ بعد از آن هر ضرب گرد می‌کند و بعد از 100 تای آن مقیاس چند واحد در آخرین خانه با 1E100 گردشدهٔ درست فاصله دارد. برای مختصات رسم نامرئی است. برای یک تبدیل همه‌منظورهٔ متن-به-double که باید هر مقدار را بیت‌به‌بیت بازتولید کند کافی نیست و به یک الگوریتم تبدیل درست‌گرد به جای آن نیاز داری

وقتی dcc64 یک TList.Count کهنه را داخل یک حلقهٔ while می‌خواند

دیدیم کامپایلر Win64 یعنی (dcc64، نسخهٔ کامپایلر 37.0) برای حلقهٔ while List.Count > Start do کدی تولید کرد که از انتهای فهرست حذف می‌کرد و به‌جای خواندن دوبارهٔ Count با یک موقت روی پشته مقایسه می‌کرد. بازنویسی‌ای که فیکسش کرد یک حلقهٔ for ... downto بود که کران‌هایش طبق تعریف دقیقاً یک بار ارزیابی می‌شوند

حلقه در v2.769.3 آمد که کد گروه شفافیت renderer را یاد داد ماسک‌های نرم ساخته‌شده داخل یک گروه را در سراسر یک رندر دومرحله‌ای زنده نگه دارد و بعداً آزادشان کند. پاک‌سازی داخل یک بلوک finally بعد از یک حلقهٔ for یک یا دومرحله‌ای، داخل حلقهٔ به‌ازای هر tile بود. کاهش‌یافته به شکلش، قبل و بعد چنین به نظر می‌رسند:

// شکلی که دیدیم dcc64 (نسخهٔ کامپایلر 37.0) اشتباه کامپایل کرد
procedure DropMasksWhile(Masks: TList; Start: Integer);
begin
  while Masks.Count > Start do
  begin
    TObject(Masks[Masks.Count - 1]).Free;
    Masks.Delete(Masks.Count - 1);
  end;
end;

// جایگزین: کران‌ها یک بار ارزیابی می‌شوند، هیچ موقتی برای کهنه شدن نیست
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
  Idx: NativeInt;               // ‏TList.Count از Delphi 12 یعنی NativeInt است
begin
  for Idx := Masks.Count - 1 downto Start do
  begin
    TObject(Masks[Idx]).Free;
    Masks.Delete(Idx);
  end;
end;

در کد Win64 تولیدشده، ‏Count داخل شرط حلقه و Count خوانده‌شده داخل بدنه یک خانهٔ پشته را شریک بودند. شرط در ورود، قبل از اینکه چیزی آن را نوشته باشد، نسبت به آن خانه مقایسه می‌کرد و هیچ چیز بعد از Delete آن را تازه نمی‌کرد. وقتی گروهی ماسک نرم خودش را نساخته بود، بدنه به هر حال اجرا می‌شد و از یک فهرست خالی آیتم -1 را می‌خواست، پس در بیلدهای 64 بیتی هر صفحهٔ حامل چنین گروه شفافیتی با EListError شکست می‌خورد. کد Win32 برای همان سورس درست بود و v2.770.1 حلقه را جایگزین کرد

تلهٔ codegen یعنی Win64 در HotPDF در پاک‌سازی renderer: حلقه‌ای از نوع while که TList.Count را دوباره می‌خواند یک خانهٔ پشته را بین شرط و بدنه شریک می‌کرد، ‏dcc64 بعد از Delete هرگز آن را تازه نمی‌کرد، گروه‌های شفافیت خالی آیتم -1 را آزاد و EListError بالا می‌دادند، و فیکس یک حلقهٔ for downto است که کران‌هایش یک بار ارزیابی می‌شوند
درس عملی ارزان‌تر از ریشهٔ اصلی است: حلقه‌های downto با کران ثابت نمی‌توانند کهنه شوند و کار renderer تا وقتی dcc64 مجموعه را اجرا نکرده تمام نیست

این را به یک بازتولید حداقلی نکاهشانده‌ایم و یک حلقهٔ مستقل کوچک مثل DropMasksWhile خیلی ممکن است درست کامپایل شود؛ ‏try/finally اطراف و حلقه‌های تودرتو به نظر می‌رسد مهم‌اند. با آن به‌شکل کد‌تولیدی که روی یک نسخهٔ کامپایلر دیدیم رفتار کن، نه به‌شکل یک عیب شناخته‌شدهٔ هر کامپایلر Win64. درس عملی ارزان‌تر از ریشهٔ اصلی است: حلقه‌ای که شرطش شمارش یک مجموعه را در حالی که بدنه آن مجموعه را کوچک می‌کند دوباره می‌خواند لایق بازنویسی به یک for ... downto با کران ثابت است، و تغییرات renderer به یک اجرای کامل تست Win64 نیاز دارند نه فقط Win32

مکان‌یابی crash ای که فقط یک بیلد بهینه‌شدهٔ Win64 نشان می‌دهد

خرابی فقط در بیلد بهینه‌شدهٔ Win64 بازتولید می‌شد، پس مکان از ابزارهای بیرون IDE آمد. یک برنامهٔ اکتشافی کوچک یک هندلر استثنای برداری با AddVectoredExceptionHandler ثبت می‌کرد، پشته را روی اولین استثنا با RtlCaptureStackBackTrace می‌گرفت و آدرس‌های بازگشت را با استفاده از فایل map مفصل که لینکر با -GD می‌نویسد به نام توابع ترجمه می‌کرد. دی‌اسمبل کردن آن تابع بعد نشان داد مقایسه یک خانهٔ پشته یعنی [rbp+0x298] را می‌خواند که فقط داخل بدنهٔ حلقه نوشته می‌شد. همان سطح شواهد است که قبل از سرزنش یک کامپایلر می‌خواهی و کمتر از قدم زدن در یک بیلد release وقت گرفت

چرا High(Int64) یک کران بالای امن برای یک Double نیست؟

یک Double نمی‌تواند High(Int64) را نمایش دهد: تبدیل 9223372036854775807 به Double دقیقاً به 2^63 گرد می‌شود، یکی بعد از بزرگ‌ترین Int64. روی Win64 آن تبدیل داخل خود مقایسه اتفاق می‌افتد، پس D <= High(Int64) برای D = 2^63 مقدار True است و Round یا Trunc بعدی سرریز می‌کند

‏Win32 این را به همان دلیلی پنهان می‌کند که مشکل Power را پنهان می‌کرد. مقایسه در دقت Extended یعنی 80 بیتی با مانتیس 64 بیتی اجرا می‌شود که در آن High(Int64) دقیق است و 2^63 درست بزرگ‌تر مقایسه می‌شود. ‏Win64 نوع عریض‌تری برای پناه گرفتن ندارد. تبدیل خارج از بازه هم زیبا نیست: در تست‌های Win64 ما ‏Round(2^63) مقدار Low(Int64) را برمی‌گرداند، یک وارونگی علامت بی‌صدا، چه exInvalidOp ماسک شده باشد چه نه. ‏Win32 وقتی ماسک باشد همان مقدار را برمی‌گرداند و وقتی ماسک نباشد ‏EInvalidOp بالا می‌دهد

تلهٔ کران Int64 در HotPDF: یک Double نمی‌تواند High(Int64) را نمایش دهد، پس یک مقایسه در Win64 کران را تا 2^63 گرد می‌کند، ‏D برابر 2^63 چک را رد می‌کند و Round بی‌صدا Low(Int64) را برمی‌گرداند، در حالی که Win32 در Extended یعنی 80 بیتی مقایسه می‌کند که در آن کران دقیق است و همان مقایسه False است
یک تبدیل کل باگ است: کران روی همان مقداری که حذفش می‌کنی گرد می‌شود، پس سقف را به‌شکل یک literal با کوچک‌تر اکید بنویس
عبارتWin32Win64
‏Power(10, N) با N = 201E201.0000000200408773E20
‏Power(10, 100)، استثناها ماسک‌شده (پیش‌فرض Delphi 12 به بعد)1E100+Inf
‏Power(10, 100)، ‏exOverflow ماسک‌نشده1E100EOverflow
D <= High(Int64) با D = 2^63FalseTrue
‏Round(2^63)، ‏exInvalidOp ماسک‌نشدهEInvalidOpLow(Int64)

‏HotPDF این را در خوانندهٔ JSON پشت مقادیر کار سندش دید. ‏JSON هیچ محدودیت بازه‌ای روی اعداد نمی‌گذارد و سریالایزر قدیمی هر مقداری با Frac(Value) = 0 را با Round به یک عدد صحیح تبدیل می‌کرد، پس یک 1e19 کاملاً قانونی یا به یک عدد صحیح اشتباه یا به یک exception تبدیل می‌شد، بسته به ماسک. از v2.770.169 یک عدد کامل فقط وقتی به‌شکل عدد صحیح نوشته می‌شود که در Int64 جا شود، هر چیز دیگری متن ممیز شناورش را نگه می‌دارد و getterهای عدد صحیح برای مقادیر خارج از بازه پیش‌فرض فراخواننده را برمی‌گردانند به‌جای یک مقدار پیچیده‌شده

const
  TwoPow63 = 9223372036854775808.0;   // ‏2^63، دقیق در Double و Extended

function TryDoubleToInt64(const Value: Double; out R: Int64): Boolean;
begin
  R := 0;
  Result := not IsNan(Value) and not IsInfinite(Value) and
    (Frac(Value) = 0) and (Value >= -TwoPow63) and (Value < TwoPow63);
  if Result then
    R := Trunc(Value);
end;

function JsonNumberText(const Value: Double): string;
var
  R: Int64;
begin
  // فراخواننده‌ها اول NaN و بی‌نهایت‌ها را رد می‌کنند: JSON هیچ املایی برایشان ندارد
  if TryDoubleToInt64(Value, R) then
    Result := IntToStr(R)
  else
  begin
{$IFDEF FPC}
    Str(Value:24, Result);       // ‏ffGeneral در FPC Win64 روی 15 رقم می‌ایستد
    Result := Trim(Result);
{$ELSE}
    Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
  end;
end;

کران بالا همان literal یعنی 9223372036854775808.0 با یک < اکید است. آن ثابت 2^63 است، دقیق در هر دو Double و Extended، پس مقایسه روی هر پلتفرمی همان معنا را دارد. کران پایین می‌تواند از >= استفاده کند چون -2^63 دقیقاً Low(Int64) است. آزمودن اول IsNan و IsInfinite با ارزیابی مبتنی بر اتصال کوتاه، ‏NaN و بی‌نهایت‌ها را از Frac و مقایسه‌ها دور نگه می‌دارد که وقتی میزبان ماسک‌ش نکرده می‌توانند EInvalidOp بالا بدهند

تبدیل float-به-متن روی Win64 واقعاً چند رقم به تو می‌دهد؟

کمتر از آنچه می‌خواهی، روی دو کامپایلر از سه کامپایلر. ‏FloatToStrF(Value, ffGeneral, 17, 0) یعنی Free Pascal 3.3.1 روی Win64 روی 15 رقم معنادار می‌ایستد، پس 1/3 به‌شکل 0.333333333333333 برمی‌گردد و دو مقدار Double متفاوت می‌توانند به متن یکسان سریالایز شوند. ‏Str(Value:24, Text) به‌دنبال‌شدهٔ Trim، ‏17 رقم معنادار در نماد علمی تولید می‌کند، ‏3.3333333333333331E-001 برای همان مقدار، و فارغ از locale همیشه نقطه را به‌عنوان جداکنندهٔ اعشار می‌نویسد. اگر HotPDF روی FPC بخشی از ماتریس بیلدت است، ‏یادداشت‌های پشتیبانی HotPDF برای Free Pascal و Lazarus Win64 بقیهٔ تفاوت‌های پلتفرم را پوشش می‌دهد

‏Delphi درخواست 17 رقمی را می‌پذیرد اما دو تارگت Delphi همچنان در خروجی اختلاف دارند: ‏FloatToStrF(0.1, ffGeneral, 17, 0) روی Win32 مقدار 0.10000000000000001 و روی Win64 مقدار 0.1 می‌دهد. ‏RTL یعنی Win64 می‌تواند یک خطای گرد کردن رقم آخر هم موقع قالب‌بندی و هم موقع parse تولید کند، پس رقم‌های بیشتر شکاف را باریک می‌کند بدون تضمین اینکه هر الگوی بیت یعنی Double از یک رفت‌وبرگشت متنی جان سالم به در ببرد. مستندات HotPDF چنین وعده‌ای نمی‌دهد و تو هم نباید بدهی مگر اینکه یک قالب‌بند و parser درست‌گرد خودت عرضه کنی. ‏TFormatSettings.Invariant را بده، یا جداکننده را روی نسخه‌های قدیمی‌تر Delphi خودت جایگزین کن، تا یک locale آلمانی یا فرانسوی ویرگول داخل JSON ننویسد

چرا Assert.AreEqual روی Win64 کامپایل را متوقف می‌کند؟

Assert.AreEqual(3, Length(Arr)) روی یک آرایه پویا برای Win32 کامپایل می‌شود و برای Win64 با E2532 شکست می‌خورد، «نمی‌توان آرگومان نوع generic را از انواع آرگومان متفاوت استنتاج کرد»، چون Length یک آرایه پویا روی Win64 مقدار NativeInt برمی‌گرداند. با یک literal یعنی Integer در یک سمت و یک NativeInt یعنی 64 بیتی در سمت دیگر، ‏Assert.AreEqual<T> یعنی DUnitX نمی‌تواند روی یک T واحد بایستد و بیلد متوقف می‌شود

TList.Count از Delphi 12 همان خطا را راه می‌اندازد، جایی که property به NativeInt تبدیل شد؛ ‏Delphi 11 همچنان آن را به‌شکل Integer declare می‌کند. ‏Length یک string در هر دو پلتفرم مقدار Integer برمی‌گرداند و تحت‌تأثیر نیست، و برای همین خطا در بعضی یونیت‌های تست ظاهر می‌شود و در بعضی نه. آرگومان نوع را صریح بنویس، ‏Assert.AreEqual<NativeInt>(3, Length(Arr))، و پروژهٔ تست را با dcc64 قبل از commit کامپایل کن. مجموعه‌ای که فقط برای Win32 بیلد می‌شود تا وقتی یک نفر دیگر امتحانش کند به تو نمی‌گوید بیلد Win64 اش شکسته است

چک‌لیست port یعنی Win64 برای کد عددی در Delphi

  • فراخوانی‌های Power( و IntPower( با آرگومان‌های عدد صحیح را جست‌وجو کن؛ مقادیر از نوع Double بده یا توان‌های ده کپ‌شده را خودت بساز
  • تست‌های عددی را دست‌کم یک بار با حذف exOverflow و exInvalidOp از طریق SetExceptionMask اجرا کن، روی هر دو Win32 و Win64
  • کران بالای Int64 را به‌شکل < 9223372036854775808.0 بنویس، هرگز <= High(Int64) نه، و ‏NaN و بی‌نهایت‌ها را قبل از هر مقایسه‌ای رد کن
  • یک عدد parse‌شده را فقط به این دلیل که Frac صفر است به Int64 تبدیل نکن؛ اعداد JSON می‌توانند خیلی بزرگ‌تر باشند
  • حلقه‌های while ای را که موقع حذف آیتم‌ها ‏Count را دوباره می‌خوانند به حلقه‌های for ... downto با کران ثابت بازنویسی کن
  • روی FPC Win64 وقتی بیشتر از 15 رقم معنادار لازم داری از Str(Value:24, Text) استفاده کن
  • برای assertهای Length و Count از Assert.AreEqual<NativeInt> استفاده کن و تست‌ها را قبل از commit با dcc64 کامپایل کن
  • بعد از هر تغییری در یک parser یا renderer مجموعه regression کامل را روی Win32 و Win64 اجرا کن، نه فقط یکی از آن‌ها

فیکس‌های سمت کتابخانه که اینجا توضیح داده شدند همه از v2.770.169 در HotPDF هستند، پس import یعنی SVG و تبدیل XPS و رندر شفافیت و هندل کردن کار JSON حالا روی Win64 مثل Win32 رفتار می‌کنند. اگر فایل‌های PDF را از Delphi یا C++Builder برای هر دو پلتفرم تولید یا پردازش می‌کنی، ‏صفحهٔ کامپوننت HotPDF Delphi PDF دانلودها و فهرست کامل قابلیت‌ها را دارد