Free Pascal na Win32 automatycznie dodaje wiodący podkreślnik do każdego importu cdecl; external, podczas gdy public name eksportuje napisany przez ciebie ciąg znak po znaku. HotPDF musi zadowolić obie konwencje w tym samym drzewie źródeł, bo build Delphi już teraz wozi deklaracje importów, które piszą podkreślnik ręcznie. Pomyłka w tej asymetrii daje błędy linkowania wskazujące symbol, którego nikt nie napisał
Rozszerzanie biblioteki Delphi na Free Pascala zwykle opisuje się jako problem przenośności i na Win64 w większości nim jest. Win32 jest inny. ABI 32-bitowego x86 Windows niesie trzydzieści lat skumulowanych konwencji o tym, jak pisze się symbole C, kto sprząta stos i jakich prywatnych helperów kompilatora jednostka translacji może zakładać, a każde z tych miejsc to punkt, w którym dwa kompilatory Pascala zgodne co do języka mogą się wciąż różnić co do pliku obiektowego
Dlaczego ten sam symbol rozwiązuje się na Win64 i wywala na Win32?
Bo prefiks podkreślnika to konwencja 32-bitowa, którą Free Pascal stosuje do importów, ale nie do eksportów. Zadeklaruj function deflate(...): Integer; cdecl; external;, a FPC szuka w pliku obiektowym _deflate na Win32 i deflate na Win64. To poprawne zachowanie, zgodne z tym, co emituje kompilator C. Pułapka siedzi po drugiej stronie mostu: rutyna oznaczona public name 'deflate' eksportuje na obu targetach dokładnie deflate, bez żadnego prefiksu
Dorzuć teraz historyczny detal, który robi z tego konkretny problem. Build Delphi już deklaruje część tych punktów wejścia z podkreślnikiem wpisanym w nazwę, bo dokładnie to zawierają jego własne pliki obiektowe. Podaj tę samą deklarację FPC na Win32, a kompilator gorliwie doda prefiks drugi raz, więc linker szuka __deflate, symbolu, którego nic nie eksportuje. Intuicyjna naprawa, czyli dodanie podkreślnika wszędzie, łamie importy, które były już zapisane poprawnie
Działa para stałych prefiksowych zamiast jednej. HPDFFPCZLib i HPDFFPCCodecStubs używają jednego prefiksu dla zwykłych importów z C i drugiego dla importów, które już niosą prefiks po stronie Delphi, a na Win64 obie stałe są puste, więc istniejące nazwy linkowania przetrwają nietknięte. Dwie stałe zamiast jednej to cała naprawa, a staje się oczywista dopiero, gdy oddzielisz regułę importową od eksportowej
// Dwa prefiksy, nie jeden: zwykłe importy z C i importy już niosące
// ręcznie wpisany prefiks Delphi dekorują się inaczej pod FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
CPrefix = '_'; // to sam dodaje FPC dla cdecl external
DelphiCName = ''; // podkreślnik już wpisany w źródle
{$ELSE}
CPrefix = '';
DelphiCName = '';
{$IFEND}
// Strona eksportowa: public name jest dosłowny na każdym targecie
procedure hpdf_codec_free(P: Pointer); cdecl;
public name 'hpdf_codec_free';
WIN32 mówi ci architekturę, nie ABI
To błąd kompilacji warunkowej z najdłuższym ogonem debugowania i warto go wypowiedzieć wprost: WIN32 i WIN64 opisują architekturę targetu i nie mówią nic o tym, jakie prywatne helpery runtime kompilatora istnieją. Free Pascal definiuje oba symbole na odpowiednich targetach Windows, dokładnie jak Delphi. Straż napisana jako {$IFDEF WIN32} wokół kodu wołającego helper runtime Delphi kompiluje się więc pod FPC i wywala dopiero przy linkowaniu
Konkretnie, w tę pułapkę wpadają trzy rodziny kodu. Trampoliny 64-bitowych liczb całkowitych Delphi dochodzone przez helpery System.@_ll, asemblerowe rutyny wsparcia MSVC dla Win32 i idące z nimi sloty importów — wszystko to istnieje dla prekompilowanych obiektów C, które linkuje build Delphi. Free Pascal tych obiektów nie linkuje, więc nie potrzebuje żadnej z tej maszynerii i każda referencja do niej musi zniknąć. Subtelność polega na tym, że deklarację i implementację trzeba wykluczyć razem. Wykluczysz tylko jedno, a kompilator zgłosi coś niepomocnego o identyfikatorze, którego nie umie niczemu dopasować
Wypadająca z tego reguła jest krótka. Straż po kompilatorze, gdy pytanie dotyczy ABI albo wsparcia runtime, straż po architekturze, gdy dotyczy szerokości wskaźnika albo liczby rejestrów, i nigdy nie pozwól, by jedno stało za drugie
Straż na deklaracjach i implementacjach razem
W blok warunkowy w sekcji interface łatwo wpaść niepostrzeżenie, a powstały komunikat błędu wskazuje wszędzie, tylko nie na przyczynę. Dodajesz deklarację metody do interfejsu klasy i naturalne miejsce na nią jest obok pokrewnych metod, co jest w porządku do momentu, w którym okaże się, że ci sąsiedzi siedzą w istniejącym bloku {$IFDEF}. Dyrektywy warunkowe nie mają wcięć, więc blok otwarty czterdzieści linii wyżej jest praktycznie niewidoczny, gdy czytasz okoliczne deklaracje
Dalej dzieje się kompilacja, która na jednym toolchainie przechodzi, a na drugim produkuje kaskadę. Jeśli otaczająca straż to sprawdzenie wersji Delphi, którego Free Pascal nie spełnia, deklaracja znika dla FPC, podczas gdy bezwarunkowa implementacja zostaje, i kompilator zgłasza długą listę pretensji o identyfikatory metod, których się spodziewał i nie znalazł. Żaden z komunikatów nie wspomni o bloku warunkowym, który to spowodował
Dwa nawyki zapobiegają całej tej klasie awarii. Przed wstawieniem czegokolwiek do sekcji interface spójrz w górę za najbliższym otwartym warunkiem, zamiast ufać wizyjnemu pogrupowaniu. I traktuj zieloną suitę testów Delphi jako dowód wyłącznie o Delphi: build biblioteki pod Free Pascalem to osobna bramka, a jedyny sposób, by wiedzieć, że przechodzi, to odpalenie build-Win32-Lib-FPC.cmd i build-Win64-Lib-FPC.cmd w ramach tej samej zmiany
Co się łamie w kodzie arytmetyki 32-bitowej
Jedno ograniczenie językowe wychodzi w dokładnie tym kodzie, który najmniej chce się zmieniać: 32-bitowy Free Pascal nie przyjmie UInt64 jako zmiennej sterującej pętli for. W unitach krzywych eliptycznych niosących X25519 i X448 pętle przechodzące po tablicach limbów były napisane z licznikami 64-bitowymi po prostu dlatego, że wszystko inne w pliku jest 64-bitowe
Naprawa musi być chirurgiczna, bo w arytmetyce ciała szerokość zmiennej jest częścią argumentu o poprawności. Indeksy pętli stają się Integer, skoro tablica limbów ma garść elementów i żaden indeks nie zbliża się do zakresu 32-bitowego. Wszystko, co bierze udział w arytmetyce — limb same w sobie, propagacja przeniesień i maski — zostaje UInt64, bo zawężenie czegokolwiek z tego po cichu zmienia wynik modulo pierwsze ciała
// 32-bitowy FPC odrzuca zmienną pętli UInt64. Zawęź tylko indeks; limb,
// maski i przeniesienia trzymają szerokość, inaczej matematyka się zmienia
var
I: Integer; // wcześniej UInt64
Carry, Mask: UInt64;
begin
Carry := 0;
for I := 0 to High(Limbs) do
begin
Limbs[I] := Limbs[I] + Carry;
Carry := Limbs[I] shr 51;
Limbs[I] := Limbs[I] and Mask;
end;
end;
Weryfikacja takiej zmiany nie może być testem rundy w obie strony. Szyfrowanie i deszyfrowanie tą samą zepsutą implementacją zgadza się ze sobą doskonale, dlatego wektory known-answer nie podlegają tu negocjacji: odpal opublikowane wektory testowe X25519 i X448 i porównaj dokładne bajty wyjścia. To jedyna kontrola, która odróżnia poprawną implementację od samospójnej, ale złej, i w równym stopniu dotyczy prymitywów symetrycznych omówionych w artykule granicach kodeków deflate i AES pod Free Pascalem
Ile wart jest build Win32 pod Free Pascalem
Praktyczna korzyść: aplikacja Lazarus targetująca 32-bitowe Windows dostaje ten sam silnik dokumentów co jej odpowiedniczka w Delphi, bez osobnego binarnego kontraktu do utrzymywania. Najbardziej liczy się to na wdrożeniach, o których rzadko się mówi: kontrolerach przemysłowych, terminalach punktów sprzedaży i długowiecznym oprogramowaniu biznesowym, gdzie runtime 32-bitowy to nie relikt, tylko ograniczenie sprzętowe
Historia Win64 była pierwsza i jest opisana w artykule wsparcie Free Pascala i Lazarusa na Win64. Win32 nie jest jej powtórką. Win64 ma jedną konwencję wywołań, zero dekoracji nazw i żadnych prywatnych helperów całkowitoliczbowych Delphi do obchodzenia, więc prawie wszystko w tym artykule jest specyficzne dla targetu 32-bitowego. Unity arytmetyczne, które wymusiły zmianę zmiennej pętli, to te same, które opisuje arytmetyka Montgomery'ego na krzywych NIST, gdzie dyscyplina szerokości jest wytłumaczona głębiej
Ogólny wniosek: praca nad przenośnością międzykompilatorową nie jest przede wszystkim o funkcjach języka. Oba kompilatory przyjmują tu ten sam Object Pascal. Różni się plik obiektowy: jak zapisuje się symbole, jakich rutyn pomocniczych zakłada się, że dostarczy runtime i które prekompilowane obiekty są w linku. HotPDF dostarcza pakiety Free Pascala i Lazarusa obok pakietów Delphi i C++Builder w komponencie PDF HotPDF dla Delphi, więc to samo drzewo źródeł karmi każdy toolchain, zamiast się rozdwajać per kompilator