รูปแบบ _ftol ดั้งเดิมใน Delphi แบบ 32 บิตดูเหมือนโค้ดบรรทัดเดียวที่ฉลาดมาก: มันคือฟังก์ชัน wrapper ของ Pascal ที่เรียกใช้แอสเซมบลีแบบอินไลน์เพื่อจัดการกับ control word ของ x87 FPU, ตัดค่าบน FPU stack และ pop ผลลัพธ์ออกมา โค้ดนี้คอมไพล์ผ่านใน DCC32 มาอย่างยาวนาน ซึ่งเป็นเหตุผลที่มันไปปรากฏอยู่ในยูนิตกราฟิกและ PDF เก่าๆ มากมายโดยไม่มีใครตั้งข้อสงสัย
แต่เมื่อเปลี่ยนเป้าหมายการบิลด์เป็น 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;
บล็อก asm ที่อยู่ภายใน begin...end ของ Pascal คือสิ่งที่ DCC64 ปฏิเสธ คอมไพเลอร์ทั้งสองมีกฎที่แตกต่างกันเกี่ยวกับตำแหน่งที่อนุญาตให้ใช้แอสเซมบลี และขอบเขตนี้มีความสำคัญมาก
ทำไม DCC64 จึงกำหนดขอบเขตต่างออกไป
DCC32 อนุญาตให้ใช้แอสเซมบลีแบบอินไลน์ภายในรูทีน Pascal ทั่วไป คอมไพเลอร์รู้จัก calling convention แบบ 32 บิตและสามารถประเมินได้ว่าตัวแปรโลคัลและพารามิเตอร์อยู่ที่ไหน ดังนั้นมันจึงอนุญาตให้ใช้โค้ดแอสเซมบลีที่เข้าถึง stack frame ผ่านชื่อได้ แต่ DCC64 มีข้อกำหนดที่เข้มงวดกว่า: โค้ดแอสเซมบลีจะต้องอยู่ในฟังก์ชันแอสเซมเบลอร์เฉพาะ โดยที่โครงสร้างทั้งหมดจะต้องเป็นแอสเซมบลีและต้องจัดการกับ calling convention อย่างชัดเจน การผสมผสานระหว่าง Pascal กับ asm ไม่รองรับเลย
เหตุผลพื้นฐานมาจากสถาปัตยกรรม ใน calling convention ของ Windows แบบ 64 บิต (Microsoft ABI) พารามิเตอร์ 4 ตัวแรกจะถูกส่งเข้ามาใน RCX, RDX, R8 และ R9 สำหรับชนิดจำนวนเต็ม หรือใน XMM0 ถึง XMM3 สำหรับค่าจุดทศนิยมลอยตัว การส่งพารามิเตอร์ปกติจะไม่มีส่วนเกี่ยวข้องกับ x87 FPU เลย; ในทางเทคนิค x87 ยังใช้งานได้ แต่ ABI ไม่ได้ใช้มันในการส่งอาร์กิวเมนต์ โค้ดแอสเซมบลีที่ทึกทักเอาว่ามีค่า "อยู่บน FPU stack" กำลังประเมินสถานะที่ ABI แบบ 64 บิตไม่เคยสร้างขึ้น
ดังนั้นโค้ดดั้งเดิมไม่ได้มีแค่ปัญหาเรื่องซินแทกซ์เท่านั้น แม้ว่า DCC64 จะยอมรับโค้ดนี้ การทึกทักเรื่องรีจิสเตอร์ก็จะผิดอยู่ดี
การเขียนเวอร์ชันแอสเซมเบลอร์ 64 บิตที่ถูกต้อง
เมื่อคุณจำเป็นต้องส่งออกซิมโบล _ftol ด้วยคอนเวนชัน cdecl เพื่อความเข้ากันได้ของไบนารีจริงๆ ฟังก์ชันจะต้องถูกเขียนเป็นรูทีนแอสเซมเบลอร์แบบเพียวๆ ภายใต้ ABI แบบ 64 บิต พารามิเตอร์ Double จะมาใน XMM0 และผลลัพธ์ที่เป็นจำนวนเต็มจะต้องอยู่ใน RAX เมื่อส่งค่ากลับ directive .NOFRAME จะบอก DCC64 ว่ารูทีนนี้จะจัดการ stack ของตัวเอง ซึ่งเหมาะสมกับฟังก์ชันใบไม้ (leaf function) ที่สั้นขนาดนี้:
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 เป็นคำสั่ง SSE2 สำหรับแปลง float แบบ double-precision ไปเป็นจำนวนเต็มแบบมีเครื่องหมายโดยปัดเศษทิ้งเข้าหาศูนย์ (truncation toward zero) ซึ่งเป็นสิ่งที่ _ftol ควรจะทำพอดี มันใช้คำสั่งเดียว โดยดึงพารามิเตอร์โดยตรงจากที่ที่ ABI ทิ้งไว้ และวางผลลัพธ์ในที่ที่ ABI คาดหวัง ไม่จำเป็นต้องยุ่งยากกับการจัดการ FPU control-word เลย
ข้อควรระวังคือ หากค่าอินพุตเกินช่วงของจำนวนเต็มแบบมีเครื่องหมาย 32 บิต CVTTSD2SI จะส่งคืนค่าจำนวนเต็ม indefinite ($80000000) นี่เป็นพฤติกรรมเดียวกับ fistp ของ x87 เมื่ออินพุตเกินช่วง การตรวจสอบว่าโค้ดที่เรียกใช้ฟังก์ชันของคุณสามารถสร้างค่าดังกล่าวได้หรือไม่นั้นคุ้มค่าที่จะยืนยันก่อนจะถือว่าการย้ายระบบ (migration) เสร็จสมบูรณ์
เมื่อใดที่ Trunc เป็นคำตอบที่ดีกว่า
เวอร์ชันแอสเซมเบลอร์ข้างต้นคุ้มค่าที่จะเขียนก็ต่อเมื่อคุณมีข้อกำหนดเรื่องความเข้ากันได้ของไบนารีที่แท้จริง: คือมีการเรียกใช้จากภายนอกที่คาดหวังซิมโบล _ftol ด้วย calling convention ที่เฉพาะเจาะจง และคุณไม่สามารถเปลี่ยนผู้เรียกเหล่านั้นได้ สถานการณ์เช่นนี้ไม่พบบ่อยนัก ส่วนใหญ่แล้ว _ftol จะเป็นเพียงฟังก์ชันช่วยเหลือส่วนตัว (private helper) ที่ใช้ภายในยูนิตเดียวกันเท่านั้น และไม่มีการพึ่งพาจากภายนอกในเรื่องชื่อหรือคอนเวนชันของมันเลย
สำหรับกรณีนั้น ให้แทนที่มันด้วยโค้ด Pascal ธรรมดา:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc จะปัดเศษทิ้งเข้าหาศูนย์ ซึ่งตรงกับสิ่งที่ _ftol ทำร่วมกับการตั้งค่า x87 control word เป็นโหมด truncation มันสามารถคอมไพล์บน DCC32 และ DCC64 ได้โดยไม่ต้องแก้ไข โค้ดคอมไพเลอร์จะสร้างคำสั่งที่เหมาะสมสำหรับแต่ละเป้าหมาย: บน x64 ปกติแล้วมันจะสร้าง CVTTSD2SI ออกมาเช่นกัน ซึ่งเป็นคำสั่งเดียวกับเวอร์ชันที่เขียนด้วยมือ คุณจะได้พฤติกรรมที่เหมือนกันเป๊ะ ไม่ต้องพึ่งเงื่อนไขแพลตฟอร์ม และไม่ต้องคอยดูแลโค้ดแอสเซมบลี
ความแตกต่างทางความหมายจุดหนึ่งที่ควรตรวจสอบคือ: Trunc จะทำให้เกิด exception EInvalidOp ในค่าคอนฟิกเริ่มต้นของ Delphi เมื่ออินพุตเป็น NaN หรืออินฟินิตี้ (infinity) ในขณะที่ fistp ของ x87 ในโค้ดดั้งเดิมจะแค่เขียนรูปแบบบิตลงไปโดยไม่ทำให้เกิดอะไรขึ้นเลย หากโค้ดของคุณป้อนค่า floating-point ที่ผิดปกติเข้าไปในฟังก์ชันนี้ และพฤติกรรมเดิมคือการเงียบหายไป ให้ป้องกันโดยใช้ IsNaN และ IsInfinite จากยูนิต Math ก่อนที่จะเรียก Trunc
การคอมไพล์แบบมีเงื่อนไขเมื่อทั้งสองเป้าหมายยังคงถูกใช้งาน
บางโปรเจ็กต์ยังต้องส่งมอบทั้งไบนารี 32 บิตและ 64 บิตต่อไป หากเวอร์ชันแอสเซมเบลอร์ดั้งเดิมต้องถูกเก็บไว้ใช้สำหรับ 32 บิต และต้องจัดเตรียมการนำไปใช้ใหม่สำหรับ 64 บิต ให้ใช้เงื่อนไข 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;
นั่นคือการแก้ไขเชิงกลไกขั้นต่ำสุด และควรพิจารณาให้เป็นวิธีชั่วคราว โค้ดเบสที่พกพาแอสเซมบลีเฉพาะสถาปัตยกรรมไว้ในฟังก์ชันตัวช่วย (helper) ซึ่งมีวัตถุประสงค์เพียงเพื่อปัดเศษทศนิยมเป็นจำนวนเต็มนั้น เป็นการแบกรับหนี้ทางเทคนิคโดยไม่จำเป็น เส้นทาง 32 บิตสามารถถูกลบออกไปได้ทั้งหมดทันทีที่คุณยืนยันได้ว่าไม่มีสิ่งใดที่พึ่งพาผลข้างเคียงจาก FPU ในระบบเดิม
หากฟังก์ชันนี้ปรากฏในคอมโพเนนต์ที่ใช้ข้ามหลายยูนิต ให้ค้นหาคำว่า _ftol ในโค้ดเบสทั้งหมดก่อนตัดสินใจว่าจะย้ายระบบอย่างไร ซิมโบลชื่อนี้อาจถูกประกาศไว้ในหลายที่; ลิงเกอร์จะเลือกมาตัวหนึ่งและข้ามตัวอื่น ๆ ไปอย่างเงียบ ๆ ซึ่งหมายความว่าคุณอาจจะแก้ไขชุดหนึ่ง แต่ดันไปลิงก์กับอีกชุดหนึ่งที่ไม่เคยถูกแตะต้องเลย