HotPDF kompiluje się i działa pod Free Pascalem 3.2.2 z Lazarusem, a uczciwe streszczenie tego portu zmieści się w dwóch zdaniach. Tworzenie dokumentów, wczytywanie, zapis, kompresja, dekompresja, szyfrowanie i deszyfrowanie działają na backendach wyłącznie pascalskich, więc aplikacja Lazarusa może produkować i konsumować prawdziwy PDF bez jakiejkolwiek zależności od C. Opcjonalne natywne kodeki obrazów nie, bo wstępnie zbudowane obiekty Win64 używają odmiany COFF, której żaden konsolidator Free Pascala nie skonsumuje, więc na tym toolchainie punkty wejścia rozwiązują się do stubów, które zawodzą bezpiecznie
Droga od „kompiluje się” do „działa” wymagała konkretnego zestawu poprawek, a każda z nich jest pułapką, która złapie każdą inną bazę kodu Delphi przenoszoną na Free Pascal. Warte zanotowania w kolejności, w jakiej bolały
Dlaczego skompilowana jednostka niczego nie dowodzi?
Bo jednostka Pascala może odwoływać się do symbolu, który nigdy niczego użytecznego nie zrobi, i wciąż zaspokoić kompilator. W momencie, gdy wszystkie 113 jednostek biblioteki budowało się czysto pod Free Pascalem, handlery kontenerów archiwalnych naprawdę działały, zweryfikowane testem dymnym, który otwierał CBZ i konwertował go do PDF. Spłaszczanie formularzy XFA nie działało w ogóle, bo spłaszczanie musi zdekompresować skompresowany strumień pakietu /XFA, a punkt wejścia deflate był wciąż stubem. Nic w wyjściu builda nie odróżniało tych dwóch przypadków
Reguła, która z tego wynikła, jest krótka. Zanim napiszesz w nocie wydania, że funkcja działa na nowym toolchainie, napisz sondę uruchomieniową, która ćwiczy tę funkcję od końca do końca na tym toolchainie. Pokrycie kompilacją to warunek wstępny, nigdy dowód. Szerszy obraz tego, co port pokrywa, jest w notach wsparcia Free Pascal i Lazarus Win64
Raise wewnątrz stubu cdecl nie dociera do wywołującego
Ta zasługuje na własną sekcję, bo objaw jest tak mylący. Jednostki stubów wystawiają punkty wejścia C tak, jak zrobiłaby to biblioteka statyczna, więc stub wygląda tak
// Wygląda rozsądnie. Nie jest.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
public name 'inflate';
begin
raise ENotSupportedException.Create('codec unavailable');
end;
Na Free Pascalu dla Win64 ten wyjątek nie propaguje się do wywołującego. Nie ma handlera try..except, który by go zobaczył, bo rozwijanie przez granicę cdecl zadeklarowaną w ten sposób nie niesie pascalskiej ramki wyjątku; proces kończy się kodem wyjścia 217. Od strony aplikacji nie ma błędu, komunikatu ani linii w logu, tylko program, który znika. To jest ściśle gorsze niż zła odpowiedź, bo złą odpowiedź można obsłużyć
Kusząca poprawka to sprawienie, by stub zwracał kod awarii, i dla inflate to jest słuszne, bo zlib ma dobrze zdefiniowany zwrot błędu. W ogólności to błędne: stub dla jpeg_read_header zwracający zero mówi wywołującemu, żeby ciągnął dalej ze strukturą, której nikt nie zainicjalizował. Trwała poprawka to bramkowanie na pascalskim punkcie wejścia, a nie wewnątrz stubu o kształcie C, używając konwencji awarii, którą to API już ma
function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
// Odmów zanim stub zostanie osiągnięty, używając własnej konwencji
// awarii tego API, a nie wyjątku przez granicę cdecl
Bitmap := nil;
Result := False;
Exit;
{$ENDIF}
Result := DecodeJPEGNative(Data, Bitmap);
end;
paszlib to nie zlib, a różnica to dwie klasy dokumentów
Pascalska implementacja deflate dostępna na Free Pascalu obsługuje dwa opakowania: opakowanie zlib i surowy deflate. Nie obsługuje opakowania gzip, które zlib wybiera przez wartości windowBits od 16 do 31, ani trybu automatycznej detekcji wybieranego przez wartości od 32 do 47. HotPDF potrzebuje obu. Bezpieczna ścieżka importu SVG prosi o 31, a wczytywanie ma drabinę awaryjną proszącą o 47, gdy opakowanie strumienia jest niejednoznaczne. Pomiń którekolwiek i cała rodzina dokumentów przestaje się otwierać, z błędem dekodowania wskazującym na strumień, a nie na brakujące opakowanie
Jest i druga, ostrzejsza niezgodność. Rekord z_stream deklarowany przez paszlib nie ma tego samego układu pamięci co ten z C: jego pole msg to short string, a nie wskaźnik, a total_in i total_out są 64-bitowe tam, gdzie ABI C ma słowa maszynowe. Rekord wywołującego nie może więc przejść na wprost. Działające rozwiązanie to trzymanie stanu paszlib za wskaźnikiem state, który publiczny rekord już rezerwuje, i kopiowanie publicznych pól w obie strony przy każdym wywołaniu. CRC gzip i ośmiobajtowy ogon długości są rozliczane w tej samej warstwie shim, co jest dla nich naturalnym miejscem, bo to ona już posiada decyzję o opakowaniu
Przekazywanie tablicy dynamicznej do nietypowanego parametru var
To jest błąd, który najbardziej prawdopodobnie siedzi właśnie teraz w twoim kodzie. Gdy przekazujesz tablicę dynamiczną do nietypowanego parametru var, wywoływany dostaje adres zmiennej tablicy, czyli adres wskaźnika, a nie adres ładunku. Odczyt do niej nadpisuje więc samą zmienną i to, co siedzi obok
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// Źle: oddaje adres zmiennej FBuffer
FStream.Read(FBuffer, Length(FBuffer));
// Dobrze: oddaje adres pierwszego bajtu ładunku
FStream.Read(FBuffer[0], Length(FBuffer));
end;
Na Delphi zła forma częstokroć wygląda na działającą, bo psuje sąsiednie gniazdo stosu, którego nic potem nie czyta. Na Free Pascalu ta sama linia daje naruszenie segmentacji przy pierwszym użyciu. To, co sprawia, że tak trudno to dostrzec okiem, to fakt, że tablice statyczne nie mają takiego problemu, bo zmienna tablicy statycznej jest własnym ładunkiem, więc obie pisownie są poprawne w tym samym pliku, zależnie od deklaracji sto kilkaset linii dalej
Kontenery ZIP bez System.Zip
Free Pascal nie ma odpowiednika jednostki zip z RTL, a dostępna alternatywa ma i inną powierzchnię API, i brak wsparcia dla starszego szyfrowania, którego starsze formaty kontenerów wciąż używają, więc mały czytnik we własnym kodzie biblioteki okazał się krótszy niż adaptowanie do niej. Dwa szczegóły formatu kosztowały czas i łatwo je zepsuć
Pierwszy to bajt kontrolny nagłówka szyfrowania. Jego dwunasty bajt to normalnie wysoki bajt CRC, ale gdy bit 3 flagi ogólnego przeznaczenia jest ustawiony, czyli rozmiary mieszczą się w końcowym deskryptorze danych, a CRC nie jest jeszcze znane, bajt kontrolny pochodzi z wysokiego bajtu czasu modyfikacji. Zaimplementuj tylko formę z CRC, a każde archiwum zapisane w trybie strumieniowym odrzuci poprawne hasło. Drugi to pole dodatkowe ZIP64: jego trzy pola 64-bitowe pojawiają się w stałej kolejności, ale są zapisywane tylko wtedy, gdy odpowiadające pole 32-bitowe jest przesycone, więc czytanie ich pod stałymi offsetami działa na archiwach, które przetestowano, i zawodzi na następnym. Parsuj je pozycyjnie względem tego, które pola 32-bitowe są przesycone
Jedna wygoda warta znajomości: strumień dekompresji Free Pascala przyjmuje drugi argument konstruktora, który pomija nagłówek zlib, i to jest dokładnie to, czego potrzebują wpisy ZIP, bo przechowują surowy deflate. Ta ścieżka w ogóle nie dotyka bibliotecznego shimu zlib, więc brakujący backend C jej nie dotyczy
Przezroczystość kolorowych glifów pod LCL
Odczyt kanału alfa zrastrowanego kolorowego glifu to jedyny szczegół graficzny bez bezpośredniego tłumaczenia. Klasa PNG w LCL nie ma akcesora linii skanowania wystawiającego alfę, a przypisanie PNG do bitmapy ją wyrzuca, więc kolorowy emoji przychodzi w pełni nieprzezroczysty i komponuje się z czarnym prostokątem za sobą. Działająca droga to obraz interfejsowy: utwórz go z PNG, potem czytaj piksele przez akcesor koloru, pamiętając, że jego składowe są 16-bitowe i wymagają przesunięcia w dół o osiem, by stać się bajtami. Ta powierzchnia używa też naturalnej kolejności wierszy z góry na dół, więc inwersja Height - 1 - Y, której wymaga kod linii skanowania VCL, musi zostać usunięta, a nie przeniesiona
Dwie uwagi systemu budowania, zanim zgłosisz błąd
Pełna przebudowa sporadycznie zawodzi z niezdefiniowanym symbolem, którego nazwa kończy się sufiksem $crc i wartością szesnastkową. Ten sufiks jest liczony z typów parametrów i przestaje się zgadzać, gdy jeden build kompiluje jednostkę względem dwóch różnych wersji interfejsu w tym samym przebiegu. Ponowne uruchomienie builda to czyści; sygnatura nie jest zła
Po drugie, Free Pascal 3.2.2 nie ma metod anonimowych, więc wszędzie tam, gdzie biblioteka używała domknięć do spięcia równoległego potoku, build Free Pascala bierze zamiast tego deterministyczny serialowy fallback. Wyjście jest identyczne, przepustowość nie; jeśli zależysz na równoległym renderowaniu stron, to powód, by na razie zostać przy Delphi, a projekt potoku opisuje artykuł o równoległym potoku renderowania. Sytuacja kodeków obrazów to drugie miejsce, gdzie wybór toolchaina zmienia możliwości, a nie tylko szybkość, więc wdrożenie Lazarusa powinno zaplanować swoje formaty obrazów odpowiednio; aktualna macierz per toolchain jest na stronie produktu HotPDF Delphi PDF component