Tekninen artikkeli

Valinnaiset PDFium-viennit: ominaisuusportit Delphissä

Pdfium.dll:si latautuu hyvin, ja silti yksi proseduuri puuttuu. PDFium Component käsittelee tämän jakamalla sidontansa kahteen luokkaan: pakolliset viennit, jotka ratkaistaan CheckGetProcAddress:lla ja jotka keskeyttävät latauksen suoralta kädeltä, ja valinnaiset viennit, jotka ratkaistaan TryGetProcAddress:lla ja jotka jättävät nil-osoittimen ja ominaisuustarkistuksen jälkeensä sen sijaan

Tämä ei ole sama ongelma kuin DLL, jota ei löydy. Jos sovelluksesi kuolee huonoon EXE-muotoon, puuttuvaan tiedostoon tai arkkitehtuurin epäsuhtaan, se tarina kerrotaan artikkelissa pdfium.dll:n käyttöönotto ja latausvikojen diagnosointi. Tässä lataaja onnistui. Moduulikahva on kelvollinen, sadat viennit ratkesivat, ja ajo silti päättyy ennen ensimmäisen sivusi renderöintiä, koska yksi sisäänmenopiste, joka saapui uudemmassa PDFium-käännöksessä, ei ole levyllä olevassa binaarissa

Miksi yksi puuttuva vienti rikkoo koko kirjaston?

Koska pakollinen sidonta on kova sopimus, ja se pannaan täytäntöön yhden kaikki-tai-ei-mitään-sidontasekvenssin aikana. PDFium Component ratkaisee koko vientitaulukkonsa LoadLibrary:n sisällä, yksi CheckGetProcAddress-kutsu toisensa jälkeen. Ensimmäinen nil-tulos nostaa EPdfError:n ja kutsuu UnloadLibrary:a ennen sitä, mikä on tarkoituksellista: osittainen sidonta muuten jättäisi jo ratkaistut osoittimet tähtäämään moduuliin, joka on kohta vapautettava, hiljaa voittaen jokaisen alavirtaan olevan Assigned-vartijan

Seuraus on vikamoodi, joka tuo ihmiset tänne. Päivität komponentin, toimitat saman pdfium.dll:n, jota olet toimittanut kaksi vuotta, ja sovellus ei käynnisty. Virhe nimeää viennin ominaisuudelle, jota et ole koskaan kutsunut. Mikään, mitä teet kutsupisteessä, ei auta, koska kutsupiste ei koskaan aja; epäonnistuminen tapahtui sidonnan aikana, ennen kuin yhtäkään asiakirjaa avattiin

function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // A missing required export means the deployed pdfium.dll is older
    // than this build of the binding. Drop every pointer resolved so far
    // so no caller can reach into the module we are about to free.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Optional export. nil is a legitimate answer here; every caller is
  // required to test Assigned() before dereferencing the variable.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

Pakollinen vai valinnainen: mihin raja todella asettuu

Sääntö, jota PDFium Component soveltaa, on tylsä. Vienti on pakollinen, kun sen puuttuminen tekee komponentista kykenemättömän tekemään sen työn, jota varten se on olemassa, ja valinnainen, kun sen puuttuminen vain poistaa yhden lehtiominaisuuden. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage ovat pakollisia, ja äänekäs epäonnistuminen niissä on oikein: katseluohjelma, joka ei voi renderöidä, ei ole rappeutunut katseluohjelma, se on rikki

Kaikki, mihin suvaitseva lataaja tänään pääsee käsiksi, on lehti. FPDFBookmark_GetColor saapui M109:n jälkeen ja tarjoaa vain valinnaisen /C-värivalikoiman ääriviivamerkinnälle, joten DLL, joka on sitä vanhempi, yksinkertaisesti raportoi, ettei kirjanmerkillä ole väriä. V8-apurit FPDF_GetRecommendedV8Flags ja FPDF_GetArrayBufferAllocatorSharedInstance, sekä XFA-merkkijonoapurit FPDF_BStr_Init, FPDF_BStr_Set ja FPDF_BStr_Clear, puuttuvat rakenteellisesti jokaisesta ei-V8-käännöksestä, joten niiden kohteleminen pakollisina tekisi tavallisesta pdfium.dll:stä lataamattoman. Ja pari, joka motivoi tätä artikkelia: FPDFAttachment_SetDescription ja FPDFAttachment_GetDescription, lisätty ylävirtaan 2026-07-13, myöhemmin kuin kaikkien neljän PDFium-binäärin käännöspäivä, jotka projekti toimittaa DLLs/Win32:n ja DLLs/Win64:n alla. Tuo viimeinen tapaus on ongelman yleinen muoto, ei kertaluontoinen: sidontakerros seuraa ylävirran otsikoita, jotka liikkuvat jatkuvasti, kun taas asennusohjelmasi DLL liikkuu erillisin hypyin aina kun joku kääntää sen uudelleen. Ikkuna, jossa Pascal-puoli tietää vienneistä, joita käyttöönotettu binääri ei kanna, on aina olemassa, ja sen päättäminen etukäteen, kummalle puolelle pakollinen/valinnainen-rajaa jokainen uusi vienti putoaa, on ainoa asia, joka pitää tuon ikkunan selviydyttävänä

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

Mitä ominaisuusportin pitäisi tehdä kutsupisteessä?

Sen pitäisi olla epäsymmetrinen, ja tuo epäsymmetria on koko suunnittelu. Luvulla, joka ei voi ajaa, on rehellinen tyhjä vastaus. Kirjoituksella, joka ei voi ajaa, ei ole lainkaan rehellistä vastausta, joten sen täytyy nostaa poikkeus. PDFium Component jakaa liitteen kuvausominaisuuden täsmälleen tuota rajaa pitkin, ja jako on se, mikä estää puuttuvaa vientiä muuttumasta hiljaiseksi datan menetykseksi. TPdf.GetAttachmentDescription testaa Assigned(FPDFAttachment_GetDescription):n ja poistuu tyhjällä WString:llä. Se ei ole valhe: DLL:llä ilman vientiä komponentti ei aidosti voi kertoa, kantaako liite /Desc-merkinnän, ja tyhjä kuvaus lukeutuu samoin kuin liite, jolla ei koskaan ollut sellaista. Loput liite-API:sta, käsitelty artikkelissa PDF-liitteiden käsittely Delphissä PDFium Componentilla, jatkaa toimintaansa koskemattomana

TPdf.SetAttachmentDescription ottaa vastakkaisen reitin. Se kutsuu Check:ia samalle Assigned-testille ja nostaa EPdfError:n tekstillä "Attachment descriptions are not supported by the loaded PDFium DLL". Hiljaa palaaminen olisi tässä huonoin vaihtoehto: kutsuja asettaisi kuvauksen, ei saisi virhettä, tallentaisi tiedoston ja toimittaisi PDF:n, jossa kuvaus yksinkertaisesti puuttuu. Kukaan ei huomaa, ennen kuin alavirran kuluttaja kysyy, minne se katosi

function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Read side degrades: an old DLL cannot report /Desc, and '' is
  // indistinguishable from an attachment that carries no description.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Write side refuses: silently dropping the value would produce a file
  // the caller believes carries a description and does not.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;

Ominaisuuden luotaaminen ennen sen tarjoamista

Poikkeuksen kiinniottaminen on huono tapa selvittää, mitä käyttöönottosi osaa, joten PDFium Component paljastaa saman testin nimettynä funktiona. AttachmentDescriptionFeaturesAvailable kutsuu LoadLibrary:a ja palauttaa, ratkesivatko parin molemmat puoliskot. Se istuu V8FeaturesAvailable:n, XfaBStrHelpersAvailable:n ja XfaFeaturesAvailable:n rinnalla, jotka noudattavat identtistä mallia omille valinnaisryhmilleen. Luotaimen nimeäminen merkitsee enemmän kuin miltä se näyttää: totuusarvo nimeltä AttachmentDescriptionFeaturesAvailable kertoo seuraavalle ylläpitäjälle, että tämä ominaisuus on ehdollinen käyttöönotetulle binäärille, mitä paljas Assigned-testi haudattuna ominaisuusasettajaan ei koskaan tee. Se antaa myös käyttöliittymäkerrokselle jotain sidottavaa, joten kuvauksen muokkauslaatikko poistetaan käytöstä alusta lähtien sen sijaan, että se hyväksyisi syötteen ja hylkäisi sen tallennuksessa

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Ask once, at form setup, instead of discovering the limit on save.
  DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
  if not DescriptionEdit.Enabled then
    DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;

procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
  if not AttachmentDescriptionFeaturesAvailable then
    Exit;
  Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;

Miksi sidontakattavuus täytyy todistaa työkalulla?

Koska luvut ovat pisteen ohitse, jossa ihmiseen voisi luottaa niiden kanssa. PDFium Component auditoi 21 julkista PDFium-otsikkotiedostoa 2026-07-29-ylävirtakantaa vasten ja löysi 470 vietyä C ABI -funktiota. Sidonta kattoi jo 468 niistä. Kukaan ei paikantanut tuota kahden aukkoa lukemalla otsikoita; skripti teki sen sekunnissa, ja se tekee sen uudelleen seuraavan ylävirtapäivityksen yhteydessä. tools/audit_pdfium_public_api.py on tarkoituksella pieni: se regex-täsmää FPDF_EXPORT ... FPDF_CALLCONV name(:n jokaisen otsikkotiedoston yli julkisessa hakemistossa, regex-täsmää jokaisen CheckGetProcAddress('Name'):n ja TryGetProcAddress('Name'):n PDFium.pas:issa, ja tulostaa kaksi joukkoeroa: missing vienneille, joilla ei ole sidontaa, stale sidonnoille, joiden vienti ei enää ole olemassa ylävirrassa. Se poistuu nollasta poikkeavalla koodilla, kun kumpi tahansa joukko on tyhjentymätön, joten se putoaa käännösvaiheeseen ilman lisäseremoniaa. Nykyinen tulos on 470 sidottu 470:sta, puuttuu 0, vanhentunut 0

Vanhentunut-suunta ansaitsee paikkansa yhtä paljon kuin puuttuva. Vienti, jonka ylävirta poistaa, jättää jälkeensä CheckGetProcAddress-rivin, joka epäonnistuu kovasti jokaisessa tulevassa latauksessa, ja tuollainen rappeutuminen on näkymätön siihen päivään asti, kun joku päivittää DLL:n. Manuaalinen katselmointi löytää funktion, jota ajattelit; se ei löydä sitä, jota et ajatellut. Huomaa myös, että auditointi tarkoituksella laskee molemmat lataajat kattavuudeksi, mikä on oikea ratkaisu API-ajautumiselle ja syy, miksi pakollinen/valinnainen-jaon täytyy olla dokumentoitu päätös eikä sen sivutuote, kuka tahansa lisäsi rivin

Missä valinnainen sidonta lakkaa olemasta rehellinen

Kaksi rajaa on syytä sanoa suoraan, koska kuviota on helppo soveltaa liikaa. Ensimmäinen on, että nil-funktioosoitin on turvallinen vain, jos kirjaimellisesti jokainen polku, joka koskettaa sitä, testaa Assigned:in ensin. Yksikössä, joka ilmoittaa satoja cdecl-funktiomuuttujia, yksi vartioimaton kutsu on käyttöoikeusrikkomus osoitteessa, joka ei tarkoita mitään pinojäljessä. Sama kurinalaisuus, joka hallitsee kutsukäytäntöjä ja elinaikoja C-rajan yli, pätee tässä, ja se on aiheena artikkelissa PDFium-sidonnan vahvistaminen ABI- ja muistiturvallisuusvikoja vastaan

Toinen raja on laajuus. Valinnainen sidonta ei ole yleinen lupa tehdä kaikesta suvaitsevaa. Jos FPDF_RenderPageBitmap olisi valinnainen, komponentti latautuisi iloisesti ja epäonnistuisi sitten jokaisella sivulla, muuttaen yhden selvän käynnistysvirheen hajonnaksi ajonaikaisia virheitä ilman ilmeistä syytä. Pakollinen on oikea oletus. Valinnainen on poikkeus, johon tarttuu, kun ominaisuus on aidosti lehti, kun puuttuminen tarjoaa puolustettavan rappeutuneen käyttäytymisen lukupuolella, ja kun kirjoituspuoli voi kieltäytyä viestillä, joka nimeää syyn

Lataajan suunnittelu, ominaisuusluotaimet ja tässä kuvattu auditointityökalu toimitetaan osana PDFium Componenttia Delphille ja C++Builderille; tuotesivu listaa mukana tulevat PDFium-binäärit ja niiden paljastaman täyden API-pinnan