تشحن HotXLS قاعدة كود واحدة بـObject Pascal إلى كل إصدارات Delphi وC++Builder منذ XE5، وbuild-All-Lib-TRIAL.cmd هو السكربت الذي يثبت ذلك: 43 مسار بناء، يغطي 12 إصدارًا من Delphi على Win32 وWin64، إضافة إلى 10 حزم C++Builder لـWin32 و9 حزم لـWin64. ومن v2.363 إلى v2.374 لم يُشغّل ذلك السكربت حتى النهاية قط، وكان مسار XE5 مكسورًا طوال الوقت
لم يكن في الفشل أي غموض بعد رؤيته. فخمسة تراكيب متميزة يقبلها المصرّف الحالي بلا تعليق هي أخطاء صريحة في RAD Studio XE5، الذي تسميه مصفوفة البناء 12.0. أصلح إصدار v2.375.0 الحالات الخمس كلها، وعادت المصفوفة خضراء عند 43 من 43. وما يلي هو كل رفض، وسبب كون المصرّف القديم محقًا من ناحية النوع في حالتين منها، والجزء الأكثر إحراجًا: أن سكربت probe المكتوب لتشخيص الفوضى أبلغ عن نجاح زائف في تشغيله الأول
لماذا تعفّن مسار XE5 من دون أن يلاحظ أحد؟
تعفّن مسار XE5 لأن التطوير اليومي كان يشغّل مجموعة السكربتات الأربعة لإصدار 37.0 فقط، والبناء المحلي الأخضر لا يقول شيئًا عن مصرّف لم تستدعِه. والمصفوفة الكاملة سكربت منفصل بطيء يستدعيه مثبّت التجربة قبل أن يجمع Inno Setup الملفات، ولذلك يُشغّل وقت التغليف لا وقت commit. وقد اتسع هذا الفراغ لاثني عشر إصدارًا
يستحق حساب المسارات تفصيلًا لأنه موضع وهم التغطية. تعدّد DELPHI_TRIAL_VERSIONS الإصدارات من 12.0 إلى 37.0، ويبني كل إصدار من الإصدارات الـ12 مرتين، لـWin32 وWin64. ويسرد CB_TRIAL_WIN32_VERSIONS عشرة إصدارات، بينما يسرد CB_TRIAL_WIN64_VERSIONS تسعة فقط، لأن XE5 يملك مشروع حزمة C++Builder لكنه لا يشحن كائن بدء حزمة Win64 c0pkg64.o. اثنا عشر زائد اثني عشر زائد عشرة زائد تسعة تساوي 43. وتشغيل أربعة منها وتسميه قابلية الكود للنقل خطأ في التصنيف، وهو الخطأ المحدد الذي سمح بحدوث ذلك
وقد اصطدمت HotXLS بالشكل نفسه من المشكلة في الاتجاه المعاكس. فالوحدة الجديدة التي يمكن الوصول إليها عبر عبارة uses لكنها غائبة عن قائمة ملفات .cbproj تُترجم تمامًا تحت Delphi، لأن dcc يسحب الوحدات غير المدرجة ضمنيًا إلى الحزمة ولا يصدر في أسوأ الأحوال إلا تلميح W1033. أما C++Builder فيصدر ملف .obj فقط للوحدات المسماة في <DelphiCompile>، ولذلك يموت الكود نفسه في مرحلة ilink بسبب external غير محلول. تخفي سلسلة أدوات ما تلتقطه الأخرى. وهذه هي الحجة كلها لتشغيل المصفوفة بدل الثقة في مصرّف ممثل
تحويلات نوع صريحة يرفضها مصّرفو Win32 القدامى
حالتان من الحالات الخمس هما الخطأ نفسه بملابس مختلفة: تحويل نوع صريح مطبق على تعبير فاصلة عائمة لا على متغير. ففي Win32 تقيم المصرّفات الأقدم الحساب عبر مكدس x87، ولذلك يُحمل الجمع الذي يتضمن Double بدقة زائدة 80 بتًا ويصبح نوعه الساكن Extended ذي 10 بايتات. وتحويل 10 بايتات إلى TDateTime ذي 8 بايتات ليس تحويل نوع قانونيًا، فيقول المصرّف ذلك بالخطأ E2089 Invalid typecast
والتفصيل المحير هو أن صيغة المتغير سليمة. فـTDateTime(Serial) تُترجم في كل إصدار من المصفوفة، لأن Serial حجمه 8 بايتات أصلًا والتحويل يحافظ على الحجم. أضف أي شيء إليه، وسيتسع التعبير تحتك. والإصلاح ليس تحويلًا أوسع ولا تعريفًا شرطيًا، بل التوقف عن التحويل: فالـassignment الضمني من real إلى real يحول بصورة صحيحة في كل مصرّف تدعمه HotXLS، ويقول ما يعنيه الكود فعلًا
// مرفوض في XE5 (Win32): يُقيّم كل جمع كـExtended من 10 بايتات
// ويؤدي التحويل من 10 إلى 8 إلى E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // هذا مقبول: لا يوجد جمع
// آمن عبر الإصدارات: دع assignment من real إلى real ينفذ التحويل
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// الرفض من الفئة نفسها في حزم قيمة الخلية: تحويل Double صريح
// لعدد صحيح. اقسم بدلًا منه، فالمعامل يعيد قيمة real أصلًا
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // محمول
;
إن فرع Serial < 60 هو خيال السنة الكبيسة لعام 1900، وليس off-by-one: فالرقم التسلسلي 60 هو 1900-02-29 غير الموجود في Excel، ولذلك تحتاج الأرقام التسلسلية دونه إلى اليوم الإضافي قبل أن تراها DecodeDate. ولا ينبغي لأعمال قابلية النقل أن تغيّر هذا النوع من المنطق بصمت، ولهذا يزيل التعديل الآمن التحويل ويترك الحساب كما هو
ماذا ينكسر عندما يكون nil معاملًا إجرائيًا؟
يفشل ربط nil العاري أثناء حل overload عندما يُتوقع نوع إجرائي في المصرّفات الأقدم. وموضع الاستدعاء في HotXLS هو ResolveIndexedColor، وهي دالة overloaded تأخذ callback من نوع TXLSTryResolveSystemColor لا يحتاجه معظم المستدعين. تحل المصرّفات الأحدث nil مقابل المعامل الإجرائي وتختار overload الصحيحة. أما XE5 فلا يفعل ذلك، ويشير التشخيص إلى مجموعة overload بدل المعامل، وهكذا تضيع عشرون دقيقة
والجواب المحمول هو إعطاء callback الفارغ نوعًا. فالمتغير ذو النوع الإجرائي على مستوى الوحدة يُهيّأ إلى الصفر بواسطة اللغة، ولذلك يكون nil أصلًا من دون مهيئ، ويحمل معلومات النوع التي يريدها المحلل القديم. وعندما يكون متغير مستوى الوحدة مبالغة، يؤدي متغير محلي مtyped مسند إليه nil المهمة نفسها
var
// لا يرتبط literal إجرائي nil في حل overload للمصرّفات الأقدم
// بينما تحمل متغيرات النوع الصريح والمهيأة إلى الصفر النوع المطلوب
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// الإصلاح نفسه مع متغير محلي ذي نوع صريح في مصنف XLSX
function TXLSXWorkbook.ResolveIndexedColor(AIndex: Int64;
ASpace: TXLSIndexedColorSpace;
out AResolution: TXLSIndexedColorResolution): Boolean;
var
NoResolver: TXLSTryResolveSystemColor;
begin
NoResolver := nil;
Result := ResolveIndexedColor(AIndex, ASpace, xicrGeneral, NoResolver,
AResolution);
end;
لاحظ أن هذا اختلاف حقيقي على مستوى اللغة وليس خطأ مصرّف يستحق الالتفاف عليه بتعريفات. فالمتغير المهيأ إلى الصفر صحيح في كل إصدار من المصفوفة ولا يكلف إلا سطرًا واحدًا، ولذلك لا يوجد هنا أي ترجمة شرطية. لا تلجأ إلى {$IF CompilerVersion} إلا عندما تختلف المنصة فعلًا بين الإصدارات، وهو ما يحدث مرة واحدة بالضبط في هذه الدفعة
تتنقل دوال VCL المحمية بين الإصدارات
تكون TPicture.LoadFromStream ذات رؤية public في VCL الحالية وprotected في الإصدارات الأقدم التي تدعمها HotXLS، ولذلك يترجم الاستدعاء المباشر الآن ويفشل حينها. وتستخدمها HotXLS للتحقق من أن حمولة صورة خلفية ورقة العمل تُفك فعلًا، وهو فحص توقيع يجري قبل التزام مُصدّر HTML بتضمين البايتات. والجواب التقليدي في Pascal ينطبق هنا: عرّف سليلًا في الوحدة نفسها لمجرد توسيع الرؤية، ثم حوّل عبره في موضع الاستدعاء
type
// تكون TPicture.LoadFromStream محمية في إصدارات VCL الأقدم التي
// تدعمها المكتبة؛ يكشفها سليل في الوحدة نفسها
TXlsxPictureAccess = class(TPicture);
// ...
Stream.WriteBuffer(AData[1], Length(AData));
Stream.Position := 0;
TXlsxPictureAccess(Picture).LoadFromStream(Stream);
Result := (Picture.Graphic <> nil) and not Picture.Graphic.Empty and
(Picture.Graphic.Width > 0) and (Picture.Graphic.Height > 0);
حيلة فئة الوصول آمنة هنا لأن السليل لا يضيف حقولًا ولا يُنشأ أبدًا؛ فالتحويل يغير فقط ما يسمح المصرّف لك بتسميته. ومع ذلك يستحق تعليقًا عند التصريح، لأن القارئ الذي يبني دائمًا على IDE حديثة سيرى نوعًا بلا فائدة واضحة. وتظهر معالجة صور الخلفية مرة أخرى في مسار التصيير المخصص لشبكة VCL، حيث تغذي الحمولة المفكوكة الورقة على الشاشة
تغير نوع رمز GdiplusStartup مرتين
الرفض الوحيد في الدفعة الذي يتطلب فعلًا ترجمة شرطية هو نوع معامل var في GdiplusStartup، إذ تغير بين أجيال VCL بطريقة لا تترك صياغة واحدة صالحة في كل مكان. وقد ثبّت الفحص إصدارًا بإصدار السلوك الفعلي: تقبل مسارات 12.0 إلى 20.0 Cardinal فقط، وتقبل مسارات 21.0 و22.0 THandle أو ULONG_PTR فقط، بينما تقبل 23.0 و37.0 كليهما. وبأسماء الإصدارات، يكون Cardinal من XE5 إلى 10.3 Rio، وTHandle منذ 10.4 Sydney. ولأن نطاقي القبول لا يتداخلان في الإصدارات من 12.0 إلى 22.0، فلا يعمل تصريح غير مشروط: يعتمد الحارس على CompilerVersion >= 34، أي Sydney، ويؤهل الاستدعاء كاملًا كـWinapi.GDIPAPI.GdiplusStartup حتى لا يستبدل ترتيب حل الوحدات تصريحًا آخر في إصدار يقع وسط النطاق
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// يتبع نوع معامل var في GDIPAPI لجيل VCL
// Cardinal حتى Rio، وTHandle منذ Sydney
{$IF CompilerVersion >= 34}
StartupToken: THandle;
{$ELSE}
StartupToken: Cardinal;
{$IFEND}
TiffEncoder: TGUID;
begin
FillChar(StartupInput, SizeOf(StartupInput), 0);
StartupInput.GdiplusVersion := 1;
CheckStatus(Winapi.GDIPAPI.GdiplusStartup(StartupToken, @StartupInput,
nil), 'startup');
if GetEncoderClsid('image/tiff', TiffEncoder) < 0 then
raise EInvalidGraphic.Create('GDI+ TIFF encoder is unavailable');
// ... encode ...
end;
هذا هو فرع TIFF في مُصدّر صور الصفحة، ولذلك فإن نصف قطر الانفجار من جعل الأمر خطأ هو سطح تصدير raster كامل، بما فيه المسارات الموصوفة في تصدير نطاق خلايا كصورة واحدة. لاحظ أيضًا ما لا يدعيه الحارس: إن ULONG_PTR وTHandle لهما العرض نفسه على المنصتين، ولذلك يتعلق الاختيار بالاسم الذي يسميه التصريح لا بصحة 32 بت مقابل 64 بت
لماذا لم يبلغ probe الأول عن شيء؟
لم يبلغ probe الإصدار عن شيء في تشغيله الأول لأن إسنادات res=$(...) كانت تُجرى داخل subshell، حيث لا تنتقل إلى الأب. ويعيد dcc32 القيمة 0 عند النجاح، ولذلك كان رمز الخروج هو الإشارة الصحيحة التي ينبغي التقاطها، لكن السكربت كان يلتقطه في متغير يتوقف عن الوجود بعد سطر واحد. عادت كل المسارات فارغة، وبدا الناتج كأنه probe لم يترجم شيئًا، وهو بالضبط ما كان عليه
أما الفشل الثاني فكان أسوأ، لأنه أنتج إجابة خاطئة بدل عدم إنتاج إجابة. فقد صنف probe مسارًا بعد عد الأسطر المطابقة لـError، ولا تسبق Delphi كل fatal بهذه الكلمة. فـF1026 File not found قاتل ولا يطابق، ولذلك حصل probe الذي لم يستطع حل وحدة أصلًا على تقييم نجاح نظيف. ولا تشحن XE5 الوحدة Winapi.GDIPOPS.dcu، وقد أصابها probe الأول بالضبط، ومع ذلك تحول إلى أخضر زائف. والقاعدة التي خرجت من ذلك ضيقة وتستحق الذكر بوضوح: احكم على probe للمصرّف من artifact الناتج أو من سطر الملخص الخاص بالمصرّف نفسه، ولا تحكم أبدًا بالبحث النصي عن كلمة. فالبحث في stderr عن Error heuristic يفشل في الاتجاه الذي لا يمكنك تحمل فشله، إذ يبلغ عن نجاح بصمت
ما كلفة دعم عقد من المصرّفات فعلًا؟
الحساب الصادق هو أن تغييرات الكود هنا بسيطة والتغييرات الإجرائية ليست كذلك. أصلحت أربع حالات من الخمس بكتابة Pascal عادي أكثر، لا بإضافة آلية إصدارات: احذف تحويلًا، واقسم بدل التحويل، وأعط nil نوعًا، وصرّح بفئة وصول. أما GdiplusStartup وحده فقد استحق {$IF}. ولا تتحول قاعدة كود تمتد من XE5 إلى الإصدار الحالي إلى غابة تعريفات شرطية ما لم تسمح أصلًا بتراكم التحويلات الصريحة الصعبة واصطلاحات أحدث مصرّف
الكلفة الحقيقية هي وقت البناء والانضباط. فثلاثة وأربعون مسارًا سكربت بطيء، وهذا بالضبط سبب انجرافه إلى وقت التغليف ثم إلى عدم تشغيله قط. والحل الأوسط الذي يمكن الدفاع عنه هو الاحتفاظ بحلقة السكربتات الأربعة السريعة للتكرار، وتشغيل المصفوفة الكاملة وفق جدول لا يمكن تخطيه، لأن نمط الفشل ليس بناءً مكسورًا تلاحظه، بل IDE مدعومًا توقف دعمه بصمت منذ اثني عشر إصدارًا
وهذا الالتزام هو الوجه الآخر لشحن مكوّن أصلي أصلًا. تقرأ HotXLS وتكتب XLS وXLSX وODS عبر Object Pascal فقط، من دون تثبيت Excel ومن دون اعتماد COM، وهو ما يجعل أتمتة المصنفات من دون Office ممكنة على خادم مقيد. والخاصية نفسها تعني أن المصرّف هو عقد المنصة كله، ولذلك فإن كل إصدار في المصفوفة وعد يجب التحقق منه من جديد لا افتراضه
تأتي مصفوفة البناء عبر المصرّفات والكود الآمن عبر الإصدارات المذكوران هنا ضمن مكوّن جداول HotXLS لـDelphi، الذي يدعم Delphi وC++Builder من XE5 إلى الإصدار الحالي مع ملفات مكتبة جاهزة لكل IDE مدعوم