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

สร้างและปลดปล่อยคอมโพเนนต์ HotPDF แบบไดนามิกใน C++Builder

การลาก THotPDF ไปวางบนฟอร์มขณะ design time นั้นเหมาะสำหรับการทำตัวต้นแบบอย่างรวดเร็ว แต่นั่นจะเป็นการผูกอายุการใช้งานของคอมโพเนนต์เข้ากับฟอร์ม ซึ่งโค้ดในระดับ production มักไม่ต้องการแบบนั้น ตัวสร้างรายงานที่ทำงานเพียงครั้งเดียวเมื่อคลิกปุ่ม, เธรดเซอร์วิสที่รวบรวมการส่งออกข้ามคืน, คลาสช่วยเหลือที่ไม่มีฟอร์มเลย: ในแต่ละสถานการณ์เหล่านี้ คุณต้องการให้คอมโพเนนต์มีอยู่ตามระยะเวลาของงาน PDF หนึ่งงานแล้วก็หายไป นั่นหมายถึงการจัดสรรหน่วยความจำขณะ runtime ซึ่งทำให้มีสองสิ่งที่ควรทำความเข้าใจก่อนเริ่มเขียนโค้ดบรรทัดแรก: ใครคือเจ้าของอ็อบเจ็กต์ และกระบวนการทำความสะอาดทำงานอย่างไรเมื่อมีบางอย่างผิดพลาด

ความหมายของ Owner ใน VCL

คอนสตรักเตอร์ของคอมโพเนนต์ VCL ทุกตัวจะรับพารามิเตอร์ Owner ที่เป็นประเภท TComponent* การส่ง this (ตัวฟอร์มเอง) จะลงทะเบียนอ็อบเจ็กต์ใหม่เข้ากับรายการคอมโพเนนต์ที่ฟอร์มเป็นเจ้าของ ดังนั้นหากฟอร์มถูกทำลายในขณะที่คอมโพเนนต์ยังมีชีวิตอยู่ VCL จะจัดการปลดปล่อยมันให้โดยอัตโนมัติ การส่ง nullptr หมายถึงไม่มีเจ้าของ: คุณจะต้องรับผิดชอบต่อพอยน์เตอร์นั้นแต่เพียงผู้เดียว และจะไม่มีอะไรมาทำความสะอาดให้คุณหากมี exception เกิดขึ้นจนทำให้สแต็กถูกคลายออก (unwind) ก่อนที่คุณจะเรียก delete อย่างชัดแจ้ง

สำหรับการส่งออกครั้งเดียวจบที่ทำเสร็จภายในฟังก์ชันเดียว ทั้งสองวิธีนี้สามารถใช้งานได้ แต่จะมีโหมดความล้มเหลว (failure modes) ที่แตกต่างกัน การใช้ this เป็นเจ้าของ โอกาสที่หน่วยความจำจะรั่วไหลนั้นเป็นไปไม่ได้ตราบใดที่ฟอร์มนั้นจะถูกปิดในท้ายที่สุด แต่ถ้าใช้ nullptr พอยน์เตอร์จะต้องไปถึงบล็อก __finally ในทางปฏิบัติแล้ว รูปแบบ nullptr ร่วมกับ __finally จะดูสะอาดตากว่าเล็กน้อยสำหรับอ็อบเจ็กต์ที่มีอายุสั้น เนื่องจากมันทำให้ขอบเขตของอายุการใช้งานชัดเจนเพียงแค่กวาดตามอง และช่วยหลีกเลี่ยงไม่ให้ฟอร์มสะสมอ็อบเจ็กต์ที่มีไว้ใช้เพียงชั่วคราว

ไทม์ไลน์วงจรชีวิต THotPDF แบบไดนามิกใน C++Builder จาก new THotPDF(nullptr) ผ่านการตั้งออปชัน, BeginDoc และ EndDoc ไปสู่การ delete Pdf ภายในบล็อก __finally ที่ทำงานแม้มี exception ถูกยกขึ้น
THotPDF แบบ runtime มีชีวิตยาวเท่างาน PDF หนึ่งงานเท่านั้น: ตั้งออปชันก่อน BeginDoc วาด ปิด และให้บล็อก __finally เป็นผู้สั่ง delete ไม่ว่าจะมีข้อยกเว้นหรือไม่

โครงสร้างที่ปลอดภัยต่อ Exception

การสร้าง PDF อาจล้มเหลวได้จากเหตุผลที่ไม่ได้เกี่ยวข้องกับ API เลย: ไดเรกทอรีสำหรับส่งออกอ่านได้อย่างเดียว (read-only), ไฟล์ฟอนต์หายไป, สตรีมถูกแฟลชก่อนกำหนด หรือข้อมูลที่ผู้เรียกจัดหาให้มีขนาดยาวเกินขีดจำกัด ไม่ว่าสาเหตุจะเป็นอะไร กระบวนการทำความสะอาด (cleanup) จะต้องทำงานเสมอ วิธีการที่เป็นสำนวนเฉพาะ (idiomatic) ใน C++Builder สำหรับการรับประกันสิ่งนั้นคือ try/__finally:

#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
#pragma link "HPDFDoc"
#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    THotPDF* Pdf = new THotPDF(nullptr);
    try
    {
        Pdf->FileName = "output.pdf";
        Pdf->Compression = cmFlateDecode;
        Pdf->FontEmbedding = true;
        Pdf->BeginDoc();
        Pdf->CurrentPage->SetFont("Arial", TFontStyles(), 12);
        Pdf->CurrentPage->TextOut(72, 720, 0, L"Hello from C++Builder");
        Pdf->EndDoc();
    }
    __finally
    {
        delete Pdf;
    }
}

มีบางสิ่งที่ควรกล่าวถึงในโค้ดตัวอย่างนั้น การกำหนดเจ้าของเป็น nullptr ทำให้ระบุอายุการใช้งานได้อย่างชัดแจ้ง การกำหนด Compression และ FontEmbedding จะถูกตั้งค่าก่อนเรียก BeginDoc: ทั้งสองอย่างนี้เป็นตัวเลือกในระดับเอกสารที่ HotPDF จะคอมมิตเมื่อเอกสารเปิดขึ้นมา และการกำหนดค่าหลังจากนั้นจะไม่มีผลใด ๆ เมธอด TextOut จะรับพิกัดในหน่วยพอยต์ (points) ซึ่งวัดจากมุมล่างซ้ายของหน้าเอกสาร โดยค่า Y จะเพิ่มขึ้นเมื่อเลื่อนขึ้นด้านบน ส่วนตัวเลข 72, 720 จะวางข้อความไว้ใกล้กับมุมซ้ายบนของหน้ากระดาษขนาด letter โดยมีระยะขอบซ้าย 1 นิ้ว คำสั่ง delete Pdf ในบล็อก __finally จะทำงานเสมอ ไม่ว่า BeginDoc, การวาด หรือ EndDoc จะเกิด exception หรือไม่ก็ตาม

หลีกเลี่ยงการเรียกใช้เมธอดใด ๆ บน Pdf หลังจาก delete ไปแล้ว หากพอยน์เตอร์ถูกจัดเก็บไว้ในตัวแปรคลาส ให้ตั้งค่าเป็น nullptr ทันทีหลังจากการลบ เพื่อที่ว่าหากมีการเข้าถึงโดยบังเอิญในภายหลัง มันจะทำให้โปรแกรมแครชอย่างชัดเจนแทนที่จะเกิดการพังแบบเงียบ ๆ (silent corruption)

การกำหนดค่าโปรเจ็กต์ (Project configuration)

C++Builder จะค้นหา THotPDF ผ่านการใช้เส้นทาง include (include paths), เส้นทาง library (library paths) และคำสั่ง pragma ร่วมกัน ไฟล์เฮดเดอร์ที่ถูกสร้างขึ้นจะอยู่คู่กับ HPDFDoc.pas ในไดเรกทอรีซอร์สของ HotPDF; ให้เพิ่มไดเรกทอรีนั้นไปที่ Project > Options > C++ Compiler > Include path คำสั่ง #pragma link "HPDFDoc" จะบอกให้ linker ดึง unit ที่คอมไพล์แล้วเข้ามาโดยไม่ต้องใส่ไว้ในไฟล์โปรเจ็กต์เองแบบแมนนวล หากคุณกำลังใช้งานแพ็กเกจ runtime แทนการเชื่อมโยงแบบ static (static linking) ให้ติดตั้งแพ็กเกจ design และ runtime ของ HotPDF เสียก่อน; ซึ่งคำสั่ง pragma ก็ยังคงใช้งานได้ตามปกติ

ควรเก็บชื่อ unit HPDFDoc ไว้ตามเดิม C++Builder จะอ้างอิงชื่อไฟล์เฮดเดอร์จากชื่อ unit ของ Pascal ดังนั้นการเปลี่ยนชื่อไฟล์หรือใช้นามแฝงของเส้นทาง (path alias) ในคำสั่ง pragma จะทำให้การค้นหาพังลงอย่างเงียบ ๆ

ขอบเขต (Scoping) และงานที่มีหลายเอกสาร

สำหรับการส่งออกเพียงครั้งเดียวที่ถูกทริกเกอร์จากการกระทำของผู้ใช้ ตัวแปรแบบ local ที่จำกัดขอบเขต (scoped) ให้อยู่ในฟังก์ชันของปุ่มคือคำตอบที่ถูกต้อง: มันจะถูกสร้าง ใช้งาน และถูกทำลายภายในหนึ่ง call frame และเจตนานั้นก็ชัดเจนสำหรับทุกคนที่มาอ่านโค้ดในภายหลัง ทางเลือกแบบ design-time นั้นมีเหตุผลรองรับเมื่อฟอร์มเดียวกันนี้ต้องขับเคลื่อนเวิร์กโฟลว์อย่างต่อเนื่อง เช่น พาเนลพรีวิวการพิมพ์ที่จะสร้างเอกสารขึ้นใหม่ทุกครั้งที่ผู้ใช้เปลี่ยนการตั้งค่า; ในกรณีนั้น การรักษาคอมโพเนนต์ให้มีชีวิตอยู่แล้วเรียกใช้ BeginDoc/EndDoc ซ้ำ ๆ ย่อมรบกวนระบบน้อยกว่าการจัดสรรและปลดปล่อยอ็อบเจ็กต์บนฮีป (heap) ซ้ำไปซ้ำมา

สำหรับงานแบบชุด (batch jobs) ที่สร้างเอกสารหลายฉบับเรียงตามลำดับ การจำกัดขอบเขตของ THotPDF หนึ่งตัวต่อเอกสารหนึ่งฉบับก็คุ้มค่ากับโอเวอร์เฮดของการจัดสรรหน่วยความจำ สถานะ (State) จะไม่ถูกส่งต่อไปมาระหว่างเอกสารหากไม่มีอ็อบเจ็กต์มารองรับสถานะนั้น และนั่นถือเป็นหนึ่งในรูปแบบของบั๊กเป็นๆ หายๆ ที่คุณจะไม่มีวันต้องไปดีบักมัน ให้ใช้รูปแบบ จัดสรรหน่วยความจำ, สร้างเอกสาร, ลบทิ้ง แล้วทำซ้ำ

หนึ่งในคุณสมบัติที่ปรากฏในเดโมของ HotPDF หลายอันคือ AutoLaunch ซึ่งจะเปิดไฟล์ที่สร้างขึ้นมาในโปรแกรมดู PDF ของระบบทันทีหลังจากเรียก EndDoc คุณสมบัตินี้มีประโยชน์ขณะกำลังเขียนแบบร่างแรกของเค้าโครง (layout) แต่ในระดับ production ขอแนะนำให้ข้ามการใช้งานไป: เปิดเส้นทางส่งออก (output path) อย่างชัดเจน, ยืนยันว่าไฟล์นั้นมีอยู่และมีขนาดไม่เป็นศูนย์, บันทึกผลลัพธ์การทำงาน (log) แล้วปล่อยให้เวิร์กโฟลว์ที่เรียกใช้งานเป็นผู้ตัดสินใจว่าการเปิดโปรแกรมดูเอกสารนั้นมีความเกี่ยวข้องหรือไม่ สำหรับในงานแบบแบตช์ AutoLaunch จะเปิดหน้าต่างโปรแกรมดูเอกสารหนึ่งหน้าต่างต่อหนึ่งเอกสาร และจะบล็อกการทำงาน (block the process) ในบางระบบเพื่อรอให้โปรแกรมดูเอกสารนั้นปิดลง

แผนภาพลูป batch ที่จัดสรร THotPDF ใหม่หนึ่งตัวต่อเอกสารและคืนหลัง EndDoc สถานะจึงไม่รั่วระหว่างรอบ พร้อมคำเตือนไม่ให้เปิด AutoLaunch ในการส่งออก PDF ระบบจริง
กำหนดขอบเขต THotPDF ใหม่หนึ่งตัวต่อเอกสารในชุดประมวลผล เพื่อไม่ให้สถานะรั่วข้ามรอบ และปล่อย AutoLaunch ปิดไว้ เพื่อไม่ให้หน้าต่าง viewer ที่เด้งขึ้นมาทำงานค้าง

คอมโพเนนต์ THotPDF และการเรียกใช้คำสั่งการวาดทั้งหมดที่แสดงในที่นี้ เป็นส่วนหนึ่งของ HotPDF Delphi Component สำหรับ Delphi และ C++Builder