Блокировката за рендиране на PDFiumPas е критична секция за всеки документ — EnterRenderLock и LeaveRenderLock, поддържани от поле TRTLCriticalSection в TPdf — която трябва да обгръща всяко извикване към растеризатора на PDFium, за да не бъде страница разтоварена или презаредена, докато рендирането още използва данните ѝ. Шест метода, разделени поравно между TPdf и TPdfView, извикваха директно API за растерни изображения и миниатюри на PDFium и изцяло пропускаха тази блокировка, докато PDFiumPas v2.26.0 не затвори пропуска, като обви и шестте със същата двойка за заключване, която вече използваха останалите входни точки за рендиране
Разглежданият пропуск не е част от укрепването на ABI, описано другаде в този блог, където бяха проследени несъответствие в конвенцията за извикване cdecl и съкращаване на ширината на указател при FPC Win64 в същата PDFium обвивка. Тук обхватът е по-тесен и по-механичен: контролен списък за покритието на блокировката при шест места на извикване, които достигат до пътя за рендиране на PDFium, причините всяко от тях лесно да бъде пропуснато и защо произтичащото от липсващата блокировка състезателно състояние е един от най-трудните за възпроизвеждане при поискване дефекти в тази кодова база
Какво всъщност защитава блокировката за рендиране
PDFiumPas сериализира рендирането, защото заредената страница в PDFium не е безопасна за четене от един поток, докато друг поток може да я освободи. TPdf притежава TRTLCriticalSection в FRenderLock, инициализирана в конструктора и защитена с флаг FRenderLockReady, така че извикване след освобождаване да се превърне в тихо пропускане, вместо да влезе в изтрита критична секция. EnterRenderLock и LeaveRenderLock са единственият разрешен начин за влизане в тази секция и излизане от нея
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile и RenderPageProgressive вече следваха тази дисциплина, преди да започне конкретният одит, като всеки метод вземаше блокировката преди извикването към PDFium и я освобождаваше в блок finally, така че фоново предварително рендиране и извеждане на страница с UnloadPage на същия екземпляр на TPdf да не се припокриват. Пропускът, открит в PDFiumPas v2.26.0, не беше в тези очевидни входни точки — той се оказа в шест метода, които приличат на достъпващи методи, а всъщност всеки от тях кара PDFium да растеризира пиксели, преди да върне резултат
Кои шест извиквания пропуснаха блокировката за рендиране
TPdf.GetObjectBitmap, TPdf.GetBitmap и TPdf.GetThumbnail съставляваха половината от списъка, а TPdfView.GetObjectBitmap, TPdfView.GetBitmap и TPdfView.GetThumbnail — другата половина: същите три операции, дублирани в двата класа компоненти, които предоставят една и съща страница. И шестте в крайна сметка извикват или FPDFImageObj_GetBitmap, или FPDFPage_GetThumbnailAsBitmap, като и двете входни точки на PDFium растеризират на място, вместо да върнат препратка към нещо вече рендирано. В нито едно от шестте имена на методи не се среща думата за рендиране, което разумно обяснява защо при първия преглед те не са били проверени по същия списък като RenderPage и RenderTile
function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
Bitmap: FPDF_BITMAP;
begin
Result:= nil;
EnterRenderLock;
try
Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
finally
LeaveRenderLock;
end;
if Bitmap<> nil then
try
Result:= ToBitmap(Bitmap);
finally
FPDFBitmap_Destroy(Bitmap);
end;
end;
Защо TPdfView защитава извикването на блокировката с проверка за nil
TPdfView не притежава собствена критична секция — всяко от шестте му извиквания за блокиране се препраща към FPdf.EnterRenderLock и FPdf.LeaveRenderLock, като първо се проверява дали свързаната препратка към TPdf не е nil. Тази защита е нужна, защото TPdfView може да стои върху форма по време на проектиране или за кратко между затварянето на един документ и отварянето на следващия, без към FPdf все още да е присвоен TPdf. Пропускането на проверката би заменило една грешка с друга, тъй като извикването за блокиране върху nil препратка не се проваля по-безопасно от състезателното състояние, което блокировката трябва да предотврати
function TPdfView.GetThumbnail: TBitmap;
var
PdfBitmap: FPDF_BITMAP;
begin
CheckActive;
Result:= nil;
if FPdf<> nil then
FPdf.EnterRenderLock;
try
PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
finally
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
if PdfBitmap<> nil then
try
Result:= ToBitmap(PdfBitmap);
finally
FPDFBitmap_Destroy(PdfBitmap);
end;
end;
Защо RenderPage(HDC) принадлежи към същия одит
TPdfView.RenderPage за контекст на устройство не е едно от шестте извиквания — то беше открито година по-рано, в PDFiumPas v2.25.0, и присъства в този списък, защото е същият дефект с различен подпис. Този претоварен метод извикваше директно FPDF_RenderPage без EnterRenderLock и без извикването SetArithmeticMask, което защитава от FPU изключения при по-стари компилатори на Delphi, докато претоварването с TBitmap няколко реда по-надолу в същия клас вече съдържаше и двете. Два одита, които откриват един и същ режим на отказ през една версия разстояние, говорят не толкова за конкретен метод, колкото за формата на дефекта: той се крие в претоварването, което никой не преглежда повторно, след като съседното изглежда правилно
procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
ArithmeticMask: TArithmeticMask;
begin
CheckActive;
if FPdf<> nil then
FPdf.EnterRenderLock;
ArithmeticMask:= SetArithmeticMask;
try
FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
Ord(Rotation), EncodeRenderOptions(Options));
finally
RestoreArithmeticMask(ArithmeticMask);
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
end;
Защо това състезателно състояние почти не може да се възпроизведе
Пропускът в блокировката за рендиране на PDFiumPas не води до отказ при всяко стартиране или дори при повечето стартирания, защото са нужни две конкретни операции върху един и същ екземпляр на TPdf едновременно: извикване за растеризация, което вече се изпълнява, и едновременно UnloadPage или ReloadPage, попаднало в същия прозорец. Тестването в един поток изобщо не упражнява този път, а дори истински многопоточните натоварвания го задействат само когато фоновото рендиране и събитие от жизнения цикъл на документа случайно се припокрият в рамките на живота на една страница. Най-реалистичният инициатор е фоново предварително рендиране на PDF, изградено върху отменяеми futures, при което работен поток растеризира следващата страница, докато потокът на потребителския интерфейс презарежда или разтоварва текущата страница в отговор на действие на потребителя
FPDFImageObj_GetBitmap и FPDFPage_GetThumbnailAsBitmap обхождат структурите на обектите на страницата, които UnloadPage може да освободи по средата на обхождането, така че реално задействаното състезателно състояние не винаги води и до незабавно нарушение на достъпа. Прочит на структура миг по-късно също толкова лесно може да върне повредени пиксели или да повреди метаданни на heap-а, които да предизвикат срив чак след няколко несвързани разпределяния, във функция, която никога не е докосвала PDF страница. Това е истинската причина този клас дефекти да оцелява в кодова база през няколко версии: стекът при момента на отказа рядко сочи близо до шестте реда, в които действително е липсвала блокировка
Какво се променя за извикващия код
GetBitmap, GetObjectBitmap, GetThumbnail и претоварването на RenderPage за HDC запазват публичните си сигнатури напълно непроменени, тъй като поправката е вътрешно блокиране около съществуващи извиквания, а не миграция. Важно е да се помни, че блокировката за рендиране е ограничена до екземпляра на TPdf, а не е глобална за процеса, така че два потока, които рендират два отделно заредени документа, продължават да работят напълно паралелно — блокировката сериализира само операциите върху единствения документ, който двата потока споделят. Ако блокирането ви вече е коректно, но рендирането все още се усеща бавно при мащабиране или превъртане, това е друг въпрос, разгледан в статията за кеша за рендиране на PDFium и производителността при мащабиране — коректността и скоростта са отделни измерения, а тази поправка засяга само първото
Шест метода и едно съседно претоварване са малка част от повърхността на PDFium, която PDFiumPas предоставя, но това беше частта, която се проявяваше неправилно само под натоварване, което никой случайно не изпълняваше в дебъгер. Самата блокировка за рендиране и пълният набор входни точки за рендиране, които тя вече покрива, се доставят като част от PDFium Component за Delphi, C++Builder и Lazarus/FPC