Articolo tecnico

Free Pascal Win32: decorazione dei simboli C in HotPDF

Free Pascal su Win32 antepone automaticamente un underscore a ogni import cdecl; external, mentre public name esporta la stringa che hai scritto, carattere per carattere. HotPDF deve soddisfare entrambe le convenzioni nello stesso albero sorgente, perché la build Delphi include già dichiarazioni di import che scrivono l'underscore a mano. Sbagliare questa asimmetria produce errori di linking che nominano un simbolo che nessuno ha scritto

Estendere una libreria Delphi a Free Pascal viene di solito descritto come un problema di portabilità, e su Win64 per lo più lo è. Win32 è un'altra storia. L'ABI Windows x86 a 32 bit porta con sé trent'anni di convenzioni accumulate su come si scrivono i simboli C, chi ripulisce lo stack, e quali helper privati del compilatore una unit di traduzione può dare per scontati, e ognuno di questi è un punto in cui due compilatori Pascal d'accordo sul linguaggio possono ancora essere in disaccordo sul file oggetto

Perché lo stesso simbolo si risolve su Win64 e fallisce su Win32?

Perché il prefisso underscore è una convenzione a 32 bit che Free Pascal applica alle import ma non alle export. Dichiara function deflate(...): Integer; cdecl; external; e FPC cerca _deflate nel file oggetto su Win32, e deflate su Win64. È un comportamento corretto e coincide con ciò che emette un compilatore C. La trappola sta dall'altro lato del ponte: una routine marcata public name 'deflate' esporta esattamente deflate su entrambi i target, senza alcun prefisso aggiunto

Ora aggiungi il dettaglio storico che rende tutto concreto. La build Delphi dichiara già alcuni di questi punti di ingresso con l'underscore scritto nel nome, perché è ciò che contengono i suoi stessi file oggetto. Dai la stessa dichiarazione a FPC su Win32 e il compilatore, zelosamente, la prefissa di nuovo, così il linker va a caccia di __deflate, un simbolo che nessuno esporta. La correzione intuitiva, aggiungere un underscore dappertutto, rompe le import che erano già scritte correttamente

Ciò che funziona è una coppia di costanti di prefisso invece di una singola. HPDFFPCZLib e HPDFFPCCodecStubs usano un prefisso per le import C semplici e un altro per le import che portano già un prefisso lato Delphi, e su Win64 entrambe le costanti sono vuote così i nomi di linking esistenti sopravvivono intatti. Due costanti invece di una sono tutta la correzione, ed è evidente solo dopo aver separato la regola delle import dalla regola delle export

Le stesse dichiarazioni di simboli C risolte da Free Pascal e Delphi su Win64 e Win32: le import cdecl guadagnano un underscore solo sul target a 32 bit, una dichiarazione Delphi già scritta con underscore diventa __deflate e fallisce il linking, mentre le export public name restano letterali su entrambe le architetture
Una sola costante di prefisso non può servire entrambe le regole: le import cdecl semplici e le import che portano già l'underscore Delphi si decorano diversamente sotto FPC su Win32, quindi HotPDF ne tiene due e le lascia entrambe vuote su Win64
// Due prefissi, non uno: le import C semplici e le import che portano già
// un prefisso Delphi scritto a mano si decorano diversamente sotto FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC lo aggiunge da sé per cdecl external
  DelphiCName = '';    // già scritto con l'underscore nel sorgente
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Lato export: 'public name' resta letterale su ogni target
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 ti dice l'architettura, non l'ABI

Questo è l'errore di compilazione condizionale con la coda di debug più lunga, e vale la pena dirlo senza giri di parole: WIN32 e WIN64 descrivono l'architettura target e non dicono nulla su quali helper runtime privati del compilatore esistano. Free Pascal definisce entrambi i simboli sui corrispondenti target Windows, esattamente come Delphi. Una guardia scritta come {$IFDEF WIN32} attorno a codice che chiama un helper runtime Delphi quindi compila sotto FPC e fallisce al momento del linking

Concretamente, tre famiglie di codice cadono in questa trappola. I trampolini Delphi per interi a 64 bit raggiunti tramite gli helper System.@_ll, le routine di supporto assembly Win32 MSVC, e gli slot di import che le accompagnano esistono tutti per servire oggetti C precompilati che la build Delphi linka. Free Pascal non linka quegli oggetti, quindi di quell'ingranaggio non ha bisogno, e ogni riferimento va fatto sparire. La finezza è che dichiarazione e implementazione vanno escluse insieme. Escludine una sola e il compilatore riporta qualcosa di poco utile su un identificatore che non riesce ad abbinare a niente

La regola che ne scende è breve. Metti la guardia sul compilatore quando la domanda riguarda l'ABI o il supporto runtime, mettila sull'architettura quando riguarda la larghezza del puntatore o il numero di registri, e non lasciare mai che l'una faccia le veci dell'altra

Mettere la guardia su dichiarazioni e implementazioni insieme

Un blocco condizionale nella sezione interface è facile incapparlo senza accorgersene, e il messaggio di errore risultante punta ovunque tranne che alla causa. Aggiungi una dichiarazione di metodo a una interfaccia di classe e il posto naturale è accanto ai metodi correlati, il che va benissimo fino al momento in cui quei vicini si trovano dentro un blocco {$IFDEF} esistente. Le direttive condizionali non sono indentate, quindi un blocco aperto quaranta righe sopra è essenzialmente invisibile mentre leggi le dichiarazioni attorno

Quello che succede dopo è una compilazione che riesce su una toolchain e produce una cascata di errori su un'altra. Se la guardia attorno è un controllo di versione Delphi che Free Pascal non soddisfa, la dichiarazione scompare per FPC mentre l'implementazione incondizionale resta, e il compilatore riporta una lunga lista di lamentele su identificatori di metodo che si aspettava e non ha trovato. Nessun messaggio menziona il blocco condizionale che l'ha causato

Due abitudini prevengono l'intera classe di guasti. Prima di inserire in una sezione interface, guarda in su per trovare il condizionale aperto più vicino invece di fidarti del raggruppamento visivo. E trattare una suite di test Delphi verde come prova solo su Delphi: la build della libreria Free Pascal è una barriera separata, e l'unico modo di sapere che passa è eseguire build-Win32-Lib-FPC.cmd e build-Win64-Lib-FPC.cmd come parte della stessa modifica

Cosa si rompe nel codice aritmetico a 32 bit

Una restrizione del linguaggio compare esattamente nel codice meno disposto a cambiare: Free Pascal a 32 bit non accetta un UInt64 come variabile di controllo di un ciclo for. Nelle unità a curve ellittiche che portano X25519 e X448, i cicli che percorrono gli array di limb erano scritti con contatori a 64 bit semplicemente perché tutto il resto nel file è a 64 bit

La correzione deve essere chirurgica, perché nell'aritmetica sui campi la larghezza di una variabile fa parte dell'argomento di correttezza. Gli indici di ciclo diventano Integer, dato che un array di limb ha una manciata di elementi e nessun indice si avvicina mai all'intervallo a 32 bit. Tutto ciò che partecipa all'aritmetica, i limb stessi, la propagazione del carry e le maschere, resta UInt64, perché restringerne qualcuno cambia silenziosamente il risultato modulo il primo del campo

// FPC a 32 bit rifiuta una variabile di ciclo UInt64. Restringi solo l'indice;
// limb, mask e carry conservano la loro larghezza, o la matematica del campo cambia
var
  I: Integer;                 // prima era UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

La verifica per una modifica del genere non può essere un test di round-trip. Cifrare e decifrare con la stessa implementazione rotta è d'accordo con se stessa alla perfezione, ed è qui che i vettori known-answer sono non negoziabili: esegui i vettori di test pubblicati di X25519 e X448 e confronta gli esatti byte di output. È l'unico controllo che distingue un'implementazione corretta da una sbagliata ma auto-coerente, e vale allo stesso modo per le primitive simmetriche discusse in i confini dei codec deflate e AES in Free Pascal

I due punti di rottura di una build HotPDF Win32 Free Pascal: una guardia {$IFDEF WIN32} attorno agli helper runtime Delphi che compila ma fallisce al linking se dichiarazione e implementazione non sono escluse insieme, e la variabile di ciclo UInt64 nei percorsi dei limb di X25519 e X448 ristretta a Integer mentre limb, carry e mask conservano la loro larghezza
Metti la guardia sul compilatore quando la domanda è ABI o supporto runtime e sull'architettura quando è larghezza del puntatore, poi dimostra le modifiche aritmetiche contro i vettori known-answer pubblicati invece che con test di round-trip

Quanto vale una build Free Pascal Win32

Il tornaconto pratico è che un'applicazione Lazarus rivolta a Windows a 32 bit ottiene lo stesso motore documentale della controparte Delphi, senza un contratto binario separato da mantenere. Conta di più per i deployment di cui si parla raramente: controllori industriali, terminali point-of-sale e software line-of-business di lunga vita dove il runtime a 32 bit non è una scelta legacy ma un vincolo hardware

La storia Win64 è arrivata prima ed è descritta in supporto Free Pascal e Lazarus su Win64. Win32 non è una sua riedizione. Win64 ha una sola convenzione di chiamata, nessuna decorazione dei nomi e nessun helper per interi privato Delphi da aggirare, quindi quasi tutto in questo articolo è specifico del target a 32 bit. Le unità aritmetiche che hanno richiesto la modifica della variabile di ciclo sono le stesse descritte in aritmetica di Montgomery sulle curve NIST, dove la disciplina sulle larghezze è spiegata più a fondo

La lezione generale è che il lavoro di portabilità tra compilatori non riguarda principalmente le caratteristiche del linguaggio. Entrambi i compilatori qui accettano lo stesso Object Pascal. Ciò che differisce è il file oggetto: come si scrivono i simboli, quali routine helper si assume che il runtime fornisca, e quali oggetti precompilati ci sono nel linking. HotPDF spedisce i pacchetti Free Pascal e Lazarus accanto a quelli Delphi e C++Builder nel HotPDF Delphi PDF component, così lo stesso albero sorgente alimenta ogni toolchain invece di biforcarsi per compilatore