Thành ngữ _ftol ban đầu trong Delphi 32 bit trông giống như một dòng mã thông minh: một hàm bọc Pascal chuyển xuống thành hợp ngữ nội tuyến (inline assembly) để thao tác trên từ điều khiển FPU x87 (x87 FPU control word), cắt ngắn giá trị trên ngăn xếp FPU, và lấy kết quả ra (pop the result). Nó được biên dịch tốt dưới DCC32 trong một thời gian dài, đó chính xác là lý do tại sao nó lại kết thúc trong rất nhiều khối đồ họa và PDF cũ mà không có ai thắc mắc
Khi chuyển mục tiêu bản dựng sang 64 bit, trình biên dịch kết thúc với lỗi E1025 Unsupported language feature: 'ASM'. Lỗi đó không phải là một cảnh báo về tính tương thích. Nó có nghĩa là DCC64 sẽ hoàn toàn không biên dịch hàm đó, bất kể đoạn mã hợp ngữ trước đây hoạt động tốt như thế nào
Phiên bản 32 bit nguyên thủy thường trông giống như sau:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Khối mã asm nằm bên trong phần thân begin...end của Pascal chính xác là những gì DCC64 từ chối. Hai trình biên dịch có các quy tắc khác nhau về nơi được phép dùng hợp ngữ, và ranh giới là điều quan trọng
Tại sao DCC64 lại vạch ranh giới khác nhau
DCC32 cho phép sử dụng hợp ngữ nội tuyến bên trong các hàm Pascal thông thường. Trình biên dịch hiểu quy ước gọi 32 bit (32-bit calling convention) và có thể lý luận về vị trí chứa biến cục bộ cùng tham số, vì vậy nó dung nạp các đoạn hợp ngữ tiếp cận vào khung ngăn xếp bằng tên. DCC64 giữ một lập trường nghiêm ngặt hơn: hợp ngữ phải nằm trong một hàm hợp ngữ chuyên dụng, tại đó toàn bộ phần thân là hợp ngữ và quy ước gọi được xử lý một cách rõ ràng. Cấu trúc kết hợp Pascal-plus-asm hoàn toàn không được hỗ trợ
Lý do căn bản là về mặt kiến trúc. Trong quy ước gọi Windows 64-bit (Microsoft ABI), bốn tham số đầu tiên đến trong RCX, RDX, R8 và R9 đối với kiểu số nguyên, hoặc trong XMM0 đến XMM3 đối với dấu phẩy động. Không có sự tham gia của FPU x87 trong việc truyền tham số thông thường; về mặt kỹ thuật, x87 khả dụng nhưng ABI không sử dụng nó để vận chuyển đối số. Hợp ngữ giả định rằng một giá trị nằm "trên ngăn xếp FPU" (on the FPU stack) là đang suy luận về một trạng thái mà ABI 64 bit không bao giờ tạo ra
Vì vậy, đoạn mã cũ không chỉ mắc vấn đề về cú pháp. Ngay cả khi DCC64 chấp nhận nó, các giả định thanh ghi cũng sẽ sai
Viết phiên bản hợp ngữ 64 bit đúng cách
Khi bạn thực sự cần xuất một ký hiệu (symbol) _ftol với quy ước cdecl để có tính tương thích nhị phân, hàm đó phải được viết thành một hàm hợp ngữ thuần túy. Dưới ABI 64 bit, một tham số Double sẽ đến trong XMM0, và kết quả số nguyên phải nằm ở RAX khi trả về. Chỉ thị .NOFRAME thông báo cho DCC64 rằng hàm tự quản lý ngăn xếp của riêng nó, điều phù hợp với một hàm lá (leaf function) ngắn thế này:
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 là lệnh SSE2 dùng để chuyển đổi kiểu số phẩy động có độ chính xác kép (double-precision float) thành số nguyên có dấu với việc cắt ngắn về số 0, đây chính xác là nhiệm vụ của _ftol. Đó là một lệnh duy nhất, lấy thẳng tham số từ nơi ABI đã để lại và đặt kết quả ở nơi mà ABI mong đợi. Không cần thủ thuật xoay xở từ điều khiển FPU nào
Xin lưu ý rằng nếu đầu vào vượt quá phạm vi của một số nguyên 32 bit có dấu, CVTTSD2SI sẽ trả về giá trị không xác định của số nguyên ($80000000). Đó là hành vi tương tự lệnh x87 fistp với dữ liệu đầu vào nằm ngoài phạm vi. Tốt hơn nên xác nhận xem các thành phần gọi (callers) của bạn có thể tạo ra các giá trị như vậy hay không trước khi tuyên bố quá trình di chuyển hoàn tất
Khi nào Trunc là giải pháp tốt hơn
Phiên bản hợp ngữ ở trên chỉ đáng viết khi bạn có một yêu cầu cụ thể về tương thích nhị phân: một hàm gọi bên ngoài nào đó mong đợi ký hiệu _ftol với một quy ước gọi cụ thể, và bạn không thể thay đổi những thành phần gọi đó. Tình huống đó không phổ biến. Trong hầu hết các trường hợp, _ftol chỉ là một hàm trợ giúp riêng tư (private helper) được sử dụng nội bộ bên trong cùng một khối, và hoàn toàn không có phụ thuộc bên ngoài nào vào tên gọi hoặc quy ước của nó
Đối với trường hợp đó, hãy thay thế nó bằng lệnh Pascal thuần túy:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc cắt ngắn về 0, khớp với những gì _ftol đang làm khi đặt từ điều khiển x87 sang chế độ cắt ngắn (truncation mode). Nó biên dịch trên DCC32 và DCC64 mà không cần sửa đổi. Trình biên dịch tạo ra lệnh phù hợp cho mỗi mục tiêu: trên x64 dù sao nó cũng thường tạo ra lệnh CVTTSD2SI, cùng một lệnh với phiên bản viết tay. Bạn có được hành vi giống hệt nhau, không có ràng buộc nền tảng, và không có hợp ngữ nào phải duy trì
Một điểm khác biệt về ngữ nghĩa đáng kiểm tra: Trunc làm phát sinh ngoại lệ EInvalidOp trong cấu hình mặc định của Delphi khi đầu vào là NaN hoặc vô cực (infinity). Lệnh x87 fistp trong mã ban đầu chỉ ghi một mẫu bit mà không làm phát sinh gì. Nếu mã của bạn đưa các giá trị phẩy động bất thường vào hàm này và hành vi cũ vốn yên ắng (silent), hãy bảo vệ (guard) bằng các hàm IsNaN và IsInfinite từ khối Math trước khi gọi tới Trunc
Biên dịch có điều kiện khi cả hai mục tiêu vẫn đang hoạt động
Một số dự án bắt buộc phải tiếp tục vận chuyển cả tệp nhị phân 32 bit và 64 bit. Nếu bắt buộc phải giữ phiên bản hợp ngữ gốc cho tệp 32 bit và phải cung cấp một trình triển khai mới cho phiên bản 64 bit, hãy sử dụng điều kiện (conditional) 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;
Đó là biện pháp sửa chữa máy móc tối thiểu, và đáng để coi như chỉ là tạm thời. Một cơ sở mã (codebase) mang theo hợp ngữ dành riêng cho kiến trúc trong một trình trợ giúp chỉ có một mục đích duy nhất là cắt bỏ phần nổi-thành-số-nguyên, là đang mang theo món nợ không cần thiết. Nhánh 32 bit có thể biến mất hoàn toàn sau khi bạn xác nhận rằng không có thứ gì phụ thuộc vào các tác dụng phụ của FPU từ trình triển khai cũ
Nếu hàm xuất hiện trong một thành phần được dùng qua nhiều khối, hãy tìm kiếm toàn bộ cơ sở mã cho _ftol trước khi quyết định cách di chuyển. Một ký hiệu mang tên đó có thể được khai báo ở nhiều nơi; trình liên kết chọn một điểm và bỏ qua những điểm khác trong thầm lặng, nghĩa là bạn có thể sửa một bản sao và vẫn liên kết với một bản khác chưa từng được đụng đến