บทความเทคนิค

การคอมไพล์ข้ามแพลตฟอร์มคอมโพเนนต์ Delphi สำหรับ C++Builder: หลีกเลี่ยง Unresolved Externals

เมื่อต้องบำรุงรักษาคอมโพเนนต์ VCL ที่เขียนใน Delphi แต่ใช้งานโดยผู้ใช้ทั้ง Delphi และ C++Builder คุณจะตระหนักได้อย่างรวดเร็วว่าระบบบิลด์ทั้งสองจัดการกับการพึ่งพา (dependencies) แตกต่างกันมาก ยูนิตที่คอมไพล์ได้อย่างสมบูรณ์ใน Delphi อาจทำให้เกิดข้อผิดพลาดของลิงเกอร์ที่รุนแรงใน C++Builder ความคลาดเคลื่อนนี้เป็นกับดักที่พบบ่อยสำหรับผู้เขียนคอมโพเนนต์ โดยเฉพาะอย่างยิ่งเมื่อเพิ่มยูนิตภายในใหม่เข้าไปในไลบรารีที่มีอยู่

นี่คือรายละเอียดโดยละเอียดว่าทำไมสิ่งนี้ถึงเกิดขึ้น โดยใช้ตัวอย่างในโลกแห่งความเป็นจริงจากคอมโพเนนต์ HotXLS และวิธีป้องกันกระบวนการคอมไพล์ข้ามแพลตฟอร์มของคุณโดยใช้ explicit includes, {$HPPEMIT} และ pragma linking

กับดัก: Implicit Compilation เทียบกับ Explicit Inclusion

สมมติว่าคุณสร้างยูนิต Delphi ใหม่ lxXlsSummary.pas เพื่อจัดการข้อมูลเมตาของเอกสาร และคุณใช้งานมันในยูนิตการแยกวิเคราะห์หลักของคุณ lxRead.pas คุณกดคอมไพล์ใน Delphi IDE การสร้างสำเร็จ และคุณจัดส่งการอัปเดต

วันรุ่งขึ้น ผู้ใช้ C++Builder ของคุณรายงานข้อผิดพลาดระหว่างขั้นตอนการเชื่อมโยง: Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

วิธีของ Delphi (dcc32)

เมื่อคอมไพเลอร์ Delphi ประมวลผลแพ็คเกจ (.dpk) มันจะดูที่ยูนิตที่ระบุไว้อย่างชัดเจนในประโยค contains หากหนึ่งในยูนิตเหล่านั้นใช้งานยูนิตภายนอก (เช่น 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

เปรียบเทียบไปป์ไลน์ build สำหรับคอมโพเนนต์ HotXLS ของ Delphi: dcc32 ของ Delphi คอมไพล์ยูนิตที่ไม่ถูกระบุเข้าไปในแพ็กเกจและ build ผ่านอย่างเขียว ขณะที่ MSBuild ของ C++Builder ไม่เคยสร้างออบเจ็กต์ไฟล์ของมันและ ILINK32 ยกเลิกด้วย unresolved external
ยูนิตที่ไม่ถูกระบุชื่อตัวเดียวกันเดินสองเส้นทางที่แยกจากกัน: dcc32 ปิดบังการละไว้ด้วยการลิงก์แบบ static โดยปริยาย ขณะที่ linker ของ C++Builder ที่เข้มกว่าเปิดโปงมัน

การแก้ปัญหา Externals ด้วย Pragma Link และ HPPEMIT

หากคุณต้องการให้แน่ใจว่ายูนิตเชื่อมโยงอย่างถูกต้องใน C++ โดยไม่ต้องบังคับให้ผู้ใช้เพิ่มไฟล์ .obj ด้วยตนเองในโครงการ คุณสามารถใช้ไดเรกทิฟ {$HPPEMIT} ของ Delphi สิ่งนี้บอกให้คอมไพเลอร์ Delphi แทรกไดเรกทิฟ C++ #pragma link เฉพาะลงในไฟล์ .hpp ที่สร้างขึ้น

unit lxXlsSummary;

interface

{$IFDEF WINDOWS}
  // แทรก pragma link เข้าไปในไฟล์ header C++ ที่สร้างขึ้น
  // เพื่อบังคับให้ linker ของ 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;
  // ตรรกะการดึง metadata อยู่ตรงนี้
end;

end.

เมื่อ C++Builder รวม lxXlsSummary.hpp คอมไพเลอร์จะพบกับ #pragma link และสั่งให้ลิงเกอร์ (ILINK32/ILINK64) แก้ไขสัญลักษณ์จาก lxXlsSummary.obj โดยอัตโนมัติ

ผังสี่คอลัมน์ของกลไก pragma link HPPEMIT ใน Delphi: directive ที่ประกาศไว้ทำให้ dcc32 ฉีดลำดับ linker เข้า lxXlsSummary.hpp, C++Builder ใช้มันตอน include และ ILINK32 resolve XlsReadSummaryInformation ให้อัตโนมัติ
ลำดับของ linker ถูกเขียนครั้งเดียวในซอร์ส Pascal เดินทางภายในเฮดเดอร์ที่สร้างขึ้น และไปถึงทุกบิลด์ C++ ในอนาคตโดยไม่ต้องแก้โปรเจกต์ด้วยมือ

กฎทองสำหรับการบำรุงรักษาคอมโพเนนต์

เพื่อหลีกเลี่ยงไม่ให้ C++Builder แตกสลายโดยสิ้นเชิง คุณต้องนำนโยบายการลงทะเบียนที่เข้มงวดมาใช้ เมื่อใดก็ตามที่มีการเพิ่มยูนิต Pascal ใหม่ลงในไลบรารีของคุณ จะต้องลงทะเบียนอย่างชัดเจนในไฟล์โครงการทั้งสามประเภทพร้อมกัน

1. อัปเดตโครงการ C++Builder (.cbproj / .bpk)

เปิดไฟล์ .cbproj ในเท็กซ์เอดิเตอร์และเพิ่มยูนิตใหม่ในรายการคอมไพล์ ตรวจสอบให้แน่ใจว่าคุณระบุลำดับการสร้างที่ไม่ซ้ำกัน หากใช้ 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 ที่สำเร็จเพียงอย่างเดียวก่อนที่จะจัดส่งคอมโพเนนต์สองภาษา สคริปต์บิลด์ของคุณต้องเรียกใช้ MSBuild หรือเครื่องมือบรรทัดคำสั่ง bcc32c บนโครงการ C++Builder (เช่น build-Win32-Lib-CB.cmd) และเรียกใช้งานลิงก์ที่สมบูรณ์ของเวอร์ชันทดลองและเดโมฉบับเต็มของ C++ ต่อเมื่อลิงเกอร์ C++ ทำงานสำเร็จเท่านั้น คุณจึงจะแน่ใจได้ว่ายูนิต Delphi ทั้งหมดได้รับการลงทะเบียนอย่างถูกต้องและเปิดเผยสัญลักษณ์ของพวกมันไปยังรันไทม์ C++

หมายเหตุ: ความเข้ากันได้ของคอมไพเลอร์ข้ามแพลตฟอร์มได้รับการบำรุงรักษาอย่างเคร่งครัดในรุ่น Delphi และ C++Builder ของ HotXLS Delphi VCL Component