Bài viết kỹ thuật

Link object OMF sang COFF cho FPC Win32 trong PDFlibPas

PDFlibPas build được trên Free Pascal cho Windows 32-bit, và phần khó chưa bao giờ là Pascal. Vấn đề nằm ở các object file: object AES và OpenJPEG mà Delphi link là OMF, internal linker của Free Pascal lại đòi COFF, và convert giữa hai định dạng tạo ra những tên section cùng section-definition symbol khiến linker fail bằng internal error thay vì diagnostic

Ai đã từng link C object vào một library Pascal đều quen vùng đất này. Win64 khá lịch sự: một object format, một calling convention, không có name decoration. Win32 thì giữ nguyên mọi tầng lịch sử mà platform tích lũy, và một library statically link code C của bên thứ ba sẽ đụng tất cả những tầng đó cùng một lúc

Thư mục compiler không nói cho bạn biết target

Bắt đầu từ build entry point, vì đoán sai ở đây làm bạn mất hàng giờ trước khi bất kỳ object file nào ra đời. Tên thư mục cài Free Pascal chỉ cho biết compiler chính nằm đâu, không cho biết nó sinh ra cái gì. Một host compiler 32-bit có thể gọi cross-compiler nằm cạnh bên và emit code 64-bit khi bạn truyền đúng target switch, nên suy ra target từ path là trò đoán may rủi, đúng cho tới khi ai đó sắp xếp lại toolchain của họ

Cách đáng tin là hỏi thẳng compiler. Query target processor và operating system thật qua các information switch của compiler, và chấp nhận cả hai layout cài đặt phổ biến — thư mục binary phẳng và kiểu lồng theo version — vì installer và toolchain manager khác nhau tạo ra hình dạng khác nhau. Build script hard-code một trong hai layout chỉ chạy đúng trên đúng một máy

Vì sao object file đã convert lại làm gãy internal linker?

Vì convert giữ nguyên quy ước đặt tên section của OMF và tổng hợp section-definition symbol không khớp với cái COFF linker mong đợi. Convert object OMF sang COFF là cần nhưng chưa đủ: file kết quả mang những tên section cổ điển _TEXT, _DATA_BSS, cùng các section-definition symbol sinh ra từ chúng, và nạp thứ đó vào internal linker của Free Pascal cho ra internal compiler error thay vì một message về section naming

Internal error là failure mode tệ nhất cho một vấn đề build, vì nó chẳng nói gì về chỗ sai của input. Cách sửa là một normalization pass sau convert trên COFF file: rewrite tên section về dạng mong đợi và rewrite tương ứng các section-definition symbol cho khớp, trong khi giữ nguyên symbol index, code byte và relocation. Ràng buộc cuối chính là toàn bộ độ khó. Một bản rewrite đánh lại số symbol hay dịch offset sẽ cho ra object link được rồi crash

Có một bước sơ bộ dành cho một trong hai bộ object. Object OpenJPEG build bằng compiler C++ 32-bit cổ điển phụ thuộc vào các helper routine số nguyên 64-bit riêng của Delphi, thứ mà Free Pascal không cung cấp, nên convert format bao nhiêu cũng chẳng dùng được. Chúng được rebuild trước bằng compiler dựa trên Clang — cái không emit các dependency đó — rồi mới convert sau

Pipeline mang các static C object của PDFlibPas từ Delphi OMF trong Lib\thirdparty\Win32 sang COFF link được với FPC trong Lib\thirdparty\Win32f trên Win32: conversion từ OMF sang COFF, một normalization pass rewrite tên section và section-definition symbol trong khi giữ nguyên symbol index, code byte và relocation, cùng bản rebuild Clang cho các object OpenJPEG gọi helper 64-bit của Delphi
Convert là cần nhưng chưa đủ: nạp COFF đã đổi tên nhưng chưa normalize vào internal linker của Free Pascal là nó đáp lại bằng internal error, nên một pass sau convert sửa tên và symbol mà không đụng vào offset
// Object cho target FPC nằm trong thư mục riêng. Chúng không thay
// thế bộ object của Delphi, vì cả hai toolchain build từ cùng một
// source tree và mỗi bên cần bộ link input riêng
//
//   Lib\thirdparty\Win32   object OMF của Delphi, giữ nguyên
//   Lib\thirdparty\Win32f  object COFF cho FPC, đã convert và normalize
//
// Build entry point:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Helper riêng của compiler không portable, calling convention của chúng cũng vậy

Delphi runtime cung cấp các assembly trampoline cho phép toán số nguyên 64-bit trên x86 32-bit, và các C object precompiled build cho Delphi gọi thẳng vào đó. Free Pascal có cách bài trí riêng, nên các reference đó phải được thỏa mãn theo kiểu khác chứ không thể redirect. Chi tiết khiến redirect bất khả thi là calling convention: timing helper mà code imaging dùng có argument bốn byte do callee dọn, trong khi helper chia 64-bit dọn mười sáu byte và trả kết quả qua cặp register cổ điển. Hai helper, hai convention, và một trampoline viết cho bên này thì âm thầm corrupt stack của bên kia

Name decoration thêm nửa còn lại của vấn đề. Trên Win32, Free Pascal tự động thêm underscore phía trước external C import nhưng export khai báo public name nguyên văn, nên phía import và phía export của cùng một bridge theo hai luật khác nhau. Vì vậy C runtime bridge mà OpenJPEG cần phải export đúng tuyệt đối các tên C symbol, và các variadic entry point cần jump gián tiếp 32-bit thay vì jump trực tiếp. Không cái nào lạ lẫm khi nói ra. Cũng không cái nào fail nhẹ nhàng: tất cả đều thành link error chỉ tên một symbol mà chẳng ai viết

Cái gì khiến executable Win32 chết trước cả main?

Một DLL 64-bit nằm trên search path, dính phải vì unit zlib của Free Pascal bind động thay vì link tĩnh. Triệu chứng là exit ngay lập tức với status code invalid-image, trước khi bất kỳ dòng Pascal nào trong chương trình chạy — thứ đẩy bạn về nhìn chương trình vừa build trong khi lỗi nằm ở loader resolve một import sai architecture

Bài học nằm ở giả định chứ không ở zlib. Một unit mang tên một compression library chưa chắc có library đó bên trong; nó có thể là binding chờ shared library lúc run time, và một dynamic dependency bạn không hề định đặt là gánh nặng deployment dù nó có resolve may mắn đi nữa. Chuyển sang bản stream thuần Pascal cho cả hai target một compression path được include tĩnh, không còn external dependency nào — điều mà một library nhúng vào application của người khác lẽ ra phải có từ đầu

Bản năng đó áp dụng y hệt cho external JBIG2 encoder backend. Trên target 32-bit, external encoder không được link, nên request rơi về built-in Pascal encoder, và test kiểm chứng điều đó phải check trạng thái registration của target hiện tại chứ không coi một lần encode thành công là bằng chứng external backend đang có mặt. Một fallback chạy được chính là thứ giấu một dependency bị thiếu — failure pattern được mổ xẻ trong chẩn đoán silent stub failure. Phần static linking 64-bit được nói trong static linking jbig2enc dưới FPC

Luồng chẩn đoán cho executable Win32 exit trước cả main dưới Free Pascal: loader resolve import trong khi unit initialization chạy, binding zlib tìm thấy một DLL 64-bit trên search path, và process chết với status invalid-image trước bất kỳ câu lệnh Pascal nào, đẩy PDFlibPas sang một compression path thuần Pascal được include tĩnh
Lỗi chưa từng nằm ở chương trình vừa build: unit tên zlib là một runtime binding được resolve sai architecture, và một fallback chạy được như built-in JBIG2 encoder lại giấu một dependency bị thiếu

Phép arithmetic 32-bit trên một memory stream

Code thao tác buffer size bằng unsigned arithmetic độ rộng pointer thì đúng trên Win64 và cách một cái overflow đúng một tấm ảnh lớn trên Win32. In-memory stream nuôi codec JPEG 2000 lớn lên bằng nhân đôi và tiến tới bằng cộng, và trên target 32-bit cả hai phép tính đều có thể wrap với input lớn nhưng hoàn toàn hợp lệ

Nên mọi write, skip, seek và lần cấp phát đầu đều check trước khi tính, và trần capacity là giá trị signed pointer-width tối đa, chọn để khớp với những gì block move routine và giá trị trả về của callback diễn tả nổi. Yêu cầu hành vi khi một request bị từ chối rất dễ làm sai: refuse phải không đổi stream position lẫn length. Một lần mutation nửa chừng kèm error đẩy stream vào trạng thái caller không lý giải nổi, và phép toán kế tiếp sẽ chồng thêm lỗi

// Check trước khi tính. Trên Win32 cả hai phép tính này wrap với
// input mà một ảnh JPEG 2000 lớn tạo ra hoàn toàn hợp lệ
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // từ chối, giữ nguyên position và size

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // nhân đôi sẽ overflow
  NewCapacity := NewCapacity shl 1;
end;

Hai cái bẫy build output sống lâu hơn cả bản port

Tách test và sample executable theo target architecture vào các output directory riêng cho từng target là điều hiển nhiên đúng và ngay lập tức làm gãy mọi thứ tìm test data bằng cách đếm tầng thư mục đi lên. Cách sửa là search ngược lên tìm asset directory thay vì giả định độ sâu cố định, với một giới hạn có chủ ý: signing sample chỉ chấp nhận certificate fallback từ project directory của chính nó, không bao giờ từ một ancestor tùy tiện, vì một certificate trùng tên tìm thấy ở tầng cao hơn trong tree là bất an bảo mật chứ không phải tiện lợi

Cái bẫy thứ hai sống qua mọi đợt port và đáng mang theo vào bất kỳ project FPC nào. Sau khi nâng cấp compiler, việc compiler từ chối PPU file cũ là chưa đủ, vì linker vẫn ưu tiên object file sót lại trong unit search path kể cả khi PPU nó load ra từ đúng directory, và thêm một object output path tường minh cũng không overriding được sở thích đó. Câu trả lời duy nhất đáng tin là một temporary unit directory mới tinh cho mỗi vòng build. Thiếu một chút thôi là binary được link từ hai version compiler, fail theo kiểu trông cứ như bug nguồn

Platform conditional là mảnh cuối, và chọn đúng trục thì quan trọng hơn vẻ ngoài của nó. Câu hỏi đúng thường là code có Windows-specific hay không chứ không phải một widget library cụ thể có mặt hay không, như công việc convert metafile trong EMF vector import và platform conditional đã cho thấy: đổi guard đó từ điều kiện control-library sang điều kiện platform biến một bản rewrite tưởng như cần thiết thành thay đổi đúng một directive. Free Pascal và Lazarus hỗ trợ cả hai Windows target đi kèm PDFlibPas Delphi PDF library, build từ cùng bộ source với các package Delphi và C++Builder