Teknik Makale

32 bit Delphi'den DCC64'e _ftol Inline Assembly Taşıma (Porting)

32 bit Delphi'deki orijinal _ftol deyimi zekice bir tek satırlık koda (one-liner) benzer: x87 FPU kontrol kelimesini manipüle etmek, FPU yığınındaki değeri kırpmak (truncate) ve sonucu çıkarmak için satır içi (inline) assembly'ye düşen bir Pascal işlev sarmalayıcısı. DCC32 altında uzun süre sorunsuz derlendi; tam da bu yüzden kimse sorgulamadan bu kadar çok eski grafik ve PDF birimlerinde son buldu

Derleme hedefini 64 bite değiştirdiğinizde, derleyici E1025 Unsupported language feature: 'ASM' ile sonlanır. Bu hata bir uyumluluk uyarısı değildir. Bu, montajın daha önce ne kadar iyi çalıştığından bağımsız olarak DCC64'ün rutini hiçbir şekilde derlemeyeceği anlamına gelir

32 bitlik orijinal genellikle şuna benzerdi:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Pascal'ın begin...end gövdesi içindeki o asm bloğu, tam olarak DCC64'ün reddettiği şeydir. İki derleyicinin montaja (assembly) nerede izin verildiğine dair farklı kuralları vardır ve bu sınır önemlidir

DCC64 sınırı neden farklı çiziyor?

DCC32, sıradan Pascal rutinleri içinde satır içi (inline) assembly'ye izin verir. Derleyici, 32 bitlik çağrı kuralını (calling convention) bilir ve yerel değişkenlerin ve parametrelerin nerede yaşadığı hakkında mantık yürütebilir, bu nedenle yığın çerçevesine (stack frame) adıyla ulaşan assembly parçalarına tolerans gösterir. DCC64 ise daha katı bir tutum benimser: assembly, tüm gövdenin assembly olduğu ve çağrı kuralının açıkça ele alındığı ayrılmış bir assembler işlevi içinde olmalıdır. Karma Pascal-artı-asm (Pascal-plus-asm) kesinlikle desteklenmez

Altta yatan neden mimaridir. 64 bit Windows çağrı kuralında (Microsoft ABI), ilk dört parametre tam sayı türleri için RCX, RDX, R8 ve R9'a veya kayan nokta (floating-point) türleri için XMM0'dan XMM3'e ulaşır. Normal parametre iletiminde x87 FPU katılımı yoktur; x87 teknik olarak mevcuttur ancak ABI bunu argüman taşıma için kullanmaz. Bir değerin "FPU yığınında" olduğunu varsayan bir assembly, 64 bit ABI'nin asla yaratmadığı bir durum hakkında mantık yürütüyor demektir

Yani eski parçanın (fragment) sadece bir sözdizimi sorunu yoktur. DCC64 bunu kabul etse bile, yazmaç (register) varsayımları yanlış olacaktır

Uygun bir 64 bit assembler sürümü yazma

İkili (binary) uyumluluk için bir _ftol sembolünü cdecl kuralı ile gerçekten dışa aktarmanız gerektiğinde, işlev saf bir assembler rutini olarak yazılmalıdır. 64 bit ABI altında bir Double parametresi XMM0'a gelir ve dönüş anında tam sayı sonucu RAX'ta olmalıdır. .NOFRAME yönergesi, DCC64'e rutinin kendi yığınını (stack) yönettiğini söyler; bu, bu kadar kısa bir yaprak işlevi (leaf function) için uygundur:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

CVTTSD2SI, çift hassasiyetli bir kayan noktayı sıfıra doğru kırpma (truncation) ile işaretli bir tam sayıya dönüştürmek için kullanılan bir SSE2 talimatıdır ve _ftol'ün yapması beklenen şey de tam olarak budur. Tek bir talimattır, parametreyi doğrudan ABI'nin bıraktığı yerden alır ve sonucu ABI'nin beklediği yere koyar. FPU kontrol kelimesi (control-word) hokkabazlığına gerek yoktur

Girdi 32 bitlik işaretli bir tam sayının aralığını aşarsa, CVTTSD2SI'nin belirsiz tam sayı (indefinite integer) değerini ($80000000) döndürdüğüne dikkat edin. Bu, sınır dışı (out-of-range) girdideki x87 fistp ile aynı davranıştır. Taşıma işlemini tamamlandı ilan etmeden önce, çağıranlarınızın (callers) bu tür değerler üretip üretemeyeceğini doğrulamaya değer

Trunc'ın (Kırpmanın) daha iyi bir yanıt olduğu durumlar

Yukarıdaki assembler sürümü yalnızca gerçek bir ikili uyumluluk gereksiniminiz olduğunda yazmaya değerdir: harici bir çağıran, _ftol sembolünü belirli bir çağrı kuralı ile bekler ve o çağıranları değiştiremezsiniz. Bu durum nadir görülür. Çoğu zaman, _ftol yalnızca aynı birim içinde kullanılan özel (private) bir yardımcı idi ve adına veya kuralına yönelik herhangi bir dış bağımlılık (external dependency) yoktur

Bu durum için, onu düz (plain) Pascal ile değiştirin:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc sıfıra doğru kırpar; bu da _ftol'ün x87 kontrol kelimesi kırpma moduna ayarlanmış haldeyken yaptığı şeyle eşleşir. Hem DCC32 hem de DCC64'te değişiklik yapılmadan derlenir. Derleyici her hedef için uygun talimatı üretir: x64'te zaten el ile yazılmış sürümle aynı talimat olan CVTTSD2SI'yi yayacaktır (emit). Aynı davranışı, platform koşulları (conditionals) ve bakımını yapacağınız bir assembly olmadan elde edersiniz

Kontrol etmeye değer tek semantik fark şudur: Trunc, girdi bir NaN (Bir Sayı Değil) veya sonsuz olduğunda Delphi'nin varsayılan yapılandırmasında bir EInvalidOp istisnası (exception) fırlatır. Orijinal koddaki x87 fistp ise hiçbir şey fırlatmadan sadece bir bit kalıbı (bit pattern) yazıyordu. Eğer kodunuz bu işleve olağandışı kayan nokta değerleri besliyorsa ve eski davranış sessiz idiyse, Trunc'ı çağırmadan önce Math'den IsNaN ve IsInfinite ile koruma sağlayın

Her iki hedef de aktif kaldığında koşullu derleme

Bazı projeler hem 32 bit hem de 64 bitlik ikili dosyaları (binaries) sağlamaya devam etmelidir. Eğer orijinal assembler sürümü 32 bit için saklanmalı ve 64 bit için yeni bir uygulama sağlanmalıysa, CPUX64 koşullusunu (conditional) kullanın:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bit path: DCC32 accepts inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

Bu en az seviyedeki mekanik düzeltmedir ve geçici olarak ele alınmaya değer. Tek amacı kayan noktadan tam sayıya kırpma olan bir yardımcıda (helper) mimariye özgü assembly taşıyan bir kod tabanı, gereksiz bir borç taşımaktadır. Eski uygulamanın FPU yan etkilerine (side-effects) hiçbir şeyin bağlı olmadığını onayladığınızda 32 bit dalı tamamen ortadan kalkabilir

Eğer işlev birden fazla birimde kullanılan bir bileşende görünüyorsa, nasıl geçiş (migrate) yapacağınıza karar vermeden önce tüm kod tabanında _ftol araması yapın. Bu ada sahip bir sembol birden fazla yerde bildirilebilir (declared); bağlayıcı (linker) birini seçer ve diğerlerini sessizce yoksayar, bu da bir kopyayı düzeltebileceğiniz ve yine de dokunulmamış farklı bir kopyaya bağlanabileceğiniz anlamına gelir