PDFlibPas compila sotto Free Pascal per Windows a 32 bit, e la parte difficile non è mai stata il Pascal. Sono stati i file oggetto: gli oggetti AES e OpenJPEG che la build Delphi collega sono in formato OMF, il linker interno di Free Pascal richiede COFF, e la conversione tra i due produce nomi di sezione e simboli di definizione sezione che fanno fallire il linker con errori interni invece che con una diagnostica sensata
Chi ha mai collegato oggetti C dentro una libreria Pascal conosce questo territorio. Win64 è relativamente civile: un solo formato oggetto, una sola convenzione di chiamata, nessuna decorazione dei nomi. Win32 conserva ogni strato di storia accumulato dalla piattaforma, e una libreria che linka staticamente codice C di terze parti si scontra con tutti quegli strati in una volta sola
La directory del compilatore non ti dice qual è il target
Partiamo dal punto di ingresso della build, perché sbagliare qui fa perdere ore prima ancora di toccare un file oggetto. Il nome di una directory di installazione di Free Pascal identifica dove vive il compilatore principale, non cosa produce. Un compilatore host a 32 bit può invocare un cross-compiler seduto accanto a lui ed emettere codice a 64 bit quando passi gli switch di target giusti, quindi dedurre il target dal percorso è un tiro a indovinare che funziona finché qualcuno non riorganizza la toolchain
L'approccio affidabile è chiedere al compilatore. Interroga il processore target e il sistema operativo effettivi tramite gli switch informativi del compilatore stesso, e accetta entrambi i layout di installazione comuni, la directory binaria piatta e quella annidata per versione, perché installer e gestori di toolchain diversi producono forme diverse. Uno script di build che hard-coda uno dei due layout funziona esattamente su una macchina
Perché un file oggetto convertito rompe il linker interno?
Perché la conversione preserva la convenzione di denominazione delle sezioni OMF e sintetizza simboli di definizione sezione che non corrispondono a ciò che il linker COFF si aspetta. Convertire gli oggetti OMF in COFF è necessario ma non sufficiente: i file risultanti portano i classici nomi di sezione _TEXT, _DATA e _BSS, più i nomi dei simboli di definizione sezione derivati da essi, e dare tutto questo in pasto al linker interno di Free Pascal produce errori interni del compilatore invece di un messaggio sui nomi delle sezioni
Un errore interno è il peggior modo di fallire per un problema di build, perché non dice nulla su cosa fosse sbagliato nell'input. La correzione è un passaggio di normalizzazione post-conversione sul file COFF: riscrivere i nomi delle sezioni nella forma attesa e riscrivere di conseguenza i simboli di definizione sezione corrispondenti, lasciando intatti l'indice dei simboli, i byte del codice e le rilocazioni. Questo ultimo vincolo è tutta la difficoltà. Una riscrittura che rinumera i simboli o sposta gli offset produce un oggetto che linka e poi va in crash
C'è un passaggio preliminare per uno dei due set di oggetti. Gli oggetti OpenJPEG costruiti con il classico compilatore C++ a 32 bit dipendono dalle routine helper private di Delphi per gli interi a 64 bit, che Free Pascal non fornisce, quindi nessuna quantità di conversione di formato li rende utilizzabili. Quelli vanno ricompilati prima con il compilatore basato su Clang, che non emette quelle dipendenze, e convertiti dopo
// Gli oggetti per il target FPC vivono in una directory dedicata. Non
// sostituiscono il set di oggetti Delphi, perché entrambe le toolchain
// compilano dallo stesso albero sorgente e ognuna ha bisogno dei propri input di linking
//
// Lib\thirdparty\Win32 oggetti OMF Delphi, invariati
// Lib\thirdparty\Win32f oggetti COFF per FPC, convertiti e normalizzati
//
// Punti di ingresso della build:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
Gli helper privati del compilatore non sono portabili, e le loro convenzioni nemmeno
Il runtime Delphi fornisce trampolini in assembly per le operazioni su interi a 64 bit su x86 a 32 bit, e gli oggetti C precompilati costruiti per Delphi li chiamano. Free Pascal ha un suo assetto, quindi quei riferimenti vanno soddisfatti in modo diverso, non rediretti. Il dettaglio che rende impossibile la redirezione è la convenzione di chiamata: l'helper di timing usato dal codice di imaging si fa ripulire l'argomento a quattro byte dal callee, mentre l'helper di divisione a 64 bit ripulisce sedici byte e restituisce il risultato nella classica coppia di registri. Due helper, due convenzioni, e un trampolino scritto per uno corrompe silenziosamente lo stack per l'altro
La decorazione dei nomi aggiunge la seconda metà del problema. Su Win32, Free Pascal antepone automaticamente un underscore alle importazioni C esterne mentre esporta alla lettera le dichiarazioni public name, quindi il lato importazione e il lato esportazione dello stesso ponte seguono regole diverse. Il ponte verso il runtime C di cui OpenJPEG ha bisogno deve quindi esportare i nomi dei simboli C esatti, e i punti di ingresso variadici richiedono un salto indiretto a 32 bit invece di uno diretto. Detto così, nulla di tutto questo è esotico. E tutto fallisce come errore di linking che nomina un simbolo che nessuno ha mai scritto
Perché un eseguibile Win32 moriva prima del main?
Una DLL a 64 bit sul percorso di ricerca, raggiunta perché l'unità zlib di Free Pascal silega dinamicamente invece di linkare staticamente. Il sintomo era un'uscita immediata con lo status code di immagine non valida, prima che girasse qualsiasi istruzione Pascal del programma, il che ti manda a ispezionare il programma appena costruito quando il difetto sta nel loader che risolve un import contro l'architettura sbagliata
La lezione riguarda le assunzioni, non zlib in sé. Un'unità che prende il nome da una libreria di compressione non necessariamente ne contiene una: può essere un binding che si aspetta una libreria condivisa a runtime, e una dipendenza dinamica non voluta è una responsabilità in deployment anche quando per caso si risolve. Passare all'implementazione stream in Pascal puro dà a entrambi i target un percorso di compressione incluso staticamente senza alcuna dipendenza esterna, che è ciò che una libreria incorporata nell'applicazione di qualcun altro dovrebbe avere fin dall'inizio
Lo stesso istinto vale per il backend dell'encoder JBIG2 esterno. Sul target a 32 bit l'encoder esterno non viene linkato, quindi le richieste ricadono sull'encoder Pascal integrato, e il test che verifica questo comportamento deve controllare lo stato di registrazione del target corrente invece di prendere una codifica riuscita come prova della presenza del backend esterno. Un fallback che funziona è esattamente la cosa che nasconde una dipendenza mancante, ed è lo schema di guasto esaminato in diagnosi dei guasti silenziosi degli stub. Il lavoro sul linking statico a 64 bit è trattato in linking statico di jbig2enc sotto FPC
Aritmetica a 32 bit su uno stream in memoria
Codice che manipola le dimensioni dei buffer con aritmetica unsigned della larghezza del puntatore è corretto su Win64 e su Win32 è a un'immagine grande dall'overflow. Lo stream in memoria che alimenta il codec JPEG 2000 cresce per raddoppio e avanza per addizione, e su un target a 32 bit entrambe le operazioni possono andare in wrap su input grandi ma del tutto legittimi
Quindi ogni scrittura, skip, seek e allocazione iniziale verifica prima di calcolare, e il tetto di capacità è il massimo valore signed della larghezza del puntatore, scelto per corrispondere a ciò che la routine di block move e i valori di ritorno delle callback possono esprimere. Il requisito comportamentale quando una richiesta viene rifiutata è facile sbagliare: il rifiuto non deve cambiare né la posizione né la lunghezza dello stream. Una mutazione parziale seguita da un errore lascia lo stream in uno stato su cui il chiamante non può ragionare, e l'operazione successiva peggiora le cose
// Controlla prima di calcolare. Su Win32 entrambe queste operazioni
// vanno in wrap su input che una grande immagine JPEG 2000 produce legittimamente
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // rifiuta, posizione e dimensione restano com'erano
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // il raddoppio andrebbe in overflow
NewCapacity := NewCapacity shl 1;
end;
Due trappole dell'output di build che sopravvivono al porting
Separare gli eseguibili di test e di esempio per architettura target in directory di output per target è ovviamente la scelta giusta e rompe immediatamente tutto ciò che localizzava i propri dati di test contando i livelli di directory verso l'alto. La correzione è cercare verso l'alto la directory degli asset invece di assumere una profondità fissa, con una restrizione voluta: il sample di firma accetta un certificato di fallback solo dalla propria directory di progetto, mai da una directory antenata arbitraria, perché un certificato con lo stesso nome trovato più su nell'albero è una sorpresa di sicurezza, non una comodità
La seconda trappola sopravvive a ogni porting e vale la pena portarsela dietro in qualsiasi progetto FPC. Dopo un aggiornamento del compilatore, far rifiutare al compilatore i file PPU obsoleti non basta, perché il linker preferisce comunque i file oggetto rimasti nel percorso di ricerca delle unità anche quando il PPU caricato proveniva dalla directory corretta, e aggiungere un percorso di output oggetto esplicito non scavalca quella preferenza. L'unica risposta affidabile è una directory di unità temporanea fresca a ogni giro di build. Qualcosa di meno produce un binario linkato da due versioni del compilatore, che fallisce in modi che sembrano bug nel sorgente
I condizionali di piattaforma sono l'ultimo tassello, e scegliere l'asse giusto conta più di quanto sembri. La domanda giusta di solito è se il codice sia specifico per Windows, non se una particolare libreria di widget sia presente, come ha mostrato il lavoro sulla conversione dei metafile in import vettoriale EMF e condizionali di piattaforma: spostare quella guardia da una condizione sulla libreria di controlli a una condizione sulla piattaforma ha trasformato una presunta riscrittura nella modifica di una direttiva. Il supporto di Free Pascal e Lazarus per entrambi i target Windows è incluso nella PDFlibPas Delphi PDF library, compilata dalle stesse sorgenti dei pacchetti Delphi e C++Builder