คอมไพเลอร์ Rust to C มีอัตราผ่านการทดสอบถึง 95.9%
(fractalfir.github.io)rustc_codegen_clrเป็นโปรเจกต์ที่สร้างโค้ด Rust ไปยัง C และ .NET และกำลังเตรียม การนำเสนอใน Rust Week พร้อมกับการปรับปรุงอัตราการผ่านการทดสอบ- อัตราการผ่านการทดสอบ Rust core เพิ่มจาก 92% เมื่อสองเดือนก่อนเป็น 95.9% และการทดสอบที่เหลือประมาณ 65 รายการก็ดูเหมือนมีสาเหตุคล้ายกัน
- การแก้ไขล่าสุดเน้นที่ intrinsic สำหรับจำนวนเต็ม 128 บิต, checked arithmetic และ subslicing ขณะที่ฝั่ง .NET ก็สามารถรันการทดสอบ Rust core ได้ 96.3% แล้ว
- มีเป้าหมายให้ output ใกล้เคียง C99 หรือ ANSI C และใช้ API มาตรฐาน POSIX เพื่อรองรับ C compiler ให้มากขึ้น โดยใน ANSI C compiler บางตัวสามารถรันโปรแกรม Rust ที่เรียบง่ายมากได้สำเร็จ
- กำลังทำทั้ง optimization เพื่อลดขนาดไฟล์เมื่อสร้างโค้ด C ขนาดใหญ่ และการ refactor IR ภายใน ซึ่งเป็นพื้นฐานในการขยายขอบเขตการรัน Rust ไปยังแพลตฟอร์มเก่าหรือเฉพาะทาง
การนำเสนอใน Rust Week และสถานะความคืบหน้าของโปรเจกต์
- มีกำหนดนำเสนอโปรเจกต์
rustc_codegen_clrในงาน Rust Week ที่เมือง Utrecht ประเทศเนเธอร์แลนด์ - การนำเสนอพยายามรักษาสมดุลระหว่างการทำให้ผู้เริ่มต้นเข้าถึงได้ กับการครอบคลุมหัวข้อขั้นสูงอย่างการคอมไพล์ Rust-to-C
- ในช่วงหลายเดือนที่ผ่านมา มีการทำทั้งการแก้ไขการทดสอบ, compatibility กับ C compiler, การปรับปรุง performance และการ refactor ภายใน
อัตราการผ่านการทดสอบ Rust core เพิ่มขึ้น
- อัตราการผ่านการทดสอบ Rust core เพิ่มจาก 92% เมื่อสองเดือนก่อนเป็น 95.9%
- ยังเหลือการทดสอบประมาณ 65 รายการ แต่สาเหตุดูคล้ายกัน ทำให้ลักษณะของงานแก้ไขที่เหลือค่อนข้างแคบลงแล้ว
- ฝั่ง .NET ก็ได้รับผลจากการแก้ไขเดียวกัน และสามารถรันการทดสอบ Rust core ได้ 96.3%
การแก้ไขจำนวนเต็ม 128 บิตและ checked arithmetic
- การปรับปรุงส่วนใหญ่หลายรายการมาจากการแก้ไข intrinsic 128 บิต, checked arithmetic และ subslicing
- intrinsic
popcountของ C มีอยู่สามแบบคือ__builtin_popcount,__builtin_popcountl,__builtin_popcountll__builtin_popcountllแม้ชื่อจะเป็นเช่นนั้น แต่ทำงานกับunsigned long longไม่ใช่__int128_t- บน x86_64 Linux และ GCC นั้น
unsigned longและunsigned long longต่างก็มีขนาด 64 บิต
- implementation เดิมตัด จำนวนเต็ม 128 บิต ใน intrinsic สำหรับ bit counting ให้เหลือ 64 บิตอย่างเงียบ ๆ ทำให้ได้ผลลัพธ์ผิด
popcount128 บิตถูก emulate โดยคำนวณจำนวน set bit ของ 64 บิตล่างและ 64 บิตบนแยกกัน แล้วนำมาบวกกัน- overflow check ของการคูณ 128 บิตยังหาวิธีที่มีประสิทธิภาพไม่ได้ จึงจัดการด้วยวิธีง่าย ๆ
- ถ้า
bไม่ใช่ 0 และ(a * b) / b == aก็ถือว่าไม่มี overflow - ไม่ใช่วิธีที่ก้าวกระโดด แต่เพียงพอให้ผ่านการทดสอบเพิ่มได้อีกหลายรายการ
- ถ้า
บั๊ก subslicing และ fallback intrinsic
- บั๊ก subslicing เกิดจากการขาด
sizeofทำให้ data pointer ของ slice ถูก offset เป็นหน่วยไบต์ ไม่ใช่ หน่วย element - บั๊กนี้จะพังเฉพาะตอน subslicing จากด้านท้าย ไม่ใช่จากจุดเริ่มต้นของ slice และเป็นรูปแบบที่มักใช้ใน pattern matching จึงไม่ถูกพบอยู่นาน
- สำหรับ byte slice และ string slice นั้น ขนาดของ byte และ UTF-8 code unit คือ 1 ไบต์ ทำให้ผ่านการทดสอบของตัวเอง และเพิ่งปรากฏใน test suite ทั้งชุดของ Rust compiler
- intrinsic บางตัวสามารถใช้ fallback implementation ของ Rust compiler ได้โดยไม่ต้อง implement เองโดยตรง
carrying_mul_addต้องทำการคูณในจำนวนเต็มที่ใหญ่กว่า input 2 เท่า- รองรับได้ถึง 64 บิต แต่ input 128 บิตต้องใช้จำนวนเต็ม 256 บิต
- LLVM รองรับจำนวนเต็ม 256 บิต แต่ C และ .NET ไม่รองรับ
- fallback implementation ของ Rust compiler ทำการคูณและบวกแบบ 256 บิตด้วยจำนวนเต็ม 128 บิต จึงใช้เป็นแนวทางในการ emulate การคำนวณ 128 บิตด้วยจำนวนเต็ม 64 บิตเท่านั้นได้
รองรับ C compiler ให้มากขึ้น
- โปรเจกต์นี้พยายามเพิ่มโอกาสให้โค้ด Rust รันบน C compiler เฉพาะทาง ได้มากขึ้น
- เคยมีกรณีที่โค้ด Rust รันบน Game Boy, คอมไพล์ด้วยคำสั่ง move เท่านั้น หรือรันด้วย Holly C ของ Temple OS
- ไม่สามารถรองรับ proprietary C compiler ที่เข้าถึงไม่ได้โดยตรง แต่หากขยายขอบเขตการรองรับ obscure C compiler ก็จะเพิ่มโอกาสให้โค้ด Rust รันในสภาพแวดล้อมเหล่านั้นได้
- หลายแพลตฟอร์มไม่ได้รับการรองรับเพราะขาดเอกสารและเข้าถึงได้ยาก
- ในประเด็นการหารือเรื่องการเขียนบางส่วนของ Git ด้วย Rust แพลตฟอร์ม proprietary อย่าง NonStop ไม่รองรับ Rust, LLVM หรือ GCC จึงอาจเกิดปัญหา Git รองรับลดลงหรือยุติการรองรับ
- หากคอมไพล์ Rust เป็น C ในทางทฤษฎีก็สามารถรัน Rust ได้ทุกที่ที่มี C
- ยังไม่แน่ชัดว่าจะเลี่ยงปัญหาได้ในทุกแพลตฟอร์มหรือไม่
- การจะได้ compiler สำหรับแพลตฟอร์มนั้นอย่างถูกกฎหมายจำเป็นต้องซื้อเซิร์ฟเวอร์ ซึ่งเกินงบประมาณไปมาก
- ดูเหมือนว่า Rust จะยังไม่น่ารันได้บนแพลตฟอร์มเหล่านี้ในเร็ว ๆ นี้
กลยุทธ์ output เป็น C ในปัจจุบัน
- แผนปัจจุบันคือสร้างโค้ดที่เป็น C99 ที่เป็นไปตามมาตรฐาน ให้มากที่สุด หรือใกล้เคียง ANSI C
- เนื่องจากต้องใช้ threading support บางส่วนสำหรับการ initialize แบบ thread-local จึงกำหนดทิศทางให้ใช้เฉพาะ POSIX API มาตรฐาน
- มี fallback implementation สำหรับ intrinsic บางตัวเอง และกำลังค่อย ๆ เพิ่มรายการเหล่านั้น
- ใน ANSI C compiler บางตัว สามารถรันโปรแกรม Rust ที่เรียบง่ายมากได้สำเร็จ
- เป้าหมายคือให้แม้แพลตฟอร์มที่ตอนนี้ยังรองรับได้ยาก ก็สามารถเพิ่มเข้ามาได้ค่อนข้างง่ายเมื่อเกิดความจำเป็น
การปรับปรุง performance และขนาดไฟล์เล็ก ๆ น้อย ๆ
- ในการ output integer literal มีการสะท้อนข้อเท็จจริงว่าจำนวนเต็มที่เล็กกว่า 2^32 นั้น decimal จะมีความยาวเท่ากับหรือสั้นกว่า hexadecimal
255สั้นกว่า0xFF1 ไบต์65536ก็สั้นกว่า0xFFFF- เพราะ prefix
0xทำให้ก่อนถึง 2^32 การแสดงผลแบบ hexadecimal จะยังไม่เล็กกว่า
- ในกรณีสุดโต่งอย่างการแปลง Rust compiler ทั้งหมดเป็น C เคยสร้าง C source file ขนาด สูงสุด 1GB ดังนั้นการลดขนาดไฟล์แม้เพียงสัดส่วนเล็กน้อยก็มีผล
- directive
#lineที่ใช้แทรก debug info ก็ถูกปรับให้ฉลาดขึ้น- ใส่ source file name เฉพาะเมื่อมีการเปลี่ยนเท่านั้น
- สามารถลดขนาดไฟล์ได้มากเมื่อใช้ debug info
การ refactor ภายในและการจัดระเบียบ IR
- แยกบางฟีเจอร์ภายใน
rustc_codegen_clrออกเป็น crate แยกต่างหาก เพื่อเพิ่มความเร็วของ incremental build - กำลังย้ายไปใช้ interned IR ที่มีประสิทธิภาพด้าน memory มากขึ้นด้วย
- IR เดิมมี rvalue/lvalue แปลก ๆ ที่ map ไปยัง C ได้ไม่ดี และปัญหาจะใหญ่ขึ้นกับฟีเจอร์ของ Rust อย่าง dynamically sized type
- ตัวอย่างเช่น ใน custom dynamically sized type อย่าง
MyStrนั้น&self.sดูเหมือนเรียบง่าย แต่จริง ๆ ต้องจัดการ fat pointer metadata- หากใช้ compound literal ของ C99 ก็สามารถสร้าง
struct FatPtr_strได้ในบรรทัดเดียว - ใน ANSI C ต้องใส่
dataและmetaลงใน temporary variable แยกกันแล้ว return
- หากใช้ compound literal ของ C99 ก็สามารถสร้าง
- เมื่อหนึ่งบรรทัดของ Rust และ MIR ถูกขยายเป็น C หลายบรรทัด old IR จัดการด้วยวิธี internal scope ที่มี temporary local และ sub-statement
- วิธีใหม่มีขั้นตอน setup ที่ซับซ้อนกว่า แต่ทำให้ IR ทั้งหมดเรียบง่ายขึ้น และหากแก้กรณียากสุดท้ายที่เหลือได้ ก็จะสามารถลบฟีเจอร์ส่วนนั้นของ old IR ได้
งานถัดไป
- ระยะเวลาทำงานของโปรเจกต์อยู่ที่ประมาณ 1.5 ปี แล้ว และเมื่อบั๊กลดลง การค้นหาบั๊กที่เหลือก็ต้องใช้เวลามากขึ้น
- กำลังทำบทความ “Rust panics under the hood” ตอนที่ 2 และพยายามอธิบายกระบวนการ panic ของ Rust ทีละขั้น
- แค่คำอธิบายการสร้าง panic message ก็มีความยาว 10 นาทีแล้ว จึงกำลังพิจารณาแบ่งบทความออกเป็นสองส่วน
- กำลังทำ memory profiler ขนาดเล็กและแม่นยำสำหรับ Rust ด้วย
- ขนาดโค้ดประมาณ 2K LOC
- แม้ตารางเวลาจะค่อนข้างแน่น แต่ก็อยากเขียนบทความเกี่ยวกับเรื่องนี้ภายในไม่กี่สัปดาห์
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เดิมทีโปรเจกต์ rustc_codegen_gcc เคยสัญญาเรื่องนี้ไว้ แต่สุดท้ายก็ทำไม่ได้
[0] https://github.com/rust-lang/compiler-team/issues/458
ฝั่ง GCC/Clang “มีแค่” 80%
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
แฮ็กนี้เป็นแบ็กเอนด์ของคอมไพเลอร์ Rust แบ็กเอนด์รับคำสั่งเฉพาะแพลตฟอร์มเป็นอินพุต ดังนั้นโค้ด C ที่สร้างออกมาและไม่ใช่โค้ดแบบเรียบง่ายจะไม่สามารถพกพาข้ามแพลตฟอร์มได้
ผู้ใช้ต้องได้รับซอร์สที่สร้างไว้ล่วงหน้าเฉพาะแพลตฟอร์ม หรือไม่ก็ติดตั้งคอมไพเลอร์ Rust พร้อมแบ็กเอนด์นี้เพื่อสร้างเอง
extern "c"ของ Rust ทำไม่ได้อยู่แล้ว?“การแทนค่า .NET IR ของผมแมปกับ C ได้ดี และด้วยเหตุนี้จึงสามารถเพิ่มการรองรับการคอมไพล์ Rust เป็น C ได้ด้วยโค้ด 2–3 พันบรรทัด โดยนำ codebase เกือบทั้งหมดกลับมาใช้ซ้ำ และโค้ดเฉพาะสำหรับ C กับ .NET มีอยู่แค่ในขั้นตอนสุดท้ายสุดของการคอมไพล์เท่านั้น”
ตอนแรกเริ่มจากแบ็กเอนด์ .NET แต่พบว่าวิธีเข้าหานี้รองรับการสร้างโค้ด C ได้ง่ายด้วย เลยเพิ่มเข้ามา มันแปลงสิ่งที่ rustc ส่งมาให้เป็น intermediate representation (IR) ของตัวเองแล้วประมวลผลต่อ
การรับประกันแบบ dynamic อย่างการตรวจสอบขอบเขตก็สามารถ implement ใน C runtime ได้ไม่มีปัญหา
unsafeขนาดใหญ่บล็อกเดียวC ไม่ได้ส่งผ่านข้อมูลทั้งหมดที่จำเป็นต้องรู้อย่างชัดเจนเพื่อให้คอมไพล์ได้ใน Rust
https://www.youtube.com/watch?v=1VgptLwP588
unsafeเยอะ และโดยทั่วไปโค้ดที่ได้ไม่ค่อยเป็น Rust สักเท่าไรตัวอย่างเช่น การเปลี่ยน state machine ที่มีสถานะอิงกับตัวแปร global ให้เป็น state machine ที่ดูเป็น Rust มากขึ้น เช่นใช้ enum ที่มีชื่อ เป็นเรื่องยากมาก
ถ้าได้ AI ที่ทรงพลังพอช่วยอาจเป็นไปได้ แต่ AI ตอนนี้ยังขาดความสามารถในการทำสิ่งที่ตั้งใจไว้ให้สำเร็จจริง ๆ อยู่มาก จึงยังพูดได้ยากว่าพร้อมแล้ว และคงต้องมีหน่วยความจำมากพอจะใส่ codebase ทั้ง C และ Rust ลงใน context window ได้ ซึ่งพอโค้ดเกินระดับหนึ่งก็จะต้องใช้ฮาร์ดแวร์ราคาแพงมากอย่างรวดเร็ว ไม่เช่นนั้นก็จะสร้างโค้ดแยกกันที่เข้ากันไม่ได้ เหมือน LLM ช่วยเขียนโค้ดจำนวนมาก
อย่างไรก็ดี ถ้าต้องการขยายโปรเจกต์ C ด้วย Rust หรือค่อย ๆ เขียนใหม่ทีละส่วน https://c2rust.com/ ก็ใช้ได้ทันที
Rust เป็นภาษาสมัยใหม่ที่มี package management, เครื่องมือ build/test แบบบูรณาการที่เรียบง่ายขึ้น, ภาระเก่า ๆ น้อยกว่ามาก, และมีฟีเจอร์กับไวยากรณ์ระดับสูงที่ผู้คนชอบใช้จริง ๆ
C ก็เรียบง่ายดี แต่ codebase ที่ซับซ้อนได้ประโยชน์จากภาษาสมัยใหม่ที่ช่วยสร้าง abstraction ที่แข็งแรงโดยยังคงความเร็วของ C ไว้ได้ แน่นอนว่ายังไม่ต้องพูดถึง borrow checker และความปลอดภัยของหน่วยความจำ
ถ้ามีตัวแปลง Rust→C ที่สร้างโค้ด C ได้มาตรฐานพอ ก็จะช่วยอุดช่องว่างนั้นได้