Artykuł techniczny

Ładowanie natywnej biblioteki PDFium na każdym celu

Komponent PDFium znajduje swoją bibliotekę natywną przez stały, uporządkowany łańcuch wyszukiwania, zamiast zostawiać to konsolidatorowi systemu operacyjnego, bo drzewo wdrożenia, które jest jawne, to drzewo wdrożenia, które możesz debugować. Na Windows ten łańcuch szuka podkatalogu Win32 albo Win64, który instalator już dostarcza. Na innych celach buduje nazwę podkatalogu z makr celu Free Pascala, jako <cpu>-<os>, więc drzewo wdrożenia czyta się dokładnie jak drzewo skompilowanych jednostek. Ta ostatnia decyzja wprowadziła błąd wart całego artykułu, bo przyczyną była wielka litera, a objawem było milczenie

Łańcuch, po kolei

Cztery lokalizacje, próbowane po kolei, potem konsolidator platformowy jako ostatnia deska ratunku. Najpierw preferowany układ, katalog DLLs obok pliku wykonywalnego zawierający jeden podkatalog per cel. Potem układ alternatywny z podkatalogiem celu bezpośrednio obok pliku wykonywalnego. Trzeci to płaski starszy układ, biblioteka siedząca obok pliku wykonywalnego bez żadnego podkatalogu. Czwarty, tylko na Windows, katalog systemowy, który wymaga uwagi, bo proces 32-bitowy musi zajrzeć do SysWOW64, a 64-bitowy do System32, a na 32-bitowym Windows ten pierwszy nie istnieje, więc wyszukiwanie musi się cofnąć. Dopiero po tym wszystkim konsolidator dostaje polecenie szukania na własną rękę

Diagram łańcucha wyszukiwania natywnej biblioteki PDFium dla Delphi, od podkatalogu celu DLLs przez układy alternatywny, płaski i katalog systemowy Windows do konsolidatora platformowego
Cztery jawne lokalizacje są sondowane po kolei, zanim konsolidator OS zostanie poproszony o szukanie na własną rękę

Poza Windows celowo nie ma kroku katalogu systemowego. Własna ścieżka wyszukiwania konsolidatora platformowego, prowadzona przez konfigurację konsolidatora czasu wykonania i środowisko ścieżek bibliotek, już pokrywa ten teren, a dublowanie jej w Pascalu znaczyłoby reimplementowanie reguł różniących się między dystrybucjami. Diagnozowanie awarii w łańcuchu Windows jest omówione osobno w artykule o wdrażaniu biblioteki DLL PDFium i diagnozowaniu awarii ładowania

Skąd bierze się nazwa podkatalogu

Na Windows to Win32 albo Win64, decydowane przez bitowość działającego procesu, a nie systemu operacyjnego, bo to determinuje, który plik binarny da się załadować. Wszędzie indziej nazwa jest budowana z makr celu kompilatora, żeby maszyna budująca dla dwóch architektur produkowała dwa wyraźnie rozdzielone drzewa i żeby folder trzymający bibliotekę natywną siedział obok folderu ze skompilowanymi jednostkami pod tą samą nazwą

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Makra kompilatora kapitalizują OS ("Linux", "Darwin"), a wyjściowy
  // katalog jednostek pakietu nie, więc oba zgadzają się dopiero po
  // złożeniu do małych liter. Na systemie plików rozróżniającym wielkość
  // liter ta różnica to całe wyszukiwanie
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Czemu wielka litera połamała cały łańcuch

Makro kompilatora pisze docelowy system operacyjny wielką literą początkową: Win64, Linux, Darwin. Pakiet Lazarusa zapisuje wyjście jednostek do katalogu nazwanego od własnej zmiennej celu, która jest z małych liter: win64, linux, darwin. Dwie pisownie tej samej rzeczy i żadnego sposobu, by to zauważyć na Windows, gdzie system plików ich nie rozróżnia

Na Linuksie to dwa różne katalogi. Wdrożenie kładące bibliotekę współdzieloną w DLLs/x86_64-linux jest niewidzialne dla konsolidatora szukającego DLLs/x86_64-Linux, więc wszystkie cztery jawne kroki łańcucha mijają i kod spada do pozwolenia konsolidatorowi platformowemu szukać. Czasem to działa, jeśli biblioteka akurat jest zainstalowana w całym systemie, a czasem nie, i tak czy inaczej starannie ułożone drzewo wdrożenia nie wnosi nic. Awaria nie ma komunikatu błędu, bo nic nie zawiodło: każdy krok poprawnie zgłosił, że pliku nie ma tam, gdzie zaglądał

Jak jedna wielka litera w makro celu FPC łamie wyszukiwanie biblioteki DLL PDFium na Linuksie: konsolidator szuka DLLs/x86_64-Linux, a wdrożony folder to DLLs/x86_64-linux, co pasuje tylko na nierozróżniającym wielkości liter Windows
Ten sam cel zapisany dwiema pisowniami pasuje na Windows i po cichu mija na systemie plików rozróżniającym wielkość liter

Program sondy: skompilowany i uruchomiony

Tego rodzaju błędu nie znajdziesz czytaniem i kompilowaniem też nie. Zwykła technika weryfikacji gałęzi platformowej, która nigdy się nie kompiluje na maszynie rozwojowej, to skopiowanie jednostki do katalogu tymczasowego, przemianowanie jej, zastąpienie warunku platformowego symbolem nigdy niezdefiniowanym i skompilowanie kopii; jeśli się kompiluje, klauzula uses i sygnatury wywołań na tej ścieżce są przynajmniej samospójne. To dobrze działa dla samowystarczalnej jednostki

Tutaj to nie działa. Główna jednostka wiążąca jest bardzo duża i ciągnie LCL, więc nie da się jej po prostu skopiować i skompilować z wyłączonym symbolem Windows. Zamiast tego garść funkcji, których dotknęła zmiana, została przepisana słowo w słowo do małego samowystarczalnego programu i ten program został uruchomiony. Wydrukował x86_64-Win64 i niezgodność była widoczna w jednej linii wyjścia. Skompilowanie tego samego programu nie powiedziałoby nic, bo łańcuch jest w pełni poprawny; zła jest tylko jego wartość

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Drukuj, nie asertywuj. Chodzi o obejrzenie wartości, do której makro
  // faktycznie się rozwija na tym toolchainie
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

Ogólna lekcja: gdy międzyplatformowa zmiana dotyczy wartości czegoś, a nie jej typu, weryfikacja wyłącznie kompilacją nie jest weryfikacją. Wydrukuj to. Szerszy zestaw różnic międzykompilatorowych między Delphi i Free Pascalem jest zebrany w artykule o pułapkach międzykompilatorowych Delphi i FPC

Pozwól platformie wytłumaczyć własne awarie ładowania

Gałąź Windows konsolidatora ręcznie wylicza powody, dla których ładowanie może zawieść, bo użyteczne rozróżnienia tam — niezgodność architektury, brakująca zależność tranzytywna, ścieżka, która się nie rozwiązuje — mapują się na kody błędów warte nazwania pojedynczo. Poza Windows przenośna jednostka konsolidatora już zwraca opisowy łańcuch pokrywający ten sam grunt, więc gałąź nie-Windows używa jej wprost, zamiast wyprowadzać kategorie z numeru błędu znaczącego różne rzeczy na różnych systemach

Opieranie się pokusie znormalizowania tych dwóch do jednego komunikatu jest zamierzone. Awaria ładowania to problem wdrożeniowy, a osoba czytająca komunikat potrzebuje własnego słownictwa platformy, żeby go wyszukać

Kolizja nazw, która się rekuruje

Jeszcze jedna pułapka, mała i ostra. Przenośna jednostka konsolidatora eksportuje procedurę zwana UnloadLibrary, a jednostka wiążąca ma procedurę tej samej nazwy robiącą własną księgowość przed zwolnieniem uchwytu. Wewnątrz tej procedury niewykwalifikowane wywołanie UnloadLibrary rozwiązuje się do tej w bieżącej jednostce, która wywołuje samą siebie. Poprawką jest wykwalifikowanie wywołania nazwą jednostki

To ten sam kształt co problemy zasłaniania identyfikatorów, które generalnie dominują porty Free Pascala: jednostka Windows eksportuje całkowitoliczbowe funkcje minimum i maksimum zacieniające zmiennoprzecinkowe oraz typ synchronizacji zacieniający klasę tej samej nazwy, a w każdym przypadku rozwiązanie zależy od kolejności klauzuli uses. Wykwalifikowanie miejsca wywołania to poprawka niezależna od kogoś, kto później zachowa tę kolejność

Kolizja nazw UnloadLibrary w komponencie PDFium: niewykwalifikowane wywołanie w jednostce wiążącej Delphi rekuruje do siebie, a wykwalifikowane jednostką wywołanie dociera do przenośnej jednostki konsolidatora i zwalnia uchwyt
Wykwalifikowanie miejsca wywołania kieruje zwolnienie przez jednostkę konsolidatora, zamiast rekkurować do jednostki wiążącej

Checklista wdrożenia

Trzy rzeczy odpowiadają za większość awarii ładowania, gdy arytmetyka ścieżek jest już dobra. Architektura musi pasować do procesu, nie do maszyny, więc aplikacja 32-bitowa na 64-bitowym Windows potrzebuje pliku binarnego 32-bitowego. Build z włączonym V8 ma inną nazwę pliku, więc wdrożenie je mieszające będzie wyglądać poprawnie i nie załaduje niczego. I tylko jeden wariant może w danej chwili mieszkać w katalogu systemowym, co jest dobrym powodem, by przedkładać jawny układ podkatalogów nad instalowanie czegokolwiek w całym systemie

Dla Lazarusa konkretnie: połóż bibliotekę natywną pod DLLs/<cpu>-<os> z małych liter, obok pliku wykonywalnego, a zostanie znaleziona przez pierwszy krok łańcucha na każdym celu. Przykład przeglądarki ćwiczący to na Lazarusie jest opisany w artykule o przeglądarce Lazarus i FPC, a aktualne wsparcie platform jest wypisane na stronie produktu PDFium Delphi component