HotXLS builds under Free Pascal and Lazarus on Windows, and the port turned on four decisions that have nothing to do with Object Pascal syntax: keep the core in DELPHIUNICODE mode, declare the OLE structured-storage interfaces as CORBA interfaces with hand-managed reference counting, replace the Win32 AES object files with a Pascal implementation, and fix an inflate loop that could accept a truncated ZIP as complete
Anyone who has ported a mature Delphi library knows the shape of this work. The compiler accepts almost everything on the first pass. What follows is a long tail of behavioral differences that compile cleanly and produce wrong results, and a spreadsheet engine is unusually exposed to them because it touches text encoding, COM structured storage, compression and cryptography in a single code path
Why does the core insist on DELPHIUNICODE rather than plain DELPHI?
Because the formula engine depends on String and Char carrying UTF-16 semantics, and the ANSI alternative loses characters before anything reaches the file. It is tempting to build the core in FPC DELPHI mode, since that is the compatibility switch most ports reach for, and the code compiles. Then a workbook with Chinese sheet names or Cyrillic labels round-trips through the calculation path and the characters are gone by the time the writer sees them, with no error anywhere
The mode is not uniform across the library, and that is deliberate rather than untidy. The PNG byte decoder and the LCL overrides genuinely need ANSI signatures, because they deal in bytes and in what the widgetset hands them. Those units enable a separate LX_FPC_ANSI switch. Two modes in one library sounds like a smell until you notice the alternative is a byte decoder that treats its input as text
There is a companion detail that catches people later. DELPHIUNICODE does not make TFormatSettings.DecimalSeparator a WideChar in the FPC runtime. Input carrying a Unicode decimal separator has to be normalized to an ASCII separator inside the Unicode string first, and any input whose separator does not match the expected one must be rejected rather than silently truncated at the character the parser did not recognize
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // must come first: initializes the LCL widgetset
SysUtils, lxHandle; // and the UTF-8 conversion layer
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.
The Interfaces unit is not optional and it has to come first. It is what initializes the LCL widgetset and the UTF-8 conversion layer, and HotXLS relies on both once fonts, file paths or text cross the RTL and LCL boundary. A console program that skips it will compile and will misbehave on any non-ASCII path. This is also the reason a successful compile proves so little here: the port was only demonstrably working once real documents with real font names and real paths made a full round trip
A class VMT is not a COM vtable
Free Pascal will not let you hand a class VMT to Windows as a COM interface vtable, even when the declaration looks identical to the one Delphi accepts. The layouts differ in ways that produce a call into the wrong slot, which manifests as a crash somewhere unrelated to the call site. Structured storage matters here because the classic binary workbook format is an OLE compound file, and reading or writing one means implementing ILockBytes that the Windows storage API will call back into
The working arrangement is a CORBA interface with the COM slots declared explicitly and AddRef and Release managed by hand. That means giving up automatic reference counting for these types and taking responsibility for the lifetime, which is a fair trade for a handful of interfaces that live inside one unit. The specific trap inside that work is QueryInterface: it must return an interface pointer, not the object pointer. Both compile. One of them hands Windows an address whose first machine word is not a vtable
The FPC-specific declarations live in lxOleInterfaces.inc, next to lxAESBackend.inc and lxZlibBackend.inc in the FPC source directory, so the compiler-specific choices sit in one place rather than being scattered through the engine. The format itself and how the library navigates it are described in reading OLE2 compound files in Pascal
One more type detail belongs to the same family. LargeInt must resolve to Int64 in the FPC branch, and the compiler classification of Comp differs enough between the two toolchains that overload resolution can pick a different candidate. Test large-offset behavior with a file stream rather than an HGLOBAL stream: the Windows global-memory stream wraps around on seeks past 4 GiB by itself, so a passing test there proves nothing about your own arithmetic
What a self-consistent AES implementation hides
The Win32 AES object files that the Delphi build links are OMF, and the Free Pascal linker cannot consume them, so the FPC branch uses a Pascal AES implementation instead. Delphi continues to link the object files it always did, which keeps the released binary unchanged for existing customers
The verification requirement is the part worth carrying into any project. Encrypting data and decrypting it again with the same implementation proves nothing at all: a symmetric algorithm with a wrong key schedule, wrong block order or wrong chaining is perfectly self-consistent and will round-trip its own output every time. Only known-answer vectors catch it, checking the key expansion, block order and CBC chaining against published values. Ship a self-consistent wrong implementation and the symptom appears the first time a customer opens the file in Excel
Compression had a defect of a different character. A Pascal inflate backend can still have output pending after it has consumed all its compressed input, so the caller must keep calling until the stream reports its end. Treating exhausted input as end-of-stream truncates the last block. Worse, it turns a damaged archive into a silently accepted one, which is exactly the failure mode the hardening in validating the ZIP end-of-central-directory record exists to prevent. The rule is that no progress plus not finished is a truncation error, never an EOF
Two build-system traps that cost real hours
The LCL search paths have to precede the FPC package wildcard paths, or the Free Vision Menus unit shadows the LCL unit of the same name and you get a PPU checksum mismatch that says nothing about either. A Lazarus installation that was moved after being installed can also leave stale paths in fpc.cfg, so the build entry points specify unit and binary paths explicitly rather than inheriting whatever the environment offers
The second trap has nothing to do with Pascal. A .cmd batch file written with LF line endings works until the file grows past the size of the interpreter read buffer, at which point call :label fails with a claim that the batch label does not exist, and the failure appears at whichever program happens to sit past the boundary. Any tool that rewrites a batch script has to write CRLF back. And lazbuild --build-all clears the package unit output directory before compiling, so an options file parked in that directory is deleted before it can be read: keep it outside, and remember the @ path is resolved relative to the package directory because lazbuild invokes the compiler from there
// Lazarus grid export: TGridToXLS ships in the Lazarus package, so the
// same DB-grid export code works in an 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;
What a compiler warning is worth
Free Pascal reports uninitialized local variables that Delphi does not, and running the FPC build turned that difference into two real defects in the calculation unit. One function read a count variable that was never assigned before use, and another used two coordinates in one branch before the code that computed them ran in a different branch. Under Delphi both behaved according to whatever the stack happened to hold, which is the definition of a bug that reproduces on one machine and not another
The practical conclusion is that the second compiler is worth keeping in the loop even for a product that ships primarily on the first. Scanning the FPC warning classes periodically is a cheap static analysis pass over a Delphi codebase, and it finds a category of defect that no test suite reliably reaches. The wider version-matrix discipline this sits inside is described in the cross-compiler build matrix
Free Pascal and Lazarus support for Windows ships with the HotXLS Delphi spreadsheet component as a Lazarus package alongside the Delphi and C++Builder packages, built from the same source tree rather than a fork. That is the point of the exercise: one engine, four toolchains, and the compiler-specific decisions isolated in include files where they can be read in one sitting