Bir PDF kütüphanesi kendi ana dilinden çıktığı anda ortaya çıkan bir problem işte burada. Windows'ta C#'tan mükemmel çalışan bir bağlamanız var. Aynı çağrılara macOS'ta Python'dan ihtiyacınız var, bu yüzden Windows bildirim dosyasını kopyalıyor, ikili dosya adını değiştiriyor ve çalıştırıyorsunuz. Her sembol çözümleniyor. İlk çağrı çöp veri döndürüyor, ikincisi bir erişim ihlaliyle çöküyor ve PDF kodunuzun hiçbiri değişmedi. Hata, PDF'in bir katman altında yatıyor: Windows dışa aktarımları Stdcall çağrı kuralını kullanır, macOS dylib'i aynı fonksiyonları başında alt çizgi olan Cdecl olarak dışa aktarır ve bu ayrıntılardan birini yanlış anlayan bir yabancı fonksiyon bildirimi, tek bir belge açılmadan yığını bozar
Bu hata sınıfının tamamı, önceden anlaşılmaya değer tek bir tasarım kararından kaynaklanır. losLab'ın Delphi ve C++Builder için kaynak koduyla birlikte gelen PDF motoru PDF Library for Delphi, tüm nesne modelini tek bir düz cephe sınıfında, TPDFlib'de sarmalar, ardından bu cepheyi üç ikili biçimde gönderir: yaklaşık 1.250 dışa aktarılmış fonksiyona sahip bir Windows DLL'i, bir COM/ActiveX otomasyon nesnesi ve bir macOS dylib'i. PDF semantiği üçünde de aynıdır. Sizi ısıran kısım, altındaki ABI'de yaşar: çağrı kuralları, dize kodlamaları, tutamaç sahipliği ve hangi tarafın hangi arabelleği serbest bırakmaya izinli olduğu
Tek cephe, üç ikili biçim
TPDFlib'in her genel fonksiyonunun, DL artı metod adıyla adlandırılmış düz bir karşılığı vardır. LoadFromFile, DLLoadFromFile olur, Encrypt, DLEncrypt olur, NewSignProcessFromFile, DLNewSignProcessFromFile olur. Neredeyse her dışa aktarımın ilk parametresi, bir Delphi çağıranın aksi halde tutacağı nesne referansının yerine geçen, DLCreateLibrary tarafından döndürülen bir InstanceID'dir. Bu eşlemeyi erkenden içselleştirin. Bu, Delphi API referansının başka her dil için de belgeleme görevi görmesi anlamına gelir: sınıf ne yapabiliyorsa, DLL de tahmin edilebilir bir ad altında yapabilir ve Python veya C#'tan ihtiyacınız olan çağrıyı öğrenmek için bir Pascal metod imzasını okuyabilirsiniz
Windows derlemesi PDFlibDLL32.dll ve PDFlibDLL64.dll üretir; ana işleminizin bit genişliğine uyanı seçin, çünkü 64 bit bir Java veya .NET işlemi, bildirim ne söylerse söylesin 32 bit kütüphaneyi yükleyemez
Windows: Stdcall örnekleri ve W/A fonksiyon çiftleri
Dize alan her dışa aktarım iki kez bulunur. Geniş bir sürüm PWideChar alır (.NET, Java ve Python'un c_wchar_p'ı için doğal uyum olan UTF-16), ve A soneki taşıyan bir sürüm PAnsiChar alır. İkisi aynı semantiği taşır ve yalnızca kodlamada farklılık gösterir, bu da tam olarak ikisini karıştırmayı izlemesi bu kadar acı verici yapan şeydir: hiçbir şey fırlatılmaz, hiçbir hata kodu dönmez, meta veride yalnızca anlamsız karakterler ya da düz ASCII'yi aşan bir karakter içeren herhangi bir yol için sahte bir "dosya bulunamadı" alırsınız. Bir ekibin bu şekilde karşılaştığı ilk kodlama hatası genellikle bir öğleden sonrayı yer, çünkü belirti veriye işaret eder ve neden bildirimdedir
// Windows bağlaması (PDFlibDLL64.dll): Stdcall, düz dışa aktarım adları
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';
// macOS bağlaması: aynı fonksiyon, Cdecl ve dışa aktarımda alt çizgi öneki
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Ana dil başına bir karakter genişliği seçin ve bunu bağlama üreticisinde kodlayın. Pratik bir kural: ana dilin yerel UTF-16 dizeleri varsa, W sürümlerini her yerde bağlayın ve bir daha A ailesine dokunmayın
macOS: aynı adlar, farklı ABI
Dylib, aynı DL fonksiyon kümesini iki sistematik değişiklikle dışa aktarır. Çağrı kuralı Stdcall değil Cdecl'dir ve her dışa aktarım adı başında bir alt çizgi taşır (_DLCreateLibrary, _DLLoadFromFile ve benzerleri). Her iki değişiklik de tamamen mekaniktir, bu da onları üretilmiş bir bağlama için ideal ve Windows dosyasının elle düzenlenmiş bir kopyası için tehlikeli yapar. Araçlarınız izin veriyorsa, tek bir kanonik fonksiyon listesi tutun ve platform başına bildirimleri ondan üretin. Bunu atlarsanız, bu sayfanın başında anlatılan tam yığın bozulmasını elde edersiniz, üstelik yalnızca CI'nizin en az egzersiz yaptığı platformda tekrarlanarak
COM ve ActiveX konakları: Safecall ve Olevariant yükleri
VB.NET, C#, VBScript ve eski otomasyon konakları için OCX derlemesi, aynı cepheyi her metodu Safecall olarak bildirilmiş bir IDispatch otomasyon nesnesi olan IPDFlibrary'de sarmalar. Bu kural, hataların size nasıl ulaştığını değiştirir. Safecall, dahili bir hatayı bir COM HRESULT'a çevirir, bu yüzden bir C# çağıran, düz DLL'in çağıranın kontrol etmesi gereken sessiz bir tam sayı döndüreceği yerde bir istisna yakalar. Aynı işlem, hangi ikili dosyayı yüklediğinize bağlı olarak iki farklı hata deyimi
İkili veri, ikinci bir COM'a özgü kuralı izler. Otomasyon arayüzünde hiç işaretçi parametresi yoktur. İçeri giren görüntü baytları ya da dışarı çıkan PDF baytları gibi ikili herhangi bir şey, sınırı AddImageFromVariant ve AppendToVariant gibi metodlar aracılığıyla bir Olevariant olarak geçer. Bir bayt dizisini bir variant'a taşımak .NET'te tek bir satırdır. Aynı işlem olduğu gerekçesiyle bunun yerine ham bir işaretçi vermeye çalışın, gönderim katmanı çağrıyı reddeder veya bozar. Bir kayıt ayrıntısı daha dağıtımları sekteye uğratır: COM kaydı bit genişliğine özgüdür, bu yüzden 32 bit regsvr32 ile kaydedilmiş bir OCX, 64 bit bir konak için görünmezdir. Bu uyumsuzluk, müşteri makinesinde, kütüphane sizin makinenizden ayrıldıktan çok sonra, ünlü derecede işe yaramaz "sınıf kayıtlı değil" olarak ortaya çıkar
Tutamaç disiplini: örnekler belgelere sahiptir
Düz API tam sayı tutamaçlar üzerinde çalışır. DLCreateLibrary bir örnek döndürür. Bir dosyayı yüklemek, o örneğin içinde bir belge ID'si döndürür. İmzalama süreçleri, dize listeleri ve doğrudan erişim dosyaları, hepsi aynı örneğe bağlı olarak kendi tam sayı tutamaçlarını döndürür. Yaşam döngüsü herhangi bir FFI konağından aynı görünür, burada temiz okunduğu için Pascal ile gösterilmiştir:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // işçi iş parçacığı başına bir örnek
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // bir DocumentID döndürür, başarısızlıkta 0
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); // örneğin sahip olduğu her belgeyi serbest bırakır
end;
end;
Bu sahiplik ağacından iki şey çıkar. DLReleaseLibrary, kesinlikle ihtiyaç duyduğunuz tek temizlik çağrısıdır, çünkü örneğin altındaki her belge ve süreç tutamacını tek seferde söker. Kısa bir betikte bu yeterlidir. Uzun süre çalışan bir serviste, fazladan törenle birlikte yavaş bir sızıntıya dönüşür, bu yüzden belgeleri örnek ölünceye kadar birikmesine izin vermek yerine işiniz bittiğinde serbest bırakın. Örnek aynı zamanda iş parçacığı yalıtımının doğal birimidir. Her işçi iş parçacığına kendi InstanceID'sini verin ve tıpkı iki iş parçacığı arasında tek bir TPDFlib nesnesini asla paylaşmayacağınız gibi, harici kilitleme olmadan bir örneği asla iş parçacıkları arasında paylaşmayın
Döndürülen dizeler ödünç alınmıştır, sahip olunmamıştır
DLGetPageText gibi metin döndüren fonksiyonlar, kütüphane örneğinin sahip olduğu ve geri dönüştürdüğü bir arabelleğe işaret eden bir PWideChar veya PAnsiChar geri verir. Sözleşme şudur: hemen kopyalayın, asla serbest bırakmayın
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // kütüphanenin sahip olduğu bir arabelleğe işaretçi
PageText := P; // şimdi kopyalayın; sonraki bir çağrı arabelleği yeniden kullanabilir
end;
C#'ta bu, bir sonraki kütüphane çağrısından önce IntPtr'yi yönetilen bir dizeye taşımak anlamına gelir. Python ctypes'ta, geniş dizeyi işaretçiden hemen dilimlemek anlamına gelir. Ham işaretçiyi çağrılar arasında tutarsanız, her birim testinden geçen ve ardından iki isteğin üretimde ilk kez üst üste bindiği anda başarısız olan bir hata yazmış olursunuz, çünkü ikinci çağrı, ilkinin hâlâ okumakta olduğu arabelleği geri dönüştürmüştür. Aynı sahiplik kuralı, DLSetProgressCallback aracılığıyla kaydedilen geri çağırmalar için ters yönde de işler. Kütüphanenin geri çağırmanıza verdiği herhangi bir işaretçi, yalnızca o geri çağırmanın gövdesi için geçerlidir ve geri çağırma nesnesinin kendisi, örnek onu hâlâ çağırabildiği sürece canlı kalmalıdır (çöp toplamalı bir konakta sabitlenmiş olarak). İş ortasında toplanan bir delege, aylarca temiz çalışmış bir .NET bağlamasında ortaya çıkan "rastgele" erişim ihlalinin ders kitabı kaynağıdır
Bağlamanın kendisine bir duman testi kurun ve üretilmiş herhangi bir bildirim kümesi gönderilmeden önce çalıştırın. ABI hatalarını ortaya çıkarma eğiliminde olan her kategoriden bir çağrıyı egzersiz edin: kuralın doğru olduğunu kanıtlamak için DLCreateLibrary gibi parametresiz bir fonksiyon, kodlamanın doğru olduğunu kanıtlamak için ASCII olmayan karakterler içeren bir yolla beslenen dize-giren bir fonksiyon, ödünç alınan arabellek işlemenin doğru olduğunu kanıtlamak için dize-çıkan bir fonksiyon ve bir hatanın konağınıza nasıl ulaştığını izleyebilmeniz için kasıtlı olarak başarısız olan bir işlem. Bu on beş dakikalık bir iştir ve aksi halde aylar sonra bir müşteri çökme dökümü olarak gelecek çağrı kuralı ve kodlama hatalarını yakalar
Python ctypes örneği, somut olarak
Python ctypes, en sık elle yazılmış gördüğüm bağlamadır ve platformlar arası ayrımı göstermeyi kolaylaştırır. Windows'ta, ctypes'ın Stdcall uygulaması için kütüphaneyi ctypes.WinDLL ile yükleyin, soneksiz W fonksiyonlarını bağlayın ve her dize parametresini c_wchar_p olarak bildirin. macOS'ta, Cdecl için ctypes.CDLL ile yükleyin, aynı fonksiyon listesini koruyun ve adları başındaki alt çizgi olmadan çözümleyin. Çoğu FFI katmanı, ctypes dahil, macOS'ta alt çizgi kuralını sizin için geri katlar, ancak bunun üzerine yüzlerce bildirim üretmeden önce tek bir çözümlenmiş çağrıyla doğrulanmaya değer olan varsayım budur
Bağlama çalışmasının ardından iki dağıtım sorusu gelir ve bunların net cevapları vardır. Düz DLL hiçbir kayıt gerektirmez: regsvr32 yalnızca ActiveX derlemesi için geçerlidir ve DLL dosya kopyalamayla gönderilir, ki bu da onu kayıt defterine hiç dokunmak istemeyeceğiniz Windows servisleri ve konteynerler için tercih etmenin başlıca nedenidir. İş parçacığı güvenliği, yukarıda zaten geçerli olan kurala indirgenir: iş parçacığı başına bir örnek. Örnek tutamacı, motorun izlediği her değişebilir durum parçasını, seçili belgeyi, render seçeneklerini, çıkarma ayarlarını tutar, bu yüzden bir örneği paylaşan iki iş parçacığı, her bir çağrı ayrı ayrı başarı döndürse bile birbirinin durumunu birbirine karıştırır
Bir bağlama sağlam olduğunda, onun ötesindeki işlemler, Delphi makalelerinin derinlemesine ele aldığı tam olarak aynı işlemlerdir, PDF şifrelemesi uygulamak ve denetlemek ve mevcut belgelerden metin ve görüntü çıkarmak dahil
Üç entegrasyon katmanının tümü için ikili indirmeler kütüphaneyle birlikte gönderilir; sürümler ve lisanslama için PDF Library for Delphi ürün sayfasına bakın