Free Pascal trên Win32 tự thêm underscore phía trước mọi import cdecl; external, trong khi public name export đúng chuỗi bạn viết, từng ký tự một. HotPDF phải thỏa mãn cả hai quy ước trong cùng một source tree, vì bản build Delphi đã kèm sẵn các import declaration ghi tay underscore trong tên. Làm sai sự bất đối xứng này cho ra link error chỉ tên một symbol mà chẳng ai viết
Việc mở rộng một library Delphi sang Free Pascal thường được kể như một bài toán portability, và trên Win64 thì đại khái đúng vậy. Win32 thì khác. ABI của Windows x86 32-bit mang ba mươi năm quy ước tích lũy về cách C symbol được viết, ai dọn stack, và một translation unit được phép mặc định những compiler-private helper nào — và mỗi điểm đó là một chỗ hai compiler Pascal đồng thuận về ngôn ngữ vẫn có thể bất đồng về object file
Vì sao cùng một symbol resolve được trên Win64 mà fail trên Win32?
Vì prefix underscore là quy ước 32-bit mà Free Pascal áp cho import nhưng không áp cho export. Khai báo function deflate(...): Integer; cdecl; external; và FPC tìm _deflate trong object file trên Win32, còn trên Win64 thì tìm deflate. Đó là hành vi đúng và khớp với cái C compiler emit. Cái bẫy nằm ở đầu bên kia của cây cầu: một routine đánh dấu public name 'deflate' export đúng literally deflate trên cả hai target, không thêm prefix nào
Giờ thêm chi tiết lịch sử khiến mọi chuyện cụ thể hẳn ra. Bản build Delphi đã khai báo một số entry point với underscore ghi sẵn trong tên, vì đó là cái nằm trong object file của chính nó. Đưa cùng declaration đó cho FPC trên Win32 và compiler tận tụy thêm prefix lần nữa, để linker đi tìm __deflate — một symbol không ai export. Fix trực giác là thêm một underscore ở khắp nơi, và nó phá luôn những import vốn đã được viết đúng
Cái chạy được là một cặp prefix constant thay vì một cái. HPDFFPCZLib và HPDFFPCCodecStubs dùng một prefix cho C import thường và một prefix khác cho import đã mang sẵn prefix phía Delphi, còn trên Win64 cả hai constant đều rỗng nên các link name hiện có giữ nguyên vẹn. Hai constant thay vì một chính là toàn bộ cái fix, và nó chỉ hiển nhiên sau khi bạn tách được import rule khỏi export rule
// Hai prefix chứ không phải một: C import thường và import đã mang sẵn
// prefix Delphi viết tay decorate khác nhau dưới FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
CPrefix = '_'; // FPC tự thêm cái này cho cdecl external
DelphiCName = ''; // đã ghi sẵn underscore trong source
{$ELSE}
CPrefix = '';
DelphiCName = '';
{$IFEND}
// Export side: 'public name' giữ nguyên văn trên mọi target
procedure hpdf_codec_free(P: Pointer); cdecl;
public name 'hpdf_codec_free';
WIN32 cho biết architecture, không cho biết ABI
Đây là lỗi conditional compilation có đuôi debug dài nhất, và đáng nói thẳng: WIN32 và WIN64 mô tả target architecture và chẳng nói gì về việc tồn tại những compiler-private runtime helper nào. Free Pascal định nghĩa cả hai symbol trên các Windows target tương ứng, y như Delphi. Một guard viết {$IFDEF WIN32} quanh code gọi một Delphi runtime helper do đó compile được dưới FPC và fail lúc link
Cụ thể, ba họ code rơi vào cái bẫy này. Các Delphi 64-bit integer trampoline với tới qua helper System.@_ll, các MSVC Win32 assembly support routine, và các import slot đi kèm — tất cả tồn tại để phục vụ những C object precompiled mà bản build Delphi link. Free Pascal không link những object đó, nên nó không cần bất kỳ phần máy móc nào ở trên, và mọi reference tới chúng phải biến mất hoàn toàn. Điểm tinh tế là declaration và implementation phải bị loại cùng nhau. Loại một trong hai thôi và compiler báo một thứ chẳng giúp gì về một identifier nó không khớp được với bất cứ thứ gì
Rule rút ra rất ngắn. Guard theo compiler khi câu hỏi là về ABI hay runtime support, guard theo architecture khi câu hỏi là về pointer width hay số register, và đừng bao giờ để cái này đứng thay cái kia
Guard declaration và implementation cùng nhau
Một khối conditional trong interface section dễ dính phải mà không hay biết, và error message thu được chỉ tay ra mọi hướng trừ nguyên nhân. Thêm một method declaration vào class interface, chỗ đặt tự nhiên là cạnh các method liên quan, và điều đó ổn cho tới đúng khoảnh khắc những hàng xóm đó nằm trong một khối {$IFDEF} có sẵn. Conditional directive không được indent, nên một khối mở từ bốn mươi dòng phía trên về cơ bản là vô hình khi bạn đang đọc các declaration xung quanh
Chuyện xảy ra tiếp theo là một lần compile thành công trên toolchain này và tạo ra một chuỗi lỗi trên toolchain khác. Nếu guard bao quanh là một check version Delphi mà Free Pascal không thỏa, declaration biến mất với FPC trong khi implementation vô điều kiện vẫn còn, và compiler báo một danh sách dài phàn nàn về những method identifier nó mong đợi mà không tìm thấy. Không message nào nhắc tới khối conditional gây ra chuyện đó
Hai thói quen ngăn cả lớp failure này. Trước khi chèn vào interface section, nhìn ngược lên tìm conditional mở gần nhất thay vì tin vào cách nhóm trực quan. Và coi một test suite Delphi xanh lè chỉ là bằng chứng về Delphi: bản build library Free Pascal là một cổng riêng, và cách duy nhất để biết nó pass là chạy build-Win32-Lib-FPC.cmd và build-Win64-Lib-FPC.cmd như một phần của cùng một thay đổi
Cái gì gãy trong code arithmetic 32-bit
Một giới hạn ngôn ngữ xuất hiện đúng ở đoạn code ít muốn bị động tới nhất: Free Pascal 32-bit không chấp nhận UInt64 làm biến điều khiển vòng lặp for. Trong các unit đường cong elliptic mang X25519 và X448, những vòng lặp đi qua limb array được viết với counter 64-bit đơn giản vì mọi thứ khác trong file đều 64-bit
Cái fix phải nằm dao chính xác, vì trong phép toán trên field, độ rộng của một biến là một phần của lập luận đúng sai. Loop index thành Integer, vì một limb array chỉ có một nắm phần tử và không index nào chạm tới dải 32-bit. Mọi thứ tham gia vào phép tính — bản thân các limb, carry propagation và các mask — giữ nguyên UInt64, vì thu hẹp bất kỳ cái nào trong số đó sẽ âm thầm đổi kết quả modulo field prime
// FPC 32-bit từ chối biến loop kiểu UInt64. Chỉ thu hẹp index thôi;
// limb, mask và carry giữ nguyên độ rộng, nếu không phép toán field sẽ đổi
var
I: Integer; // trước đây là 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;
Phần kiểm chứng cho một thay đổi kiểu này không thể là round-trip test. Encrypt rồi decrypt bằng cùng một bản triển khai hỏng thì chúng khớp nhau hoàn hảo, đó là lý do known-answer vector ở đây không phải thứ để mặc cả: chạy các test vector X25519 và X448 đã công bố và so sánh chính xác từng output byte. Đó là phép check duy nhất phân biệt được một bản triển khai đúng với một bản triển khai sai nhưng tự nhất quán, và nó áp dụng y hệt cho các symmetric primitive nói trong ranh giới codec deflate và AES của Free Pascal
Một bản build Win32 Free Pascal đáng giá điều gì
Lợi ích thực dụng là một application Lazarus nhắm Windows 32-bit có cùng một document engine với người anh em Delphi, không cần duy trì một binary contract riêng. Điều đó quan trọng nhất với những deployment ít bị nhắc tới: industrial controller, terminal point-of-sale và phần mềm line-of-business sống lâu, nơi runtime 32-bit không phải lựa chọn legacy mà là ràng buộc phần cứng
Câu chuyện Win64 đến trước và được kể trong hỗ trợ Free Pascal và Lazarus trên Win64. Win32 không phải bản chạy lại của nó. Win64 có một calling convention, không name decoration, không Delphi-private integer helper phải né, nên gần như mọi thứ trong bài này là đặc thù của target 32-bit. Các unit arithmetic cần đổi loop variable chính là những unit nói trong Montgomery arithmetic trên các đường cong NIST, nơi kỷ luật về độ rộng được giải thích sâu hơn
Bài học chung là công việc portability đa compiler không chủ yếu nằm ở ngôn ngữ. Cả hai compiler ở đây chấp nhận cùng một Object Pascal. Khác nằm ở object file: symbol được viết thế nào, runtime được mặc định cung cấp những helper routine nào, và những object precompiled nào có mặt trong lần link. HotPDF ship các package Free Pascal và Lazarus cạnh các package Delphi và C++Builder trong HotPDF Delphi PDF component, để cùng một source tree nuôi mọi toolchain thay vì fork theo từng compiler