Teknisk artikkel

HotXLS på Free Pascal: Unicode, COM-slots og zlib

HotXLS bygges under Free Pascal og Lazarus på Windows, og porteringen hang på fire beslutninger som ikke har noe med Object Pascal-syntaks å gjøre: holde kjernen i DELPHIUNICODE-modus, deklarere OLE structured storage-grensesnittene som CORBA-grensesnitt med håndstyrt referansetelling, erstatte Win32 AES-objektfilene med en Pascal-implementasjon, og fikse en inflate-løkke som kunne godta en avkortet ZIP som fullstendig

Alle som har portert et modent Delphi-bibliotek, kjenner formen på dette arbeidet. Kompilatoren godtar nesten alt i første gjennomløp. Det som følger, er en lang hale av atferdsforskjeller som kompilerer rent og gir feil resultater, og en regnearkmotor er uvanlig eksponert for dem fordi den berører tekstkoding, COM structured storage, kompresjon og kryptografi i én kodevei

Hvorfor insisterer kjernen på DELPHIUNICODE snarere enn ren DELPHI?

Fordi formelmotoren avhenger av at String og Char bærer UTF-16-semantikk, og ANSI-alternativet mister tegn før noe som helst når filen. Det er fristende å bygge kjernen i FPC DELPHI-modus, siden det er kompatibilitetsbryteren de fleste porteringer griper etter, og koden kompilerer. Så runder en arbeidsbok med kinesiske arkenavn eller kyrilliske merkelapper turen gjennom beregningsveien, og tegnene er borte i det skriveren ser dem, uten noen feil noe sted

Modusen er ikke ensartet på tvers av biblioteket, og det er bevisst snarere enn uordnet. PNG-byte-dekoderen og LCL-override-ene trenger genuint ANSI-signaturer, fordi de omgås byte og det widgetsettet gir dem. De enhetene slår på en separat LX_FPC_ANSI-bryter. To moduser i ett bibliotek høres ut som en code smell helt til du legger merke til at alternativet er en byte-dekoder som behandler inndataene sine som tekst

Det finnes en tilhørende detalj som fanger folk senere. DELPHIUNICODE gjør ikke TFormatSettings.DecimalSeparator til en WideChar i FPC-kjøretiden. Inndata med et Unicode-desimaltegn må normaliseres til et ASCII-tegn inne i Unicode-strengen først, og alle inndata hvis skilletegn ikke matcher det forventede, må avvises i stedet for å bli stille avkortet ved tegnet parseren ikke gjenkjente

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // må komme først: initialiserer LCL-widgetsettet
  SysUtils, lxHandle;  // og UTF-8-konverteringslaget

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.

Enheten Interfaces er ikke valgfri, og den må komme først. Det er den som initialiserer LCL-widgetsettet og UTF-8-konverteringslaget, og HotXLS er avhengig av begge så snart skrifter, filstier eller tekst krysser RTL- og LCL-grensen. Et konsollprogram som hopper over den, vil kompileres og vil oppføre seg galt på enhver ikke-ASCII-sti. Dette er også grunnen til at en vellykket kompilering beviser så lite her: porteringen var bare demonstrabelt fungerende da ekte dokumenter med ekte skriftnavn og ekte stier gjorde en full rundtur

En klasse-VMT er ikke en COM-vtable

Free Pascal lar deg ikke gi en klasse-VMT til Windows som en COM-grensesnitt-vtable, selv når deklarasjonen ser identisk ut med den Delphi aksepterer. Oppsettene skiller seg på måter som gir et kall inn i feil slot, noe som manifesterer seg som et krasj et sted urelatert til kallstedet. Structured storage betyr noe her fordi det klassiske binære arbeidsbokformatet er en OLE compound-fil, og å lese eller skrive én betyr å implementere ILockBytes som Windows storage API vil kalle tilbake til

Ordningen som fungerer, er et CORBA-grensesnitt med COM-slots deklarert eksplisitt og AddRef og Release håndtert for hånd. Det betyr å gi opp automatisk referansetelling for disse typene og ta ansvar for levetiden, noe som er en rimelig avveining for en neve grensesnitt som lever inne i én enhet. Den spesifikke fellen inne i det arbeidet er QueryInterface: den må returnere en grensesnittpeker, ikke objektpekeren. Begge kompilerer. En av dem gir Windows en adresse hvis første maskinord ikke er en vtable

Diagram som sammenligner Free Pascal klasse-VMT med COM-grensesnitt-vtable-en HotXLS må presentere for Windows structured storage API: ulike slotordener for samme Pascal-deklarasjon, pluss QueryInterface-fellen der å returnere objektpekeren i stedet for en grensesnittpeker sender et ILockBytes-kall inn i en klasse-slot og krasjer langt fra kallstedet
Free Pascal nekter å servere en klasse-VMT som en COM-vtable, så HotXLS deklarerer CORBA-grensesnitt med eksplisitte COM-slots og håndstyrt AddRef og Release, og QueryInterface returnerer grensesnittpekeren Windows kan dereferere

De FPC-spesifikke deklarasjonene ligger i lxOleInterfaces.inc, ved siden av lxAESBackend.inc og lxZlibBackend.inc i FPC-kildekatalogen, så de kompilatorspesifikke valgene ligger ett sted i stedet for å være strødd gjennom motoren. Formatet selv og hvordan biblioteket navigerer det, er beskrevet i lesing av OLE2 compound-filer i Pascal

Enda en typedetalj hører til samme familie. LargeInt må løses til Int64 i FPC-grenen, og kompilatorens klassifisering av Comp skiller nok mellom de to verktøykjedene til at overload-oppløsning kan velge en annen kandidat. Test stor-offset-atferd med en filstrøm snarere enn en HGLOBAL-strøm: Windows global-minne-strømmen wrappes rundt på søk forbi 4 GiB av seg selv, så en bestående test der beviser ingenting om aritmetikken din

Hva en selvkonsistent AES-implementasjon skjuler

Win32 AES-objektfilene som Delphi-bygget kobler inn, er OMF, og Free Pascal-linkeren kan ikke konsumere dem, så FPC-grenen bruker en Pascal AES-implementasjon i stedet. Delphi fortsetter å koble inn objektfilene den alltid har gjort, noe som holder den utgitte binærfilen uendret for eksisterende kunder

Verifiseringskravet er delen verdt å ta med inn i ethvert prosjekt. Å kryptere data og dekryptere dem igjen med samme implementasjon beviser ingenting i det hele tatt: en symmetrisk algoritme med feil nøkkelplan, feil blokkrekkefølge eller feil kjeding er perfekt selvkonsistent og runder turen med sitt eget utdata hver gang. Bare kjente-svar-vektorer fanger det, ved å sjekke nøkkelekspansjonen, blokkrekkefølgen og CBC-kjedingen mot publiserte verdier. Send ut en selvkonsistent gal implementasjon, og symptomet dukker opp første gang en kunde åpner filen i Excel

Kompresjonen hadde en defekt av en annen karakter. En Pascal inflate-backend kan fortsatt ha utdata hengende etter at den har konsumert alt sitt komprimerte inndata, så kalleren må fortsette å kalle til strømmen rapporterer sin slutt. Å behandle utmattet inndata som slutt på strømmen avkorter siste blokk. Verre, det gjør et skadet arkiv til et stille akseptert et, noe som er nøyaktig feilmoden herdingen i validering av ZIP end-of-central-directory-posten finnes for å forhindre. Regelen er at ingen fremgang pluss ikke ferdig er en avkortingsfeil, aldri en EOF

To byggssystemfeller som koster ekte timer

LCL-søkestiene må komme foran FPC-pakkens jokertegnstier, ellers skygger Free Vision-enheten Menus LCL-enheten med samme navn, og du får en PPU-sjekksum-mismatch som ikke sier noe om noen av dem. En Lazarus-installasjon som ble flyttet etter installasjon, kan også etterlate foreldede stier i fpc.cfg, så bygg-inngangspunktene spesifiserer enhets- og binærstier eksplisitt i stedet for å arve hva miljøet tilbyr

Den andre fellen har ingenting med Pascal å gjøre. En .cmd-batchfil skrevet med LF-linjeslutt fungerer til filen vokser forbi størrelsen på tolkens lesebuffer, og da feiler call :label med påstanden om at batch-etiketten ikke finnes, og feilen viser seg ved det programmet som tilfeldigvis ligger forbi grensen. Ethvert verktøy som omskriver et batchskript, må skrive CRLF tilbake. Og lazbuild --build-all tømmer pakkens enhets-utdatakatalog før kompilering, så en alternativfil parkert i den katalogen slettes før den kan leses: hold den utenfor, og husk at @-stien løses relativt til pakkekatalogen fordi lazbuild kaller kompilatoren derfra

Kart over de to farelagene bak en ren HotXLS Free Pascal-kompilering: DELPHIUNICODE-modusen som holder String og Char i UTF-16, LX_FPC_ANSI-utveien for PNG-byte-dekoderen og LCL-override-ene, og byggfeller fra Free Vision Menus-skyggen, foreldede fpc.cfg-stier, batchfiler med kun LF og lazbuild-utdatatømming
En første kompilering beviser lite: moduskartet bestemmer hvilke tegn som overlever til skriveren, mens byggssystemfellene viser seg som sjekksum-mismatcher, spøkelsesaktige manglende etiketter og alternativfiler slettet før de leses
// Lazarus grid-eksport: TGridToXLS følger med Lazarus-pakken, så den
// samme DB-grid-eksportkoden fungerer i en LCL-applikasjon
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;

Hva en kompilatoradvarsel er verdt

Free Pascal rapporterer uinitialiserte lokale variabler som Delphi ikke gjør, og å kjøre FPC-bygget gjorde den forskjellen om til to reelle defekter i beregningsenheten. Én funksjon leste en tellevariabel som aldri var tilordnet før bruk, og en annen brukte to koordinater i én gren før koden som beregnet dem, kjørte i en annen gren. Under Delphi oppførte begge seg etter hva stabelen tilfeldigvis holdt, noe som er definisjonen på en feil som reproduserer på én maskin og ikke en annen

Den praktiske konklusjonen er at den andre kompilatoren er verdt å beholde i loopen selv for et produkt som hovedsakelig leveres på den første. Å skanne FPC-advarselklassene med jevne mellomrom er en billig statisk analyse-runde over en Delphi-kodebase, og den finner en kategori av defekter som ingen test rekkevidde pålitelig når. Den bredere versjonsmatrise-disiplinen dette sitter inne i, er beskrevet i tverrkompilator-byggmatrisen

Free Pascal- og Lazarus-støtte for Windows følger med HotXLS Delphi regnearkkomponent som en Lazarus-pakke ved siden av Delphi- og C++Builder-pakkene, bygget fra samme kildetre snarere enn en fork. Det er poenget med øvelsen: én motor, fire verktøykjeder, og de kompilatorspesifikke beslutningene isolert i include-filer der de kan leses i én sitting