Articolo tecnico

Caricare la libreria nativa PDFium su ogni target

Il componente PDFium trova la propria libreria nativa attraverso una catena di ricerca fissa e ordinata anziché lasciarla al loader del sistema operativo, perché un albero di deployment esplicito è un albero di deployment che si può fare il debug. Su Windows quella catena cerca una sottodirectory Win32 o Win64 che l'installer spedia già. Sugli altri target costruisce il nome della sottodirectory dalle macro di target di Free Pascal, come <cpu>-<os>, così l'albero di deployment si legge esattamente come l'albero delle unità compilate. Quell'ultima decisione ha introdotto un bug che merita l'intero articolo, perché la causa era una lettera maiuscola e il sintomo era il silenzio

La catena, in ordine

Quattro posizioni, provate in sequenza, poi il loader della piattaforma come ultima spiaggia. Primo il layout preferito, una directory DLLs accanto all'eseguibile contenente una sottodirectory per target. Secondo un layout alternativo con la sottodirectory del target direttamente accanto all'eseguibile. Terzo il layout piatto legacy, la libreria accanto all'eseguibile senza alcuna sottodirectory. Quarto, solo su Windows, la directory di sistema, che richiede attenzione perché un processo a 32 bit deve guardare in SysWOW64 e uno a 64 bit in System32, e su Windows a 32 bit la prima non esiste quindi la ricerca deve ripiegare. Solo dopo tutto questo al loader viene chiesto di cercare per conto suo

Diagramma della catena di ricerca della libreria nativa PDFium per Delphi, dalla sottodirectory target DLLs attraverso layout alternativo, piatto e directory di sistema Windows fino al loader della piattaforma
Quattro posizioni esplicite vengono sondate in ordine prima di chiedere al loader del sistema operativo di cercare per conto suo

Non c'è deliberatamente alcun passaggio della directory di sistema fuori da Windows. Il percorso di ricerca del loader della piattaforma, guidato dalla configurazione del linker runtime e dall'ambiente dei percorsi delle librerie, copre già quel terreno, e duplicarlo in Pascal significherebbe reimplementare regole che variano per distribuzione. La diagnosi dei guasti nella catena Windows è trattata separatamente in distribuzione della DLL PDFium e diagnosi dei guasti di caricamento

Da dove viene il nome della sottodirectory

Su Windows è Win32 o Win64, deciso dalla bitness del processo in esecuzione anziché del sistema operativo, perché è ciò che determina quale binario possa essere caricato. Ovunque altrove il nome è costruito dalle macro di target del compilatore così che una macchina che compila per due architetture produca due alberi chiaramente separati, e così che la cartella che contiene la libreria nativa stia accanto alla cartella che contiene le unità compilate con lo stesso nome

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Le macro del compilatore scrivono il sistema operativo con l'iniziale
  // maiuscola ("Linux", "Darwin") mentre la directory di output delle unità
  // del pacchetto no, così le due concordano solo dopo la riduzione
  // in minuscolo. Su un file system case-sensitive quella differenza è
  // l'intera ricerca
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Perché una maiuscola ha rotto l'intera catena

La macro del compilatore scrive il sistema operativo target con l'iniziale maiuscola: Win64, Linux, Darwin. Il pacchetto Lazarus scrive il proprio output delle unità in una directory nominata dalla propria variabile di target, che è in minuscolo: win64, linux, darwin. Due grafie della stessa cosa, e nessun modo di accorgersene su Windows, dove il file system non le distingue

Su Linux sono due directory diverse. Un deployment che mette l'oggetto condiviso in DLLs/x86_64-linux è invisibile a un loader che cerca DLLs/x86_64-Linux, così tutti e quattro i passi espliciti della catena mancano e il codice ricade sul far cercare il loader della piattaforma. A volte funziona, se la libreria per caso è installata a livello di sistema, e a volte no, e in ogni caso l'albero di deployment accuratamente disposto non contribuisce nulla. Il fallimento non ha messaggio di errore perché nulla è fallito: ogni passo ha riportato correttamente che il file non era dove guardava

Come una maiuscola nella macro di target FPC rompe la ricerca della DLL PDFium su Linux: il loader cerca DLLs/x86_64-Linux mentre la cartella distribuita è DLLs/x86_64-linux, che corrisponde solo su Windows case-insensitive
Lo stesso target scritto in due modi corrisponde su Windows e manca silenziosamente su un file system case-sensitive

Il programma sonda, compilato ed eseguito

Questa classe di bug non si trova leggendo, e non si trova nemmeno compilando. La tecnica consueta per verificare un ramo di piattaforma che non compila mai sulla macchina di sviluppo è copiare l'unità in una directory temporanea, rinominarla, sostituire la condizionale di piattaforma con un simbolo mai definito e compilare la copia; se compila, la uses clause e le firme di chiamata su quella via sono almeno autoconsistenti. Funziona bene per un'unità autosufficiente

Qui non funziona. L'unità di binding principale è molto grande e tira dentro la LCL, quindi non può semplicemente essere copiata e compilata con il simbolo Windows disattivato. Così invece le poche funzioni toccate dalla modifica sono state trascritte verbatim in un piccolo programma autosufficiente, e quel programma è stato eseguito. Stampò x86_64-Win64, e la mancata corrispondenza era visibile in una riga di output. Compilare lo stesso programma non avrebbe detto nulla, perché la stringa è perfettamente valida; solo il suo valore è sbagliato

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Stampare, non asserire. Il punto è guardare il valore a cui una macro
  // si espande davvero su questa toolchain
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

La lezione generale: quando una modifica cross-platform riguarda il valore di qualcosa anziché il suo tipo, la verifica solo per compilazione non è una verifica. Stamparlo. L'insieme più ampio di differenze tra i compilatori incrociati Delphi e Free Pascal è raccolto nell'articolo sulle insidie dei compilatori incrociati Delphi e FPC

Lasciare che la piattaforma spieghi i propri guasti di caricamento

Il ramo Windows del loader enumera a mano le ragioni per cui un caricamento può fallire, perché le distinzioni utili lì, un disallineamento di architettura, una dipendenza transitiva mancante, un percorso che non si risolve, corrispondono a codici di errore che vale la pena nominare singolarmente. Fuori da Windows l'unità loader portabile restituisce già una stringa descrittiva che copre lo stesso terreno, quindi il ramo non Windows la usa direttamente anziché ridedurre categorie da un numero di errore che significa cose diverse su sistemi diversi

Resistere alla tentazione di normalizzare quei due in un solo messaggio è deliberato. Un guasto di caricamento è un problema di deployment, e chi legge il messaggio ha bisogno del vocabolario proprio della piattaforma per cercarlo

Una collisione di nomi che ricorre

Un'altra trappola, piccola e tagliente. L'unità loader portabile esporta una procedura chiamata UnloadLibrary, e l'unità di binding ha una procedura dello stesso nome che fa la propria contabilità prima di rilasciare l'handle. Dentro quella procedura, una chiamata non qualificata a UnloadLibrary si risolve in quella dell'unità corrente, che chiama sé stessa. La correzione è qualificare la chiamata con il nome dell'unità

È la stessa forma dei problemi di oscuramento di identificatori che dominano in generale i porting Free Pascal: l'unità Windows esporta funzioni minimo e massimo a tipo intero che oscurano quelle a virgola mobile, e un tipo di sincronizzazione che oscura la classe dello stesso nome, e in ogni caso la risoluzione dipende dall'ordine della uses clause. Qualificare il call site è la correzione che non dipende da qualcuno che preservi quell'ordine più tardi

Collisione di nomi UnloadLibrary nel componente PDFium: una chiamata non qualificata nell'unità di binding Delphi ricorre in sé stessa, mentre una chiamata qualificata per unità raggiunge l'unità loader portabile e rilascia l'handle
Qualificare il call site manda il rilascio attraverso l'unità loader invece di ricorrere nell'unità di binding

Lista di controllo del deployment

Tre cose spiegano la maggior parte dei guasti di caricamento una volta che l'aritmetica dei percorsi è giusta. L'architettura deve corrispondere al processo, non alla macchina, così un'applicazione a 32 bit su Windows a 64 bit ha bisogno del binario a 32 bit. La build abilitata a V8 ha un nome file diverso, così un deployment che li mescola sembrerà corretto e non caricherà nulla. E una sola variante può vivere in una directory di sistema alla volta, il che è un buon motivo per preferire il layout esplicito a sottodirectory all'installare qualsiasi cosa a livello di sistema

Per Lazarus in particolare, mettere la libreria nativa sotto DLLs/<cpu>-<os> in minuscolo, accanto all'eseguibile, e sarà trovata dal primo passo della catena su ogni target. L'esempio di viewer che lo esercita su Lazarus è descritto nell'articolo sul viewer Lazarus e FPC, e l'attuale supporto delle piattaforme è elencato nella pagina di prodotto di PDFium Delphi component