Pascal обвивката над C библиотека се чете като обикновен Pascal. Извиквате метод, получавате запис обратно, освобождавате това, което сте разпределили. Проблемът е, че PDFium е C и C++ библиотека със собствено конвенция за извикване, собствени ширини на целите числа и собствени правила за това кой притежава паметта и кой я освобождава. Нищо от това не преминава границата на езика от само себе си. Всеки един от тези договори трябва да бъде повторен на ръка в декларациите на Pascal и една-единствена грешна дума превръща чистия на пръв поглед код в повреда на стека, съкратено отместване или двойно освобождаване. Одит на версия v1.61.0 на VCL обвивката за PDFium откри по един дефект от всеки вид. Струва си да ги разгледаме, тъй като те не са специфични за тази обвивка. Те са постоянните опасности при обвиването на всеки C API в Delphi или Lazarus.
cdecl е част от типа на функцията, а не декорация
PDFium е компресиран C. В Win32 неговите експорти и, което е по-важно, функциите за обратно извикване (callbacks), които извиква, използват конвенцията за извикване cdecl. При cdecl извикващият изчиства стека след връщането на извикването. Нативното поведение по подразбиране на Delphi е register, а стандартът на Win32 C за обратно извикване е stdcall в някои библиотеки, където вместо това почиства извиканият. Когато дадена структура подаде на PDFium указател към функция и забравите cdecl в типа на този указател, двете страни се разминават в мнението си за това кой настройва указателя на стека. И двете страни го коригират или нито една от тях, и указателят на стека се измества с размера на аргументите при всяко извикване.
Причината, поради която този дефект се открива трудно, е, че повредата не е локална. Повреденото извикване завършва и изглежда добре. Разминаването се появява по-късно, в някоя несвързана функция, чийто рамков стек сега стои върху указател на стека, който е изместен с няколко байта, и се проявява като произволно четене, грешен адрес за връщане или срив с обратно проследяване, което не сочи никъде близо до обратното извикване, което всъщност сте сбъркали. Попълването на формуляри е класическото място, където това се проявява, тъй като интерфейсът за попълване на формуляри е запис, пълен с функции за обратно извикване, към които PDFium се обръща. Една от тях, FFI_OpenFile, предава на PDFium функция, която той ще извика за отваряне на външен файл, декларирана като function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Завършващото cdecl е детайлът, който си струва да копирате. Изпуснете го и кодът все още се компилира, свързва и работи точно до момента, в който PDFium извика функцията. Конвенцията принадлежи на самия тип на функцията. Тя не е допълнителна украса и компилаторът няма да ви предупреди, когато липсва, тъй като простият тип функция е напълно законен тип на Pascal. Единствената защита е да се третира конвенцията за извикване като задължително поле за всеки импортиран подпис и всяко обратно извикване, което предавате навън.
size_t е с ширина на указател, а при FPC Win64 това означава 64 бита
Вторият дефект е несъответствие в ширината на целите числа, което се появява само при една целева платформа. Стойността на size_t в C е дефинирана да бъде достатъчно широка, за да побере всеки размер на обект, което на 64-битова платформа означава 64-битово цяло число без знак. Интерфейсите за прогресивно зареждане на PDFium комуникират в size_t байтови отмествания. Записът FX_FILEAVAIL на доставчика на наличност носи обратно извикване IsDataAvail, което PDFium извиква с отместване и размер, а обратното извикване AddSegment на записа FX_DOWNLOADHINTS получава същото. И двата параметъра са от тип size_t.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Ако декларирате тези отмествания като 32-битов тип, обвивката работи на Win32 и на Delphi Win64, след което тихомълком се срива на FPC и Lazarus Win64. Причината е тънка. В FPC Win64, NativeUInt е истински 64-битов тип с ширина на указател и size_t е негов псевдоним. Обвивката има коментар в секцията с типове, предупреждаващ точно срещу засенчване на NativeUInt в FPC, тъй като предефинирането му до 32-битов псевдоним там би наложило size_t да бъде 32 бита и би повредило всеки size_t параметър, предаден на или записан от библиотеката. 64-битово отместване, пристигащо в 32-битов параметър, губи горната си половина. За малък файл всяко отместване се побира в 32 бита и нищо не е наред. За голям файл в момента, в който отместването премине четиригигабайтовата граница, съкратената стойност сочи към съвсем друго място, PDFium пита дали грешният диапазон от байтове е наличен и прогресивното зареждане спира или чете боклуци. Дефектът е невидим, докато файлът не стане достатъчно голям и целта не е тази, при която size_t действително се е разширил.
Pascal изключение никога не трябва да се развива през C рамка
Третият клас се отнася до модела на изключенията, какъвто C няма. Когато PDFium извика някоя от вашите функции за обратно извикване, вашият код на Pascal се изпълнява в стек от C и C++ рамки, които не знаят нищо за механизма за изключения на Delphi. Ако вашето обратно извикване предизвика изключение и го остави да се разпространи, то се развива през рамки, които никога не са били създадени за това. Собственото почистване на PDFium не се изпълнява, неговите вътрешни инварианти остават наполовина актуализирани и сега процесът е в състояние, което библиотеката никога не е предвиждала. Договорът за тези обратни извиквания е код за връщане, а не изключение.
Две обратни извиквания правят това конкретно. FPDF_FILEWRITE е приемникът, в който PDFium записва записания документ, а FPDF_FILEACCESS е източникът, от който той чете входния документ. И двата са имплементирани тук над TStream на Delphi и двата могат да се сринат по начина, по който всеки поток се срива: дискът се запълва, потокът се затваря под краката ви, четенето преминава края на файла. Обратното извикване за запис обвива своя запис в поток и превръща всеки срив в код за срив на PDFium, вместо да го остави да избяга.
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
Страната за четене прави същото: неуспешно четене съобщава нула, за да съответства на договора за FPDF_FILEACCESS, вместо да предизвиква изключение през границата. Празен блок except без повторно предизвикване изглежда грешен за Pascal програмист, обучен никога да не пренебрегва изключения, и в обикновения Pascal той наистина е грешен. На ABI граница обаче това е правилната форма, тъй като единствената безопасна стойност, която може да бъде върната на извикващия от C, е код за състояние, който той знае как да интерпретира. Сривът все пак се разпространява, но чрез върнатата стойност, и извикващият код над библиотеката го представя като EPdfError, след като контролът се върне от страната на Pascal.
Двойното освобождаване се крие в пътя на грешката
Четвъртият дефект е собствеността. Идентификаторът на документ на PDFium се отваря от библиотеката и трябва да бъде затворен точно веднъж чрез FPDF_CloseDocument. Опасността е в път на грешка, който освобождава дескриптор, който второ почистване също притежава. Представете си подпрограма, която създава обект-обвивка, присвоява му прясно отворен дескриптор на документ и след това извършва допълнителни настройки, които могат да се сринат. Ако настройката хвърли изключение, манипулаторът за ранно връщане, който извиква FPDF_CloseDocument върху необработения дескриптор, ще го затвори, а след това собственият деструктор на обекта-обвивка ще го затвори отново, когато обектът бъде освободен. Дескрипторът се освобождава два пъти, което е недефинирано поведение и вероятен срив.
Одитът установи това в импортиращ път в стил налагане (imposition), който изгражда TPdf около вече отворен дескриптор. Корекцията е прехвърлянето на собствеността да се превърне в единствен източник на истина. След като дескрипторът бъде присвоен на полето на обвивката, обвивката го притежава и единственото почистване по пътя на грешката е освобождаването на обвивката. Деструкторът на обвивката извиква FPDF_CloseDocument вместо вас, така че второ явно затваряне би освободило двойно същия документ. Коригираният манипулатор на грешки освобождава обекта и го извиква повторно, и има точно един път до затварянето.
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Управляваните записи и библиотека, пълна с експорти, се нуждаят от изрично разрушаване
Последният клас се отнася до паметта, която компилаторът управлява от ваше име, която навикът от C тихомълком ще повреди. Много от помощните функции на тази обвивка връщат запис, който съдържа WideString или динамичен масив. Това са референтно преброени полета и компилаторът излъчва скрито счетоводство, за да поддържа броя им. Инстинктът, пренесен от C, е нов запис да се изчисти с FillChar(Result, SizeOf(Result), 0). Това поставя нули върху управляваната референция в записа, без първо да я намали. Компилаторът използва повторно една скрита временна променлива за резултат от функция в итерациите на цикъла, така че при втората итерация FillChar презаписва жив показалец на низ, който никога не е бил освобождаван, и низът, към който сочи, изтича. Извикайте функцията в цикъл над хиляда анотации и ще изтекат хиляда низа.
Корекцията е да оставите езика да изчисти записа по начина, по който знае, с Default(T), което освобождава всяко управлявано поле преди нулирането му.
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
Свързан проблем със собствеността живее на границата на зареждане на библиотека. Тази обвивка разрешава няколкостотин указатели към функции от PDFium DLL с GetProcAddress след LoadLibrary. Ако липсва един необходим експорт, частично свързаното състояние е опасно: десетки указатели са валидни, останалите са nil или остарели и всяко по-нататъшно извикване през някой от тях скача в модул, който вече може да е разтоварен. Обвивката се справя с това, като разтоварва библиотеката и изпълнява пълно ClearAllBindings, което връща всеки импортиран указател обратно към nil, винаги когато изискван експорт не успее да се разреши. След това нито един указател към функция не виси в разтоварен модул и по-нататъшно извикване се срива чисто с проверка за нил указател, вместо да се разклонява в освободен код.
Обвивката е мястото, където четири договора се повтарят на ръка
Нито един от тези пет дефекта не е екзотичен. Те са предвидимите режими на срив на тънкия слой на Pascal над C API и се групират, защото точно в този слой четири отделни договора трябва да бъдат повторно декларирани. Конвенцията за извикване трябва да бъде изписана cdecl при всяко обратно извикване. Ширината на целите числа трябва да съответства на size_t на едната цел, където тя действително се разширява. Моделът на изключения трябва да бъде преобразуван в кодове за връщане при всяко обратно извикване, което излиза извън Pascal. Собствеността на всеки дескриптор и всяко управлявано поле трябва да бъде заявена веднъж и спазвана по всеки път, включително по пътищата на грешки, които никой не упражнява до производство. Пропуснете някое от тях и ще получите дефект, чийто симптом се появява далеч от причината му, което прави тази категория скъпа. Стойността на одита беше по-малко в конкретните корекции, отколкото в третирането на всяка от тях като собствена дисциплина за проверка на цялата обвивка.
Ако искате да видите обвивката да върши реална работа, а не просто да пази краищата си, техниките за кеширане на рендиране и мащабиране в нашата бележка за производителността на кеша за рендиране и мащабиране показват пътя на рендиране, а стъпка по стъпка ръководството за крос-компилация в изграждане на Lazarus и FPC четец е мястото, където описаното тук поведение на Win64 size_t действително има значение. И двете се базират на същата работа по безопасност на паметта и ABI, която се доставя в PDFium Component за Delphi, Lazarus и C++Builder, заедно с API за рендиране, извличане на текст и формуляри, разгледани на други места в този блог.