Technical Article

ترحيل inline assembly لـ _ftol من Delphi 32 بت إلى DCC64

يبدو الأسلوب الأصلي لـ _ftol في Delphi 32 بت وكأنه سطر واحد ذكي: غلاف دالة Pascal يهبط إلى inline assembly للتعامل مع x87 FPU control word، وقطع القيمة على FPU stack، ثم إخراج النتيجة. كان يُبنى بلا مشكلة في DCC32 لفترة طويلة، ولذلك انتهى به الأمر في كثير من وحدات الرسومات وPDF القديمة من دون أن يراجع أحد الفكرة

عندما تنقل الهدف إلى 64 بت، ينهي المترجم العمل برسالة E1025 Unsupported language feature: 'ASM'. هذه الرسالة ليست تحذيرا من التوافق. معناها أن DCC64 لن يترجم الروتين إطلاقا، مهما كان نجاح الـ assembly سابقا

كان الشكل 32 بت الأصلي يبدو عادة بهذا الأسلوب:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

كتلة asm داخل جسم begin...end في Pascal هي بالضبط ما يرفضه DCC64. للمترجمين قاعدتان مختلفتان بشأن موضع السماح بالـ assembly، والحد الفاصل هنا مهم

لماذا يرسم DCC64 الحد بشكل مختلف

يسمح DCC32 بالـ inline assembly داخل روتينات Pascal العادية. يعرف المترجم اتفاقية الاستدعاء 32 بت ويمكنه استنتاج مواضع المتغيرات المحلية والمعاملات، لذلك يتسامح مع شذرات assembly تصل إلى stack frame بالاسم. أما DCC64 فيتخذ موقفا أشد: يجب أن تكون الـ assembly داخل دالة assembler مخصصة، يكون جسمها بالكامل assembly وتدار فيها اتفاقية الاستدعاء صراحة. المزج بين Pascal وasm غير مدعوم إطلاقا

السبب الجوهري معماري. في اتفاقية الاستدعاء الخاصة بـ Windows 64 بت (Microsoft ABI)، تصل المعاملات الأربع الأولى في RCX وRDX وR8 وR9 للأنواع الصحيحة، أو في XMM0 إلى XMM3 للقيم العائمة. لا يدخل x87 FPU في تمرير المعاملات العادي؛ فهو متاح تقنيا، لكن ABI لا يستخدمه لنقل الوسائط. أي assembly تفترض أن القيمة "على FPU stack" إنما تبني على حالة لا ينشئها ABI ذي 64 بت أصلا

إذن فالمشكلة القديمة ليست مشكلة صياغة فقط. حتى لو قبل DCC64 الكود، فافتراضات السجلات ستكون خاطئة

كتابة نسخة assembler صحيحة 64 بت

عندما تحتاج فعلا إلى تصدير الرمز _ftol مع اتفاقية cdecl من أجل التوافق الثنائي، يجب كتابة الدالة كروتين assembler خالص. في ABI الخاص بـ 64 بت، تصل قيمة Double في XMM0، ويجب أن تكون النتيجة الصحيحة في RAX عند الإرجاع. التوجيه .NOFRAME يخبر DCC64 أن الروتين يدير stack الخاص به، وهو مناسب هنا لأن هذه دالة leaf قصيرة جدا:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

CVTTSD2SI هي تعليمة SSE2 لتحويل عدد عشري مزدوج الدقة إلى عدد صحيح موقّع مع القطع نحو الصفر، وهو بالضبط ما يفترض أن يفعله _ftol. إنها تعليمة واحدة، تأخذ المعامل مباشرة من الموضع الذي تركه ABI، وتضع النتيجة في الموضع الذي يتوقعه ABI. ولا حاجة إلى العبث بـ FPU control word

لاحظ أنه إذا تجاوزت القيمة مدى عدد صحيح موقّع 32 بت، فإن CVTTSD2SI يعيد القيمة غير المحددة integer indefinite ($80000000). هذا هو السلوك نفسه الذي يفعله x87 fistp عند الإدخال خارج النطاق. من المفيد التأكد مما إذا كان المستدعون يمكنهم إنتاج مثل هذه القيم قبل إعلان اكتمال الترحيل

متى يكون Trunc هو الجواب الأفضل

النسخة assembler أعلاه تستحق الكتابة فقط عندما يكون لديك متطلب حقيقي للتوافق الثنائي: مستدعي خارجي يتوقع الرمز _ftol مع اتفاقية استدعاء محددة، ولا يمكنك تغيير هؤلاء المستدعين. هذا السيناريو غير شائع. في معظم الحالات، كان _ftol مساعدا خاصا داخل الوحدة نفسها، ولا توجد أي تبعية خارجية لاسمه أو اتفاقية استدعائه

في هذه الحالة، استبدله بـ Pascal عادي:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

يقص Trunc نحو الصفر، وهذا يطابق ما كان _ftol يفعله عندما كانت x87 control word مضبوطة على وضع القطع. وهو يترجم على DCC32 وDCC64 من دون أي تعديل. يولد المترجم التعليمة المناسبة لكل هدف: ففي x64 سيصدر عادة CVTTSD2SI على أي حال، وهي نفس التعليمة التي كتبناها يدويا. تحصل على سلوك مطابق، من دون شروط خاصة بكل منصة، ومن دون assembly تحتاج إلى صيانة

الفرق الدلالي الوحيد الذي يستحق الفحص: Trunc يرفع الاستثناء EInvalidOp في الإعدادات الافتراضية لـ Delphi عندما تكون القيمة NaN أو infinity. أما fistp في كود x87 الأصلي فكان يكتب فقط نمطا ثنائيا من دون رفع أي شيء. إذا كان الكود يمرر قيما عائمة غير معتادة إلى هذه الدالة وكان السلوك القديم صامتا، فاحمِ الاستدعاء بـ IsNaN وIsInfinite من Math قبل استدعاء Trunc

التجميع الشرطي عندما يبقى الهدفان نشطين

يجب على بعض المشاريع الاستمرار في شحن ثنائيات 32 بت و64 بت معا. إذا كان يجب الإبقاء على النسخة assembler الأصلية لـ 32 بت وتوفير تنفيذ جديد لـ 64 بت، فاستخدم الشرط CPUX64:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bit path: DCC32 accepts inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

هذه هي أقل إصلاح ميكانيكي ممكن، ومن الأفضل التعامل معه بوصفه مؤقتا. أي قاعدة كود تحمل assembly خاصة بالمعمارية داخل مساعد غرضه الوحيد هو قص float إلى integer إنما تحمل دينا غير ضروري. يمكن إزالة فرع 32 بت بالكامل بمجرد التأكد من أن لا شيء يعتمد على الآثار الجانبية لـ FPU في التنفيذ القديم

إذا ظهرت الدالة داخل مكوّن يُستخدم عبر وحدات متعددة، فابحث في قاعدة الكود كلها عن _ftol قبل أن تقرر كيف تهاجر. يمكن تعريف رمز بهذا الاسم في أكثر من مكان؛ والمُرابط يختار واحدا ويتجاهل البقية بصمت، وهذا يعني أنك قد تصلح نسخة واحدة بينما ما زلت ترتبط بنسخة أخرى لم تُمس