Teknik Makale

PDF Library for Delphi: DLL, ActiveX, and Dylib integration in Delphi

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

Tek bir TPDFlib cephesinin bir Stdcall Windows DLL, bir Safecall ActiveX otomasyon nesnesi ve bir Cdecl macOS dylib olarak sunulmasının mimari şeması
Üç ikili dosya da tek bir yassı PDF cephesini paylaşır; ancak çağırma kuralı, dizge işleme ve kayıt gereksinimlerinde farklılaşır

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

PDF Library for Delphi zaman çizelgesi: ödünç alınan bir DLGetPageText işaretçisini hemen kopyalamayı, kitaplık temel tamponu geri dönüştürene kadar tutmakla karşılaştırır
Dönen char işaretçileri örneğin geri dönüştürdüğü depolamayı ödünç alır; bu yüzden kopyalama, sıradaki kitaplık çağrısından önce yapılmalıdır
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

PDF Library for Delphi: çağırma kuralı, dize kodlaması, ödünç alınan tamponlar ve hata sinyalizasyonunu kapsayan bağlama duman testi sondalarının 2x2 ızgarası
Dört ucuz sonda, üretilmiş bildirimler bir müşteri makinesine ulaşmadan önce çağırma kuralı, kodlama ve sahiplik 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