HotXLS บิลด์ใต้ Free Pascal กับ Lazarus บน Windows ได้ และจุดที่ทำให้งานพอร์ตสำเร็จอยู่ที่การตัดสินใจสี่เรื่องที่ไม่เกี่ยวกับไวยากรณ์ Object Pascal เลย: รักษา core ไว้ในโหมด DELPHIUNICODE, ประกาศอินเทอร์เฟซ OLE structured storage เป็น CORBA interface ที่จัด reference counting ด้วยมือ, เปลี่ยนไฟล์ออบเจกต์ AES ฝั่ง Win32 มาเป็นอิมพลีเมนเทชันภาษา Pascal และแก้ลูป inflate ที่ตอบรับ ZIP โดนตัดท้ายไปแล้วว่าสมบูรณ์
ใครที่เคยพอร์ตไลบรารี Delphi ที่โตแล้วมาก่อนย่อมรู้จักรูปร่างงานแบบนี้ดี รอบแรกคอมไพเลอร์รับทุกอย่างเกือบหมด สิ่งที่ตามมาคือหางยาวของความต่างเชิงพฤติกรรมที่คอมไพล์ผ่านสวยงามแต่ให้ผลลัพธ์ผิด และเอนจินสเปรดชีตเป็นตัวที่โดนความต่างพวกนี้เล่นงานง่ายเป็นพิเศษ เพราะมันแตะทั้ง text encoding, COM structured storage, การบีบอัด และการเข้ารหัสลับในเส้นทางโค้ดเดียวกัน
ทำไม core ถึงยึดติดกับ DELPHIUNICODE แทนที่จะใช้ DELPHI ล้วน ๆ?
เพราะเอนจินสูตรคำนวณพึ่งพิงความหมาย UTF-16 ของ String กับ Char ส่วนทางเลือก ANSI ทิ้งตัวอักษรหายไปตั้งแต่ก่อนอะไรจะถึงไฟล์ การหลอกให้ตัวเองบิลด์ core ในโหมด DELPHI ของ FPC มันน่าลองอยู่นะ เพราะเป็นสวิตช์ความเข้ากันได้ที่งานพอร์ตส่วนใหญ่เอื้อมไปหาก่อน และโค้ดก็คอมไพล์ผ่าน แล้วพอเวิร์กบุ๊กที่มีชื่อชีตภาษาจีนหรือป้ายกำกับภาษาซีริลลิกวิ่งไปกลับผ่านเส้นทางคำนวณ ตัวอักษรก็หายวับไปก่อนที่ตัวเขียนไฟล์จะได้เห็น โดยไม่มี error โผล่ที่ไหนเลย
โหมดไม่ได้เป็นเอกภาพทั่วทั้งไลบรารี ซึ่งเป็นเรื่องที่ตั้งใจ ไม่ใช่ความรก ตัวถอดรหัสไบต์ PNG กับ override ชุด LCL ยังจำเป็นต้องมีซิกเนเจอร์ ANSI อย่างแท้จริง เพราะทั้งคู่ทำงานกับไบต์และกับสิ่งที่ widgetset ส่งมอบให้ ยูนิตพวกนี้จึงเปิดสวิตช์ LX_FPC_ANSI แยกของตัวเอง สองโหมดในไลบรารีเดียวฟังดูเหมือน code smell จนกว่าคุณจะสังเกตว่าทางเลือกคือตัวถอดรหัสไบต์ที่เอาอินพุตไปตีความเป็นข้อความ
มีรายละเอียดข้างเคียงที่คอยดักคนตอนหลัง DELPHIUNICODE ไม่ได้ทำให้ TFormatSettings.DecimalSeparator กลายเป็น WideChar ในรันไทม์ของ FPC อินพุตที่พกตัวคั่นทศนิยมแบบ Unicode ต้องถูก normalize เป็นตัวคั่น ASCII ภายในสตริง Unicode ก่อน และอินพุตใดที่ตัวคั่นไม่ตรงกับที่คาดไว้ต้องปฏิเสธทิ้ง ไม่ใช่ตัดทอนเงียบ ๆ ที่ตัวอักษรที่ parser ไม่รู้จัก
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // ต้องมาก่อนเป็นอันดับแรก: initialize widgetset ของ LCL
SysUtils, lxHandle; // พร้อมชั้นแปลงรหัส UTF-8
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('input.xls');
Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
Book.SaveToFile('output.xls');
finally
Book.Free;
end;
end.
ยูนิต Interfaces ไม่ใช่สิ่งที่เลือกได้ว่าจะใส่หรือไม่ใส่ และมันต้องมาเป็นตัวแรก ตัวมันคือสิ่งที่ initialize widgetset ของ LCL กับชั้นแปลงรหัส UTF-8 และ HotXLS พึ่งพาทั้งคู่ทันทีที่ฟอนต์ พาธไฟล์ หรือข้อความข้ามพรมแดนระหว่าง RTL กับ LCL โปรแกรมคอนโซลที่ข้ามมันจะยังคอมไพล์ผ่าน แต่จะเพี้ยนกับพาธที่ไม่ใช่ ASCII ทุกพาธ นี่ก็คือเหตุผลว่าทำไมการคอมไพล์ผ่านจึงพิสูจน์อะไรไม่ค่อยได้ในงานแบบนี้: งานพอร์ตจะพิสูจน์ว่าใช้งานได้จริงก็ต่อเมื่อเอกสารจริงที่มีชื่อฟอนต์จริงกับพาธจริงวิ่งครบรอบไปกลับมาแล้วเท่านั้น
คลาส VMT ไม่ใช่ COM vtable
Free Pascal ไม่ยอมให้คุณยัดคลาส VMT ให้ Windows ใช้เป็น COM interface vtable แม้การประกาศจะหน้าตาเหมือนแบบที่ Delphi รับได้ทุกกระเบียด การจัดวางภายในต่างกันแบบที่ทำให้เกิดการเรียกเข้า slot ผิดตัว อาการที่ปรากฏคือแครชที่จุดซึ่งดูไม่เกี่ยวกับ call site เลย structured storage เป็นหัวใจของเรื่องนี้เพราะฟอร์แมตเวิร์กบุ๊กไบนารีคลาสสิกคือไฟล์ OLE compound และการอ่านหรือเขียนมันหมายถึงต้องอิมพลีเมนต์ ILockBytes ที่ storage API ของ Windows จะโทรกลับเข้ามา
แบบแผนที่ใช้ได้จริงคือ CORBA interface ที่ประกาศ COM slot ไว้ชัดเจน และจัดการ AddRef กับ Release ด้วยมือ แปลว่าต้องละทิ้ง reference counting อัตโนมัติสำหรับไทป์กลุ่มนี้และรับผิดชอบอายุขัยของออบเจกต์เอง ซึ่งเป็นการแลกเปลี่ยนที่คุ้มสำหรับอินเทอร์เฟซเพียงไม่กี่ตัวที่อาศัยอยู่ในยูนิตเดียว กับดักเฉพาะตัวที่ซ่อนอยู่ในงานนี้คือ QueryInterface: มันต้องคืนพอยน์เตอร์ของอินเทอร์เฟซ ไม่ใช่พอยน์เตอร์ของออบเจกต์ สองแบบนี้คอมไพล์ผ่านเหมือนกัน แต่แบบหนึ่งส่งมอบที่อยู่ให้ Windows ซึ่งเวิร์ดเครื่องแรกของมันไม่ใช่ vtable
การประกาศเฉพาะ FPC อยู่ใน lxOleInterfaces.inc เคียงข้าง lxAESBackend.inc กับ lxZlibBackend.inc ในไดเรกทอรีซอร์สฝั่ง FPC ตัวเลือกที่ผูกกับคอมไพเลอร์จึงรวมอยู่ที่เดียว แทนที่จะกระจัดกระจายไปทั่วเอนจิน ส่วนตัวฟอร์แมตและวิธีที่ไลบรารีเดินทางภายในมันมีเล่าไว้ในบทความอ่านไฟล์ OLE2 compound ด้วย Pascal
มีรายละเอียดไทป์อีกเรื่องอยู่ในตระกูลเดียวกัน LargeInt ต้อง resolve เป็น Int64 ในสาขา FPC และการจำแนกชนิดของ Comp ต่างกันระหว่างสอง toolchain พอที่การ resolve overload จะเลือกตัวเลือกคนละตัวได้ ทดสอบพฤติกรรมออฟเซ็ตใหญ่ด้วย file stream ดีกว่า HGLOBAL stream เพราะ global-memory stream ของ Windows วนกลับ (wrap) เองตอน seek เลย 4 GiB เทสต์ผ่านตรงนั้นจึงไม่พิสูจน์อะไรเลยเกี่ยวกับเลขคณิตของคุณเอง
สิ่งที่อิมพลีเมนเทชัน AES ที่สอดคล้องกับตัวเองซ่อนไว้
ไฟล์ออบเจกต์ AES ฝั่ง Win32 ที่บิลด์ Delphi ลิงก์อยู่เป็น OMF ซึ่งลิงเกอร์ของ Free Pascal กลืนไม่ได้ สาขา FPC จึงเปลี่ยนไปใช้อิมพลีเมนเทชัน AES ภาษา Pascal แทน ฝั่ง Delphi ยังคงลิงก์ไฟล์ออบเจกต์ชุดเดิมตามเคย ทำให้ไบนารีที่ปล่อยออกไปไม่เปลี่ยนสำหรับลูกค้าเดิม
ข้อกำหนดด้านการตรวจสอบคือส่วนที่ควรแบกไปใช้กับโปรเจกต์อะไรก็ได้ เข้ารหัสข้อมูลแล้วถอดกลับด้วยอิมพลีเมนเทชันเดียวกันไม่พิสูจน์อะไรเลยแม้แต่นิด: อัลกอริทึม symmetric ที่ใส่ key schedule ผิด ลำดับบล็อกผิด หรือ chaining ผิด ยังสอดคล้องกับตัวเองอย่างสมบูรณ์แบบ และจะวิ่งไปกลับกับเอาต์พุตของตัวเองได้ทุกครั้ง มีแต่ known-answer vectors เท่านั้นที่จับมันได้ ด้วยการเทียบการขยายคีย์ ลำดับบล็อก และการเชื่อมโยงแบบ CBC กับค่าอ้างอิงที่เผยแพร่ไว้ ปล่อยอิมพลีเมนเทชันที่ผิดแต่สอดคล้องกับตัวเองออกไป อาการจะโผล่ครั้งแรกเมื่อลูกค้าเปิดไฟล์ใน Excel
ฝั่ง compression มีข้อบกพร่องกลิ่นอายต่างออกไป backend inflate ภาษา Pascal ยังมีเอาต์พุตค้างอยู่ได้หลังกินอินพุตบีบอัดหมดแล้ว ผู้เรียกจึงต้องเรียกต่อไปเรื่อย ๆ จนกว่าสตรีมจะรายงานว่าจบ การตีความว่าอินพุตหมดเท่ากับจบสตรีมจะตัดบล็อกสุดท้ายหาย แย่กว่านั้นมันเปลี่ยน archive ที่เสียหายให้กลายเป็นอันที่ถูกตอบรับอย่างเงียบ ๆ ซึ่งเป็นเป๊ะ ๆ กับโหมดความล้มเหลวที่การ hardening ในบทความตรวจความถูกต้อง record ZIP end-of-central-directory ถูกเขียนขึ้นมาเพื่อกัน กฎคือถ้าไม่มีความคืบหน้าและยังไม่จบ นั่นคือ truncation error ไม่ใช่ EOF เด็ดขาด
กับดักระบบบิลด์สองข้อที่กินเวลาจริงเป็นชั่วโมง
พาธค้นหาของ LCL ต้องมาก่อนพาธ wildcard ของแพ็กเกจ FPC ไม่อย่างนั้นยูนิต Menus ของ Free Vision จะบังยูนิตชื่อเดียวกันของ LCL แล้วคุณจะได้ PPU checksum mismatch ที่ไม่ได้บอกอะไรเลยว่าใครผิด การติดตั้ง Lazarus ที่ถูกย้ายหลังติดตั้งไปแล้วยังทิ้งพาธเก่าไว้ใน fpc.cfg ได้อีก จุดเข้าบิลด์จึงระบุพาธยูนิตกับไบนารีแบบชัดเจน แทนที่จะไปสืบทอดสิ่งที่สภาพแวดล้อมยื่นให้
กับดักข้อที่สองไม่เกี่ยวกับ Pascal เลย ไฟล์แบตช์ .cmd ที่เขียนด้วย line ending แบบ LF ใช้งานได้จนกระทั่งไฟล์โตเกินขนาด read buffer ของตัวแปลภาษา จากนั้น call :label จะล้มพร้อมข้อความว่าไม่มี batch label นั้นอยู่ โดยความล้มจะโผล่ที่โปรแกรมตัวใดก็ตามที่นั่งอยู่เลยเส้นแบ่งนั้น เครื่องมือใดที่เขียนสคริปต์แบตช์ใหม่ต้องเขียน CRLF คืนกลับไปให้ครบ และ lazbuild --build-all จะล้างไดเรกทอรีเอาต์พุตยูนิตของแพ็กเกจก่อนคอมไพล์ ไฟล์ options ที่จอดอยู่ในไดเรกทอรีนั้นจึงโดนลบก่อนจะได้อ่าน: เก็บไว้ข้างนอก และจำไว้ว่าพาธหลัง @ ถูก resolve เทียบกับไดเรกทอรีแพ็กเกจ เพราะ lazbuild เรียกคอมไพเลอร์จากตรงนั้น
// เอ็กซพอร์ตกริดบน Lazarus: TGridToXLS มาพร้อมแพ็กเกจ Lazarus ดังนั้น
// โค้ดเอ็กซพอร์ต DB-grid ชุดเดิมจึงใช้ได้ในแอป LCL
var
Exporter: TGridToXLS;
begin
Exporter := TGridToXLS.Create(nil);
try
Exporter.DBGrid := GridOrders;
Exporter.WorksheetName := 'Orders';
Exporter.ExportHeader := True;
Exporter.SetColumnsWidth := True;
Exporter.ExportDBGrid;
Exporter.SaveAs('orders.xls');
finally
Exporter.Free;
end;
end;
คำเตือนของคอมไพเลอร์มีค่าแค่ไหน
Free Pascal รายงานตัวแปร local ที่ไม่ได้กำหนดค่าเริ่มต้นในกรณีที่ Delphi ไม่รายงาน และการรันบิลด์ FPC เปลี่ยนความต่างนี้เป็น defect จริงสองจุดในยูนิตคำนวณ ฟังก์ชันหนึ่งอ่านตัวแปรนับที่ไม่เคยถูกกำหนดค่าก่อนใช้ อีกฟังก์ชันใช้พิกัดสองตัวในสาขาหนึ่ง ก่อนที่โค้ดที่คำนวณมันจะทำงานในอีกสาขาหนึ่ง ภายใต้ Delphi ทั้งสองกรณีทำงานตามสิ่งที่สแตกบังเอิญมีอยู่ ซึ่งคือนิยามเต็ม ๆ ของบั๊กที่เกิดซ้ำบนเครื่องหนึ่งแต่เกิดไม่ได้บนอีกเครื่องหนึ่ง
ข้อสรุปเชิงปฏิบัติคือคอมไพเลอร์ตัวที่สองคุ้มค่าที่จะคงไว้ในวงจรงาน แม้ผลิตภัณฑ์จะปล่อยจริงบนตัวแรกเป็นหลัก การสแกนคลาสคำเตือนของ FPC เป็นระยะเป็น static analysis pass ราคาถูกทับซ้อนบนโค้ดเบส Delphi และมันจับ defect กลุ่มที่ชุดเทสต์ไม่มีตัวไหนเข้าถึงได้แน่นอน ส่วนวินัยเรื่อง version matrix ที่กว้างกว่าซึ่งงานนี้วางอยู่ในนั้นมีเล่าไว้ในบทความ build matrix ข้ามคอมไพเลอร์
การรองรับ Free Pascal และ Lazarus บน Windows มาพร้อมกับHotXLS Delphi spreadsheet component ในรูปแพ็กเกจ Lazarus วางเคียงข้างแพ็กเกจ Delphi กับ C++Builder โดยบิลด์จากซอร์สทรีชุดเดียวกัน ไม่ใช่ fork แยก นั่นแหละคือประเด็นของงานทั้งหมดนี้: เอนจินหนึ่งตัว สี่ toolchain และการตัดสินใจที่ผูกกับคอมไพเลอร์ถูกแยกเก็บไว้ในไฟล์ include ที่อ่านจบได้ในที่เดียว