Artikel Teknis

Menautkan jbig2enc secara statis ke Free Pascal tanpa DLL

PDFlibPas 3.538.0 menautkan encoder JBIG2 eksternal secara statis ke program Free Pascal dan Lazarus. Proyek cukup menambahkan unit PDFlibJBIG2EncC, unit yang sudah digunakan Delphi dan C++Builder, lalu encoder berada di dalam executable tanpa file tambahan yang perlu dikirim bersamanya. Ini membalik kesimpulan sebelumnya tentang fitur ini, yaitu bahwa Free Pascal hanya dapat mengakses encoder eksternal melalui DLL

Mengapa DLL terlihat sebagai satu-satunya pilihan?

DLL terlihat sebagai satu-satunya pilihan karena tiga jalur linking gagal dengan tiga cara yang tidak saling berkaitan, dan tidak ada switch compiler yang dapat menjangkau salah satunya. Linker internal langsung menolak section COMDAT asosiatif. External link melalui binutils bawaan crash di dalam garbage collection section, yang selalu diteruskan Free Pascal pada target Windows 64-bit. Binutils yang lebih baru sama sekali tidak dapat memproses link script Free Pascal. Membangun ulang sisi C++ dengan toolchain lain hanya menukar satu penolakan dengan penolakan lain, karena instansiasi template dan inline secara bawaan menghasilkan weak external symbol dan Free Pascal melaporkannya sebagai Unsupported COFF symbol type 105. Tidak ada bukti itu yang keliru, dan uraian sebelumnya tentang backend encoder JBIG2 dan linker Free Pascal menelusuri setiap jalan buntu dalam bentuk yang masih dapat direproduksi sampai sekarang. Yang keliru adalah asumsi tentang tempat perbaikan dapat diletakkan. Setiap upaya melewati compiler atau linker, padahal keduanya tidak dapat mengubah isi object file yang sudah ada. Sejak awal, object file-lah masalahnya. ObjConv membaca COFF dan menulis COFF, dan setiap konstruksi yang membuat Free Pascal tersandung memiliki padanan mekanis yang dapat diterimanya

Error yang tidak pernah menyebut penyebabnya

Linker internal Free Pascal hanya mengimplementasikan pick-any COMDAT setengah jalan, dan implementasi setengah matang itu adalah bagian paling sulit didiagnosis di sini. Linker memang melakukan folding pada definisi duplikat, sesuai maksud formatnya. Namun TExeOutput.RemoveUnreferencedSections mengarahkan melalui exesymbol ke definisi pemenang ketika menandai section sebagai terpakai, sedangkan TCoffexeoutput.DoRelocationFixup membaca objreloc.symbol.objsection secara langsung. Ketika sebuah section yang terpakai mereferensikan symbol yang didefinisikan object-nya sendiri pada salinan yang kalah dalam folding, kedua pass melihat section yang berbeda, dan link berhenti dengan Internal error 200603061

Bandingkan dengan dua batasan di kedua sisi masalah ini. Unsupported COFF symbol type 105 berarti weak external. Associative or exact match COMDAT sections are not yet supported berarti COMDAT asosiatif dan bahkan menyebut symbol yang bermasalah. Internal error 200603061 tidak mengatakan apa pun: tidak ada nama symbol, nama section, nama file, atau fase. Ini juga merupakan kasus normal, bukan kasus pinggiran, karena MSVC menempatkan setiap string literal dan setiap instansiasi inline atau template ke dalam pick-any COMDAT, dan pada 186 object dalam set encoder ini linker melakukan 2656 folding. Build dengan /Gy- memang menjaga fungsi biasa tetap berada di luar section COMDAT per fungsi, tetapi string literal dan instansiasi template tetap berada di tempat semula

Mengapa stub symbol CRT selalu terlihat sebagai penyebab kerusakan terakhir?

Karena linker baru mencapai pass fixup setelah semua symbol berhasil di-resolve. Selama masih ada yang hilang, proses berhenti lebih awal dengan Undefined symbol dan masalah COMDAT tidak pernah sempat muncul. Lengkapi stub C runtime terakhir, lalu linker maju satu fase dan langsung masuk ke internal error 200603061. Karena itu gejala di lapangan secara sistematis menyesatkan: ketika body Pascal untuk symbol C yang direferensikan ditambahkan satu per satu, selalu terlihat bahwa penambahan paling baru merusak build, atau bahwa ambang sekitar seratus stub baru saja terlewati. Keduanya tidak benar. Symbol mana yang ditambahkan terakhir dan berapa jumlah totalnya sama-sama tidak relevan, karena kegagalan sudah tersembunyi sejak object pertama dan baru dapat dijangkau setelah resolusi berhasil. Ketika keluhan linker berubah setelah Anda memperbaiki sesuatu yang tidak berkaitan, pertimbangkan apakah Anda memajukan fase, bukan menyebabkan regresi

Perbaikannya adalah satu pass ObjConv, bukan compiler flag

Seluruh perbaikannya adalah satu perintah post-processing yang dijalankan pada setiap object hasil kompilasi: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tiga opsi ditambahkan khusus untuk pekerjaan ini. -xw menyelesaikan symbol IMAGE_SYM_CLASS_WEAK_EXTERNAL menjadi external biasa. -xn menormalkan symbol IMAGE_SYM_CLASS_NULL seperti _fltused, yang dilaporkan Free Pascal sebagai Unsupported COFF symbol type 0. -xc melakukan pekerjaan utama: menurunkan setiap section COMDAT menjadi section biasa dan membuat symbol yang didefinisikannya bersifat static. Ini menghilangkan kegagalan dengan menghilangkan keputusan yang menyebabkannya, karena tanpa section COMDAT tidak ada folding, tidak ada salinan pemenang yang harus dijadikan tujuan oleh satu pass tetapi terlewat oleh pass lain, dan section unwind asosiatif .pdata serta .xdata ikut dinormalkan. Biayanya nyata tetapi kecil, karena salinan yang sebenarnya dapat digabung kini masing-masing tetap bertahan

Penggantian prefix -np:__imp_:pdflibimp_ menyelesaikan collision terpisah. MSVC memanggil Win32 API yang diimpor melalui indirection cell bernama __imp_*, sedangkan Free Pascal mencadangkan prefix itu untuk mekanisme import-nya sendiri; mendefinisikan salah satu nama tersebut secara langsung memicu internal error 200603061 yang sama. Mengganti nama cell memungkinkan sisi Pascal menerbitkannya sebagai variabel biasa dan mengisinya saat runtime. Object itu sendiri dikompilasi dengan set flag static-link, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, serta codec image dimatikan, sehingga jalur file-I/O dan codec yang tidak terpakai membutuhkan jauh lebih sedikit stub khusus linking. Object ditempatkan di Lib\thirdparty\Win64f, sedangkan jalur Delphi dan C++Builder tetap menautkan set Win64x-nya sendiri tanpa perubahan; itulah hasil yang tepat untuk perbaikan portabilitas yang dibatasi pada satu toolchain

Apa yang masih harus diekspor oleh sisi Pascal?

Free Pascal menyelesaikan import object C berdasarkan nama symbol dan membutuhkan nama itu dituliskan secara eksplisit, sehingga setiap routine Pascal yang menggantikan entry point C memiliki clause public name. Delphi menggunakan nama routine sebagai nama symbol dan tidak memerlukan clause sama sekali. Itulah sebabnya satu unit dapat melayani kedua compiler dengan clause tersebut ditempatkan di bawah {$IFDEF FPC}. Jebakannya adalah deklarasi external 'msvcrt.dll' tidak memenuhi apa pun: deklarasi itu hanya membuat import, bukan definisi yang dapat di-bind oleh object yang sudah di-link. Body forwarding harus benar-benar ada

// Deklarasi external hanya membuat import. Tidak ada object ter-link yang dapat
// melakukan bind ke sana.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Body Pascal yang dipublikasikan dengan nama symbol C yang persis adalah yang
// benar-benar menjadi tujuan bind oleh set object ini.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Entry point variadic mematahkan pola itu, karena wrapper Pascal tidak dapat meneruskan varargs miliknya ke callee varargs lain. Jalan keluarnya adalah berhenti menjadi wrapper: ekspor routine naked dengan nama C, lalu lakukan tail-jump ke implementasi sebenarnya dengan register argumen dan stack persis seperti yang disiapkan caller. Layer JPEG 2000 sudah menangani snprintf dan vsnprintf dengan cara ini, yaitu melompat ke ejaan msvcrt ber-prefix underscore karena nama tanpa prefix hanya diekspor oleh UCRT. Ada satu batasan terkait yang berasal dari internal error yang sama: import cell yang sudah diganti namanya diisi dari section initialization melalui GetModuleHandleA dan GetProcAddress, bukan dari static initializer. Mengambil address routine yang diimpor di dalam initializer membuat compiler menghasilkan fixup yang tidak dapat ditanganinya dan kembali gagal dengan 200603061

function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
  external 'msvcrt.dll' name 'sprintf';

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Varargs tidak dapat diteruskan dari wrapper Pascal, sehingga symbol yang
// diekspor melakukan tail-jump dengan frame persis seperti yang disiapkan caller.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Apa yang kini berubah pada proyek Free Pascal?

Tidak ada selain nama unit di dalam clause uses, dan kini tidak ada lagi file yang harus dideploy. Backend mendaftarkan dirinya dari section initialization miliknya melalui RegisterJBIG2EncoderBackend, dan caller memintanya persis seperti sebelumnya: melalui options bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER yang bernilai 4, atau melalui argumen UseExternalEncoder pada extended entry point. Permintaan ini tetap merupakan preferensi, bukan jaminan, karena build yang tidak menyertakan unit tersebut akan diam-diam kembali ke encoder MMR Pascal native dan menghasilkan file yang lebih besar, bukan error

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder, dan mulai 3.538.0 Free Pascal

var
  Pdf: TPDFlib;
  Scan: TStream;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
    try
      // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
      // BlackDotSize, LossyLevel
      ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
    finally
      Scan.Free;
    end;
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Dua batasan layak dinyatakan dengan jelas. Hanya tersedia set object Win64, sehingga pada target Free Pascal lain entry point external encode mengembalikan kegagalan dan encoder Pascal native mengambil alih pekerjaan. Selain itu, regresi yang menjadi gate untuk semua ini adalah perbandingan render, bukan pemeriksaan ukuran: kedua encoder bersifat lossless pada sumber yang sama, sehingga output dirender dan dibandingkan byte demi byte, dan suite Lazarus lulus 26 dari 26 test termasuk test ini. Membandingkan ukuran stream terkompresi tidak akan membuktikan apa pun, karena halaman terbalik dapat terkompresi dengan ukuran yang kira-kira sama seperti halaman yang benar

Pelajaran yang lebih luas berlaku di luar JBIG2. DLL adalah bentuk yang tepat ketika batasnya benar-benar dinamis, seperti yang dilayani oleh permukaan integrasi DLL, ActiveX, dan dylib; bentuk itu keliru ketika hanya menjadi workaround untuk COFF reader, karena menambahkan file ke setiap installer, search path ke setiap deployment, serta failure mode akibat perbedaan versi yang tidak mungkin muncul pada static linking. Bagian upstream juga penting, karena cara gambar biner dihasilkan lebih menentukan ukuran akhir daripada encoder-nya, dan rendering monokrom berbasis region di Delphi membahas separuh pipeline tersebut. Cakupan toolchain, set object per compiler, dan target yang didukung tercantum di halaman produk losLab PDF Developer Library