Aynı Object Pascal kaynağı, Delphi ve FPC/Lazarus altında PDFium Bileşeni kodunu tekrar tekrar etkileyen dört şekilde farklı davranabilir: FPC, bir in üyelik testi onları okumayı bitirmeden önce işlev sonucu geçici kayıtları (record temporaries) serbest bırakır; dcc32 aralık kontrolü kapalı olarak geldiği için sınırların dışındaki dizi indeksleri sessizce çöp veri okur; yalnızca Delphi 13 anonim bir array of Byte ifadesini bir dönüşüm olmadan TBytes değişkenine atamayı kabul eder ve Delphi'nin AnsiString birleştirmesi, gizli bir kod sayfası çift yönlü döngüsü nedeniyle $80 veya üzerindeki baytları yok edebilir. Her biri bir derleyicide yeşil, diğerinde ise kırmızı veya daha kötüsü sessizce yanlış sonuç üreten bir duruma yol açar
Çift derleyicili bir projeyi ilk kez kuruyorsanız, Lazarus ve FPC görüntüleyici kılavuzu mutlu yolu kapsar: paketler, arama yolları ve ekranda bir oluşturma penceresi elde etme. Bu makale ise bir öğreticinin tersidir. Mutlu yol çalıştıktan sonra karşılaştığımız, CI'nın FPC altında yeşil, Delphi altında yeşil olduğu ve ardından bir tarafta geçen bir değişikliğin diğer tarafta patladığı şeylerin listesidir. Aşağıdaki her tuzak, PDFiumPas test paketindeki veya demolarındaki gerçek bir başarısızlıkdan gelir; taahhüt düzeyindeki adli incelemeler minimal bir yeniden üretime, temel nedene ve standartlaştırdığımız düzeltmeye indirgenmiştir
Neden bir küme FPC altında boş okunurken Delphi'de okunmaz?
Tek cümlelik versiyon: FPC, bir işlevin kayıt sonucunu tutan geçici değişkeni, o sonucun bir alanını okuyan bir ifade bitmeden önce sonlandırabilir; bu nedenle X in Func().Issues ifadesi, eşdeğer Delphi ifadesi çalışırken zaten serbest bırakılmış bir kümeye karşı üyelik testi yapabilir. PDF/E uygunluk testlerimiz ilk sürümünde bu durumla karşılaştı. Doğrulayıcı, Issues alanı bir ihlal bayrakları kümesi olan bir kayıt döndürür ve iddialar (assertions) çağrıyı satır içine almıştır (inlined)
// Unreliable under FPC: the function-result record temporary
// can be released before the 'in' test reads Issues
AssertTrue(pveiLzwUsed in ValidateAnsi(Pdf).Issues);
// Reliable on both compilers: pin the result to a local first
var
Vr: TPdfEValidationResult;
begin
Vr := ValidateAnsi(Pdf);
AssertTrue(pveiLzwUsed in Vr.Issues);
end;
Satır içine alınmış (inlined) biçim kümeyi FPC altında boş olarak okudu, bu nedenle bir bayrak bekleyen her iddia başarısız oldu, oysa aynı Delphi derlemesi geçti. Temel neden, iki derleyicinin daha büyük ifadeler içindeki işlev sonucu geçici değişkenlerinin ömrünü nasıl yönettiği konusundaki bir farktır: Delphi geçici değişkeni deyimin sonuna kadar canlı tutarken, FPC'nin geçici kaydı serbest bırakması hala onu okumakta olan küme üyeliği işleciyle yarışabilir. Sıfırdan yeni testler yazarken, aynı davranışı daha önce PDF/A test birimindeki FlagPresent yardımcısında bir yorumda belgelemiş ve ardından hatayı yeniden tanıtmıştık ki bu, bozuk biçimin ne kadar doğal göründüğünü gösterir. Düzeltme mekaniktir ve genel bir kural olarak benimsenmeye değerdir: bir alan erişimini veya küme testini asla doğrudan bir kayıt döndüren bir işlev çağrısına bağlamayın; önce sonucu yerel bir değişkene atayın, ardından alanı okuyun. Bir satıra mal olur ve derleyiciye bağlı kararsızlık sınıfını tamamen ortadan kaldırır
Delphi neden FPC'nin derlemeyi reddettiği bir dizi indeksini kabul eder?
Tek cümlelik versiyon: dcc32, sabit sınırlı bir diziye yönelik sınır dışı bir indeksi derler ve varsayılan aralık kontrolü kapalıyken çalışma zamanında herhangi bir hata olmadan bitişik belleği sessizce okur veya yazar; FPC ise aynı indeksi derleme zamanında reddeder. PDFium Bileşeni, QuadPoints girdilerinin genellikle nasıl numaralandırıldığına uyacak şekilde dörtgen noktalarını 1 tabanlı bir dizi olarak tanımlar: TQuadrilateralPoint = array [1..4] of TPdfPoint. Bunu dönüşümlü 0 tabanlı döngüyle dolduran bir demo Delphi altında aylarca çalıştı
var
I: Integer;
begin
for I := 0 to 3 do // wrong: the array is [1..4]
Data.AttachmentPoints[I] := Corner[I]; // dcc32 default: compiles, index 0
// silently touches adjacent memory
// FPC: compile-time range check error
for I := Low(TQuadrilateralPoint) to High(TQuadrilateralPoint) do
Data.AttachmentPoints[I] := Corner[I - 1]; // correct on both compilers
end;
Delphi derlemesi bir hatalı pozitiftti (false positive): dcc32 varsayılanı olan aralık kontrolü kapalıyken, indeks 0 kayıtta diziden önce gelen alanın üzerine geldi ve demo çalışıyor gibi göründü. Aynı demoyu Lazarus'a taşımak FPC'den anında bir derleme zamanı aralık kontrolü hatası üretti ve indeksi düzeltmek kütüphanenin açıklama yolunda çöp okumaların maskelediği ikinci, daha derin bir hatayı ortaya çıkardı; bu hata dörtgen noktaları açıklama makalesinde incelenmiştir. Bu olaydan iki ders çıktı. İlk olarak, dizi türü yapı gereği 0 tabanlı olmadığında her zaman sabit sınırlar yerine Low() ve High() işlevlerini tercih edin. İkinci olarak, bir FPC derlemesini veya en azından {$R+} etkinleştirilmiş bir Delphi derlemesini, yeni bir demo veya test için zorunlu bir ilk çalıştırma kapısı olarak görün: dcc32'nin varsayılanları size bu hata sınıfı hakkında bilgi vermez ve çalışan bir program doğru olduğunun kanıtı değildir
Yalnızca Delphi 13'ün kabul ettiği TBytes ataması
Tek cümlelik versiyon: anonim array of Byte olarak bildirilen bir alanı bir TBytes değişkenine atamak Delphi 13'te (derleyici sürümü 37.0) derlenir ancak Delphi 12 Athens ve önceki tüm sürümlerde E2010 Incompatible types: 'TArray<Byte>' and 'Dynamic array' hatasıyla başarısız olur. Bu durum Delphi-FPC ayrımından ziyade Delphi'nin kendi geçmişiyle olan bir ayrımıdır ancak aynı çoklu derleyicili kod tabanını aynı şekilde etkiler: en yeni derleyici, diğer her şeyin reddettiği bir yapıyı sessizce kabul eder
type
TValidator = class
private
FBuffer: array of Byte; // anonymous dynamic array type
end;
var
OrigBytes: TBytes;
begin
OrigBytes := FBuffer; // Delphi 13 only; E2010 on Delphi 12
// Athens and earlier
OrigBytes := TBytes(FBuffer); // compiles everywhere; same byte layout,
// safe hard cast
end;
Bunu tam olarak Delphi 13 üzerinde yerel olarak geliştirilen ve test edilen, örtük dönüşümün sessizce kabul edildiği bir doğrulama rutininde yayınladık. Tam kaynaklı yükleyici Delphi 12 ve daha eski sürümleri kullanan çok sayıda kullanıcıya hizmet veriyor ve onlar için birim derlenmedi. Yapısal düzeltme, anonim bir array of Byte ile TBytes yapısının aynı dinamik dizi düzenini paylaşması nedeniyle güvenli olan yukarıda gösterilen sert dönüşüm (hard cast) veya daha iyisi, en başta alanı TBytes gibi adlandırılmış bir tür olarak bildirmektir, böylece hiçbir dönüşüm ortaya çıkmaz. Süreç düzeltmesi daha önemlidir: en yeni araç zincirinizde derlenen bir yapı, kullanıcılarınızın gerçekten çalıştırdığı daha eski derleyiciler hakkında hiçbir şey kanıtlamaz ve bu regresyon kategorisi, her desteklenen sürüme karşı derleme yapana kadar görünmez kalır. Sürüm betiklerimiz artık kütüphaneyi tam derleyici matrisinde derliyor çünkü yerel bir 37.0 derlemesi yalnızca 13'e özgü bir toleransı yakalayamaz
Çince Windows Makinesinde Kaybolan AnsiString Baytı
Tek cümlelik versiyon: $80 veya üzerindeki ham bir baytı + ile bir AnsiString'e bağlamak Delphi altında sessizce o baytı ? ($3F) ile değiştirebilir çünkü ifade sistem kod sayfası üzerinden örtük bir AnsiString -> UnicodeString -> AnsiString çift yönlü dönüşümü gerçekleştirir. Bunu, ISO 19005-2 madde 6.1.8 uyarınca geçerli UTF-8 olmayan adları doğrulayıcının işaretlediğini doğrulamak için hiçbir zaman geçerli bir UTF-8 öncü baytı olmayan izole bir $FE baytı içeren bir ad oluşturan bir PDF/A testi aracılığıyla bulduk
var
BadName: AnsiString;
begin
// On Delphi with a multi-byte system code page (observed on CP936),
// the concatenation round-trips through UnicodeString and $FE, which
// is not a valid CP936 sequence, comes back as '?' ($3F)
BadName := '/Bad' + AnsiChar($FE) + 'Name';
// Safe: build with an ASCII placeholder, then patch the byte in place;
// indexed assignment into a settled AnsiString does not round-trip
BadName := '/Bad' + #1 + 'Name';
BadName[5] := AnsiChar($FE);
end;
Kod sayfası 936 çalıştıran bir Çince Windows sisteminde, birleştirilen dize hiçbir zaman $FE içermedi, bu nedenle kütüphane haklı olarak hiçbir şey bildirmedi ve test bir kütüphane hatası gibi görünerek kırmızıya döndü. Kütüphane hiçbir zaman yanlış değildi: gerçekten $FE baytını içeren bir PDF besleyen bir FPC test düzeneği beklenen bayrağı aldı. Bozulma, dize ifadesi değerlendirilirken Delphi test yürütülebilir dosyasının içinde gerçekleşti çünkü Delphi'nin önce-Unicode dize modeli karışık AnsiString ifadelerini UnicodeString üzerinden dönüştürür ve $FE, CP936'da geçerli bir öncü bayt değildir, bu nedenle çift yönlü dönüşüm onu değiştirir. Buradaki sınır konusunda dürüst olun: CP1252 gibi tek baytlık bir Batı kod sayfasında aynı ifade genellikle hayatta kalır, bu da bu hatanın çoğu geliştirme makinesinde gizlenip yalnızca Doğu Asya sistemlerinde veya yerelleştirilmiş CI çalıştırıcılarında ortaya çıkmasının tam nedenidir. Kabul ettiğimiz kural: AnsiString birleştirmesi yoluyla asla $80 veya üzerindeki baytları içeren ikili test vektörleri oluşturmayın; baytları dize yerleştikten sonra yukarıdaki gibi yerinde yamayın veya vektörü en baştan TBytes içinde oluşturun
Çift derleyicili bir iş akışı varsayılan olarak neleri kontrol etmeli?
Dört tuzak, bir şablon: her derleyici size hatalarınızın farklı bir alt kümesini söyler. FPC'nin derleme zamanı aralık analizi, dcc32'nin aylarca sessizce çalıştırdığı sınır dışı bir indeksi yakaladı ve dcc32'nin Unicode dize modeli, saf bayt yönelimli bir FPC derlemesinin asla tetikleyemeyeceği bir kod sayfası bağımlılığını ortaya çıkardı. Pratik sonuç, hiçbir yeşil iş hattının tek başına yeterli olmadığıdır. Çapraz derleme (cross-compiling) yalnızca bir taşınabilirlik onay kutusu değildir; ABI ve bellek güvenliği sertleştirme makalesindeki savunma amaçlı sınır kontrolleriyle aynı doğrultuda, aynı kaynağa uygulanan ikinci bir statik analizör ve ikinci bir çalışma zamanı modelidir
Bu olaylardan ortaya çıkan kurallar ezberlenecek kadar kısadır. Alanları okumadan önce işlev sonucu kayıtlarını yerel bir değişkene sabitleyin. Sabit sınırlı dizileri Low() ve High() ile yineleyin ve yeni bir demoya güvenmeden önce en az bir aralık kontrollü veya FPC derlemesi çalıştırın. Anonim dinamik dizi alanlarını açıkça dönüştürün veya en başta adlandırılmış türlerle bildirin ve sürümden önce tam derleyici matrisini derleyin. Ham yüksek baytları AnsiString birleştirmesinden tamamen uzak tutun. Bunların hiçbiri alışkanlık haline geldikten sonra ölçülebilir bir çaba gerektirmez ve her biri tek derleyicili bir iş akışının yapısal olarak göremediği bir başarısızlık modunu kapatır
Tüm bu dört sorun, Delphi, C++Builder ve FPC/Lazarus için aynı Object Pascal kaynağını sunan ve her bir araç zincirinde uygunluk ile regresyon paketlerini çalıştıran PDFium Bileşeni'nin bakımı sırasında bulundu ve düzeltildi, bu nedenle bu makaledeki tuzaklar bellek yerine testlerle korunmaktadır