32 位元 Delphi 裡那個原始的 _ftol 慣用寫法,存在的原因是 C 物件檔。當一個 unit 連結 32 位元 C 編譯器產出的 .obj 檔時,這些物件在每個浮點數轉整數的轉型處都會呼叫執行階段輔助函式 _ftol,數值留在 x87 堆疊頂端的 ST(0) 裡,而不是以參數傳入。因此 Delphi 的替身函式宣告了一個虛設的 Double 參數,用兩行內嵌組語把 ST(0) 彈進該參數,然後在 Pascal 裡截斷它。這種寫法在 DCC32 下順利編譯了很久,也正是它因此混進這麼多早期的圖形與 PDF unit、卻沒有人質疑的原因
將建置目標切換到 64 位元時,編譯器會以 E1025 Unsupported language feature: 'ASM' 終止編譯。這個錯誤不是相容性警告。它意味著 DCC64 根本不會編譯這個常式,無論這些組語以前運作得多麼良好
32 位元的原貌通常看起來像這樣:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Pascal 的 begin...end 本體內包著那個 asm 區塊,這正是 DCC64 拒絕接受的形式。這兩款編譯器對於何處允許使用組語有著不同的規則,而這條界線至關重要
為何 DCC64 的界線畫得不同
DCC32 允許在普通的 Pascal 常式內使用內嵌組語。編譯器了解 32 位元的呼叫慣例 (calling convention),且能推斷區域變數與參數的位置,因此它容許能透過名稱觸及堆疊框架的組語片段。DCC64 則採取了更嚴格的立場:組語必須位於專屬的組譯函式中,即整個本體都是組語,並且明確處理呼叫慣例。完全不支援混合 Pascal 加上 asm
根本原因是架構上的不同。在 64 位元 Windows 的呼叫慣例 (Microsoft ABI) 中,前四個參數如果是整數型別會從 RCX、RDX、R8 與 R9 傳入,如果是浮點數則會從 XMM0 到 XMM3 傳入。正常的參數傳遞過程完全沒有 x87 FPU 的參與;技術上 x87 是可用的,但 ABI 並不使用它來傳遞引數。假設某個數值在「FPU 堆疊上」的組語,是在推論一個 64 位元 ABI 從未創造過的狀態
因此,舊的片段不僅有語法上的問題。即使 DCC64 接受了它,對暫存器的假設也會是錯誤的
撰寫適當的 64 位元組譯版本
先確認 64 位元環境到底還需不需要 _ftol。Win64 的 C 編譯器——MSVC 和以 Clang 為基礎的 C++Builder 工具鏈都一樣——會用 SSE2 指令行內完成浮點數到整數的轉換,不會發出對 _ftol 的呼叫,所以 64 位元物件檔很少參照這個符號。當您確實需要為了二進位相容性、以 cdecl 慣例匯出 _ftol 符號時,函式必須寫成純組語常式。在 64 位元 ABI 下,Double 參數經 XMM0 傳入,Integer 結果則經 EAX(RAX 的低半部)回傳。.NOFRAME 指示詞告訴 DCC64 這個常式自行管理堆疊,對這麼短的葉函式 (leaf function) 來說正合適:
function _ftol: Integer; cdecl;
// 依 64 位元 ABI,Double 值預期經 XMM0 傳入
asm
.NOFRAME
cvttsd2si eax, xmm0 // 截斷為 32 位元整數,結果置於 eax
end;
CVTTSD2SI 是一個將雙精確度浮點數轉換為帶號整數 (向零截斷) 的 SSE2 指令,而這正是 _ftol 應該做的事。這只是一個指令,它直接從 ABI 留下的位置取得參數,並將結果放在 ABI 預期的位置。不需要去耍弄 FPU 控制字組
目的地用 32 位元暫存器這件事很重要。若輸入值超出 32 位元帶號整數的範圍,以 EAX 為目的地的 CVTTSD2SI 會傳回整數不定值(integer indefinite value,即 $80000000)。若改寫成 RAX,就會變成 64 位元轉換,落在 231 與 263 之間的輸入會以它們低 32 位元的偶然值傳回。這與 x87 fistp 在超出範圍輸入下的行為相同。在宣告移植完成之前,值得確認您的呼叫端是否會產生這類數值
何時 Trunc 會是更好的解答
上述的組譯版本只在您有實際的二進位相容性需求時才值得撰寫:某些外部呼叫端預期以特定的呼叫慣例存取名為 _ftol 的符號,且您無法修改這些呼叫端。這種情況並不常見。大多數時候,_ftol 只是僅在同一個 unit 內使用的私有輔助函式,根本沒有對其名稱或慣例的外部相依性
若是這種情況,請以純 Pascal 來取代它:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc 會向零截斷,這與當 x87 控制字組設定為截斷模式時 _ftol 所做的事相符。它在 DCC32 與 DCC64 上無需修改即可編譯。編譯器會為每個目標環境產生適當的指令:在 x64 上,它通常還是會發出 CVTTSD2SI,與手寫版本的指令相同。您能獲得完全一致的行為、無需平台條件式編譯,也無需維護任何組語
有一個值得檢查的語意差異:在 Delphi 的預設設定下,當輸入值為 NaN 或無限大時,Trunc 會引發 EInvalidOp 例外狀況。而原始程式碼中的 x87 fistp 只是寫入一個位元模式而不會引發任何問題。如果您的程式碼會餵給這個函式不尋常的浮點數值,且舊的行為是默默吞下,那麼在呼叫 Trunc 之前,請先使用 Math 提供的 IsNaN 與 IsInfinite 進行防護
當兩個目標環境都需保持活躍時的條件式編譯
有些專案必須繼續發布 32 位元與 64 位元的二進位檔。如果原始的組譯版本必須為 32 位元保留,同時又需為 64 位元提供新的實作,請使用 CPUX64 條件式:
function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
Result := Trunc(f);
{$ELSE}
// 32 位元路徑:DCC32 接受內嵌組語
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
{$ENDIF}
end;
這是最底限度的機械式修補,而且它值得被視為暫時性的。在程式碼基底中保留一個純粹只為了將浮點數截斷成整數、且依賴特定架構的組語輔助函式,等於是背負著不必要的技術債。一旦您確認沒有任何程式會依賴舊版實作中 FPU 的副作用後,這個 32 位元的分支就可以完全移除了
如果這個函式出現在被多個 unit 所使用的元件中,請先搜尋整個程式碼基底中的 _ftol,然後再決定該如何移植。同名的符號可能會宣告在不只一個地方;連結器會挑選其中一個並默默忽略其他的,這意味著您可能修好了一份複本,卻仍連結到另一份尚未修改的版本