مقال تقني

مصائد LP64 في Delphi على Linux64: Longint والمؤشرات وWIN64

يتّبع Delphi Linux64 نموذج البيانات LP64، لذا يحتل Longint وLongWord هناك 8 بايتات، بينما يبقيهما Delphi على Windows (Win32 وWin64) وFree Pascal على كل target عند 4. ونقل PDFlibPas، مكتبة PDF لـ Delphi، إلى dcclinux64 يصطدم بهذا الاختلاف بثلاث صور مختلفة، إضافة إلى مصيدة عرض المؤشر: اختبار {$IFDEF WIN64} يستخدم لاختيار عدد بحجم المؤشر، فيختار بهدوء العرض الخطأ على كل target بحجم 64 بت خارج Windows

لا شيء من هذه المصائد غريب. كل واحدة منها عادة يجعلها Windows آمنة بالصدفة: كتابة Integer حيث تقول RTL ‏Longint، أو محاكاة بنية C بحقول Longint، أو طرح السؤال «هل هذا Win64؟» بينما السؤال الحقيقي «هل المؤشر 8 بايتات؟». يمكن أن يحمل الكود هذه العادات سنوات دون عواقب، لأن اللفظين يعنيان الشيء ذاته على Win32 وWin64 وFPC. على Delphi Linux64 يتوقفان عن معنى الشيء نفسه، والمترجم هو أول من ينتبه

كل ما يلي يتحدث عما يمسك به المترجم. تُبنى PDFlibrary.pas بـ dcc32 وdcc64 وdcclinux64 من شجرة مصدر واحدة، وهذه هي المسائل التي تقف بين عادات Windows المنفردة وهذا الهدف الثالث

ما الذي يتغير عندما يستهدف Delphi ‏Linux64؟

الأنواع الصحيحة التي يتغير عرضها على Delphi Linux64 هي بالضبط Longint وLongWord؛ أما Integer وCardinal فيبقيان على 4 بايتات في كل منصة Delphi، وNativeInt يتبع المؤشر. ‏Windows هو LLP64 حيث تنمو المؤشرات وحدها عند 64 بت. أهداف Delphi ‏POSIX ذات 64 بت (Linux64 ومترجمات macOS وiOS ذات 64 بت) هي LP64، حيث ينمو long في C أيضًا، وجعلت Embarcadero ‏Longint في Pascal يتبعه

النوعDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 على كل target
NativeInt / Pointer488بحجم مؤشر الهدف
long في C (‏ABI المنصة)448يتبع ABI لنظام التشغيل

عمود Free Pascal هو المصيدة لمكتبة عبر المترجمات. يقدم PDFlibPas بناء Linux عبر FPC أصلًا، ويبقي FPC ‏Longint عند 32 بت على Linux x86-64. فكانت عبارة «تعمل على Linux» صادقة ولم تقل لنا شيئًا عن Delphi Linux64. وقد تبيّن أن خلفية FPC غير Windows هي الأساس الصحيح لنقل Delphi، لكن أحجام الأنواع تحتها تختلف

مصفوفة عروض Integer وLongint وNativeInt وlong في C عبر أهداف Delphi Win32 وWin64 وLinux64 وFree Pascal في مكتبة PDFlibPas
على Delphi Linux64 يتوسع Longint وLongWord وlong في C وحدهم، بينما يبقيهم FPC عند 4 بايتات على كل target

لماذا يقص اختبار WIN64 المؤشرات على Linux؟

الجواب المختصر: ‏WIN64 تعني «Windows بحجم 64 بت» لا «64 بت» فحسب، ومعاملتهما كمرادفين يختار عرض العدد الصحيح الخطأ. ‏PDFlibDither، منقّط بانتشار الخطأ على طريقة Floyd-Steinberg يخفض صورة نقطية بعمق 24 بت إلى مستويات قليلة لكل قناة، يمشي ثلاثة صفوف من حدود الخطأ بفعل حسابي خام على المؤشرات، والعدد بحجم المؤشر الكامن خلف ذلك الحساب يجب أن يتبع عرض CPU لا نظام التشغيل. الشرطي الساذج يخطئ في ذلك:

// الأسلوب الساذج: العرض يتحدد بنظام التشغيل، فيحصل كل target بحجم 64 بت غير Windows على Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// المسح المتعرج يحرّك مؤشرات صفوف الخطأ حدًا واحدًا في كل خطوة
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: يعيد كتابة الـ 32 بت الأدنى فقط
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064، لا يمكن الإسناد إلى الطرف الأيسر

// الصواب: نوع بحجم المؤشر في كل مكان
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: ‏NativeInt خارج Windows، وIntPtr / Integer على Win64 / Win32

مع ذلك الشرط، يتحول OSPtr إلى Integer من 4 بايتات على FPC لـ Linux x86-64 وmacOS x64 وARM64، وInc(OSPtr(ErrorR), ...) يعيد تفسير أول 4 بايتات من مؤشر بعرض 8 بايتات كعدد صحيح ويزيدها. على CPU صغير النهاية (little-endian) ذلك هو النصف الأدنى من العنوان. النصف الأعلى لا يُلمس مطلقًا، ولهذا يختبئ الخطأ جيدًا: النتيجة صحيحة حتى تلتف الـ 32 بت الأدنى، وعندئذ يقفز المؤشر صامتًا بمقدار 4 GiB إلى الوراء. يترجم FPC تحويل النوع للمتغير دون اعتراض. أما Delphi فيرفض تحويل متغير إلى نوع بحجم مختلف على الطرف الأيسر من الإسناد، فيتوقف dcclinux64 عند E2064 عند أول Inc

هذه هي لاتناظر المفيد هنا: مترجم يرفض تحويل النوع يلتقط خطأ عرض المؤشر الذي يقبله مترجم آخر بصمت، قبل أن يشغّل الكود سطرًا واحدًا على الهدف الجديد. يستخدم PDFlibPas لهذا TPLIntPtr من PDFlibTypes، الذي يختبر OS_WINDOWS أولًا ثم يميز بين Win64 وWin32 بعدها، فتحصل أهداف غير Windows دائمًا على NativeInt

القاعدة التي خرجنا بها: لا تستنتج عرض المؤشر أبدًا من تعريف خاص بنظام التشغيل. إن احتجت عددًا بحجم المؤشر، فاستخدم NativeInt / NativeUInt (كلاهما موجود في Delphi وFPC) أو اسمًا بديلًا في المشروع مبنيًا عليهما، وتخطَّ الشرطي كلية. وإن احتجت فعلًا التفريع على العرض في كود Delphi، فـ CPU64BITS هو التعريف الذي يجيب عن السؤال الذي تطرحه فعلًا

مخطط قرار أنواع بحجم المؤشر في كود PDFlibPas على Delphi: تعريف WIN64 يختار Integer من 4 بايتات على أهداف غير Windows ذات 64 بت حتى يحميها NativeInt
تعريف WIN64 يسأل عن نظام التشغيل، فأنواع بحجم المؤشر يجب أن تأتي من NativeInt أو من اختبار CPU64BITS بدلًا عنه

تجاوزات TStream: انسخ تواقيع RTL حرفًا بحرف

يجب على ابن TStream على Delphi Linux64 أن يصرّح وينفذ Read وWrite وSetSize بوسائط Longint، تمامًا كما تصرح بها RTL، لأن Integer لم يعد النوع نفسه. لدى PDFlibPas ‏TPDFlibWideFileStream خاص به لأسماء الملفات Unicode، وكان تصريح الصنف يستخدم Longint فعلًا. أما قسم التنفيذ فكان يستخدم Integer

Type
  TPDFlibWideFileStream= Class(TStream)
  Protected
    Procedure SetSize(NewSize: Longint); Override;
  Public
    Function Read(Var Buffer; Count: Longint): Longint; Override;
    Function Write(Const Buffer; Count: Longint): Longint; Override;
  End;

// قبل: مقبول لسنوات على Win32 وWin64 وFPC حيث Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// بعد: تكرر ترويسة التنفيذ التصريح حرفيًا
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

على كل مترجم قابلته المكتبة، كان Integer وLongint النوع نفسه من 4 بايتات، فترويسة غير مطابقة لا يمكن تمييزها عن مطابقة. على Delphi Linux64 لم يعد التنفيذ يطابق تصريحه الخاص، فتتوقف الوحدة عن الترجمة. هذه الجرحة أعمق مما يوحي حجمها، لأن PDFlibExtra وحدة أساس تعتمد عليها معظم المكتبة، فترويسة تنفيذ واحدة غير مطابقة تمنع معظم الوحدات من الترجمة

والمنطق نفسه يسري بالاتجاه المعاكس على الكود الجديد. عندما تجاوز تابعًا من RTL، انسخ أنواع وسائطه حرفيًا، حتى عندما «تعلم» أن نوعين متطابقان. ‏Delphi Linux64 هو ببساطة أول target يتوقف عندها ذلك العلم عن صحيحه

أي السجلات تحتاج Integer بدل Longint، وأيها يجب أن يبقيها؟

السجل الذي يحاكي صيغة ملف أو بنية Windows يجب أن يستخدم أنواعًا ذات عرض ثابت (Integer وCardinal وInt32)، بينما السجل الذي يحاكي بنية C مصرّحًا بها بـ long ينبغي أن يبقي Longint / LongWord. الحالتان تبدوان متطابقتين في المصدر وتحتاجان إجابتين متضادتين، لأن LONG في Windows هو دائمًا 32 بت وlong في C هو ما يقوله ABI للمنصة

مصفوفة قرار لحقول سجلات Delphi على LP64: عرض ثابت عبر Integer للبايتات على القرص، وLongint يبقى لروابط long في C في PDFlibPas
السجلات التي تحاكي بايتات تأخذ عروضًا ثابتة، ولا يبقي Longint إلا روابط long في C، مدعومة بتأكيد حجم من سطر واحد
Type
  // Uniscribe GOFFSET: حقلان من Windows LONG، أي 8 بايتات إجمالًا دائمًا
  TGoffset= Packed Record
    du: Integer;  // كان Longint: نما السجل إلى 16 بايتة على Delphi Linux64
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE: ترويسة تُملأ بـ Move ثابتة من 68 بايتة من بيانات السجل
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // كان Longint: صارت 36 بايتة 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // PDFium FPDF_FILEACCESS: ‏m_FileLen من نوع C unsigned long، فLongWord هو الصواب
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 بايتات على Windows و8 على LP64، كالطرف C تمامًا
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

حالة EMF تظهر كم يفشل هذا بصمت. ينسخ المستورد 68 بايتًا إلى TPLEMFLogColorSpace: عشرون بايتة ترويسة، و36 بايتة نهايات CIE، و12 بايتة غاما. وبنهايات Longint على LP64، ستهبط تلك الـ 68 بايتة نفسها في مصفوفة نهايات من 72 بايتة، ولن تُكتب قيم الغاما مطلقًا، ولن يرفع شيء خطأ. وكلا السجلين يجلسان على مسارات كود خاصة بـ Windows على أي حال، وكلاهما يستخدم أنواعًا ذات عرض ثابت، لأن السجل الذي يحاكي بايتات على القرص لا ينبغي أن يعتمد على المترجم الذي صادف بناءه

ربط PDFium هو المثال المعاكس. ‏m_FileLen خاصته يحاكي unsigned long في C، وهو 32 بت على Windows و64 بت على LP64، وLongWord يتتبع ذلك بدقة في الحالتين. إعادة كتابته كـ Cardinal في جولة تنظيف ستكسره لحظة أن يكتسب مضيف PDFium محمّل POSIX. مضيف PDFium خاص بـ Windows وحده، فهذا ملاحظة للمستقبل. وجهة اصطلاح الاستدعاء في روابط C لها مصائد هي الأخرى، مغطاة في مقالة روابط DLL وActiveX وdylib لمكتبة PDF

حارس رخيص لأي سجل يحاكي بايتات: أثبت حجمه مرة واحدة عند الإقلاع، مثل Assert(SizeOf(TGoffset) = 8) في قسم initialization. لا يكلف شيئًا ويحول تغييرًا صامتًا في التخطيط إلى فشل فوري في أول تشغيل على target جديد

ضبط تعريفات الشرطي كما ينبغي: DELPHI_LINUX وAndroid وTypes

قبل أن تظهر أي من مسائل الأنواع، كان لا بد لـ Delphi على Linux أن يتوقف عن تمثيل أنه Windows. لم يعرّف PDFlibDefines.inc شيئًا على الإطلاق لـ Delphi Linux، فسقط المترجم مباشرة في الافتراضات Windows/VCL، وكان أول خطأ هو عدم العثور على Vcl.Graphics. توجه التعريفات Delphi Linux نحو الخلفية نفسها التي يستخدمها FPC على أهداف غير Windows: تصيير Cairo وfontconfig وOpenSSL وlibcurl تُحمَّل عند وقت التشغيل

{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}

تفصيلان في تلك الكتلة سهلا الفقدان. استبعاد ANDROID موجود لأن Delphi يعرّف LINUX لأهداف Android أيضًا، وهذه الخلفية لا تغطي Android. والتعريف الجديد منفصل عن قصد عن فرع Delphi macOS القائم (OS_MAC دون FPC) الذي يعتمد على وحدات Macapi؛ فتعريف Linux يبني على مسار FPC غير Windows بدلًا

كان الخطأ المتكرر الآخر بقاء TPoint وTRect بلا تصريح. على Windows تصل تلك الأنواع إلى معظم الوحدات بشكل غير مباشر عبر وحدة Windows. على Delphi Linux لا توجد وحدة Windows في جملة uses، لذا تسرد وحدات مثل PDFlibFont وPDFlibPageTree الآن Types صراحة تحت DELPHI_LINUX. إن كان كودك يترجم على Windows فقط لأن Windows يسحب System.Types معه، فتوقع الجدار نفسه. والنمط العام لكود لا يعمل على مترجم واحد إلا بالصدفة هو موضوع خمس عادات في Delphi لا تعمل إلا بمحض الصدفة حتى يفضحها FPC

حيث يتوقف المترجم عن المساعدة

مسائل LP64 أعلاه هي ما يستطيع المترجم إيجاده. أما ما لا يستطيع إيجاده، كسجل بحجم خاطئ لا يمر أبدًا عبر تحويل نوع، فلا يظهر إلا حين تشغَّل الاختبارات فعلًا على الهدف، وهو بالضبط ما وُجدت له تأكيدات الأحجام الموصوفة أعلاه

إن كان خرج 1-bit هو ما جاء بك إلى كود التنقيط من الأصل، فالمسار المدعوم للصفحات المصيَّرة مختلف، وهو تنزيل halftone عبر GDI خارج المصيِّر، موصوف في تصيير صفحات PDF إلى صور نقطية أحادية اللون بعمق 1-bit

العمل التأسيسي عبر المترجمات الموصوف هنا يسكن مكتبة PDF لـ Delphi، التي تستهدف اليوم Delphi وC++Builder على Windows إضافة إلى بناءات Free Pascal headless على Linux وmacOS؛ وصفحة المنتج تسرد المترجمات والمنصات المدعومة بالكامل