Technischer Artikel

Portierung von _ftol-Inline-Assembly von 32-Bit-Delphi auf DCC64

Das ursprüngliche _ftol-Idiom in 32-Bit-Delphi sieht aus wie ein cleverer Einzeiler: ein Pascal-Funktions-Wrapper, der auf Inline-Assembly zurückgreift, um das x87-FPU-Steuerwort zu manipulieren, den Wert auf dem FPU-Stack abzuschneiden und das Ergebnis zu poppen. Es wurde lange Zeit problemlos unter DCC32 kompiliert, weshalb es in so vielen älteren Grafik- und PDF-Units landete, ohne dass es jemand in Frage stellte

Ändern Sie das Build-Ziel auf 64-Bit, bricht der Compiler mit E1025 Unsupported language feature: 'ASM' ab. Dieser Fehler ist keine Kompatibilitätswarnung. Er bedeutet, dass DCC64 die Routine überhaupt nicht kompiliert, unabhängig davon, wie gut die Assembly vorher funktioniert hat

Das 32-Bit-Original sah normalerweise etwa so aus:

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

Genau dieser asm-Block innerhalb eines Pascal-begin...end-Rumpfs wird von DCC64 abgelehnt. Die beiden Compiler haben unterschiedliche Regeln dafür, wo Assembly erlaubt ist, und die Grenze ist wichtig

Warum DCC64 die Grenze anders zieht

DCC32 erlaubt Inline-Assembly in gewöhnlichen Pascal-Routinen. Der Compiler kennt die 32-Bit-Aufrufkonvention und kann ableiten, wo lokale Variablen und Parameter liegen, sodass er Assembly-Fragmente toleriert, die namentlich auf den Stack-Frame zugreifen. DCC64 nimmt eine strengere Position ein: Assembly muss in einer dedizierten Assembler-Funktion stehen, bei der der gesamte Rumpf aus Assembly besteht und die Aufrufkonvention explizit gehandhabt wird. Gemischtes Pascal-plus-Asm wird überhaupt nicht unterstützt

Der zugrunde liegende Grund ist architektonischer Natur. Bei der 64-Bit-Windows-Aufrufkonvention (Microsoft ABI) kommen die ersten vier Parameter in RCX, RDX, R8 und R9 für Integer-Typen oder in XMM0 bis XMM3 für Fließkommazahlen an. Bei der normalen Parameterübergabe ist die x87-FPU nicht beteiligt; x87 ist technisch verfügbar, aber die ABI verwendet sie nicht für den Argumententransport. Assembly, das davon ausgeht, dass ein Wert "auf dem FPU-Stack liegt", basiert auf einem Zustand, den die 64-Bit-ABI nie erzeugt

Das alte Fragment hat also nicht nur ein Syntaxproblem. Selbst wenn DCC64 es akzeptieren würde, wären die Registerannahmen falsch

Schreiben einer ordnungsgemäßen 64-Bit-Assembler-Version

Wenn Sie aus Gründen der Binärkompatibilität wirklich ein _ftol-Symbol mit cdecl-Konvention exportieren müssen, muss die Funktion als reine Assembler-Routine geschrieben werden. Unter der 64-Bit-ABI kommt ein Double-Parameter in XMM0 an, und das Integer-Ergebnis muss bei der Rückgabe in RAX liegen. Die Direktive .NOFRAME teilt DCC64 mit, dass die Routine ihren eigenen Stack verwaltet, was für eine derart kurze Leaf-Funktion angemessen ist:

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 ist die SSE2-Anweisung zur Konvertierung einer Fließkommazahl mit doppelter Genauigkeit in eine vorzeichenbehaftete Ganzzahl, wobei in Richtung Null abgeschnitten wird, was genau das ist, was _ftol tun soll. Es ist eine einzige Anweisung, nimmt den Parameter direkt von dort, wo die ABI ihn abgelegt hat, und platziert das Ergebnis dort, wo die ABI es erwartet. Es ist kein Jonglieren mit dem FPU-Steuerwort erforderlich

Beachten Sie, dass CVTTSD2SI den unbestimmten Integer-Wert ($80000000) zurückgibt, wenn die Eingabe den Bereich einer vorzeichenbehafteten 32-Bit-Ganzzahl überschreitet. Das ist dasselbe Verhalten wie bei fistp auf x87 bei Bereichsüberschreitungen. Es lohnt sich zu überprüfen, ob Ihre Aufrufer solche Werte erzeugen können, bevor Sie die Migration für abgeschlossen erklären

Wann Trunc die bessere Antwort ist

Das Schreiben der obigen Assembler-Version lohnt sich nur, wenn Sie eine tatsächliche Anforderung an die Binärkompatibilität haben: Ein externer Aufrufer erwartet das Symbol _ftol mit einer bestimmten Aufrufkonvention, und Sie können diese Aufrufer nicht ändern. Diese Situation ist ungewöhnlich. Meistens war _ftol ein privater Helfer, der nur innerhalb derselben Unit verwendet wurde, und es gibt überhaupt keine externe Abhängigkeit von seinem Namen oder seiner Konvention

Ersetzen Sie ihn in diesem Fall durch reines Pascal:

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

Trunc schneidet in Richtung Null ab, was dem entspricht, was _ftol tat, wenn das x87-Steuerwort auf den Abschneidemodus gesetzt war. Es kompiliert ohne Modifikation unter DCC32 und DCC64. Der Compiler generiert die entsprechende Anweisung für jedes Ziel: Auf x64 wird er typischerweise ohnehin CVTTSD2SI ausgeben, dieselbe Anweisung wie bei der handgeschriebenen Version. Sie erhalten identisches Verhalten, keine Plattformbedingungen und kein zu pflegendes Assembly

Der einzige semantische Unterschied, den es zu prüfen lohnt: Trunc löst in der Standardkonfiguration von Delphi eine EInvalidOp-Ausnahme aus, wenn die Eingabe ein NaN oder Unendlich ist. Das x87-fistp im ursprünglichen Code schrieb lediglich ein Bitmuster, ohne etwas auszulösen. Wenn Ihr Code ungewöhnliche Fließkommawerte in diese Funktion einspeist und das alte Verhalten stumm war, sichern Sie dies vor dem Aufruf von Trunc mit IsNaN und IsInfinite aus Math ab

Bedingte Kompilierung, wenn beide Ziele aktiv bleiben

Einige Projekte müssen weiterhin sowohl 32-Bit- als auch 64-Bit-Binärdateien ausliefern. Wenn die ursprüngliche Assembler-Version für 32-Bit beibehalten und eine neue Implementierung für 64-Bit bereitgestellt werden muss, verwenden Sie die Bedingung CPUX64:

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;

Das ist die minimale mechanische Lösung, und es lohnt sich, sie als vorübergehend zu betrachten. Eine Codebasis, die architekturspezifisches Assembly in einem Helfer mitführt, dessen einziger Zweck das Abschneiden von Fließkommazahlen zu Ganzzahlen ist, birgt unnötige Schulden. Der 32-Bit-Zweig kann vollständig entfallen, sobald Sie bestätigen, dass nichts von den FPU-Nebeneffekten der alten Implementierung abhängt

Wenn die Funktion in einer Komponente erscheint, die über mehrere Units hinweg verwendet wird, durchsuchen Sie die gesamte Codebasis nach _ftol, bevor Sie sich für eine Migration entscheiden. Ein Symbol mit diesem Namen kann an mehr als einer Stelle deklariert sein; der Linker wählt eines aus und ignoriert die anderen stillschweigend. Das bedeutet, dass Sie möglicherweise eine Kopie korrigieren und dennoch gegen eine andere linken, die nicht berührt wurde