Dezelfde Object Pascal-broncode kan zich onder Delphi en FPC/Lazarus op vier manieren anders gedragen die herhaaldelijk de code van de PDFium Component treffen: FPC verwijdert tijdelijke records van functieresultaten voordat een lidmaatschapstest in klaar is met het lezen ervan, dcc32 wordt geleverd met range-checking uitgeschakeld waardoor array-indices buiten de grenzen stilletjes ongeldige gegevens lezen, alleen Delphi 13 accepteert het toewijzen van een anonieme array of Byte aan TBytes zonder cast, en Delphi's AnsiString-samenvoeging (concatenation) kan bytes op of boven $80 vernietigen door een verborgen code-page round-trip. Elk van deze problemen leidt tot een testsuite die slaagt (groen) op de ene compiler en faalt (rood), of erger nog, stilletjes onjuist is op de andere
Als u voor het eerst een project voor twee compilers opzet, behandelt de handleiding voor de Lazarus- en FPC-viewer de probleemloze weg: packages, zoekpaden en het weergeven van een renderingvenster op het scherm. Dit artikel is het tegenovergestelde van een handleiding. Het is de lijst met problemen die we tegenkwamen nadat het basistraject werkte, toen de CI slaagde onder FPC, slaagde onder Delphi, en een wijziging die aan de ene kant werd goedgekeurd aan de andere kant ontplofte. Elke onderstaande valkuil is afkomstig van een echte fout in de PDFiumPas testsuite of de bijbehorende demo's, waarbij het sporenonderzoek op commit-niveau is samengevat in een minimale reproductie, de hoofdoorzaak en de oplossing die we hebben gestandaardiseerd
Waarom leest een set leeg uit onder FPC maar niet in Delphi?
De samenvatting in één zin: FPC kan de tijdelijke variabele die het recordresultaat van een functie bevat al finaliseren voordat een expressie die een veld van dat resultaat leest klaar is, zodat X in Func().Issues het lidmaatschap kan testen tegen een reeds vrijgegeven set, terwijl de equivalente Delphi-expressie wel correct werkt. Onze PDF/E-conformiteitstests liepen hier in hun eerste versie tegenaan. De validator retourneert een record waarvan het veld Issues een set overtredingsvlaggen is, en de asserties activeerden de aanroep in-line
// Unreliable under FPC: the function-result record temporary
// can be released before the 'in' test reads Issues
AssertTrue(pveiLzwUsed in ValidateAnsi(Pdf).Issues);
// Reliable on both compilers: pin the result to a local first
var
Vr: TPdfEValidationResult;
begin
Vr := ValidateAnsi(Pdf);
AssertTrue(pveiLzwUsed in Vr.Issues);
end;
De inlined-vorm las de set onder FPC als leeg, waardoor elke assertie die een vlag verwachtte faalde, terwijl de identieke Delphi-build slaagde. De hoofdoorzaak is een difference in hoe the twee compilers de levensduur van tijdelijke functieresultaten beheren binnen grotere expressies: Delphi houdt het tijdelijke resultaat in leven tot het einde van de instructie, terwijl FPC's vernietiging van het tijdelijke record kan racen met de set-lidmaatschapsoperator die het nog aan het lezen is. We hadden exact hetzelfde gedrag al eens eerder gedocumenteerd in een commentaar bij de helper FlagPresent in de PDF/A-testunit, en introduceerden de bug vervolgens toch opnieuw bij het vanaf nul schrijven van nieuwe tests, wat aangeeft hoe natuurlijk de foutieve vorm eruitziet. De oplossing is mechanisch en de moeite waard om als algemene regel aan te nemen: koppel een veldtoegang of set-test nooit rechtstreeks aan een functieaanroep die een record retourneert; wijs het resultaat eerst toe aan een lokale variabele en lees dan het veld. Het kost één regel code en elimineert een hele klasse van compiler-afhankelijke onvoorspelbaarheid
Waarom accepteert Delphi een array-index die FPC weigert te compileren?
Waarom accepteert Delphi een array-index die FPC weigert te compileren?
De samenvatting in één zin: dcc32 compileert een index buiten het bereik van een array met vaste grenzen en leest of schrijft tijdens runtime stilletjes aangrenzend geheugen zonder foutmelding wanneer de standaard bereikscontrole (range checking) is uitgeschakeld, terwijl FPC dezelfde index al tijdens het compileren weigert. De PDFium Component declareert quad-punten als een 1-gebaseerde array, TQuadrilateralPoint = array [1..4] of TPdfPoint, passend bij hoe QuadPoints-vermeldingen in PDF's doorgaans genummerd zijn. Een demo die deze vulde met de automatische zero-based lus werkte maandenlang onder Delphi
var
I: Integer;
begin
for I := 0 to 3 do // wrong: the array is [1..4]
Data.AttachmentPoints[I] := Corner[I]; // dcc32 default: compiles, index 0
// silently touches adjacent memory
// FPC: compile-time range check error
for I := Low(TQuadrilateralPoint) to High(TQuadrilateralPoint) do
Data.AttachmentPoints[I] := Corner[I - 1]; // correct on both compilers
end;
De Delphi-build was een vals positief resultaat: met bereikscontrole uitgeschakeld (de standaard voor dcc32) kwam index 0 terecht op het veld dat in het record voorafgaat aan de array, en de demo leek te werken. Het overzetten van dezelfde demo naar Lazarus leverde direct een compile-time range check error op van FPC. Het corrigeren van de index legde vervolgens een tweede, diepere bug bloot in het annotatiepad van de bibliotheek die door de ongeldige geheugentoegang was gemaskeerd, de bug die is ontleed in het artikel over quad-points annotaties. Twee lessen kwamen voort uit dat incident. Ten eerste: geef de voorkeur aan Low() en High() boven letterlijke grenzen wanneer het array-type niet inherent zero-based is opgebouwd. Ten tweede: behandel een FPC-compilatie, of minimaal één Delphi-build met {$R+} ingeschakeld, als een verplichte eerste controle voor elke nieuwe demo of test. De standaardinstellingen van dcc32 vertellen u niets over dit soort bugs, en een programma dat start is geen bewijs dat het correct werkt
De TBytes-toewijzing die alleen Delphi 13 accepteert
De samenvatting in één zin: het toewijzen van een veld dat gedeclareerd is als een anonieme array of Byte aan een TBytes-variabele compileert op Delphi 13 (compilerversie 37.0) maar faalt op Delphi 12 Athens en elke eerdere versie met E2010 Incompatible types: 'TArray<Byte>' and 'Dynamic array'. Dit is niet zozeer een Delphi-versus-FPC-verschil als wel een Delphi-versus-zichzelf-verschil uit het verleden, maar het treft dezelfde multi-compiler broncode op dezelfde manier: de nieuwste compiler accepteert stilletjes een constructie die alle andere versies weigeren
type
TValidator = class
private
FBuffer: array of Byte; // anonymous dynamic array type
end;
var
OrigBytes: TBytes;
begin
OrigBytes := FBuffer; // Delphi 13 only; E2010 on Delphi 12
// Athens and earlier
OrigBytes := TBytes(FBuffer); // compiles everywhere; same byte layout,
// safe hard cast
end;
We leverden exact dit op in een validatieroutine, lokaal ontwikkeld en getest op Delphi 13, waar de impliciete conversie stilletjes werd geaccepteerd. De full-source installer bedient echter een groot aantal gebruikers op Delphi 12 en ouder, en voor hen compileerde de unit simpelweg niet. De structurele oplossing is ofwel de harde cast zoals hierboven getoond, die veilig is omdat een anonieme array of Byte en TBytes een identieke structuur voor dynamische arrays delen, of beter nog, het veld in eerste instantie declareren als een benoemd type zoals TBytes, zodat er nooit een conversie nodig is. De procesmatige oplossing is belangrijker: een constructie die compileert op uw nieuwste toolchain bewijst niets voor de oudere compilers die uw gebruikers daadwerkelijk draaien, en deze categorie van regressie is onzichtbaar totdat u bouwt tegen elke ondersteunde versie. Onze release-scripts compileren de bibliotheek nu over de volledige compiler-matrix, juist omdat een lokale 37.0-build een Delphi 13-specifieke soepelheid niet kan opvangen
De AnsiString-byte die verdwijnt op een Chinese Windows-machine
De samenvatting in één zin: het samenvoegen van een ruwe byte op of boven $80 in een AnsiString met + kan onder Delphi die byte stilletjes vervangen door ? ($3F), omdat de expressie een impliciete AnsiString-naar-UnicodeString-naar-AnsiString round-trip maakt via de systeemcodepagina. We ontdekten dit via een PDF/A-test die een naam opbouwt met een geïsoleerde $FE-byte (wat nooit een geldige UTF-8-leadbyte is) om te verifiëren of de validator namen signaleert die niet geldig zijn conform ISO 19005-2 clausule 6.1.8
var
BadName: AnsiString;
begin
// On Delphi with a multi-byte system code page (observed on CP936),
// the concatenation round-trips through UnicodeString and $FE, which
// is not a valid CP936 sequence, comes back as '?' ($3F)
BadName := '/Bad' + AnsiChar($FE) + 'Name';
// Safe: build with an ASCII placeholder, then patch the byte in place;
// indexed assignment into a settled AnsiString does not round-trip
BadName := '/Bad' + #1 + 'Name';
BadName[5] := AnsiChar($FE);
end;
Op een Chinees Windows-systeem met codepagina 936 bevatte de samengevoegde string helemaal geen $FE, waardoor de bibliotheek terecht niets rapporteerde en de test faalde. Dit leek op een fout in de bibliotheek, maar dat was het niet. Een FPC-testsuite die een PDF invoerde die daadwerkelijk de $FE-byte bevatte, kreeg de verwachte vlag. De corruptie trad op binnen de test-executable van Delphi tijdens het evalueren van de string-expressie. Omdat Delphi's Unicode-first stringmodel gemengde AnsiString-expressies converteert via UnicodeString, en $FE geen geldige lead-byte is in CP936, vervangt de round-trip deze. Laten we eerlijk zijn over de grenzen: op een single-byte westerse codepagina zoals CP1252 overleeft dezelfde expressie meestal wel, wat precies de reden is waarom deze bug op de meeste ontwikkelmachines onzichtbaar blijft en alleen de kop opsteekt op Oost-Aziatische systemen of gelokaliseerde CI-runners. De regel die we hebben aangenomen: bouw nooit binaire testvectoren die bytes op of boven $80 bevatten via AnsiString-samenvoeging; patch bytes achteraf in-place nadat de string is vastgesteld zoals hierboven, of bouw de vector vanaf het begin op in TBytes
Wat een dual-compiler workflow standaard moet controleren
Vier valkuilen, één patroon: elke compiler vertelt u over een ander deel van uw bugs. FPC's compile-time range-analyse ving een index buiten bereik op die dcc32 maandenlang stilletjes uitvoerde, en dcc32's Unicode-stringmodel bracht een codepagina-afhankelijkheid aan het licht die een pure byte-georiënteerde FPC-build nooit zou veroorzaken. Het praktische gevolg is dat geen van beide afzonderlijke succesvolle pipelines (CI) voldoende is. Cross-compileren is niet alleen een vinkje voor overdraagbaarheid, het is een tweede statische analyzer en een tweede runtime-model toegepast op dezelfde broncode, in dezelfde geest als de defensieve bereikscontroles in het artikel over ABI- en geheugenbeveiliging hardening
De vaste regels die uit deze incidenten zijn voortgevloeid zijn kort genoeg om te onthouden. Koppel records van functieresultaten aan een lokale variabele alvorens velden te lezen. Doorloop arrays met vaste grenzen met Low() en High(), en draai minimaal één build met range-checking ingeschakeld of een FPC-build voordat u een nieuwe demo vertrouwt. Cast anonieme dynamische-arrayvelden expliciet of declareer ze met benoemde typen, en bouw de volledige compiler-matrix voor de release. Houd ruwe hoge bytes volledig buiten AnsiString-samenvoeging. Geen van deze regels kost meetbare inspanning zodra ze een gewoonte zijn geworden, en elk ervan elimineert een faalmodus die een workflow met slechts één compiler structureel niet kan detecteren
Alle vier de problemen zijn gevonden en verholpen tijdens het onderhoud van de PDFium Component, die dezelfde Object Pascal-broncode levert voor Delphi, C++Builder en FPC/Lazarus, en de conformiteits- en regressietests uitvoert op elk van die toolchains. De valkuilen in dit artikel worden dus bewaakt door tests in plaats van door herinnering