PDFlibPas buduje się pod Free Pascalem na 32-bitowy Windows i najtrudniejsze w tym nigdy nie był sam Pascal. Najtrudniejsze były pliki obiektów: obiekty AES i OpenJPEG, które linkuje build Delphi, mają format OMF, wewnętrzny linker Free Pascala wymaga COFF, a konwersja z jednego formatu do drugiego wytwarza nazwy sekcji i symbole definicji sekcji, na których linker wywala się błędami wewnętrznymi zamiast podać konkretną diagnozę
Każdy, kto kiedykolwiek linkował obiekty C do biblioteki w Pascalu, zna ten teren. Win64 jest względnie cywilizowany: jeden format obiektów, jedna konwencja wywołań i zero dekoracji nazw. Win32 zachowuje każdą warstwę historii, jaką platforma nagromadziła przez lata, a biblioteka statycznie linkująca zewnętrzny kod C spotyka je wszystkie naraz
Katalog kompilatora nie mówi ci, jaki jest target
Zacznij od punktu wejścia builda, bo pomyłka na tym etapie kosztuje godziny, zanim w ogóle ruszą pliki obiektów. Nazwa katalogu instalacji Free Pascala mówi tylko, gdzie mieszka główny kompilator, a nie co on produkuje. Kompilator 32-bitowy potrafi odpalić cross-kompilator leżący obok i wygenerować kod 64-bitowy, gdy przekażesz właściwe przełączniki targetu, więc zgadywanie targetu ze ścieżki to zgadywanka, która działa dokładnie do momentu, aż ktoś przemebluje sobie toolchain
Niezawodne podejście to zapytać kompilator. Odpytaj o rzeczywisty procesor docelowy i system operacyjny przez własne przełączniki informacyjne kompilatora i zaakceptuj oba popularne układy instalacji, czyli płaski katalog binarny oraz ten zagnieżdżony w katalogu wersji, bo różne instalatory i menedżery toolchainów produkują różne kształty. Skrypt builda zaszyty na sztywno pod jeden z układów działa na dokładnie jednej maszynie
Dlaczego skonwertowany plik obiektów wysypuje wewnętrzny linker?
Bo konwersja zachowuje konwencję nazewnictwa sekcji z OMF i syntetyzuje symbole definicji sekcji, które nie pasują do tego, czego oczekuje linker COFF. Konwersja obiektów OMF do COFF jest konieczna, ale niewystarczająca: powstałe pliki noszą klasyczne nazwy sekcji _TEXT, _DATA i _BSS plus wywodzące się z nich nazwy symboli definicji sekcji, a podanie tego wewnętrznemu linkerowi Free Pascala kończy się wewnętrznymi błędami kompilatora zamiast komunikatu o nazewnictwie sekcji
Błąd wewnętrzny to najgorszy tryb awarii przy problemach z buildem, bo nie mówi nic o tym, co było nie tak z wejściem. Rozwiązaniem jest przejście normalizacyjne po konwersji na pliku COFF: przepisz nazwy sekcji do oczekiwanej postaci i przepisz odpowiadające im symbole definicji sekcji, nie ruszając przy tym indeksu symboli, bajtów kodu ani relokacji. To ostatnie ograniczenie to cała trudność. Przepisanie, które przenumeruje symbole albo przesunie offsety, daje obiekt, który się linkuje, a potem wywala program
Dla jednego z dwóch zestawów obiektów potrzebny jest wstępny krok. Obiekty OpenJPEG zbudowane klasycznym 32-bitowym kompilatorem C++ zależą od prywatnych procedur pomocniczych Delphi do arytmetyki 64-bitowej, których Free Pascal nie dostarcza, więc żadna ilość konwersji formatu nie uczyni ich użytecznymi. Te obiekty trzeba najpierw przebudować kompilatorem opartym na Clangu, który takich zależności nie emituje, a dopiero potem konwertować
// Obiekty dla targetu FPC mieszkają we własnym katalogu. Nie zastępują
// zestawu obiektów Delphi, bo oba toolchainy budują z tego samego drzewa
// źródeł i każdy potrzebuje własnych wejść linkowania
//
// Lib\thirdparty\Win32 obiekty OMF Delphi, bez zmian
// Lib\thirdparty\Win32f obiekty COFF pod FPC, skonwertowane i znormalizowane
//
// Punkty wejścia builda:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
Prywatne helpery kompilatora nie są przenośne, a ich konwencje też nie
Runtime Delphi dostarcza trampolin asemblerowych do 64-bitowych operacji na liczbach całkowitych przy 32-bitowym x86, a prekompilowane obiekty C budowane pod Delphi wołają je bezpośrednio. Free Pascal ma własny układ, więc tych referencji nie da się przekierować — trzeba je zaspokoić inaczej. Detalem, który uniemożliwia przekierowanie, jest konwencja wywołań: helper chronometrażu używany przez kod obrazowania ma czterobajtowy argument sprzątany po stronie wołanego, podczas gdy helper dzielenia 64-bit sprząta szesnaście bajtów i zwraca wynik w klasycznej parze rejestrów. Dwa helpery, dwie konwencje, i trampolina napisana pod jedną po cichu psuje stos drugiej
Drugą połowę problemu dokłada dekoracja nazw. Na Win32 Free Pascal automatycznie dodaje podkreślnik do zewnętrznych importów z C, eksportując jednocześnie deklaracje public name dosłownie, więc strona importowa i eksportowa tego samego mostu rządzą się innymi prawami. Most runtime C, którego potrzebuje OpenJPEG, musi więc eksportować dokładne nazwy symboli C, a wariadyczne punkty wejścia wymagają 32-bitowego skoku pośredniego zamiast bezpośredniego. Gdy się to wszystko wypunktuje, brzmi jak banaly. W praktyce całość wysypuje się błędem linkera wskazującym symbol, którego nikt nie napisał
Co sprawiało, że plik wykonywalny Win32 umierał przed main?
64-bitowa DLL na ścieżce wyszukiwania, do której dochodziło, bo unit zlib Free Pascala binduje się dynamicznie zamiast linkować statycznie. Objawem było natychmiastowe wyjście z kodem stanu invalid-image, zanim jakikolwiek kod Pascala w programie zdążył się wykonać, co posyła cię do analizy programu, który właśnie zbudowałeś, podczas gdy wina leży po stronie loadera rozwiązującego import przeciwko złej architekturze
Wniosek dotyczy założeń, nie samego zlib. Unit nazwany po bibliotece kompresji niekoniecznie takową zawiera; może to być binding oczekujący w czasie działania biblioteki współdzielonej, a niezamierzona zależność dynamiczna to obciążenie przy wdrożeniu, nawet gdy akurat się rozwiąże. Przejście na czysto pascalową implementację strumienia daje obu targetom statycznie włączoną ścieżkę kompresji bez żadnej zewnętrznej zależności, czyli dokładnie to, co biblioteka wbudowywana w cudzą aplikację powinna mieć od początku
Ten sam instynkt stosuje się do zewnętrznego backendu enkodera JBIG2. Na targecie 32-bitowym zewnętrzny enkoder nie jest linkowany, więc żądania schodzą na wbudowany enkoder w Pascalu, a test weryfikujący to musi sprawdzać stan rejestracji bieżącego targetu, zamiast brać udane kodowanie za dowód obecności backendu zewnętrznego. Działający fallback to dokładnie ta rzecz, która skrywa brakującą zależność — to schemat awarii rozłożony na czynniki w artykule diagnozowaniu cichych awarii stubów. Pracę nad statycznym linkowaniem w 64 bitach opisuje statyczne linkowanie jbig2enc pod FPC
Arytmetyka 32-bitowa na strumieniu pamięciowym
Kod, który operuje na rozmiarach buforów za pomocą arytmetyki bezznakowej o szerokości wskaźnika, jest poprawny na Win64, a od przepełnienia na Win32 dzieli go jeden duży obraz. Strumień pamięciowy zasilający kodek JPEG 2000 rośnie przez podwajanie i przesuwa się przez dodawanie, a na targecie 32-bitowym obie operacje potrafią się zawinąć przy danych wejściowych dużych, ale całkiem legalnych
Każdy zapis, skip, seek i początkowa alokacja sprawdza więc, zanim policzy, a sufitem pojemności jest maksymalna znakowana wartość o szerokości wskaźnika, dobrana pod to, co potrafi wyrazić rutyna przenoszenia bloków i wartości zwracane z callbacków. Wymaganie behawioralne przy odrzuceniu żądania łatwo zepsuć: odmowa nie może zmieniać pozycji strumienia ani jego długości. Częściowa mutacja zakończona błędem zostawia strumień w stanie, którego wołający nie potrafi logicznie ogarnąć, a następna operacja nakręca bałagan dalej
// Sprawdź, zanim policzysz. Na Win32 obie te rzeczy zawijają się przy
// danych, jakie legalnie produkuje duży obraz JPEG 2000
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // odmów, zostaw pozycję i rozmiar w spokoju
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // podwajanie przepełniłoby pojemność
NewCapacity := NewCapacity shl 1;
end;
Dwie pułapki outputu builda, które przeżyją sam port
Rozdzielenie plików wykonywalnych testów i przykładów po architekturze targetu do katalogów wyjściowych per target jest oczywiście słuszne i natychmiast wywala wszystko, co lokalizowało swoje dane testowe przez liczenie poziomów katalogów w górę. Rozwiązaniem jest szukanie w górę katalogu z zasobami zamiast zakładania stałej głębokości, z jednym świadomym ograniczeniem: przykład podpisujący przyjmuje fallback certyfikatu wyłącznie z własnego katalogu projektu, nigdy z dowolnego przodka, bo certyfikat o tej samej nazwie znaleziony wyżej w drzewie to niespodzianka bezpieczeństwa, a nie udogodnienie
Druga pułapka przeżywa każdy port i warto zabrać ją do każdego projektu FPC. Po upgrade kompilatora samo odrzucanie nieświeżych plików PPU nie wystarcza, bo linker wciąż przedkłada pozostałe po poprzednich buildach pliki obiektów na ścieżce przeszukiwania unitów, nawet gdy załadowany PPU przyszedł z właściwego katalogu, a jawna ścieżka wyjściowa obiektów tej preferencji nie nadpisuje. Jedyna niezawodna odpowiedź to świeży tymczasowy katalog unitów na każdą rundę builda. Cokolwiek mniej daje binarkę linkowaną z dwóch wersji kompilatora, która awaruje w sposób wyglądający jak błędy w źródłach
Warunki platformowe to ostatni kawałek, a wybór właściwej osi ma większe znaczenie, niż się wydaje. Właściwe pytanie brzmi zwykle, czy kod jest specyficzny dla Windows, a nie czy obecna jest konkretna biblioteka widgetów, co pokazała praca nad konwersją metafile'ów opisana w artykule import wektorów EMF a warunki platformowe: przestawienie tego warunku z warunku biblioteki kontrolek na warunek platformy zamieniło rzekome przepisywanie na zmianę jednej dyrektywy. Wsparcie Free Pascala i Lazarusa dla obu targetów Windows jest częścią biblioteki PDF PDFlibPas dla Delphi, budowanej z tych samych źródeł co pakiety Delphi i C++Builder