Kai pdfium.dll atsisako įsikelti kliento kompiuteryje, PDFium komponentas, skirtas Delphi ir Lazarus, paverčia paslaptingą vietinę klaidą konkrečia priežastimi. Funkcija CheckLoadLibrary nuskaito ERROR_BAD_EXE_FORMAT (193) kaip 32/64 bitų architektūros nesutapimą, o CheckGetProcAddress praneša apie trūkstamą eksportą kaip apie pdfium.dll failą, kuris yra senesnis už susiejimą. Abu pranešimai nurodo taisymą, užuot palikę jus spėlioti
Tai yra svarbu, nes vietinė DLL gali sugesti trimis skirtingais būdais, o neapdorotas operacinės sistemos tekstas juos visus suplaka į vieną. Architektūra gali būti neteisinga, įdiegtas dvejetainis failas gali būti per senas jį iškviečiančiam Pascal susiejimui, arba dvi gijos gali konkuruoti dėl jo susiejimo tuo pačiu metu. Kiekvienas iš šių atvejų turi skirtingą sprendimą, o diagnostikos, įdiegtos v2.11.0 įkėlimo gyvavimo ciklo darbuose, tikslas yra parodyti, į kurį iš jų iš tikrųjų žiūrite
Kodėl pdfium.dll nepavyksta įkelti dėl blogo EXE formato klaidos?
„Windows“ klaida 193, ERROR_BAD_EXE_FORMAT, reiškia, kad failas buvo rastas bei atidarytas, tačiau jo PE mašinos tipas nesutampa su priimančiuoju procesu. 64 bitų vykdomasis failas negali įkelti 32 bitų pdfium.dll, o 32 bitų vykdomasis failas negali įkelti 64 bitų bibliotekos. Failas yra vietoje, todėl instinktas ieškoti trūkstamos DLL nukreipia jus visiškai neteisinga linkme
PDFium komponentas perima šį atvejį funkcijoje CheckLoadLibrary: kai LoadLibrary grąžina nulinę rodyklę ir GetLastError yra 193, ji prideda užuominą, kad DLL buvo rasta, tačiau jos architektūra nesutampa su priimančiuoju procesu, bei nurodo atitinkamą DLLs/Win32 arba DLLs/Win64 versiją. Subkatalogas pasirenkamas per BuildDllSubDir, kuris grąžina Win64, kai IsWin64 yra True, bei priešingu atveju — Win32. Įdiekite DLL kataloge, kurį iš tikrųjų tikrina susiejimas, ir nesutapimas išnyks
Ką praneša trūkstamas PDFium eksportas?
Antroji klaidų klasė yra versijų nesutapimas. Laikui bėgant PDFium išleidžia naujus eksportus, o Pascal susiejimas išsprendžia kiekvieną reikalingą funkciją per CheckGetProcAddress funkciją LoadLibrary vykdymo metu. Jei reikalingo eksporto nėra, susiejimas yra naujesnis už diske esančią pdfium.dll biblioteką, ir sąžininga diagnozė yra ta, kad įdiegta DLL yra pasenusi, o ne sugadinta. Funkcija CheckGetProcAddress būtent tai ir nurodo: ji iškelia EPdfError klaidą, pranešdama, kad įdiegta pdfium.dll yra senesnė už šią PDFiumPas susiejimo versiją, bei nurodo DLLs/<subdir> kelią, kuriame turi būti atitinkamas dvejetainis failas
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// Deploy pdfium.dll next to the executable, matched to the build target:
// <AppDir>\DLLs\Win32\pdfium.dll for a 32-bit host process
// <AppDir>\DLLs\Win64\pdfium.dll for a 64-bit host process
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // first use lazily binds pdfium.dll
except
on E: EPdfError do
// The message already names the real cause: a 32/64-bit mismatch
// (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Ką praneša trūkstamas PDFium eksportas?
Antroji klaidų klasė yra versijų nesutapimas. Laikui bėgant PDFium išleidžia naujus eksportus, o Pascal susiejimas išsprendžia kiekvieną reikalingą funkciją per CheckGetProcAddress funkciją LoadLibrary vykdymo metu. Jei reikalingo eksporto nėra, susiejimas yra naujesnis už diske esančią pdfium.dll biblioteką, ir sąžininga diagnozė yra ta, kad įdiegta DLL yra pasenusi, o ne sugadinta. Funkcija CheckGetProcAddress būtent tai ir nurodo: ji iškelia EPdfError klaidą, pranešdama, kad įdiegta pdfium.dll yra senesnė už šią PDFiumPas susiejimo versiją, bei nurodo DLLs/<subdir> kelią, kuriame turi būti atitinkamas dvejetainis failas
Dvi detalės padaro šį kelią patikimą. Pirma, CheckGetProcAddress iškviečia UnloadLibrary prieš iškeldama klaidą, todėl dalinis susiejimas niekada nepalieka pusiau išspręstų funkcijų rodyklių, už kurių galėtų užkliūti kitas bandymas. Antra, DLL pavadinimas, apie kurį pranešama, ateina iš BuildDllName, kuris įprastai grąžina pdfium.dll bei pdfium.v8.dll, kai yra nustatyta globali EnableV8Engine vėliavėlė. Jei jūsų programa įjungia V8 JavaScript variklį XFA arba skriptinėms formoms, klaidos tekstas nurodo V8 versiją, o ne įprastą, todėl iš pirmo karto pakeisite reikiamą failą
Ar PDFium DLL saugu įkelti iš foninės gijos?
Bibliotekos įkėlimas dabar yra saugus iš bet kurios gijos; tačiau PDFium API iškvietimas iš kelių gijų vienu metu vis dar nėra saugus. Tai dvi atskiros garantijos, ir jų atskyrimas yra skirtumas tarp stabilaus darbininkų telkinio bei atsitiktinio užlūžimo. Foninis atvaizdavimas paprastai vykdo kiekvieną puslapio atvaizdavimą per TPdfFuture<T> darbininką, o pirmoji ateitis, kuri paliečia TPdf, sukelia tingųjį susiejimą. Teisiškai, be apsaugos du darbininkai galėtų pamatyti neįkeltą biblioteką, abu paleisti susiejimo seką, perrašyti modulio rodyklę bei prarasti pirmąjį įkėlimą
v2.11.0 versijos darbai uždaro šią spragą naudodami PDFiumLoadLock — procesui globalią TRTLCriticalSection, kuri apgaubia tiek LoadLibrary, tiek UnloadLibrary iškvietimus. Kritinė sekcija padaro tikrinimo bei įkėlimo seką atominę, todėl jokios dvi gijos negali pamatyti Loaded=False būsenos ir atlikti dvigubo susiejimo, bei jokia gija negali atlaisvinti DLL, kol kita ją įkelia. Užraktas sukuriamas modulio initialization dalyje bei panaikinamas finalization dalyje, saugomas PDFiumLoadLockReady vėliavėlės, todėl poravimas yra saugus net ir išjungimo metu. Jei jums reikia viso darbininko bei atsakymo šablono su atšaukimu, susijęs straipsnis apie foninį atvaizdavimą su atšaukiamomis ateitimis išsamiai aprašo visą procesą
uses
PDFium, FPdfAsync;
// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // safe concurrent bind
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // this TPdf is owned by one thread only
finally
Pdf.Free;
end;
end;
procedure TReportForm.StartRender;
begin
TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;
procedure TReportForm.ThumbnailReady(
const AResult: TPdfFutureResult<TBitmap>);
begin
if AResult.IsSuccess then
Preview.Picture.Assign(AResult.Value);
end;
Ko užraktas neapsaugo
Ribą verta įvardyti aiškiai, nes lengva neteisingai suprasti šį pataisymą. PDFiumLoadLock serijomis rikiuoja tik globalią susiejimo būseną: modulio rodyklę bei FPDF_* funkcijų rodyklių priskyrimus. Po juo esanti PDFium C API vis dar nėra saugi gijose, būtent taip, kaip deklaruoja pirminės antraštės, todėl FPDF_* funkcijų iškvietimas ar vieno FPDF_DOCUMENT dalijimasis tarp gijų išlieka jūsų atsakomybė. Saugi forma yra nurodyta aukščiau: kiekvienas darbininkas turi savo TPdf bei niekada neperduoda veikiančio dokumento kitai gijai. Norėdami sužinoti daugiau apie šią ribą bei ABI taisykles, skaitykite straipsnį PDFium VCL susiejimo apsauga nuo ABI bei atminties klaidų
Kodėl svarbu finalization ir FPDF_DestroyLibrary
Subtilesnė spraga buvo išjungimo kelyje. PDFium modulis neturėjo finalization sekcijos, o tai reiškė, kad FPDF_DestroyLibrary niekada nebuvo vykdoma proceso pabaigoje; operacinė sistema tiesiog atlaisvindavo DLL atmintį bei praleisdavo pačios PDFium valymą, būtent darbininkų gijų sujungimą, V8 izoliato atlaisvinimą, kai nustatyta EnableV8Engine vėliavėlė, bei šriftų ir talpyklos panaikinimą. Paprasto atvaizdavimo atveju tai yra nepastebimas nuotėkis išjungimo metu, tačiau įjungus V8 variklį kiekvieno paleidimo metu nuteka JavaScript izoliatas, kas greitai pasireiškia pakartotinio automatizavimo metu
Dabar finalization sekcija iškviečia UnloadLibrary, kuri suaktyvina FPDF_DestroyLibrary bei atlaisvina modulį. Tai ne veltui yra modulio ribose: TPdf.Destroy tik nustato Active := False, kad uždarytų esamą dokumentą, bei sąmoningai neatjungia globalios bibliotekos, todėl kelių egzempliorių programa palaiko vieną bendrą PDFium susiejimą viso proceso gyvavimo metu. Išvalymo perkėlimas į finalization reiškia, kad bendra biblioteka atlaisvinama tiksliai vieną kartą, kai modulis iškraunamas, o UnloadLibrary pirmiausia patikrina savo Loaded vėliavėlę, todėl iškvietimas yra nekenksmingas ir nieko nedaro, jei PDFium niekada nebuvo naudojama
Trumpas diegimo kontrolinis sąrašas
Trys įpročiai padeda išvengti beveik visų įkėlimo klaidų praktikoje. Suderinkite DLL architektūrą su priimančiuoju procese bei pateikite ją kataloguose DLLs/Win32 arba DLLs/Win64; laikykite pdfium.dll sinchronizuotą su susiejimo versija, kad netrūktų jokių reikalingų eksportų; bei niekada nesidalykite TPdf ar dokumento rodykle tarp gijų, net jei pats susiejimas dabar yra atominis. Jei kuriate tą patį šaltinį tiek Delphi, tiek Lazarus aplinkoms, paketų skirtumai, kurie apsunkina diegimą, aprašyti pastabose apie Delphi bei FPC kryžminių kompiliatorių spąstus. Diagnostika, užraktas bei finalization valymas platinami kartu su PDFium komponentu, skirtu Delphi ir C++Builder