Trình biên dịch Rust sang C đạt tỷ lệ vượt qua kiểm thử 95,9%
(fractalfir.github.io)rustc_codegen_clrlà dự án tạo mã Rust sang C và .NET; cùng với việc cải thiện tỷ lệ vượt qua kiểm thử, dự án đang chuẩn bị cho bài trình bày tại Rust Week- Tỷ lệ vượt qua kiểm thử Rust core đã tăng từ 92% hai tháng trước lên 95,9%, và khoảng 65 bài kiểm thử còn lại dường như có nguyên nhân khá giống nhau
- Các bản sửa gần đây tập trung vào intrinsic số nguyên 128 bit, checked arithmetic và subslicing; phía .NET cũng đã có thể chạy 96,3% các bài kiểm thử Rust core
- Dự án nhắm tới output gần với C99 hoặc ANSI C và dùng API POSIX chuẩn để hướng đến nhiều trình biên dịch C hơn; một số trình biên dịch ANSI C đã chạy thành công các chương trình Rust rất đơn giản
- Các tối ưu giảm kích thước file khi tạo mã C quy mô lớn và việc tái cấu trúc IR nội bộ đang được tiến hành song song, tạo nền tảng để mở rộng phạm vi chạy Rust sang các nền tảng cũ hoặc đặc thù
Bài trình bày tại Rust Week và tình hình dự án
- Bài trình bày về dự án
rustc_codegen_clrđược lên lịch tại Rust Week tổ chức ở Utrecht, Hà Lan - Bài nói cố gắng cân bằng giữa việc dễ tiếp cận với người mới và chủ đề nâng cao là biên dịch Rust sang C
- Trong vài tháng gần đây, việc sửa kiểm thử, tương thích với trình biên dịch C, cải thiện hiệu năng và tái cấu trúc nội bộ đã được tiến hành cùng lúc
Tỷ lệ vượt qua kiểm thử Rust core tăng
- Tỷ lệ vượt qua kiểm thử Rust core đã tăng từ 92% hai tháng trước lên 95,9%
- Vẫn còn khoảng 65 bài kiểm thử chưa qua, nhưng nguyên nhân có vẻ khá giống nhau, nên phạm vi bản chất của các sửa đổi còn lại đã được thu hẹp tương đối
- Phía .NET cũng được hưởng lợi từ cùng các bản sửa, và đã có thể chạy 96,3% các bài kiểm thử Rust core
Sửa số nguyên 128 bit và checked arithmetic
- Phần lớn cải thiện đến từ việc sửa intrinsic 128 bit, checked arithmetic và subslicing
- Intrinsic
popcountcủa C có ba dạng:__builtin_popcount,__builtin_popcountl,__builtin_popcountll- Trái với tên gọi,
__builtin_popcountllhoạt động trênunsigned long long, không phải__int128_t - Trên x86_64 Linux và GCC, cả
unsigned longlẫnunsigned long longđều là 64 bit
- Trái với tên gọi,
- Cách triển khai cũ âm thầm cắt số nguyên 128 bit xuống 64 bit trong intrinsic đếm bit, dẫn đến kết quả sai
popcount128 bit được mô phỏng bằng cách tính riêng số set bit của 64 bit thấp và 64 bit cao rồi cộng lại- Với overflow check của phép nhân 128 bit, do chưa tìm được cách hiệu quả nên xử lý bằng phương pháp đơn giản
- Nếu
bkhác 0 và(a * b) / b == athì được xem là không overflow - Không phải cách đột phá, nhưng đủ để vượt qua thêm vài bài kiểm thử
- Nếu
Lỗi subslicing và intrinsic fallback
- Lỗi subslicing là vấn đề offset con trỏ dữ liệu của slice theo đơn vị byte thay vì theo phần tử do thiếu
sizeof - Lỗi này chỉ gây hỏng khi subslicing từ cuối slice chứ không phải từ đầu, và vì đây là dạng thường dùng trong pattern matching nên đã lâu không bị phát hiện
- Với byte slice và string slice, kích thước của byte và UTF-8 code unit đều là 1 byte nên các bài kiểm thử riêng vẫn qua, còn lỗi lộ ra trong toàn bộ test suite của Rust compiler
- Một số intrinsic có thể dùng triển khai fallback của Rust compiler thay vì tự triển khai trực tiếp
carrying_mul_addcần thực hiện phép nhân trên số nguyên lớn gấp đôi input- Đến 64 bit thì có thể xử lý, nhưng input 128 bit cần số nguyên 256 bit
- LLVM hỗ trợ số nguyên 256 bit, nhưng C và .NET thì không
- Triển khai fallback của Rust compiler thực hiện phép nhân và cộng 256 bit bằng số nguyên 128 bit, nên có thể tham khảo để mô phỏng phép toán 128 bit chỉ bằng số nguyên 64 bit
Hỗ trợ nhiều trình biên dịch C hơn
- Dự án muốn tăng khả năng mã Rust chạy được trên nhiều trình biên dịch C đặc thù hơn
- Đã có các ví dụ Rust chạy trên Game Boy, được biên dịch chỉ bằng lệnh move, hoặc chạy bằng Holly C của Temple OS
- Không thể trực tiếp hỗ trợ các proprietary C compiler không thể tiếp cận, nhưng nếu mở rộng phạm vi hỗ trợ các obscure C compiler thì khả năng mã Rust chạy trong các môi trường đó sẽ tăng lên
- Nhiều nền tảng không được hỗ trợ do thiếu tài liệu và khó tiếp cận
- Liên quan đến thảo luận viết một phần Git bằng Rust, các nền tảng proprietary như NonStop không hỗ trợ Rust, LLVM hay GCC, có thể dẫn đến vấn đề giảm hoặc ngừng hỗ trợ Git
- Nếu biên dịch Rust sang C, về lý thuyết có thể chạy Rust ở bất cứ nơi nào có C
- Chưa chắc có thể обход được vấn đề trên mọi nền tảng
- Để có hợp pháp trình biên dịch cho nền tảng đó cần mua máy chủ, vượt xa ngân sách
- Có vẻ ít khả năng Rust sẽ sớm chạy được trên các nền tảng như vậy
Chiến lược output C hiện tại
- Kế hoạch hiện tại là tạo mã tuân thủ C99 chuẩn nhiều nhất có thể hoặc gần với ANSI C
- Vì cần một phần threading support để khởi tạo thread-local, hướng đi là chỉ dùng API POSIX chuẩn
- Dự án đang tự có các triển khai fallback cho một số intrinsic nhất định và tăng dần danh sách đó
- Một số trình biên dịch ANSI C đã chạy thành công các chương trình Rust rất đơn giản
- Mục tiêu là để ngay cả các nền tảng hiện khó hỗ trợ cũng có thể được thêm tương đối dễ khi phát sinh nhu cầu
Cải thiện nhỏ về hiệu năng và kích thước file
- Khi xuất integer literal, dự án phản ánh thực tế rằng các số nguyên nhỏ hơn 2^32 thì dạng decimal bằng hoặc ngắn hơn dạng hexadecimal
255ngắn hơn0xFF1 byte65536cũng ngắn hơn0xFFFF- Do prefix
0x, biểu diễn hexadecimal không nhỏ hơn trước mốc 2^32
- Trong trường hợp cực đoan chuyển toàn bộ Rust compiler sang C, từng có lúc tạo ra C source file lên tới 1GB, nên cả mức giảm kích thước file theo tỷ lệ nhỏ cũng có tác động
#linedirective dùng để chèn debug info cũng được làm thông minh hơn- Tên source file chỉ được đưa vào khi thay đổi
- Có thể giảm đáng kể kích thước file khi dùng debug info
Tái cấu trúc nội bộ và dọn dẹp IR
- Một số chức năng bên trong
rustc_codegen_clrđược tách thành crate riêng để tăng tốc incremental build - Công việc chuyển sang interned IR tiết kiệm bộ nhớ hơn cũng đang diễn ra
- IR cũ có những rvalue/lvalue đặc thù không ánh xạ tốt sang C, và vấn đề trở nên lớn hơn với các tính năng Rust như dynamically sized type
- Ví dụ, trong custom dynamically sized type như
MyStr,&self.strông có vẻ đơn giản nhưng thực tế phải xử lý fat pointer metadata- Nếu dùng compound literal của C99, có thể tạo
struct FatPtr_strtrong một dòng - Trong ANSI C, cần đưa
datavàmetalần lượt vào biến tạm rồi trả về
- Nếu dùng compound literal của C99, có thể tạo
- Khi một dòng Rust và MIR mở rộng thành nhiều dòng C, IR cũ xử lý bằng cơ chế scope nội bộ có temporary local và sub-statement
- Cách mới có giai đoạn setup phức tạp hơn, nhưng làm toàn bộ IR đơn giản hơn; khi giải quyết xong các trường hợp khó cuối cùng còn lại, có thể loại bỏ chức năng tương ứng của IR cũ
Công việc tiếp theo
- Thời gian thực hiện dự án đã khoảng 1,5 năm, và càng ít bug thì càng mất nhiều thời gian hơn để tìm các bug còn lại
- Đang thực hiện phần 2 của “Rust panics under the hood”, nhằm phân tích từng bước quá trình panic của Rust
- Riêng phần giải thích việc tạo panic message đã dài 10 phút, nên cũng đang cân nhắc chia bài viết thành hai phần
- Dự án cũng đang làm một memory profiler nhỏ và chính xác cho Rust
- Quy mô mã khoảng 2K LOC
- Lịch trình khá gấp, nhưng tác giả muốn viết bài liên quan trong vài tuần tới
1 bình luận
Ý kiến trên Hacker News
Ban đầu dự án rustc_codegen_gcc từng hứa điều này nhưng cuối cùng không thực hiện được
[0] https://github.com/rust-lang/compiler-team/issues/458
Phía GCC/Clang chỉ là “80%” thôi
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
Hack này là một backend compiler Rust. Vì backend nhận đầu vào là các lệnh theo từng nền tảng, mã C không đơn giản được sinh ra sẽ không có tính portable
Người dùng sẽ phải nhận source đã được tạo sẵn theo từng nền tảng, hoặc tự cài compiler Rust cùng backend này để tự tạo ra nó
extern "c"của Rust chưa làm được?“Biểu diễn .NET IR của tôi ánh xạ tốt sang C, nhờ đó tôi có thể thêm hỗ trợ biên dịch Rust sang C chỉ với 2–3 nghìn dòng mã. Gần như toàn bộ codebase được tái sử dụng, còn mã dành riêng cho C và .NET chỉ tồn tại ở bước cuối cùng của quá trình biên dịch”
Ban đầu nó khởi đầu như một backend .NET, nhưng họ nhận ra cách tiếp cận này cũng có thể hỗ trợ sinh mã C một cách dễ dàng nên đã thêm vào. Nó chuyển những gì rustc truyền sang thành biểu diễn trung gian (IR) riêng để xử lý
Các bảo đảm động như kiểm tra phạm vi cũng có thể triển khai trên runtime C mà không vấn đề gì
unsafelớnC không truyền tất cả thông tin mà Rust cần biết một cách tường minh để có thể biên dịch được
https://www.youtube.com/watch?v=1VgptLwP588
unsafe, và thường thì mã kết quả không có chất RustVí dụ, việc biến một state machine dựa trên biến toàn cục thành một state machine mang phong cách Rust hơn, chẳng hạn dùng enum có tên, sẽ rất khó
Có thể sẽ làm được nếu có sự trợ giúp của AI đủ mạnh, nhưng AI hiện vẫn còn khá thiếu khả năng thực sự làm đúng điều ta muốn, nên khó nói là đã sẵn sàng. Nó cũng cần đủ bộ nhớ để đưa toàn bộ codebase C và Rust vào cửa sổ ngữ cảnh; nếu mã vượt quá một quy mô nhất định thì rất nhanh sẽ cần phần cứng cực kỳ đắt đỏ. Nếu không, giống như nhiều LLM hỗ trợ lập trình, nó sẽ sinh ra các đoạn mã độc lập không tương thích với nhau
Dù vậy, nếu muốn mở rộng một dự án C bằng Rust hoặc viết lại dần dần, https://c2rust.com/ có thể dùng ngay
Rust là một ngôn ngữ hiện đại với quản lý gói, công cụ build/test tích hợp gọn nhẹ, ít gánh nặng cũ hơn nhiều, cùng các tính năng và cú pháp bậc cao mà mọi người thực sự thích
C cũng gọn gàng, nhưng các codebase phức tạp hưởng lợi từ một ngôn ngữ hiện đại giúp tạo ra các abstraction vững chắc trong khi vẫn giữ tốc độ của C. Tất nhiên cũng không thể bỏ qua borrow checker và an toàn bộ nhớ
Nếu có một bộ chuyển đổi Rust→C sinh ra mã C đủ chuẩn, nó có thể lấp khoảng trống đó