Bir C kütüphanesi üzerindeki Pascal bağlaması sıradan bir Pascal gibi okunur. Bir yöntemi çağırırsınız, bir kayıt (record) geri alırsınız, tahsis ettiğinizi serbest bırakırsınız. Sorun şu ki PDFium, kendi çağırma kuralı, kendi tamsayı genişlikleri ve belleğe kimin sahip olacağı ve kimin serbest bırakacağına dair kendi kuralları olan bir C ve C++ kütüphanesidir. Bunların hiçbiri dil sınırını kendi başına geçmez. Bu sözleşmelerin her birinin Pascal bildirimlerinde elle yeniden belirtilmesi gerekir ve tek bir yanlış kelime temiz görünen bir çağrıyı yığın (stack) bozulmasına, kırpılmış bir kaymaya (offset) veya çift serbest bırakmaya (double free) dönüştürür. Bir PDFium VCL bağlamasının v1.61.0 denetimi her türden bir kusur ortaya çıkardı. Bunları incelemeye değer çünkü bu bağlamaya özgü değillerdir. Herhangi bir C API'sini Delphi veya Lazarus'ta sarmalamanın sürekli karşılaşılan tehlikeleridir
cdecl işlev türünün bir parçasıdır, süsleme değildir
PDFium derlenmiş C'dir. Win32'de dışa aktarımları ve daha da önemlisi çağırdığı geri aramalar (callbacks) cdecl çağırma kuralını kullanır. cdecl altında, çağıran çağrı döndükten sonra yığını temizler. Delphi'nin yerel varsayılanı register'dır ve geri aramalar için Win32 C standardı bazı kütüphanelerde çağrılanın temizlediği stdcall'dur. Bir yapı PDFium'a bir işlev işaretçisi teslim ettiğinde ve o işaretçinin türündeki cdecl ifadesini unuttuğunuzda, iki taraf yığın işaretçisini kimin ayarlayacağı konusunda anlaşamaz. Ya ikisi de düzeltir ya da hiçbiri düzeltmez ve yığın işaretçisi her çağrıda bağımsız değişkenlerin boyutu kadar kayar
Bu kusurun bulunmasının zor olmasının nedeni, hasarın yerel olmamasıdır. Bozulmuş çağrı döner ve iyi görünür. Yanlış hizalama daha sonra, çerçevesi artık birkaç bayt uzaktaki bir yığın işaretçisi üzerinde oturan ilgisiz bir işlevde ortaya çıkar ve rastgele bir okuma, hatalı bir dönüş adresi veya gerçekte yanlış yaptığınız geri aramaya hiç yakın olmayan bir yığın izleme (backtrace) içeren bir çökme olarak kendini gösterir. Form doldurma bu hatanın ısırdığı klasik yerdir, çünkü form doldurma arayüzü PDFium'un geri çağırdığı geri aramalarla dolu bir kayıttır. Bunlardan biri olan FFI_OpenFile, harici bir dosyayı açmak için PDFium'a çağıracağı bir işlev teslim eder; bu işlev function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl şeklinde bildirilmiştir. Sondaki cdecl kopyalamaya değer noktadır. Bunu bıraktığınızda kod yine de derlenir, bağlanır ve PDFium işlevi çağırana kadar çalışır. Çağırma kuralı işlev türünün kendisine aittir. İsteğe bağlı bir süs değildir ve derleyici eksik olduğunda sizi uyarmaz çünkü düz bir işlev türü mükemmel derecede yasal bir Pascal türüdür. Tek savunma, çağırma kuralını içe aktarılan her imzanın ve dışarı aktardığınız her geri aramanın zorunlu bir alanı olarak ele almaktır
size_t işaretçi genişliğindedir, ve FPC Win64'te bu 64 bit anlamına gelir
İkinci kusur, yalnızca bir hedefte görünen bir tamsayı genişliği uyumsuzluğudur. C'nin size_t türü, herhangi bir nesne boyutunu tutacak kadar geniş olarak tanımlanmıştır ve bu 64 bitlik bir platformda 64 bitlik işaretsiz bir tamsayı anlamına gelir. PDFium'un aşamalı yükleme arayüzleri size_t bayt kaymalarıyla konuşur. Kullanılabilirlik sağlayıcısının FX_FILEAVAIL kaydı, PDFium'un bir kayma ve bir boyutla çağırdığı bir IsDataAvail geri araması taşır ve FX_DOWNLOADHINTS kaydının AddSegment geri araması da aynısını alır. Her iki parametre de size_t'dir
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Bu kaymaları 32 bitlik bir tür olarak bildirirseniz, bağlama Win32'de ve Delphi Win64'te çalışır, ardından FPC ve Lazarus Win64'te sessizce bozulur. Nedeni incedir. FPC Win64'te, NativeUInt gerçek bir işaretçi genişliğinde 64 bitlik türdür ve size_t buna takma adlandırılmıştır. Bağlamanın tür bölümünde, FPC üzerinde NativeUInt'in gölgelenmesine karşı uyarıda bulunan bir yorum vardır, çünkü onu orada 32 bitlik bir takma adla yeniden tanımlamak size_t'yi 32 bite zorlayacak ve kütüphaneye geçirilen veya kütüphane tarafından yazılan her size_t parametresini bozacaktır. 32 bitlik bir parametreye ulaşan 64 bitlik bir kayma üst yarısını kaybeder. Küçük bir dosya için every kayma 32 bite sığar ve yanlış bir şey yoktur. Büyük bir dosya için, bir kayma dört gigabayt sınırını geçtiği an, kırpılmış değer tamamen başka bir yeri işaret eder, PDFium yanlış bayt aralığının kullanılabilir olup olmadığını sorar ve aşamalı yükleme durur veya çöp okur. Kusur, dosya yeterince büyük olana ve hedef, size_t'nin gerçekten genişlediği hedef olana kadar görünmez
Bir Pascal istisnası asla bir C çerçevesi boyunca geri sarılmamalıdır
Üçüncü sınıf, C'nin sahip olmadığı istisna modeliyle ilgilidir. PDFium geri aramalarınızdan birini çağırdığında, Pascal kodunuz Delphi'nin istisna mekanizması hakkında hiçbir şey bilmeyen bir C ve C++ çerçeveleri yığını içinde çalışır. Geri aramanız yükselirse ve istisnanın yayılmasına izin verirse, geri sarılmak üzere asla oluşturulmamış çerçeveler boyunca geri sarılır. PDFium'un kendi temizliği çalışmaz, dahili değişmezleri yarı güncellenmiş olarak kalır ve işlem artık kütüphanenin hiç tahmin etmediği bir durumdadır. Bu geri aramaların sözleşmesi bir istisna değil, bir dönüş kodudur
İki geri arama bunu somutlaştırır. FPDF_FILEWRITE, PDFium'un kaydedilmiş bir belgeyi yazdığı havuzdur ve FPDF_FILEACCESS, bir girdi belgesini okuduğu kaynaktır. Her iki profesyonel de burada bir Delphi TStream üzerinde uygulanmıştır ve her ikisi de herhangi bir akışın başarısız olduğu şekilde başarısız olabilir: disk dolar, akış altınızdan kapanır, bir okuma sonun ötesine geçer. Yazma geri araması, akış yazmasını sarar ve kaçmasına izin vermek yerine herhangi bir başarısızlığı PDFium'un başarısızlık koduna dönüştürür
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
Okuma tarafı da aynısını yapar: başarısız bir okuma, sınır genelinde yükseltmek yerine FPDF_FILEACCESS sözleşmesine uymak için sıfır bildirir. Yeniden yükseltme (re-raise) içermeyen çıplak bir except, istisnaları asla yutmamak üzere eğitilmiş bir Pascal programcısına yanlış görünür ve sıradan Pascal'da bu yanlıştır. Bir ABI sınırında bu doğru şekildir, çünkü C çağıranına geri verilecek tek güvenli değer, nasıl yorumlayacağını bildiği bir durum kodudur. Başarısızlık yine de yayılır, sadece dönüş değeri aracılığıyla ve kontrol Pascal tarafına geri döndüğünde kütüphanenin üzerindeki çağıran kod bunu EPdfError olarak yüzeye çıkarır
Çift serbest bırakma hata yolunda gizlenir
Dördüncü kusur mülkiyettir. Bir PDFium belge tutamacı (handle) kütüphane tarafından açılır ve FPDF_CloseDocument ile tam olarak bir kez kapatılmalıdır. Tehlike, ikinci bir temizliğin de sahip olduğu bir tutamacı serbest bırakan bir hata yoludur. Bir sarmalayıcı nesne oluşturan, ona yeni açılmış bir belge tutamacı atayan ve ardından başarısız olabilecek daha fazla kurulum yapan bir rutin düşünün. Kurulum hata verirse, ham tutamaçta FPDF_CloseDocument'ı çağıran bir erken dönüş işleyicisi onu kapatır ve ardından nesne serbest bırakıldığında sarmalayıcı nesnenin kendi yıkıcısı (destructor) onu tekrar kapatır. Tutamaç iki kez serbest bırakılır; bu belirsiz bir davranıştır ve olası bir çökmeye yol açar
Denetim bunu, zaten açık olan bir tutamaç etrafında bir TPdf inşa eden montaj (imposition) tarzı bir içe aktarma yolunda buldu. Çözüm, mülkiyet aktarımını tek gerçeklik kaynağı yapmaktır. Tutamaç sarmalayıcının alanına atandıktan sonra, sarmalayıcı ona sahip olur ve hata yolundaki tek temizlik sarmalayıcıyı serbest bırakmaktır. Sarmalayıcının yıkıcısı sizin için FPDF_CloseDocument'ı çağırır, bu nedenle ikinci bir açık kapatma aynı belgeyi çift serbest bırakacaktır. Düzeltilmiş hata işleyicisi nesneyi serbest bırakır ve yeniden yükseltir; kapatmaya giden tam olarak tek bir yol vardır
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Yönetilen kayıtlar ve dışa aktarımlarla dolu bir kütüphane açık bir şekilde sonlandırılmalıdır
Son sınıf, derleyicinin sizin adınıza yönettiği bellek hakkındadır; bir C alışkanlığı bunu sessizce bozacaktır. Bu bağlamanın yardımcı işlevlerinin çoğu, bir WideString veya dinamik bir dizi içeren bir kayıt (record) döndürür. Bunlar referans sayımlı alanlardır ve derleyici, sayımlarını korumak için gizli defter tutma işlemleri yayar. C'den aktarılan içgüdü, yeni bir kaydı FillChar(Result, SizeOf(Result), 0) ile temizlemektir. Bu, kaydın içindeki yönetilen referansın üzerine, önce onu azaltmadan sıfırları damgalar. Derleyici, döngü yinelemeleri boyunca bir işletim sonucu için bir gizli geçici dosyayı yeniden kullanır; bu nedenle ikinci yinelemede FillChar, hiç serbest bırakılmamış canlı bir dize işaretçisinin üzerine yazar ve işaret ettiği dize sızar. İşlevi bin açıklama üzerinde bir döngüde çağırırsanız bin dize sızdırırsınız
Çözüm, dilin kaydı kendi bildiği şekilde, sıfırlamadan önce yönetilen her alanı serbest bırakan Default(T) ile temizlemesine izin vermektir
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
İlişkili bir mülkiyet sorunu kütüphane yükleme sınırında yaşar. Bu bağlama, bir LoadLibrary'den sonra GetProcAddress ile PDFium DLL'sinden birkaç yüz işlev işaretçisini çözer. Gerekli bir dışa aktarım eksikse, kısmen bağlanmış durum tehlikelidir: düzinelerce işaretçi geçerlidir, geri kalanı nil veya eskidir ve bunlardan biri aracılığıyla yapılan daha sonraki herhangi bir çağrı, zaten kaldırılmış olabilecek bir modüle atlar. Bağlama, gerekli bir dışa aktarım çözülemediğinde kütüphaneyi kaldırarak ve içe aktarılan her işaretçiyi tekrar nil değerine sıfırlayan tam bir ClearAllBindings çalıştırarak bunu işler. Bundan sonra, hiçbir işlev işaretçisi kaldırılmış bir modüle sarkmaz ve daha sonraki bir çağrı, serbest bırakılmış koda dallanmak yerine bir nil-işaretçi kontrolü ile temiz bir şekilde başarısız olur
Sarmalayıcı, dört sözleşmenin elle yeniden belirtildiği yerdir
Bu beş kusurun hiçbiri egzotik değildir. C API'si üzerindeki ince bir Pascal katmanının öngörülebilir başarısızlık modlarıdır ve kümelenirler çünkü o katman tam olarak dört ayrı sözleşmenin yeniden bildirilmesi gereken yerdir. Çağırma kuralının her geri aramada cdecl olarak yazılması gerekir. Tamsayı genişliği, gerçekten genişlediği tek hedefte size_t ile eşleşmelidir. İstisna modeli, Pascal dışına çıkan her geri aramada dönüş kodlarına dönüştürülmelidir. Her tutamacın ve yönetilen her alanın mülkiyeti bir kez belirtilmeli ve üretime kadar kimsenin uygulamadığı hata yolları da dahil olmak üzere her yolda uyulmalıdır. Herhangi birini kaçırırsanız, belirtisi nedeninden çok uzakta ortaya çıkan bir kusur elde edersiniz; bu da bu kategoriyi maliyetli kılan şeydir. Denetimin değeri, tek bir düzeltmeden ziyade, bunların her birini tüm bağlama boyunca kontrol edilecek kendi disiplini olarak ele almaktaydı
Bağlamanın kenarlarını korumak yerine gerçek iş yaptığını görmek istiyorsanız, işleme önbelleği ve yakınlaştırma performansı hakkındaki notumuz işleme yolunu gösterir ve Lazarus ve FPC görüntüleyici oluşturma konusundaki çapraz derleyici kılavuzu, burada açıklanan Win64 size_t davranışının gerçekten önemli olduğu yerdir. Her ikisi de, bu blogun başka yerlerinde ele alınan işleme, metin çıkarma ve form API'lerinin yanı sıra Delphi, Lazarus ve C++Builder için PDFium Bileşeni'nde sunulan aynı bellek güvenliği ve ABI çalışması üzerine inşa edilmiştir