Berikut sebuah masalah yang muncul begitu sebuah pustaka PDF meninggalkan bahasa asalnya. Anda punya binding yang bekerja sempurna dari C# di Windows. Anda membutuhkan pemanggilan yang sama dari Python di macOS, jadi Anda menyalin file deklarasi Windows, mengganti nama binary-nya, dan menjalankannya. Setiap symbol resolve dengan baik. Pemanggilan pertama mengembalikan data sampah, yang kedua crash dengan access violation, dan tidak satu pun kode PDF Anda yang berubah. Kesalahannya berada satu lapis di bawah PDF: export Windows menggunakan calling convention Stdcall, dylib macOS meng-export fungsi yang sama sebagai Cdecl dengan underscore di depan, dan sebuah deklarasi foreign-function yang salah pada salah satu detail ini akan merusak stack sebelum satu dokumen pun sempat dibuka
Seluruh kelas kegagalan ini berasal dari satu keputusan desain yang layak dipahami sejak awal. PDF Library for Delphi, PDF engine source-available milik losLab untuk Delphi dan C++Builder, membungkus seluruh object model-nya dalam satu flat facade class, TPDFlib, lalu mengirimkan facade itu dalam tiga bentuk binary: sebuah Windows DLL dengan kurang lebih 1.250 fungsi yang di-export, sebuah automation object COM/ActiveX, dan sebuah dylib macOS. Semantik PDF-nya identik di ketiganya. Bagian yang menggigit Anda ada di ABI di bawahnya: calling convention, encoding string, kepemilikan handle, dan pihak mana yang diizinkan membebaskan buffer yang mana
Satu facade, tiga bentuk binary
Setiap fungsi publik dari TPDFlib memiliki pasangan flat yang diberi nama DL ditambah nama method-nya. LoadFromFile menjadi DLLoadFromFile, Encrypt menjadi DLEncrypt, NewSignProcessFromFile menjadi DLNewSignProcessFromFile. Parameter pertama dari hampir setiap export adalah sebuah InstanceID yang dikembalikan oleh DLCreateLibrary, yang menggantikan posisi object reference yang biasanya dipegang oleh caller Delphi. Internalisasikan pemetaan ini sejak awal. Artinya, referensi API Delphi sekaligus berfungsi sebagai dokumentasi untuk setiap bahasa lain: apa pun yang bisa dilakukan class-nya, DLL-nya juga bisa lakukan dengan nama yang bisa diprediksi, dan Anda bisa membaca signature method Pascal untuk mempelajari pemanggilan yang Anda butuhkan dari Python atau C#
Build Windows menghasilkan PDFlibDLL32.dll dan PDFlibDLL64.dll; pilih yang cocok dengan bitness proses host Anda, karena proses Java atau .NET 64-bit tidak bisa memuat pustaka 32-bit, sebagus apa pun deklarasinya terlihat
Windows: instance Stdcall dan pasangan fungsi W/A
Setiap export yang menerima string ada dalam dua versi. Versi wide menerima PWideChar (UTF-16, yang cocok secara alami untuk .NET, Java, dan c_wchar_p milik Python), dan versi dengan akhiran A menerima PAnsiChar. Keduanya membawa semantik yang identik dan hanya berbeda pada encoding, dan justru itulah yang membuat pencampuran keduanya begitu menyakitkan untuk dilacak: tidak ada yang throw, tidak ada yang mengembalikan kode error, Anda hanya akan mendapati mojibake di metadata atau sebuah "file tidak ditemukan" yang keliru untuk path mana pun yang memuat karakter di luar ASCII biasa. Bug encoding pertama yang ditemui sebuah tim dengan cara ini biasanya menghabiskan waktu satu sore, karena gejalanya menunjuk ke data padahal penyebabnya ada di deklarasi
// Binding Windows (PDFlibDLL64.dll): Stdcall, nama export polos
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// Binding macOS: fungsi yang sama, Cdecl, dan prefix underscore pada export-nya
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Pilih satu lebar karakter per host dan bakukan itu di dalam binding generator Anda. Sebuah aturan praktis: jika bahasa host memiliki string UTF-16 native, bind versi W di mana-mana dan jangan pernah menyentuh keluarga A lagi
macOS: nama sama, ABI berbeda
Dylib meng-export set fungsi DL yang sama dengan dua perubahan sistematis. Calling convention-nya adalah Cdecl, bukan Stdcall, dan setiap nama export membawa underscore di depan (_DLCreateLibrary, _DLLoadFromFile, dan seterusnya). Kedua perubahan ini murni mekanis, yang membuat keduanya ideal untuk sebuah binding yang di-generate dan berbahaya untuk salinan file Windows yang diedit manual. Simpan satu daftar fungsi kanonis dan hasilkan deklarasi per-platform darinya, jika tooling Anda memungkinkan. Lewati itu dan Anda akan mendapati stack corruption yang persis seperti yang dijelaskan di awal halaman ini, yang hanya muncul kembali pada platform yang paling jarang dijalankan CI Anda
Host COM dan ActiveX: Safecall dan payload Olevariant
Untuk VB.NET, C#, VBScript, dan host automation lama, build OCX membungkus facade yang sama ke dalam sebuah automation object IDispatch, IPDFlibrary, dengan setiap method dideklarasikan sebagai Safecall. Konvensi itu mengubah cara error sampai ke Anda. Safecall menerjemahkan kegagalan internal menjadi sebuah HRESULT COM, sehingga caller C# menangkap sebuah exception, padahal DLL flat akan mengembalikan sebuah integer diam-diam yang harus diingat caller untuk diperiksa sendiri. Operasi yang sama, dua idiom kegagalan yang berbeda, tergantung binary mana yang Anda muat
Data biner mengikuti aturan kedua yang spesifik untuk COM. Automation interface-nya sama sekali tidak memiliki parameter pointer. Apa pun yang bersifat biner, byte gambar yang masuk atau byte PDF yang keluar, menyeberangi batas itu sebagai sebuah Olevariant lewat method seperti AddImageFromVariant dan AppendToVariant. Marshaling sebuah byte array ke dalam variant hanya satu baris di .NET. Coba serahkan raw pointer sebagai gantinya, dengan alasan toh masih dalam proses yang sama, dan dispatch layer-nya akan menolak atau merusak pemanggilan itu. Satu detail registrasi lagi yang menjegal deployment: registrasi COM bersifat per-bitness, sehingga sebuah OCX yang didaftarkan dengan regsvr32 32-bit tidak terlihat oleh host 64-bit. Ketidakcocokan itu muncul sebagai "class not registered" yang terkenal tidak membantu di mesin pelanggan, lama setelah file itu meninggalkan mesin Anda
Disiplin handle: instance memiliki dokumen
API flat berjalan di atas integer handle. DLCreateLibrary mengembalikan sebuah instance. Memuat sebuah file mengembalikan sebuah document ID di dalam instance itu. Sign process, string list, dan direct-access file masing-masing mengembalikan integer handle-nya sendiri, semuanya scoped ke instance yang sama. Lifecycle-nya terlihat sama dari FFI host mana pun, ditunjukkan di sini dalam Pascal karena lebih mudah dibaca:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // satu instance per worker thread
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // mengembalikan sebuah DocumentID, 0 jika gagal
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // membebaskan setiap dokumen yang dimiliki instance ini
end;
end;
Dua hal mengikuti dari ownership tree tersebut. DLReleaseLibrary adalah satu-satunya cleanup call yang benar-benar Anda perlukan, karena ia meruntuhkan setiap document dan process handle di bawah instance tersebut dalam satu langkah. Dalam sebuah skrip pendek, itu sudah cukup. Dalam sebuah service yang berjalan lama, itu menjadi kebocoran lambat dengan seremoni tambahan, jadi lepaskan dokumen begitu Anda selesai dengannya, alih-alih membiarkannya menumpuk sampai instance-nya mati. Instance ini juga merupakan unit alami untuk isolasi thread. Berikan setiap worker thread InstanceID-nya sendiri, dan jangan pernah membagikan satu instance ke banyak thread tanpa locking eksternal, dengan alasan yang sama seperti Anda tidak akan pernah membagikan satu object TPDFlib di antara beberapa thread
String yang dikembalikan itu dipinjam, bukan dimiliki
Fungsi yang mengembalikan teks, seperti DLGetPageText, menyerahkan kembali sebuah PWideChar atau PAnsiChar yang menunjuk ke sebuah buffer yang dimiliki dan didaur ulang oleh instance pustaka. Kontraknya adalah: salin segera, jangan pernah dibebaskan
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer ke dalam buffer milik pustaka
PageText := P; // salin sekarang; pemanggilan berikutnya bisa memakai ulang buffer ini
end;
Di C# itu berarti melakukan marshaling IntPtr ke sebuah managed string sebelum pemanggilan pustaka berikutnya. Di Python ctypes, itu berarti memotong wide string dari pointer-nya segera. Pegang raw pointer itu melewati beberapa pemanggilan dan Anda sudah menulis sebuah bug yang lolos dari setiap unit test lalu gagal pada saat pertama kali dua request saling tumpang tindih di production, karena pemanggilan kedua mendaur ulang buffer yang masih dibaca oleh pemanggilan pertama. Aturan kepemilikan yang sama berlaku dalam arah sebaliknya untuk callback yang didaftarkan lewat DLSetProgressCallback. Pointer apa pun yang diserahkan pustaka ke dalam callback Anda hanya valid selama body callback itu berjalan, dan objek callback itu sendiri harus tetap hidup (pinned, pada host yang garbage-collected) selama instance-nya masih mungkin memanggilnya. Sebuah delegate yang di-collect di tengah pekerjaan adalah sumber klasik dari access violation "acak" yang muncul pada sebuah binding .NET yang sudah berjalan bersih selama berbulan-bulan
Bangun sebuah smoke test ke dalam binding itu sendiri, dan jalankan sebelum set deklarasi yang di-generate mana pun dirilis. Jalankan satu pemanggilan dari setiap kategori yang cenderung mengungkap kesalahan ABI: sebuah fungsi tanpa parameter seperti DLCreateLibrary untuk membuktikan convention-nya benar, sebuah fungsi string-in yang diberi sebuah path dengan karakter non-ASCII untuk membuktikan encoding-nya benar, sebuah fungsi string-out untuk membuktikan penanganan borrowed-buffer-nya benar, dan satu operasi yang sengaja dibuat gagal supaya Anda bisa melihat bagaimana sebuah error sampai ke host Anda. Itu hanya lima belas menit kerja, dan itu menangkap kesalahan calling-convention serta encoding yang kalau tidak akan muncul berbulan-bulan kemudian sebagai crash dump pelanggan
Kasus Python ctypes, secara konkret
Python ctypes adalah binding yang paling sering saya lihat dibuat manual, dan ia membuat pemisahan cross-platform ini mudah didemonstrasikan. Di Windows, muat pustaka dengan ctypes.WinDLL supaya ctypes menerapkan Stdcall, bind fungsi W tanpa akhiran, dan deklarasikan setiap parameter string sebagai c_wchar_p. Di macOS, muat dengan ctypes.CDLL untuk Cdecl, pertahankan daftar fungsi yang identik, dan resolve nama-namanya tanpa underscore di depan. Sebagian besar FFI layer, termasuk ctypes, sudah melipat kembali konvensi underscore itu untuk Anda di macOS, tetapi itulah satu asumsi yang perlu Anda konfirmasi dengan satu pemanggilan yang sudah ter-resolve sebelum Anda men-generate ratusan deklarasi di atasnya
Dua pertanyaan deployment mengekor pekerjaan binding ini dan punya jawaban yang tegas. DLL biasa tidak butuh registrasi: regsvr32 hanya berlaku untuk build ActiveX, dan DLL-nya dikirim lewat penyalinan file, yang menjadi alasan utama untuk memilihnya pada Windows service dan container di mana Anda lebih baik tidak menyentuh registry sama sekali. Thread safety-nya berujung pada aturan yang sudah berlaku di atas, satu instance per thread. Instance handle-nya menyimpan setiap bagian mutable state yang dilacak engine ini, dokumen yang dipilih, render option, pengaturan ekstraksi, sehingga dua thread yang berbagi satu instance akan saling menyelipkan state satu sama lain bahkan ketika setiap pemanggilan individual mengembalikan sukses
Begitu sebuah binding sudah solid, operasi-operasi di sisi seberangnya persis sama dengan yang dibahas secara mendalam oleh artikel-artikel Delphi, termasuk menerapkan dan mengaudit enkripsi PDF dan mengekstrak teks dan gambar dari dokumen yang sudah ada
Unduhan binary untuk ketiga lapisan integrasi ini dikirim bersama pustakanya; lihat halaman produk PDF Library for Delphi untuk edisi dan lisensi