يستطيع كود Delphi على Win64 أن يفشل حيث يجري المصدر نفسه بنظافة على Win32، وقد اصطدم مكوّن HotPDF Delphi PDF component بخمس حالات كهذه خلال جولة تحصين حديثة: ربط Power(10, N) بالتحميل الزائد Single، وحلقة while تقرأ TList.Count متقادماً، وحد High(Int64) يقرّب صعوداً إلى 2^63، ونص فواصل من 15 رقماً على FPC، وتأكيدات اختبار توقف عن التجميع
لا يظهر شيء من ذلك إن كنت تبني وتختبر Win32 فقط، وهذه بالضبط هي طريقة تسللها. الحالات أدناه تأتي من مستوردي SVG و XPS لدى HotPDF ومن محرك عرض صفحاته وقارئ مهام JSON عنده، والنتائج العددية المقتبسة أُعيد إنتاجها ببرامج تنقيط صغيرة بُنيت لـ Win32 و Win64. وإن كنت تنقل قاعدة كود Delphi إلى 64-بت فكل حالة منها تستحق grep
لماذا يفيض Power(10, 100) على Win64 وحده؟
على Win64 يفاضل System.Math.Power(10, N) بوسائط أعداد صحيحة إلى التحميل الزائد Single، فتُحسب النتيجة وتعاد بدقة مفردة وكل ما فوق نحو 3.4E38 تقريباً يفيض. وعلى Win32 يفاضل النداء نفسه إلى التحميل الزائد Extended ويجري على وحدة FPU من نوع x87 بدقة 80-بت، فـ Power(10, 100) هي ببساطة 1E100
يصرّح System.Math عن Power للقيم Extended و Double و Single، زائد عائلة IntPower مطابقة يستدعيها Power حين يكون الأس عدداً كاملاً. وعلى Win64 فإن Extended مجرد اسم بديل لـ Double (SizeOf(Extended) = 8)، ولوسيطين عددين يختار المترجم نسخة Single. والمكشِف هو الدقة لا الفيضان وحده: على Win64 تعيد Power(10, 20) القيمة 1.0000000200408773E20، وهي بالضبط Single(1E20). أما نتيجة Double فستُطبع 1E20. رأينا الربط نفسه مع كل مترجم Win64 جربناه، من Delphi 10.3 حتى نسخة المترجم 37.0
وما يجري بعدها يتوقف على قناع استثناءات الفاصلة العائمة. تخفي Delphi 12 وما بعدها كل استثناءات الفاصلة العائمة افتراضياً، فالفيضان صامت: تعيد Power(10, 100) القيمة +Inf وتعيد Power(10, -100) القيمة 0. أما Delphi 11 وما قبلها فتترك exOverflow مكشوفاً، ويرفع النداء نفسه EOverflow. والتطبيقات التي تضبط القناع بنفسها، والـ DLLs المحمَّلة في مثل تلك المضيفات، تحصل على أي سلوك اختاره المضيف، ولهذا لا تستطيع مكتبة أن تفترض أياً من النتيجتين
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 طيلة مدة اختبار هو أرخص طريقة لترى ما يراه مترجم أقدم أو مضيف صارم. وعلى مترجم حديث بالإعدادات الافتراضية لا يُسقط الخللُ البرنامجَ بل ينتج لانهايات وأصفاراً، وهذه أصعب بكثير على الالتقاط في سجل اختبار. وأعِد القناع السابق في finally: القناع حالة لكل خيط، وبقية جولة الاختبار ترث ما تركته وراءك
كيف بلغ التحميل الزائد استيراد SVG و XPS لدى HotPDF
يتقاسم قارئا مسارَي SVG و XPS لدى HotPDF ماسحَ أعداد واحد، وكان ذلك الماسح يكيّف المنصّف بـ Power(10, Exponent) متى قرأ أساً. وأي SVG يمرر إلى THotPDF.ImportSVGFormXObject (مدخل استيراد SVG إلى PDF بوصفه form XObjects قابلة لإعادة الاستخدام)، وأي هندسة مسارات تعالج أثناء تحويل 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؛ وبعدها تقرّب كل ضربة، وبعد مئة ضربة يجلس المقياس على بُعد بضع وحدات في آخر خانة عن 1E100 المقرّبة صحيحاً. لإحداثيات الرسم ذلك غير مرئي. أما لتحويل نص إلى double عام الغرض يجب أن يعيد كل قيمة بتاً ببت فليس كافياً، وتحتاج خوارزمية تحويل مقرّبة صحيحة بدلاً منه
حين يقرأ dcc64 قيمة TList.Count متقادمة في حلقة while
لاحظنا أن مترجم Win64 (dcc64، نسخة المترجم 37.0) ولّد كوداً لحلقة while List.Count > Start do تحذف من نهاية القائمة وتقارن مقابل مؤقت على المكدّس بدل إعادة قراءة Count. والإعادة التي أصلحتها كانت حلقة for ... downto تُقيَّم حدودها مرة واحدة بالضبط بحكم التعريف
وصلت تلك الحلقة في v2.769.3 التي علّمت كود مجموعات الشفافية في المحرك أن يبقي الأقنعة الرخوة المنشأة داخل مجموعة حية عبر عرض ذي تمريرتين ويحررها بعدها. وجلس التنظيف في كتلة finally بعد حلقة for بتمريرة أو تمريرتين، داخل حلقة كل بلاطة. مختصَرةً إلى شكلها يبدو قبلها وبعدها هكذا:
// الشكل الذي رأيناه يُجمَّع خطأً بـ 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 من نوع NativeInt منذ Delphi 12
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 الحلقة
لم نخفض هذه الحالة إلى إعادة إنتاج مصغرة، وحلقة مستقلة صغيرة مثل DropMasksWhile قد تُجمَّع صحيحة تماماً؛ فالـ try/finally المحيطة والحلقات المتداخلة يبدو أنها تؤثر. عاملها توليدَ كود لاحظناه على نسخة مترجم واحدة لا عطباً معروفاً في كل مترجم Win64. والدرس العملي أرخص من السبب الجذري: الحلقة التي يعيد شرطها قراءة عدد مجموعة بينما الجسم يقلصها تستحق إعادة كتابة حلقةَ for ... downto بحدود ثابتة، وتغييراتُ المحرك تحتاج جولة اختبار كاملة على Win64 لا على Win32 وحده
تحديد موقع انهيار لا يظهره إلا بناء Win64 مُحسَّن
لم يعُد الفشل إلا في بناء Win64 المُحسَّن، فجاء الموقع من أدوات خارج بيئة التطوير. سجّل برنامج تنقيط صغير معالجَ استثناءات موجَّهاً بـ AddVectoredExceptionHandler، والتقط المكدّس عند أول استثناء بـ RtlCaptureStackBackTrace، وترجم عناوين العودة إلى أسماء توابع بملف الخريطة المفصّل الذي يكتبه الرابط مع -GD. وأظهر تفكيك ذلك التابع حينها المقارنة تقرأُ خانةَ مكدّس، [rbp+0x298]، لم يكتبها قط سوى داخل جسم الحلقة. تلك هي درجة الدليل التي تريدها قبل أن تتهم مترجماً، وكلّفت أقل من التنقل بخطوة خطوة في بناء إصدار
لماذا ليست High(Int64) حدّاً أعلى آمناً لـ Double؟
لا يستطيع Double تمثيل High(Int64): تحويل 9223372036854775807 إلى Double يقرّب صعوداً إلى 2^63 بالضبط، أي ما بعد أكبر قيمة Int64 بخطوة. وعلى Win64 يقع ذلك التحويل داخل المقارنة نفسها، فـ D <= High(Int64) تساوي True عند D = 2^63، وتفيض قيمة Round أو Trunc التالية
ويخفي Win32 هذا للسبب نفسه الذي أخفى به مشكلة Power. تجري المقارنة بدقة Extended ذات 80-بت ومنصِّف 64-بت، حيث تكون High(Int64) تامة وتقارن 2^63 صحيحاً أنها أكبر. وليس لدى Win64 نوع أوسع يلجأ إليه. وتحويل خارج المدى ليس جميلاً هو الآخر: في اختباراتنا على Win64 أعادت Round(2^63) القيمة Low(Int64)، قلبَ إشارة صامتاً، سواء كان exInvalidOp مخفياً أم لا. ويعيد Win32 القيمة نفسها حين يكون مخفياً ويرفع EInvalidOp حين يكون مكشوفاً
| التعبير | Win32 | Win64 |
|---|---|---|
Power(10, N) عند N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100) مع إخفاء الاستثناءات (افتراضي Delphi 12 وأحدث) | 1E100 | +Inf |
Power(10, 100) مع كشف exOverflow | 1E100 | EOverflow |
D <= High(Int64) عند D = 2^63 | False | True |
Round(2^63) مع كشف exInvalidOp | EInvalidOp | Low(Int64) |
قابلت HotPDF ذلك في قارئ JSON خلف قيم مهام المستندات عندها. لا يضع JSON حدَّ مدى للأعداد، وكان المُسلسِل القديم يحول أي قيمة بـ Frac(Value) = 0 إلى عدد صحيح بـ Round، فصار 1e19 المشروعة تماماً إما عدداً صحيحاً خاطئاً وإما استثناءً، بحسب القناع. ومنذ v2.770.169 لا يُكتب العدد الكامل عدداً صحيحاً إلا إذا اتسع في Int64، ويبقي كل ما عداه نصه ذا الفاصلة العائمة، وتعيد دوال جلب العدد الصحيح افتراضَ المستدعي للقيم خارج المدى بدل قيمة ملفوفة
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;
الحد الأعلى هو الحرف 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 للقيمة نفسها، وتكتب دائماً نقطةً فاصلة عشرية مهما كان المكان المحلي. وإن كانت HotPDF على FPC جزءاً من مصفوفة بنائك فملاحظات دعم HotPDF لـ Free Pascal و Lazarus على Win64 تغطي بقية فروق المنصة
ويقبل Delphi طلب الـ 17 رقماً، لكن الهدفين في Delphi لا يزالان يختلفان على المخرج: تعطي FloatToStrF(0.1, ffGeneral, 17, 0) القيمة 0.10000000000000001 على Win32 والقيمة 0.1 على Win64. ويستطيع RTL على Win64 أيضاً إدخال خطأ تقريب في آخر خانة عند التنسيق وعند التحليل معاً، فمزيد الرقوم يضيّق الفجوة دون أن يضمن أن ينجو كل نمط بتات Double من جولة نص ذهاباً وعوداً. لا يعد توثيق HotPDF بمثل ذلك الوعد، ولا ينبغي لك أن تعد بمثله إلا إن شحنت منسِّقاً ومحللاً مقرَّبَين صحيحين من صنعك. ومرّر TFormatSettings.Invariant، أو استبدل الفاصل بنفسك على إصدارات Delphi الأقدم، حتى لا يكتب مكان محلي ألماني أو فرنسي فاصلةً في JSON
لماذا يتوقف Assert.AreEqual عن التجميع على Win64؟
يُجمَّع Assert.AreEqual(3, Length(Arr)) على مصفوفة ديناميكية لـ Win32 ويفشل لـ Win64 بالخطأ E2532، "Couldn't infer generic type argument from different argument types"، لأن Length لمصفوفة ديناميكية تعيد NativeInt على Win64. وبحرف عدد صحيح Integer في جهة و NativeInt ذي 64-بت في الأخرى لا تستطيع Assert.AreEqual<T> العامة في DUnitX الاستقرار على T واحد، فيتوقف البناء
ويوقظ TList.Count الخطأ نفسه منذ Delphi 12، حيث صارت الخاصية NativeInt؛ وما زالت Delphi 11 تصرّح عنها Integer. أما Length لسلسلة string فتعيد Integer على المنصتين وغير متأثرة، ولهذا يظهر الخطأ في بعض وحدات الاختبار ولا يظهر في غيرها. اكتب وسيط النوع صراحةً، Assert.AreEqual<NativeInt>(3, Length(Arr))، وجمِّع مشروع الاختبار بـ dcc64 قبل الإيداع. فالمجموعة التي لا تُبنى إلا لـ Win32 لن تخبرك أن بناء Win64 عندها مكسور حتى يحاول ذلك شخص آخر
قائمة تحقق نقل Win64 لكود Delphi العددي
- ابحث عن نداءات
Power(وIntPower(بوسائط أعداد صحيحة؛ مرّر قيماً بنوعDoubleأو ابنِ قوى العشرة المحدودة بنفسك - جرِّ الاختبارات العددية مرة واحدة على الأقل مع إزالة
exOverflowوexInvalidOpعبرSetExceptionMask، على Win32 و Win64 معاً - اكتب الحد الأعلى
Int64بصيغة< 9223372036854775808.0لا<= High(Int64)أبداً، وارفض NaN واللانهايات قبل أي مقارنة - لا تحوّل عدداً محلولاً إلى
Int64لمجرد أنFracتساوي 0؛ فأعداد JSON قد تكون أكبر بكثير - أعد كتابة حلقات
whileالتي تعيد قراءةCountوهي تحذف عناصر كحلقاتfor ... downtoبحدود ثابتة - على FPC Win64 استخدم
Str(Value:24, Text)حين تحتاج أكثر من 15 رقماً معنوياً - استخدم
Assert.AreEqual<NativeInt>لتأكيداتLengthوCount، وجمّع الاختبارات بـ dcc64 قبل الإيداع - بعد أي تغيير على محلل أو محرك عرض، جرِّ المجموعة الاختبارية الكاملة على Win32 و Win64 لا على أحدهما فقط
الإصلاحات على جانب المكتبة الموصوفة هنا كلها في HotPDF منذ v2.770.169، فيتصرف استيراد SVG وتحويل XPS وعرض الشفافية ومعالجة مهام JSON الآن بالطريقة نفسها على Win64 كما على Win32. وإن كنت تولّد أو تعالج ملفات PDF من Delphi أو C++Builder للمنصتين معاً فصفحة مكوّن HotPDF Delphi PDF component فيها التنزيلات وقائمة الميزات الكاملة