HotXLS build được dưới Free Pascal và Lazarus trên Windows, và bản port xoay quanh bốn quyết định chẳng dính gì tới cú pháp Object Pascal: giữ lõi ở mode DELPHIUNICODE, khai báo các OLE structured-storage interface dưới dạng CORBA interface với reference counting tự quản, thay các AES object file Win32 bằng một bản triển khai Pascal, và sửa một vòng inflate có thể nhận một ZIP cụt làm đầy đủ
Ai đã từng port một library Delphi trưởng thành đều nhận ra hình dáng của công việc này. Compiler chấp nhận gần như mọi thứ ngay lần chạy đầu. Phía sau là một cái đuôi dài của các khác biệt hành vi compile sạch sẽ mà cho ra kết quả sai, và một spreadsheet engine tiếp xúc với chúng nhiều bất thường vì nó đụng tới text encoding, COM structured storage, nén và mã hóa trong cùng một code path
Vì sao lõi khăng khăng DELPHIUNICODE thay vì DELPHI thuần?
Vì formula engine phụ thuộc vào String và Char mang semantics UTF-16, và phương án ANSI làm mất ký tự trước khi bất cứ thứ gì kịp chạm tới file. Thật cám dỗ để dựng lõi ở mode DELPHI của FPC, vì đó là công tắc tương thích mà đa số bản port với tới, và code compile được. Rồi một workbook mang tên sheet tiếng Trung hay nhãn chữ Cyrillic đi qua đường tính toán và ký tự biến mất trước khi writer nhìn thấy, không một tiếng error
Mode không đồng nhất khắp library, và đó là chủ ý chứ không phải bừa bộn. PNG byte decoder và các LCL override thật sự cần signature ANSI, vì chúng làm việc với byte và với cái widgetset đưa cho. Những unit đó bật một công tắc riêng LX_FPC_ANSI. Hai mode trong một library nghe như một code smell cho tới khi bạn nhận ra phương án còn lại là một byte decoder coi input của nó là text
Có một chi tiết bạn đồng hành bắt người ta về sau. DELPHIUNICODE không biến TFormatSettings.DecimalSeparator thành WideChar trong FPC runtime. Input mang một Unicode decimal separator phải được normalize về separator ASCII bên trong chuỗi Unicode trước, và mọi input mà separator không khớp với cái mong đợi phải bị từ chối thay vì bị cắt lặng lẽ tại đúng ký tự parser không nhận ra
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // phải đứng đầu: khởi tạo LCL widgetset
SysUtils, lxHandle; // cùng tầng chuyển đổi 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.
Unit Interfaces không phải tùy chọn và phải đứng đầu. Nó khởi tạo LCL widgetset cùng tầng chuyển đổi UTF-8, và HotXLS dựa vào cả hai ngay khi font, file path hay text vượt qua ranh giới RTL và LCL. Một console program bỏ nó sẽ compile được và sẽ hành xử sai trên mọi path ngoài ASCII. Đây cũng là lý do một lần compile thành công chứng minh được ít điều đến thế: bản port chỉ được chứng minh là chạy khi những document thật với tên font thật và path thật đi trọn một vòng round-trip
VMT của class không phải vtable của COM
Free Pascal không cho bạn đưa VMT của class cho Windows làm COM interface vtable, kể cả khi declaration nhìn giống hệt cái Delphi chấp nhận. Layout khác nhau theo cách tạo ra một lời gọi vào nhầm slot, hiện tượng là một crash ở đâu đó chẳng liên quan gì tới call site. Structured storage ở đây quan trọng vì binary workbook format kinh điển là một OLE compound file, và đọc hay ghi nó nghĩa là triển khai ILockBytes mà Windows storage API sẽ gọi ngược vào
Bố cục chạy được là một CORBA interface với các COM slot khai tường minh và AddRef cùng Release tự quản tay. Nghĩa là từ bỏ automatic reference counting cho những type này và nhận trách nhiệm về vòng đời — một cái giá công bằng cho một nắm interface sống trong đúng một unit. Cái bẫy cụ thể trong công việc đó là QueryInterface: nó phải trả về interface pointer, không phải object pointer. Cả hai đều compile được. Một trong hai đưa cho Windows một địa chỉ mà machine word đầu tiên không phải một vtable
Các declaration riêng cho FPC nằm trong lxOleInterfaces.inc, cạnh lxAESBackend.inc và lxZlibBackend.inc trong thư mục source FPC, để những lựa chọn theo compiler nằm một chỗ thay vì rải khắp engine. Bản thân format cùng cách library len qua nó được mô tả trong đọc OLE2 compound file bằng Pascal
Còn một chi tiết type cùng họ. LargeInt phải resolve thành Int64 trong nhánh FPC, và cách compiler phân loại Comp khác nhau đủ nhiều giữa hai toolchain để overload resolution chọn ứng viên khác nhau. Hãy test hành vi offset lớn bằng một file stream thay vì một HGLOBAL stream: Windows global-memory stream tự wrap khi seek quá 4 GiB, nên một test pass ở đó chẳng chứng minh gì về phép tính của chính bạn
Một bản triển khai AES tự nhất quán che giấu điều gì
Các AES object file Win32 mà bản build Delphi link là OMF, và Free Pascal linker không tiêu hóa được chúng, nên nhánh FPC dùng một bản triển khai AES bằng Pascal. Delphi tiếp tục link những object file như mọi khi, giữ cho binary đã phát hành không đổi với các khách hàng hiện hữu
Yêu cầu kiểm chứng mới là phần đáng mang đi mọi project. Encrypt dữ liệu rồi decrypt lại bằng cùng một bản triển khai chẳng chứng minh gì cả: một thuật toán đối xứng với key schedule sai, thứ tự block sai hay chaining sai vẫn tự nhất quán hoàn hảo và round-trip output của chính nó mỗi lần. Chỉ known-answer vector bắt được nó, đối chiếu key expansion, thứ tự block và CBC chaining với các giá trị đã công bố. Ship một bản triển khai sai mà tự nhất quán thì triệu chứng xuất hiện ngay lần đầu một khách hàng mở file trong Excel
Phần nén có một defect tính cách khác. Một inflate backend viết bằng Pascal vẫn có thể còn output đọng sau khi đã tiêu hết compressed input, nên caller phải gọi tiếp cho tới khi stream báo hết. Coi input cạn là end-of-stream là cắt cụt block cuối. Tệ hơn, nó biến một archive hư hại thành một archive được chấp nhận lặng lẽ — đúng failure mode mà phần hardening trong validate ZIP end-of-central-directory record tồn tại để ngăn. Luật là: không tiến thêm cộng chưa xong là một truncation error, không bao giờ là EOF
Hai cái bẫy build system tốn thật nhiều giờ
Các LCL search path phải đứng trước các wildcard path của FPC package, nếu không unit Menus của Free Vision che khuất unit LCL cùng tên và bạn nhận một PPU checksum mismatch chẳng nói gì về bên nào. Một bản cài Lazarus từng bị dời đi sau khi cài cũng có thể để lại path cũ trong fpc.cfg, nên các build entry point chỉ định unit path và binary path tường minh thay vì thừa hưởng cái gì môi trường cho
Cái bẫy thứ hai chẳng dính gì tới Pascal. Một file batch .cmd viết với LF line endings chạy tốt cho tới khi file vượt quá kích thước read buffer của interpreter, lúc đó call :label fail với một lời khẳng định batch label không tồn tại, và failure xuất hiện ở đúng chương trình nào tình cờ nằm sau ranh giới. Bất kỳ công cụ nào rewrite một batch script đều phải ghi trả CRLF. Và lazbuild --build-all dọn thư mục output unit của package trước khi compile, nên một options file đậu trong thư mục đó bị xóa trước khi kịp đọc: hãy để nó bên ngoài, và nhớ rằng đường dẫn @ được resolve tương đối theo thư mục package vì lazbuild gọi compiler từ đó
// Export grid Lazarus: TGridToXLS nằm trong Lazarus package, nên
// cùng code export DB-grid chạy được trong một LCL application
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;
Một compiler warning đáng giá bao nhiêu
Free Pascal báo các biến cục bộ chưa khởi tạo mà Delphi không báo, và việc chạy bản build FPC biến sự khác biệt đó thành hai defect có thật trong unit tính toán. Một hàm đọc một biến đếm chưa từng được gán trước khi dùng, và một hàm khác dùng hai tọa độ ở một nhánh trước khi code tính chúng chạy ở một nhánh khác. Dưới Delphi cả hai hành xử theo những gì stack tình cờ đang giữ — định nghĩa chuẩn xác của một bug tái hiện trên máy này mà không trên máy khác
Kết luận thực dụng là compiler thứ hai đáng giữ trong vòng lặp ngay cả với một product chủ yếu ship trên compiler đầu tiên. Quét các warning class của FPC định kỳ là một pass static analysis rẻ cho một codebase Delphi, và nó bắt được một hạng defect mà không test suite nào với tới đáng tin cậy. Kỷ luật version matrix rộng hơn mà chuyện này nằm trong được mô tả trong build matrix đa compiler
Hỗ trợ Free Pascal và Lazarus cho Windows đi kèm HotXLS Delphi spreadsheet component dưới dạng một Lazarus package cạnh các package Delphi và C++Builder, build từ cùng một source tree chứ không phải một fork. Đó chính là điểm của toàn bộ cuộc chơi: một engine, bốn toolchain, và các quyết định theo compiler được cách ly trong include file để đọc hết trong một lần ngồi