مقال تقني

الترجمة المتقاطعة (Cross-Compiling) لمكونات Delphi لـ C++Builder: تجنب المشكلات الخارجية غير المحلولة (Unresolved Externals)

عند الحفاظ على مكون VCL مكتوب بلغة Delphi ولكن يستخدمه كل من مستخدمي Delphi و C++Builder، ستدرك سريعًا أن كلا نظامي البناء يتعاملان مع التبعيات بشكل مختلف تمامًا. الوحدة التي يتم تجميعها بشكل مثالي في Delphi قد تتسبب في أخطاء رابط (linker) كارثية في C++Builder. هذا التناقض هو فخ متكرر لمؤلفي المكونات، خاصة عند إضافة وحدات داخلية جديدة إلى مكتبة موجودة

إليك تفصيل دقيق لسبب حدوث ذلك، باستخدام أمثلة من العالم الحقيقي من مكون HotXLS، وكيفية حماية عملية الترجمة المتقاطعة الخاصة بك باستخدام التضمينات الصريحة (explicit includes)، و {$HPPEMIT}، وربط pragma

الفخ: التجميع الضمني مقابل التضمين الصريح

لنفترض أنك أنشأت وحدة Delphi جديدة، lxXlsSummary.pas، للتعامل مع البيانات الوصفية للمستند، وتستخدمها (uses) في وحدة التحليل الرئيسية الخاصة بك، lxRead.pas. تضغط على تجميع (compile) في بيئة التطوير المتكاملة Delphi، فيتحول البناء إلى اللون الأخضر، وتقوم بشحن التحديث

في اليوم التالي، يبلغ مستخدمو C++Builder عن خطأ أثناء مرحلة الربط (linking): Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

طريقة Delphi (dcc32)

عندما يعالج مترجم Delphi حزمة (.dpk)، فإنه ينظر إلى الوحدات المدرجة صراحةً في جملة contains. إذا استخدمت إحدى هذه الوحدات (uses) وحدة خارجية (مثل lxXlsSummary.pas) غير موجودة في قائمة contains، فإن مترجم Delphi يقوم بـ ربط ثابت ضمني (implicit static linking). يجد ببساطة ملف .pas في مسار البحث، ويجمعه إلى .dcu، ويدمجه في .bpl الناتج. ينجح البناء، مخفيًا الإغفال تمامًا

طريقة C++Builder (MSBuild / .cbproj)

نظام بناء C++Builder أكثر صرامة بكثير. فهو ينشئ فقط ملفات كائنات C++ (.obj) وترويسات (.hpp) لوحدات Delphi المدرجة صراحةً في مجموعة عناصر <DelphiCompile> لملف .cbproj. نظرًا لأن lxXlsSummary.pas لم يتم تسجيلها صراحةً في ملف المشروع أبدًا، فلن يتم إنشاء lxXlsSummary.obj. عندما يحاول الرابط حل الاستدعاءات التي تم إجراؤها بواسطة lxRead.obj، تكون الرموز مفقودة، مما يؤدي إلى خطأ خارجي غير محلول (unresolved external)

مقارنة مسار بناء لمكوّن HotXLS لـ Delphi: يصرّف dcc32 في Delphi ضمنًا وحدة غير مدرجة في الحزمة ويبني بنجاح، بينما لا يولد MSBuild في C++Builder ملف الكائن الخاص به أبدًا ويتوقف ILINK32 بمرجع خارجي غير محلول
يسلك الوحدة غير المدرجة نفسها مسارين متباعدين: يُخفي dcc32 الإغفال عبر الربط الساكن الضمني بينما يفضحه رابط C++Builder الأصرم

حل المشكلات الخارجية باستخدام ربط Pragma و HPPEMIT

إذا كنت ترغب في ضمان ربط الوحدة بشكل صحيح في C++ دون إجبار المستخدم على إضافة ملف .obj يدويًا إلى مشروعه، يمكنك استخدام توجيه {$HPPEMIT} الخاص بـ Delphi. يخبر هذا مترجم Delphi بحقن توجيه #pragma link معين لـ C++ في ملف .hpp الذي تم إنشاؤه

unit lxXlsSummary;

interface

{$IFDEF WINDOWS}
  // حقن توجيه pragma link في ملف ترويسة C++ المُنشأ
  // يجبر هذا رابط C++ على تضمين ملف .obj المطابق
  {$HPPEMIT '#pragma link "lxXlsSummary.obj"'}
{$ENDIF}

uses
  SysUtils, Classes;

type
  TXlsSummaryInfo = class(TObject)
  public
    Title: string;
    Author: string;
    CreateTime: TDateTime;
  end;

function XlsReadSummaryInformation(const FileName: string): TXlsSummaryInfo;

implementation

function XlsReadSummaryInformation(const FileName: string): TXlsSummaryInfo;
begin
  Result := TXlsSummaryInfo.Create;
  // منطق استخراج البيانات الوصفية هنا
end;

end.

عندما يتضمن C++Builder lxXlsSummary.hpp، يواجه المترجم #pragma link ويخبر الرابط (ILINK32/ILINK64) تلقائيًا بحل الرموز من lxXlsSummary.obj

تدفق بأربعة أعمدة لآلية ربط توجيه HPPEMIT في Delphi: يجعل التوجيه المصرَّح به dcc32 يحقن أمر الرابط في lxXlsSummary.hpp، ويستهلكه C++Builder عند التضمين، ويحل ILINK32 رمز XlsReadSummaryInformation تلقائيًا
تُكتب ترتيبة الرابط مرة واحدة في مصدر Pascal، وتنتقل داخل الترويسة المولَّدة، وتبلغ كل بناء C++ قادم دون تعديلات يدوية للمشروع

القاعدة الذهبية لصيانة المكونات

لتجنب كسر بناءات C++Builder بالكامل، يجب عليك اعتماد سياسة تسجيل صارمة. كلما تمت إضافة وحدة Pascal جديدة إلى مكتبتك، يجب تسجيلها صراحةً عبر جميع أنواع ملفات المشاريع الثلاثة في وقت واحد

1. تحديث مشروع C++Builder (.cbproj / .bpk)

افتح ملف .cbproj في محرر نصوص وأضف الوحدة الجديدة إلى قائمة التجميع، مع التأكد من توفير ترتيب بناء (build order) فريد. إذا كنت تستخدم إصدارات C++Builder أقدم مع ملفات .bpk، فتأكد من إضافة الوسم <file containerid="PascalCompiler" designclass="" filename="lxXlsSummary.pas" formname="" localcommand="" unitname="lxXlsSummary"></file>

<DelphiCompile Include="lxXlsSummary.pas">
  <BuildOrder>101</BuildOrder>
</DelphiCompile>

2. تحديث حزمة Delphi (.dpk)

أضف الوحدة إلى جملة contains الصريحة. يضمن ذلك ألا يضطر مترجم Delphi إلى الاعتماد على الربط الضمني، والذي يعتبر عمومًا ممارسة سيئة على أي حال

package HotXLS;

{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}

requires
  rtl,
  vcl;

contains
  lxRead in 'lxRead.pas',
  lxXlsSummary in 'lxXlsSummary.pas';

end.

التحقق من التكامل المستمر (Continuous Integration)

الدفاع النهائي ضد هذا الفخ هو التحقق من صحة CI/CD. لا تعتمد أبدًا على بناء Delphi الناجح وحده قبل شحن مكون ثنائي اللغة. يجب أن تستدعي نصوص البناء (build scripts) الخاصة بك MSBuild أو أدوات سطر الأوامر bcc32c في مشاريع C++Builder (مثل build-Win32-Lib-CB.cmd) وتشغيل رابط كامل (complete link) للإصدار التجريبي لـ C++ والعروض التوضيحية الكاملة. فقط عندما ينجح رابط C++، يمكنك التأكد من أن جميع وحدات Delphi مسجلة بشكل صحيح وتعرض رموزها لوقت تشغيل C++ (runtime)

ملاحظة: يتم الحفاظ على توافق المترجم عبر الأنظمة الأساسية (cross-platform) بصرامة عبر إصدارات Delphi و C++Builder من مكون HotXLS VCL