Teknik Makale

HotPDF sağlamlaştırırken çıkan Win64'e özgü Delphi hataları

Win64 Delphi kodu, aynı kaynağın Win32'de tertemiz koştuğu yerde başarısız olabilir ve HotPDF Delphi PDF bileşeni yakın bir sağlamlaştırma geçişinde beş böyle olguya çarptı: Single overload'una bağlanan Power(10, N), bayat bir TList.Count okuyan while döngüsü, 2^63'e yukarı yuvarlanan bir High(Int64) sınırı, FPC'de 15 haneli float metni ve derlemeyi durduran test assertion'ları

Yalnızca Win32 derleyip sınarsanız hiçbiri görünmez; içeri sızmalarının yolu da tam olarak budur. Aşağıdaki olgular HotPDF'in SVG ile XPS içe aktarıcılarından, page renderer'ından ve JSON iş okuyucusundan gelir; aktarılan sayısal sonuçlar, Win32 ile Win64 için derlenmiş küçük sonda programlarıyla yeniden üretildi. Bir Delphi kod tabanını 64 bite taşıyorsanız her biri bir grep'e değer

Power(10, 100) neden yalnızca Win64'te taşar?

Win64'te integer argümanlı System.Math.Power(10, N), Single overload'una çözülür; sonuç tek hassasiyette hesaplanır, tek hassasiyette döndürülür ve kabaca 3.4E38 üstündeki her şey taşar. Win32'de aynı çağrı Extended overload'una bağlanır ve 80 bit hassasiyetli x87 FPU üzerinde koşar; dolayısıyla Power(10, 100) yalınca 1E100'dür

System.Math, Extended, Double ve Single için Power bildirir; üssü tam sayı olduğunda Power'ın çağırdığı eşleşen bir IntPower ailesiyle birlikte. Win64'te Extended, yalnızca Double için bir takma addır (SizeOf(Extended) = 8) ve iki integer argüman için derleyici Single sürümünü seçer. Ele veren şey yalnızca taşma değil, hassasiyettir: Win64'te Power(10, 20), tam olarak Single(1E20) olan 1.0000000200408773E20 döndürür. Bir Double sonuç 1E20 basardı. Aynı bağlamayı, Delphi 10.3'ten derleyici sürüm 37.0'a dek denediğimiz her Win64 derleyicisinde gördük

Sonradan ne olduğu kayan nokta istisna maskesine bağlıdır. Delphi 12 ve sonrası bütün kayan nokta istisnalarını varsayılan olarak maskeler; dolayısıyla taşma sessizdir: Power(10, 100), +Inf döndürür ve Power(10, -100) 0 döndürür. Delphi 11 ve öncesi exOverflow'u maskesiz bırakır ve aynı çağrı EOverflow fırlatır. Maskeleri kendisi set eden uygulamalar ve böyle hostlara yüklenen DLL'ler, hostun seçtiği davranışı alır; bir kütüphanenin iki sonucu da varsayamaması bundandır

HotPDF Win64 sayısal tuzağı: integer argümanlı System.Math Power, Single overload'ına bağlanır; dolayısıyla 10'un 20. kuvveti 1E20 yerine 1.0000000200408773E20 döndürür ve 10'un 100. kuvveti, istisnalar maskeliyken artı sonsuz, maskesizken EOverflow verir
hassasiyet kaybı ele verir: bir onun kuvveti Single gürültüsüyle geri dönerse yanlış overload kazanmıştır — ölçeği kendiniz kurun
uses
  System.SysUtils, System.Math;

procedure ShowPowerOverload;
var
  N: Integer;
  OldMask: TArithmeticExceptionMask;
begin
  N := 20;
  // Win32 1E20 basar; Win64 1.0000000200408773E20 basar (Single overload)
  Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));

  // Delphi 11'in ya da katı FP ayarlı bir hostun yaptığını yeniden üretir
  OldMask := GetExceptionMask;
  SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
  try
    N := 100;
    Writeln(Power(10, N));       // Win64: EOverflow; Win32: 1E100
  finally
    SetExceptionMask(OldMask);
  end;
end;

Bir test boyunca exOverflow ile exInvalidOp'yi maskesiz bırakmak, eski bir derleyicinin ya da katı bir hostun gördüğünü görmenin en ucuz yoludur. Varsayılan ayarlı modern bir derleyicide hata çökmez; sonsuzlar ve sıfırlar üretir ve onlar bir test logunda çok daha zor fark edilir. Önceki maskeyi finally'de geri koyun: maske thread-başı bir durumdur ve test koşusunun gerisi ardınızda bıraktığınızı miras alır

Overload HotPDF SVG ve XPS içe aktarımına nasıl girdi?

HotPDF'in SVG ile XPS path okuyucuları tek bir sayı tarayıcısını paylaşır ve o tarayıcı bir üs okuduktan sonra mantisi Power(10, Exponent) ile ölçekliyordu. THotPDF.ImportSVGFormXObject'a geçen her SVG (SVG'yi yeniden kullanılabilir form XObject olarak PDF'e içe aktarmayazısının ardındaki giriş noktası) ve XPS ile OpenXPS'in PDF'e dönüştürülmesisırasında işlenen her path geometrisi, o çağrıya 1e100 ya da 5e99 gibi bir koordinat besleyebiliyordu

v2.770.91 üssü hâlihazırda 100'de tavanlamış ve 1E300'ü aşacak değerleri reddetmişti; bu yeterli görünüyordu: 1E100, kabaca 1.8E308 olan Double sınırının çok uzağındadır. Win64'te yine de taşıyordu; çünkü hesap hiçbir zaman Double içinde yapılmıyordu. v2.770.155'ten beri tarayıcı onun kuvvetini kendisi kurar ve 1e-100 gibi sayılar, ya da büyük negatif üslü uzun bir mantis, 0'a çökermek yerine gerçek değerleriyle okunur

Sınırlı üsler için güvenli bir onun kuvveti

Üs sınırlıyken en güvenli onun kuvveti, Double çarpmasıyla kendiniz kurduğunuz kuvvettir. En çok 100 çarpmalı bir döngü, çevresindeki metni taramaya kıyasla hiçbir şeye mal olmaz, son ölçekten büyük bir ara değer asla üretmez ve Win32, Win64 ile Free Pascal'da birebir aynı davranır

const
  MaxDecimalExponent = 100;

function TryScaleByPowerOf10(const Value: Double; Exponent: Integer;
  out Scaled: Double): Boolean;
var
  Scale: Double;
  I: Integer;
begin
  Scaled := 0;
  Result := False;
  if (Exponent < -MaxDecimalExponent) or (Exponent > MaxDecimalExponent) then
    Exit;
  // Double aralığından çıkacak sonuçları reddeder
  if (Exponent > 0) and (Value <> 0) and
     (Log10(Abs(Value)) + Exponent > 300) then
    Exit;
  Scale := 1.0;
  for I := 1 to Abs(Exponent) do
    Scale := Scale * 10.0;       // asla 1E100'ü aşmaz
  if Exponent >= 0 then
    Scaled := Value * Scale
  else
    Scaled := Value / Scale;     // böl: 1E-100'ün tam bir Double'ı yoktur
  Result := True;
end;

Üç ayrıntı ağırlığı taşır. Aralık denetimi, Abs(Exponent) <= 100 yerine iki karşılaştırma kullanır; çünkü Abs(Low(Integer)) hâlâ negatiftir ve düpedüz geçerdi. Negatif üsler, önceden hesaplanmış 1E-100 ile çarpmak yerine ölçeğe böler; o değerin tam bir Double karşılığı yoktur ve bir yuvarlama adımı daha eklerdi. Ve Log10 ön denetimi, çarpmanın taşma şansı kalmadan Double aralığı dışındaki sonuçları reddeder

Döngünün neye feragat ettiğini açıkça söyleyin. 1E22'ye kadarki onun kuvvetleri Double'da tamdır; ötesinde her çarpma yuvarlar ve 100 çarmadan sonra ölçek, doğru yuvarlanmış 1E100'ün son yerden birkaç unit uzağında durur. Çizim koordinatları için bu görünmezdir. Her değeri bit bit yeniden üretmek zorunda olan genel amaçlı bir metin-çevirme-Double dönüşümü için yeterli değildir; doğru yuvarlayan bir dönüşüm algoritmasına ihtiyacınız olur

dcc64 bir while döngüsünde bayat TList.Count'ı ne zaman okur?

Win64 derleyicisini (dcc64, derleyici sürüm 37.0), listenin sonundan silen ve Count'u yeniden okumak yerine bir stack geçicisiyle karşılaştıran bir while List.Count > Start do döngüsü için kod üretirken gördük. Onu düzelten yeniden yazım, sınırları tanım gereği tam olarak bir kez değerlendirilen bir for ... downto döngüsüydü

Döngü v2.769.3'te gelmişti; o sürüm, renderer'ın transparency-group koduna bir grup içinde kurulan soft maskeleri iki geçişli bir render boyunca canlı tutmayı ve ardından free etmeyi öğretmişti. Temizlik, tek ya da iki geçişli bir for döngüsünün ardından gelen bir finally bloğunda, kiremit-başı döngünün içindeydi. Biçimine indirgenmiş hâliyle önce ve sonra şöyle görünür:

// dcc64'ün (derleyici sürüm 37.0) yanlış derlediğini gördüğümüz biçim
procedure DropMasksWhile(Masks: TList; Start: Integer);
begin
  while Masks.Count > Start do
  begin
    TObject(Masks[Masks.Count - 1]).Free;
    Masks.Delete(Masks.Count - 1);
  end;
end;

// Değiştirme: sınırlar bir kez değerlendirilir, bayatacak geçici yok
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
  Idx: NativeInt;               // TList.Count, Delphi 12'den beri NativeInt
begin
  for Idx := Masks.Count - 1 downto Start do
  begin
    TObject(Masks[Idx]).Free;
    Masks.Delete(Idx);
  end;
end;

Üretilen Win64 kodunda döngü koşulundaki Count ile gövde içinde okunan Count tek bir stack yuvasını paylaşıyordu. Koşul girişte, henüz hiçbir şey onu yazmamışken o yuva ile karşılaştırıyordu ve Delete sonrasında onu tazeleyen hiçbir şey yoktu. Bir grup kendi soft maskesini hiç kurmamışsa gövde yine de koşuyor ve boş bir listeden -1 numaralı girdiyi istiyordu; böylece 64 bitlik derlemelerde böyle bir transparency grubu taşıyan her sayfa EListError ile düşüyordu. Aynı kaynağın Win32 kodu doğrudu ve v2.770.1 döngüyü değiştirdi

Renderer temizliğinde HotPDF Win64 codegen tuzağı: TList.Count'ı yeniden okuyan bir while döngüsü, koşul ile gövde arasında tek bir stack yuvası paylaştı; dcc64 Delete sonrasında yuvayı hiç tazelemedi, boş transparency grupları -1 numaralı girdiyi free edip EListError fırlattı ve düzeltme, sınırları bir kez değerlendirilen bir for downto döngüsüdür
pratik ders, kök nedenden ucuzdur: sabit sınırlı downto döngüleri bayatlayamaz ve dcc64 takımı koşturmadan renderer işi bitmiş sayılmaz

Bunu minimal bir yeniden üretime indirgemedik ve DropMasksWhile gibi küçük bir bağımsız döngü büyük olasılıkla doğru derlenir; çevredeki try/finally ile iç içe döngüler önemli görünüyor. Bunu her Win64 derleyicisinin bilinen bir kusuru olarak değil, tek bir derleyici sürümünde gözlemlediğimiz kod üretimi olarak ele alın. Pratik ders daha ucuzdur: koşulu gövde küçültürken bir koleksiyonun sayısını yeniden okuyan bir döngüyü sabit sınırlı bir for ... downto olarak yeniden yazmaya değer ve renderer değişikliklerinin salt Win32 değil tam bir Win64 test koşusuna ihtiyacı vardır

Yalnızca optimize edilmiş Win64 derlemesinin gösterdiği çökmeyi bulmak

Başarısızlık yalnızca optimize edilmiş Win64 derlemesinde yeniden üretildi; dolayısıyla konum, IDE dışındaki araçlardan geldi. Küçük bir sonda programı AddVectoredExceptionHandler ile bir vectored exception handler kaydetti, ilk istisyada stack'i RtlCaptureStackBackTrace ile yakaladı ve dönüş adreslerini, linker'ın -GD ile yazdığı ayrıntılı map dosyasını kullanarak fonksiyon adlarına çevirdi. O fonksiyonu disassemble etmek ise karşılaştırmanın, yalnızca döngü gövdesinin içinde yazılan bir stack yuvasını — [rbp+0x298] — okuduğunu gösterdi. Bir derleyiciyi suçlamadan önce isteyeceğiniz kanıt düzeyi budur ve release bir derlemede adım adım ilerlemekten az zaman aldı

High(Int64) bir Double için neden güvenli bir üst sınır değildir?

Bir Double, High(Int64)'i temsil edemez: 9223372036854775807'yi Double'a çevirmek tam olarak 2^63'e, en büyük Int64'ün bir fazlasına yuvarlanır. Win64'te o dönüşüm karşılaştırmanın içinde olur; dolayısıyla D <= High(Int64), D = 2^63 için True'dur ve ardından gelen Round ya da Trunc taşar

Win32 bunu, Power sorununu sakladığı aynı nedenle saklar. Karşılaştırma, 64 bitlik mantisli 80 bit Extended hassasiyetinde koşar; orada High(Int64) tamdır ve 2^63 doğru biçimde büyük karşılaştırılır. Win64'ün yaslanacağı daha geniş bir tip yoktur. Aralık dışı dönüşüm de güzel değildir: Win64 testlerimizde Round(2^63), exInvalidOp maskeli de maskesiz de, sessiz bir işaret çevirmesi olan Low(Int64) döndürdü. Win32 maskeliyken aynı değeri döndürür, maskesizken EInvalidOp fırlatır

HotPDF Int64 sınır tuzağı: bir Double, High(Int64)'i temsil edemez; Win64 karşılaştırması sınırı 2^63e yukarı çevirir, 2^63e eşit D denetimi geçer ve Round sessizce Low(Int64) döndürür; Win32 ise 80 bit Extended'ta karşılaştırır, sınır orada tamdır ve aynı karşılaştırma False olur
tek bir dönüşüm hatanın tamamıdır: sınır, dışladığınız değerin üzerine yuvarlanır; o yüzden tavanı katı küçüktür karşılaştırmasıyla bir literal olarak yazın
İfadeWin32Win64
Power(10, N), N = 201E201.0000000200408773E20
Power(10, 100), istisnalar maskeli (Delphi 12+ varsayılanı)1E100+Inf
Power(10, 100), exOverflow maskesiz1E100EOverflow
D <= High(Int64), D = 2^63FalseTrue
Round(2^63), exInvalidOp maskesizEInvalidOpLow(Int64)

HotPDF bunla, belge iş değerlerinin ardındaki JSON okuyucusunda karşılaştı. JSON sayılara aralık sınırı koymaz ve eski serileştirici, Frac(Value) = 0 olan her değeri Round ile bir integer'a çeviriyordu; böylece tamamen meşru bir 1e19, maskeye bağlı olarak ya yanlış bir integer'a ya bir istisyona dönüşüyordu. v2.770.169'dan beri tam sayı, yalnızca Int64'e sığdığında integer yazılır, geri kalan her şey kayan nokta metnini korur ve integer getter'lar, aralık dışı değerler için sarmalanmış bir değer yerine çağıranın varsayılanını döndürür

const
  TwoPow63 = 9223372036854775808.0;   // 2^63, Double'da ve Extended'ta tam

function TryDoubleToInt64(const Value: Double; out R: Int64): Boolean;
begin
  R := 0;
  Result := not IsNan(Value) and not IsInfinite(Value) and
    (Frac(Value) = 0) and (Value >= -TwoPow63) and (Value < TwoPow63);
  if Result then
    R := Trunc(Value);
end;

function JsonNumberText(const Value: Double): string;
var
  R: Int64;
begin
  // Çağıranlar önce NaN ile sonsuzları reddeder: JSON için yazımı yoktur
  if TryDoubleToInt64(Value, R) then
    Result := IntToStr(R)
  else
  begin
{$IFDEF FPC}
    Str(Value:24, Result);       // FPC Win64 ffGeneral 15 hanede durur
    Result := Trim(Result);
{$ELSE}
    Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
  end;
end;

Üst sınır, katı bir < ile yazılmış 9223372036854775808.0 literalidir. O sabit 2^63'tür ve Double'da da Extended'ta da tamdır; dolayısıyla karşılaştırma her platformda aynı anlamı taşır. Alt sınır, -2^63 tam olarak Low(Int64) olduğundan >= kullanabilir. IsNan ile IsInfinite sınamalarını kısa devre değerlendirmeyle önce yapmak, NaN ile sonsuzları Frac'ten ve host maskesini kaldırmışsa EInvalidOp fırlatabilecek karşılaştırmalardan uzak tutar

Win64'te float-metin dönüşümü size gerçekten kaç hane verir?

İstediğinizden azını, üç derleyiciden ikisinde. Free Pascal 3.3.1'in Win64'teki FloatToStrF(Value, ffGeneral, 17, 0)'u 15 anlamlı hanede durur; böylece 1/3, 0.333333333333333 olarak döner ve iki farklı Double değeri aynı metne serileşebilir. Str(Value:24, Text) ardından Trim, bilimsel gösterimde 17 anlamlı hane üretir — aynı değer için 3.3333333333333331E-001 — ve yerel ayardan bağımsız olarak ondalık ayracı olarak daima nokta yazar. FPC üzerindeki HotPDF derleme matrisinizin parçasıysa, HotPDF Free Pascal ve Lazarus Win64 destek notları platform farklarının gerisini kapsar

Delphi 17 haneli isteği kabul eder ama iki Delphi hedefi çıktıda hâlâ anlaşamaz: FloatToStrF(0.1, ffGeneral, 17, 0), Win32'de 0.10000000000000001, Win64'te 0.1 verir. Win64 RTL, biçimlendirirken de ayrıştırırken de son hane yuvarlama hatası ekleyebilir; dolayısıyla daha çok hane aralığı daraltır ama her Double bit örüntüsünün metin gidiş-dönüşünden sağ çıktığını garanti etmez. HotPDF'in belgeleri böyle bir vaat vermez; sizinki de kendi doğru yuvarlayan biçimlendiricinizi ve ayrıştırıcınızı göndermedikçe vermemelidir. Alman ya da Fransız bir yerel ayarın JSON'a virgül yazmaması için TFormatSettings.Invariant geçirin ya da ayracı eski Delphi sürümlerinde kendiniz değiştirin

Assert.AreEqual Win64'te neden derlemeyi bırakır?

Dinamik bir dizi üzerinde Assert.AreEqual(3, Length(Arr)) Win32 için derlenir ve Win64 için E2532 ile düşer: "Couldn't infer generic type argument from different argument types". Nedeni, dinamik bir dizinin Length'inin Win64'te NativeInt döndürmesidir. Bir yanda Integer literal, öte yanda 64 bitlik bir NativeInt varken DUnitX'in generic Assert.AreEqual<T>'si tek bir T üzerinde anlaşamaz ve derleme durur

TList.Count, property'nin NativeInt olduğu Delphi 12'den beri aynı hatayı tetikler; Delphi 11 onu hâlâ Integer olarak bildirir. Bir string'in Length'i her iki platformda da Integer döndürür ve etkilenmez; hatanın bazı test unit'lerinde görünip bazılarında görünmemesi bundandır. Tip argümanını açıkça yazın — Assert.AreEqual<NativeInt>(3, Length(Arr)) — ve commit etmeden önce test projesini dcc64 ile derleyin. Yalnızca Win32 için derlenen bir takım, Win64 derlemesinin bozuk olduğunu başkası denemeden söylemez

Delphi sayısal kodu için Win64 taşıma kontrol listesi

  • Integer argümanlı Power( ile IntPower( çağrılarını arayın; Double tipli değerler geçirin ya da sınırlı onun kuvvetlerini kendiniz kurun
  • Sayısal testleri, SetExceptionMask üzerinden exOverflow ile exInvalidOp'yi kaldırarak hem Win32'de hem Win64'te en az bir kez koşturun
  • Int64 üst sınırını < 9223372036854775808.0 olarak yazın, asla <= High(Int64) değil, ve her karşılaştırmadan önce NaN ile sonsuzları reddedin
  • Ayrıştırılmış bir sayıyı yalnızca Frac sıfır olduğu için Int64'e çevirmeyin; JSON sayıları çok daha büyük olabilir
  • Silme yaparken Count'u yeniden okuyan while döngülerini sabit sınırlı for ... downto döngüleri olarak yeniden yazın
  • FPC Win64'te 15 anlamlı haneden fazlasına ihtiyaç duyduğunuzda Str(Value:24, Text) kullanın
  • Length ile Count assertion'ları için Assert.AreEqual<NativeInt> kullanın ve commit etmeden önce testleri dcc64 ile derleyin
  • Herhangi bir parser ya da renderer değişikliğinden sonra tam regresyon takımını Win32'de de Win64'te de koşturun, yalnızca birinde değil

Burada anlatılan kütüphane tarafı düzeltmelerin hepsi HotPDF'te v2.770.169'dan beri var; böylece SVG içe aktarma, XPS dönüşümü, transparency rendering ve JSON iş işleme, Win64'te artık Win32'dekiyle aynı davranıyor. Delphi ya da C++Builder'dan her iki platform için de PDF üretiyor ya da işliyorsanız indirmeler ile tam özellik listesi HotPDF Delphi PDF component sayfasındadır