Ο αρχικός ιδιωματισμός (idiom) _ftol στο 32-bit Delphi μοιάζει με μια έξυπνη εντολή μιας γραμμής (one-liner): ένα περιτύλιγμα (wrapper) συνάρτησης Pascal που καταφεύγει στην (drops into) inline assembly για να χειριστεί τη λέξη ελέγχου (control word) FPU του x87, να περικόψει (truncate) την τιμή στη στοίβα (stack) του FPU και να βγάλει (pop) το αποτέλεσμα. Μεταγλωττίστηκε (built) ωραία με το DCC32 για πολύ καιρό, γι' αυτό ακριβώς κατέληξε σε τόσες πολλές παλαιότερες μονάδες (units) γραφικών και PDF χωρίς κανείς να το αμφισβητήσει
Αλλάξτε τον στόχο (target) μεταγλώττισης (build) σε 64-bit και ο μεταγλωττιστής (compiler) τερματίζει με E1025 Unsupported language feature: 'ASM'. Αυτό το σφάλμα δεν είναι προειδοποίηση συμβατότητας (compatibility warning). Σημαίνει ότι το DCC64 δεν θα μεταγλωττίσει καθόλου τη ρουτίνα, ανεξάρτητα από το πόσο καλά λειτουργούσε η assembly (συναρμολόγηση) στο παρελθόν
Το πρωτότυπο 32-bit συνήθως έμοιαζε κάπως έτσι:
function _ftol: Integer;
var
ctrlWord: Word;
begin
asm
wait
fnstcw ctrlWord
wait
mov ax, ctrlWord
or ah, $0c
mov ctrlWord, ax
fldcw ctrlWord
fistp dword ptr [ebp-$04]
fldcw ctrlWord
end;
end;
Αυτό το μπλοκ asm μέσα σε ένα σώμα begin...end του Pascal είναι ακριβώς αυτό που αρνείται το DCC64. Οι δύο μεταγλωττιστές έχουν διαφορετικούς κανόνες σχετικά με το πού επιτρέπεται η assembly και το όριο έχει σημασία
Γιατί το DCC64 τραβάει τη γραμμή διαφορετικά
Το DCC32 επιτρέπει (permits) inline assembly (ενσωματωμένη assembly) μέσα σε συνηθισμένες (ordinary) ρουτίνες Pascal. Ο μεταγλωττιστής γνωρίζει τη σύμβαση κλήσης (calling convention) των 32-bit και μπορεί να αιτιολογήσει (reason) πού ζουν οι τοπικές μεταβλητές (local variables) και οι παράμετροι (parameters), επομένως ανέχεται (tolerates) αποσπάσματα assembly που φτάνουν στο πλαίσιο στοίβας (stack frame) με το όνομα (by name). Το DCC64 παίρνει μια πιο αυστηρή θέση: η assembly πρέπει να είναι σε μια αποκλειστική (dedicated) συνάρτηση assembler, μια συνάρτηση όπου ολόκληρο το σώμα είναι assembly και η σύμβαση κλήσης (calling convention) αντιμετωπίζεται ρητά (explicitly). Ο μικτός κώδικας (mixed) Pascal συν asm δεν υποστηρίζεται καθόλου
Η υποκείμενη (underlying) αιτία είναι αρχιτεκτονική. Στη σύμβαση κλήσης Windows 64-bit (Microsoft ABI), οι πρώτες τέσσερις παράμετροι (parameters) φτάνουν στα RCX, RDX, R8 και R9 για ακέραιους τύπους (integer types), ή στα XMM0 έως XMM3 για τύπους κινητής υποδιαστολής (floating-point). Δεν υπάρχει καμία συμμετοχή του x87 FPU στην κανονική μεταβίβαση παραμέτρων (parameter passing)· το x87 είναι τεχνικά διαθέσιμο, αλλά το ABI δεν το χρησιμοποιεί για τη μεταφορά (transport) ορισμάτων (argument). Μια assembly που προϋποθέτει ότι μια τιμή είναι "στη στοίβα FPU" αιτιολογεί μια κατάσταση (state) που το ABI 64-bit δεν δημιουργεί ποτέ
Έτσι το παλιό απόσπασμα δεν έχει απλώς συντακτικό πρόβλημα (syntax problem). Ακόμα κι αν το δεχόταν το DCC64, οι υποθέσεις στους καταχωρητές (register assumptions) θα ήταν λάθος
Γράφοντας μια σωστή έκδοση assembler 64-bit
Όταν χρειάζεστε πραγματικά (genuinely) να εξαγάγετε ένα σύμβολο _ftol με τη σύμβαση cdecl για δυαδική συμβατότητα (binary compatibility), η συνάρτηση πρέπει να γραφτεί ως καθαρή ρουτίνα assembler (pure assembler routine). Κάτω από το 64-bit ABI μια παράμετρος Double φτάνει στο XMM0 και το ακέραιο (integer) αποτέλεσμα πρέπει να είναι στο RAX κατά την επιστροφή. Η οδηγία .NOFRAME λέει στο DCC64 ότι η ρουτίνα διαχειρίζεται τη δική της στοίβα (stack), η οποία είναι κατάλληλη για μια τέτοια σύντομη συνάρτηση "φύλλο" (leaf function):
function _ftol(val: Double): Integer; cdecl; assembler;
asm
.NOFRAME
cvttsd2si eax, xmm0
end;
Η CVTTSD2SI είναι η εντολή (instruction) SSE2 για τη μετατροπή (converting) μιας κινητής υποδιαστολής (float) διπλής ακρίβειας σε έναν ακέραιο με πρόσημο (signed integer) με περικοπή (truncation) προς το μηδέν, που είναι ακριβώς αυτό που υποτίθεται ότι κάνει η _ftol. Είναι μία εντολή, παίρνει την παράμετρο κατευθείαν από εκεί που την άφησε το ABI και τοποθετεί το αποτέλεσμα εκεί που το περιμένει το ABI. Δεν χρειάζεται juggling (ταχυδακτυλουργία) της λέξης ελέγχου (control-word) του FPU
Σημειώστε (Note) ότι εάν η είσοδος υπερβαίνει το εύρος ενός 32-bit ακέραιου με πρόσημο (signed integer), η CVTTSD2SI επιστρέφει την τιμή αόριστου ακέραιου (integer indefinite value) ($80000000). Αυτή είναι η ίδια συμπεριφορά (behavior) με το fistp του x87 σε είσοδο εκτός εύρους (out-of-range). Αξίζει να επιβεβαιώσετε (confirming) εάν οι καλούντες σας (callers) μπορούν να παράγουν (produce) τέτοιες τιμές, προτού κηρύξετε τη μετανάστευση (migration) τελειωμένη
Πότε η Trunc είναι η καλύτερη απάντηση
Η παραπάνω έκδοση assembler αξίζει να γραφτεί (worth writing) μόνο όταν έχετε μια πραγματική απαίτηση δυαδικής συμβατότητας: κάποιος εξωτερικός καλών περιμένει (expects) το σύμβολο _ftol με μια συγκεκριμένη σύμβαση κλήσης (calling convention) και δεν μπορείτε να αλλάξετε αυτούς τους καλούντες. Αυτή η κατάσταση είναι ασυνήθιστη (uncommon). Τις περισσότερες φορές, το _ftol ήταν ένας ιδιωτικός βοηθός (private helper) που χρησιμοποιήθηκε μόνο εντός της ίδιας μονάδας (unit) και δεν υπάρχει καμία εξωτερική εξάρτηση (external dependency) στο όνομα ή στη σύμβασή του καθόλου
Για αυτήν την περίπτωση (case), αντικαταστήστε την (replace it) με απλό (plain) Pascal:
function _ftol(val: Double): Integer;
begin
Result := Trunc(val);
end;
Η Trunc περικόπτει (truncates) προς το μηδέν, κάτι που ταιριάζει με το τι έκανε η _ftol με τη λέξη ελέγχου x87 ρυθμισμένη (set) σε λειτουργία περικοπής (truncation mode). Μεταγλωττίζεται (compiles) σε DCC32 και DCC64 χωρίς τροποποίηση (modification). Ο μεταγλωττιστής (compiler) δημιουργεί (generates) την κατάλληλη εντολή (instruction) για κάθε στόχο: στο x64 συνήθως (typically) θα εκπέμψει την CVTTSD2SI ούτως ή άλλως (anyway), την ίδια εντολή με τη χειρόγραφη έκδοση (hand-written version). Παίρνετε πανομοιότυπη συμπεριφορά, όχι συνθήκες (conditionals) πλατφόρμας και όχι assembly για συντήρηση (maintain)
Η μόνη σημασιολογική διαφορά (semantic difference) που αξίζει να ελεγχθεί: η Trunc εγείρει (raises) μια εξαίρεση (exception) EInvalidOp στην προεπιλεγμένη διαμόρφωση του Delphi, όταν η είσοδος είναι ένα NaN ή το άπειρο (infinity). Το fistp του x87 στον αρχικό κώδικα απλώς έγραψε ένα μοτίβο bit χωρίς να εγείρει (raising) τίποτα. Αν ο κώδικάς σας τροφοδοτεί (feeds) με ασυνήθιστες (unusual) τιμές κινητής υποδιαστολής (floating-point) αυτήν τη συνάρτηση (function) και η παλιά συμπεριφορά ήταν αθόρυβη, προστατέψτε με IsNaN και IsInfinite από την Math προτού καλέσετε την Trunc
Μεταγλώττιση υπό συνθήκη (Conditional compilation) όταν και οι δύο στόχοι παραμένουν ενεργοί
Ορισμένα έργα πρέπει να συνεχίσουν να αποστέλλουν (shipping) και 32-bit και 64-bit δυαδικά (binaries). Εάν η αρχική έκδοση (original version) assembler πρέπει να διατηρηθεί (kept) για 32-bit και να παρασχεθεί (provided) μια νέα υλοποίηση (implementation) για 64-bit, χρησιμοποιήστε τη συνθήκη (conditional) CPUX64:
function _ftol(val: Double): Integer;
{$IFDEF CPUX64}
begin
Result := Trunc(val);
end;
{$ELSE}
var
ctrlWord: Word;
begin
asm
wait
fnstcw ctrlWord
// ... rest of the original 32-bit assembly
end;
end;
{$ENDIF}
Αυτή είναι η ελάχιστη (minimum) μηχανική (mechanical) διόρθωση και αξίζει να αντιμετωπίζεται (treating) ως προσωρινή. Μια βάση κώδικα (codebase) που μεταφέρει (carries) assembly ειδικά για μια συγκεκριμένη αρχιτεκτονική σε έναν βοηθό του οποίου ο μοναδικός σκοπός (sole purpose) είναι η περικοπή float-σε-ακέραιο, φέρει (carrying) περιττό χρέος (unnecessary debt). Η διακλάδωση (branch) των 32-bit μπορεί να φύγει εντελώς μόλις επιβεβαιώσετε (confirm) ότι τίποτα δεν εξαρτάται από τις παρενέργειες (side-effects) του FPU της παλιάς (old) υλοποίησης
Εάν η συνάρτηση (function) εμφανίζεται (appears) σε ένα στοιχείο (component) που χρησιμοποιείται σε πολλές (multiple) μονάδες, κάντε αναζήτηση (search) σε ολόκληρη (whole) τη βάση κώδικα για _ftol προτού (before) αποφασίσετε (deciding) πώς (how) να κάνετε μετανάστευση (migrate). Ένα σύμβολο με (by) αυτό (that) το όνομα (name) μπορεί να δηλωθεί (declared) σε περισσότερα από ένα μέρη· ο συνδέτης (linker) επιλέγει (picks) ένα και αγνοεί (ignores) σιωπηλά (silently) τα άλλα, το οποίο (which) σημαίνει (means) ότι (that) ίσως διορθώσετε (fix) ένα (one) αντίγραφο (copy) και εξακολουθείτε (still) να συνδέεστε (link) με (against) ένα διαφορετικό που (that) δεν (not) έχει αγγιχθεί (touched)