Articolo tecnico

HotPDF su Free Pascal e Lazarus: limiti del supporto Win64

La risposta breve a quel ticket di supporto è sì, con limiti. HotPDF 2.730.0 compila con Free Pascal 3.2.2 e Lazarus 4.6 per Win64, e i percorsi principali di creazione, caricamento e salvataggio funzionano. Ciò che non segue è tutto ciò che poggia su un oggetto codec nativo linkato staticamente o sui metodi anonimi Delphi

La domanda di solito arriva allo stesso modo: un team standardizza su Lazarus per uno strumento multipiattaforma, o eredita una codebase Free Pascal, e vuole lo stesso componente PDF per cui ha già una licenza Delphi. Portare una libreria Delphi matura raramente è una questione di sintassi. La parte interessante è ciò che il porting rivela su dove la libreria era silenziosamente accoppiata a una toolchain, e in questo caso l'accoppiamento sta in due posti molto precisi: l'ABI dei file oggetto dei codec inclusi, e le caratteristiche del compilatore nascoste dietro un simbolo di versione

Una matrice di capacità che confronta la build Delphi di HotPDF con la build Free Pascal 3.2.2 e Lazarus 4.6 per Win64, mostrando quali percorsi documentali sono condivisi e quali API di codec, compressione, rendering parallelo e metodi anonimi arrivano a uno stub che solleva eccezioni
I percorsi principali di creazione, caricamento e salvataggio sono identici nelle due build, e il divario sta interamente nei codec linkati staticamente e nelle API a metodi anonimi

Cosa richiede Free Pascal 3.2.2 prima che HPDFDoc compili

HotPDF compila sotto Free Pascal solo in modalità Delphi, e solo quando le directory delle unit LCL di Lazarus sono nel percorso di ricerca. Nessuna delle due è negoziabile. HotPDF.inc commuta il compilatore con {$MODE DELPHI} e {$H+} dentro il suo blocco {$IFDEF FPC} e rifiuta qualsiasi cosa più vecchia con un {$FATAL} quando FPC_FULLVERSION è sotto 30202, così un'installazione 3.0.x fallisce rumorosamente invece di produrre una unit rotta. Il pacchetto runtime Lazarus HotPDFLaz.lpk codifica il resto: LCL come pacchetto richiesto e -Mdelphi come opzione personalizzata

Il requisito LCL sorprende chi vuole solo output da console, ma è strutturale. HPDFFPCCompat fornisce i tipi VCL Delphi per cui Free Pascal non ha equivalenti, mappando TMetafile e TMetafileCanvas su classi bitmap e canvas LCL e aliasando TRichEdit a TMemo, mentre HPDFDoc aliasa TPNGObject a Graphics.TPortableNetworkGraphic. Trattali come shim di compilazione, non come parità di funzionalità: una classe metafile supportata da una bitmap tiene la unit in compilazione, non fa sì che i percorsi metafile si comportino come su Delphi. Perfino lo smoke test non GUI tira dentro Interfaces, e lo script di build passa -Fu per lcl\units\x86_64-win64 e la directory di output lazutils

Perché D2009+ non può fare anche da gate di versione

È allettante trattare la build Free Pascal come un compilatore moderno e definire semplicemente il simbolo della più recente caratteristica Delphi. HotPDF non lo fa, e il motivo merita di essere detto chiaramente: D2009+ non significa solo stringhe Unicode, fa anche da gate alle unit la cui API pubblica è espressa con metodi anonimi. Free Pascal 3.2.2 non supporta né i metodi anonimi Delphi né quelle API, quindi prendere in prestito il simbolo trascinerebbe dentro codice che non può compilare. La clausola uses di HPDFDoc porta quindi due code condizionali separate, e la sovrapposizione tra loro è deliberata anziché accidentale

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

Perché i codec nativi si fermano al linker?

Perché sono oggetti COFF Win64 emessi da una toolchain particolare, e nessuno dei due linker Free Pascal su Win64 li consuma: né il linker interno, né il percorso GNU ld esterno. È un problema di ABI dei file oggetto, non un problema Pascal, e nessuna quantità di sorgente condizionale lo risolve. La libreria prende l'unica strada onesta disponibile. Ogni direttiva {$L} che tira dentro un oggetto codec statico è avvolta in {$IFNDEF FPC}, così la build Free Pascal semplicemente li omette, e HPDFFPCCodecStubs poi fornisce ogni simbolo esterno mancante come stub che solleva eccezioni invece di ritornare

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

Quella tabella di stub è lunga, e leggerla ti dice esattamente quali capacità sono oggi solo Delphi: i punti di ingresso deflate di zlib-ng e zopfli, compressione e decompressione libjpeg, il codec JPEG 2000 OpenJPEG, libtiff e i suoi inizializzatori per compressione, codifica e decodifica JBIG2, i punti di ingresso di trasformazione colore Little-CMS, e le primitive AES. La scelta di progetto dietro gli stub conta più dell'elenco. Un simbolo mancante al link ti dà un muro di riferimenti indefiniti da una unit che non hai mai toccato; uno stub che solleva ENotSupportedException ti dà una build che gira, un messaggio che nomina il motivo, e uno stack trace che punta al sito della chiamata. Significa anche che una build Free Pascal non produce mai in silenzio byte sbagliati dove una build Delphi ne produrrebbe di corretti. Nota anche l'effetto di secondo ordine: eseguire codec di immagini non fidati in un processo isolato è una decisione che nasce solo sulla build Delphi, perché una build Free Pascal non ha per cominciare alcun decodificatore nativo in-process da sandboxare

Su Delphi gli oggetti codec statici di HotPDF vengono linkati e girano nativamente, mentre la build Free Pascal Win64 salta le direttive di link e instrada ogni simbolo esterno mancante a uno stub che solleva un'eccezione nominata al sito della chiamata
Saltare le direttive di link e fare stub di ogni simbolo esterno trasforma un muro di riferimenti indefiniti in una build che gira e nomina i propri limiti

Compressione: la prima riga da cambiare è cmNone

Prima di portare qualsiasi altra cosa, imposta Compression su cmNone. THPDFCompressionMethod offre esattamente due valori, cmNone e cmFlateDecode, e il secondo instrada dritto nei punti di ingresso deflate che sono stub in una build Free Pascal. Verifica prima il modello di oggetti principale con la compressione spenta, poi decidi che altro ti serve. È l'ordine che usa lo smoke test spedito: crea un documento di una pagina non compresso, ricaricalo, e asserisci che il numero di pagine è tornato uno. L'output non compresso è più grande, ed è comunque un PDF perfettamente valido

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecode arriva a un simbolo stub
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

Cosa succede al rendering parallelo delle pagine?

Compila ancora, ritorna bitmap corretti, e smette di essere parallelo. THotPDF.RenderLoadedPagesParallel e THotPDF.RenderLoadedPagesParallelOrdered sono costruiti su TThread.CreateAnonymousThread con una chiusura procedure inline, che Free Pascal 3.2.2 non sa esprimere, così il ramo Free Pascal esegue un fallback seriale deterministico: percorre gli indici di pagina in ordine, chiama RenderLoadedPageToBitmap per ciascuno, e conta i successi. La forma dell'API, il valore di ritorno e l'array di output sono invariati, ed è ciò che permette a una sola codebase di compilare in entrambi i modi

La stessa chiamata di render parallelo HotPDF gira su thread worker sovrapposti sotto Delphi e percorre gli indici di pagina in serie sotto Free Pascal, con il record di info della pipeline che riporta un numero di worker di uno invece di nascondere il fallback
Il ramo Free Pascal mantiene la forma dell'API e l'array di output mentre riporta un numero di worker di uno, così il codice che già legge il record di info vede la verità
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount è quanto il budget di memoria permetteva
  // Free Pascal: Info.WorkerCount è sempre 1, pagine in ordine di indice
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Il fallback non è silenzioso, ed è la parte che vale la pena progettare attorno. Riempie THPDFParallelRenderPipelineInfo onestamente: PageCount dalla richiesta, RequestedWorkerCount che ripete ciò che hai chiesto, WorkerCount impostato a 1, e i conteggi di completati e consegnati che corrispondono a ciò che è davvero tornato. Il codice che già ispeziona Info per dimensionare una barra di avanzamento o un budget di memoria continua a funzionare e legge la verità anziché un'assunzione. Se il tuo piano di throughput dipende da la pipeline di render parallelo e il suo modello di backpressure, quel piano è un piano Delphi; su Free Pascal, prevedi il costo single-thread di renderizzare una pagina in bitmap moltiplicato per il numero di pagine

Quale build dovresti davvero spedire?

Scegli per capacità, non per preferenza. Se il tuo flusso di lavoro è assemblaggio documenti, disegno testo e vettoriale, compilazione di moduli, caricamento e salvataggio, la build Free Pascal su Win64 lo copre, e dovresti validare con la compressione spenta prima di accendere qualsiasi cosa. Se coinvolge immagini JPEG o JPEG 2000 o TIFF o JBIG2, trasformazioni colore ICC, output compresso, o throughput che dipende da molti core, resta su Delphi o C++Builder per ora. Il confine è tracciato da un ABI di file oggetto e da una caratteristica del linguaggio mancante, entrambi visibili nel sorgente anziché sepolti in una matrice di supporto, ed entrambi falliscono con un errore nominato anziché con un risultato sbagliato

Il pacchetto Free Pascal e Lazarus viene spedito nella stessa distribuzione delle unit Delphi e C++Builder, così una licenza copre entrambi e puoi testare il percorso Lazarus sui tuoi documenti prima di impegnarti; la pagina prodotto del componente HotPDF Delphi PDF porta l'attuale matrice di supporto dei compilatori e il riferimento API completo