Những điều làm tốt
- Việc viết lại được thực hiện theo các bước nhỏ (dần dần, stop-and-go), hoạt động tốt, và mã mới trở nên dễ đọc, dễ hiểu hơn
- Có được cái nhìn bao quát toàn bộ mã nguồn nên tìm ra các cơ hội tối ưu hiệu năng
- Loại bỏ được khoảng 1/3 ~ 1/2 lượng mã không còn được sử dụng. Các ngôn ngữ lập trình hiện đại như Rust hay Go phát hiện dead code tốt hơn và báo cho lập trình viên biết
- Không phải lo truy cập ngoài phạm vi hay overflow/underflow
- Framework kiểm thử tích hợp sẵn rất hữu ích
- Rất vui vì có thể bỏ các file CMake
Những điều không suôn sẻ
Vẫn phải lần theo undefined behavior
- Khi viết lại dần dần từ C/C++ sang Rust, đã phải dùng rất nhiều raw pointer và các khối
unsafe{}
- Các quy tắc của Rust vẫn áp dụng bên trong
unsafe, nhưng vì compiler không kiểm tra nên undefined behavior rất dễ xảy ra
- Trong
unsafe, rất dễ phá vỡ quy tắc nhiều con trỏ chỉ đọc XOR một con trỏ có thể thay đổi
- Miri đóng vai trò cứu tinh khi bắt được những lỗi này
Miri không phải lúc nào cũng hoạt động và vẫn phải dùng Valgrind
- Nếu dùng thư viện có những phần được viết bằng C hoặc assembly, như thư viện mã hóa, thì Miri sẽ không hoạt động
- Có nhiều mã
unsafe mà Miri không kiểm tra được
- Một số bài kiểm thử phải chạy bằng
valgrind
Vẫn phải theo dõi memory leak
- Mẫu phổ biến của C API là cấp phát bộ nhớ trong
MYLIB_init() và giải phóng trong MYLIB_release(), nhưng rất dễ quên gọi MYLIB_release
- Lập trình viên Rust muốn tạo wrapper object bằng RAII, nhưng trong các bài kiểm thử dùng C API thì không thể dùng tính năng này
- Với logic phức tạp, rất khó luôn luôn gọi hàm dọn dẹp. Trong C có thể giải quyết bằng
goto, nhưng Rust không hỗ trợ
- Đã giải quyết bằng crate
defer, nhưng borrow checker không thích điều đó
Cross-compilation không phải lúc nào cũng chạy được
- Giống như Miri, nếu dùng thư viện có phần được triển khai bằng C hoặc assembly thì
cargo build --target=... sẽ không hoạt động ngay
Cbindgen không phải lúc nào cũng hoạt động
- Cbindgen được dùng nhiều để tạo C header từ codebase Rust, nhưng có những giới hạn hoặc bug
ABI không ổn định
- Những kiểu hữu ích của standard library như
Option không có ABI ổn định, nên phải sao chép thủ công bằng annotation repr(C)
Thiếu hỗ trợ cho custom memory allocator
- Nhiều thư viện C cho phép người dùng cung cấp allocator ở runtime. Trong Rust chỉ có thể chọn global allocator tại compile time
- Vấn đề dọn dẹp tài nguyên có thể được giải quyết bằng arena allocator, nhưng trong Rust điều này không mang tính idiomatic và không tích hợp với standard library
Độ phức tạp
- Phải dùng những thứ như
UnsafeCell, RefCell, MaybeUninit, Pin để xử lý FFI nên độ phức tạp tăng cao
- Rust thuần đã phức tạp sẵn, mà thêm cả lớp FFI vào nữa thì trở thành một con quái vật
- Thậm chí có những lập trình viên đã từ chối làm việc trên codebase này vì độ phức tạp của Rust
Kết luận
- Nhìn chung hài lòng với việc viết lại bằng Rust, nhưng cũng thất vọng ở một số mảng, và cần nhiều công sức hơn rất nhiều so với dự đoán
- Rust tương tác nhiều với C cho cảm giác như một ngôn ngữ hoàn toàn khác so với khi dùng Rust thuần. Có rất nhiều ma sát và cạm bẫy. Nhiều vấn đề của C++ mà Rust tuyên bố đã giải quyết thì thực ra hoàn toàn chưa được giải quyết
- Xin cảm ơn sâu sắc tới những người phát triển Rust, Miri, cbindgen, v.v. Họ đã làm nên một công việc đáng kinh ngạc. Dù vậy, ngôn ngữ và công cụ cho trường hợp dùng nhiều C FFI vẫn còn non nớt, gần như cảm giác trước cả v1.0
- Nếu ergonomics của
unsafe, standard library, tài liệu, công cụ, và ABI không ổn định được cải thiện trong tương lai, trải nghiệm có thể sẽ dễ chịu hơn nhiều
- Có vẻ Microsoft và Google cũng cảm nhận được tất cả những điều này nên đang thực sự đầu tư tiền vào lĩnh vực này
- Nếu vẫn chưa biết Rust, dự án đầu tiên nên dùng Rust thuần và tránh xa chủ đề FFI
- Ban đầu tác giả từng cân nhắc dùng Zig hoặc Odin cho lần viết lại này, nhưng không muốn dùng một ngôn ngữ chưa tới v1.0 cho codebase production của doanh nghiệp. Giờ thì lại tự hỏi liệu trải nghiệm đó có thực sự tệ hơn Rust hay không. Có lẽ mô hình Rust thực sự không hợp với mô hình C (hoặc C++), nên khi dùng cả hai cùng nhau thì ma sát quá lớn
- Nếu sau này phải làm một công việc tương tự, tác giả sẽ cân nhắc Zig một cách nghiêm túc. Mỗi khi ai đó nói “cứ viết lại bằng Rust đi”, hãy đưa cho họ bài viết này và hỏi xem họ đã đổi ý chưa
12 bình luận
Dù Zig vẫn đang ở giai đoạn pre-v1, nó vẫn dùng được khá nhiều thư viện C nên thực tế hữu dụng hơn tưởng tượng. Khi cần chồng thêm thứ gì đó lên một dự án đang chạy dựa trên C, Zig có thể phù hợp hơn Rust.
khi tìm hiểu Rust, ngay khoảnh khắc nhìn thấy từ khóa
unsafe, tôi đã có cảm giác rợn người...Tôi không nghĩ Rust có thể giải quyết những vấn đề cố hữu mà C++ đang có. Đây là góc nhìn thực tế hơn là góc nhìn về cú pháp.
Lý do là:
Đã có quá nhiều hệ thống production đang dùng C/C++. Và chúng đang vận hành ổn định. Hơn nữa, phần lớn trong số đó cũng không nhất thiết muốn port sang Rust.
Ngay từ đầu, phần cứng không được tạo ra với giả định về reference counting. Nhiều trường hợp dùng C/C++ là để điều khiển phần cứng, OS, driver, tầng binary ở tốc độ cao; nhưng để hỗ trợ Rust thì lập trình viên low-level cuối cùng vẫn phải trực tiếp quản lý vòng đời tài nguyên bằng
unsafe, và đây cũng là một chi phí lớn.Tôi cho rằng trải nghiệm của tác giả quan trọng hơn giá trị tiềm ẩn của ngôn ngữ hay những câu chuyện mang tính lý thuyết.
Tôi cảm thấy mức độ quản lý tài nguyên trong những lĩnh vực thực sự cần ngôn ngữ cấp C/C++ giống như một miếng sườn gà khi muốn thay thế bằng Rust.
Bài này cũng lao vào Rust với một sự hiểu sai về nó.
Nhìn nội dung thì có vẻ đây là một thư viện phải thường xuyên giao tiếp với bên ngoài Rust, mà đến thời điểm đó thì gần như chắc chắn sẽ trở nên lộn xộn... Ngay từ đầu đã chẳng có ngôn ngữ native nào là không lộn xộn, còn Rust thì vì ở cấp độ ngôn ngữ nó bọc phần đó lại một cách an toàn, nên càng có nhiều điểm tiếp xúc với bên ngoài ngôn ngữ thì lợi thế đó càng giảm đi nhiều.
Điều này đúng ở một mức độ nào đó, nhưng trong môi trường phát triển của bài gốc thì vốn dĩ không thể giải quyết được, nên tôi nghĩ vấn đề là ở chỗ họ đã tiếp cận Rust như thể nó là thuốc chữa bách bệnh.
Tôi cảm thấy ý của bài là vì trong quá trình chuyển đổi dần từ C/C++ sang Rust thì buộc phải dùng
unsafe, nên việc chuyển sang Rust không có nhiều ý nghĩa. Thay vì chuyển đổi dần sang Rust, tôi sẽ chọn Zig. Nhưng trong bài gốc có chỗ nào nói rằng đó là một thư viện phải thường xuyên giao tiếp với bên ngoài Rust không?Việc dùng FFI đồng nghĩa với việc giao tiếp với bên ngoài Rust.
Và nhìn vào nội dung bài thì có vẻ không chỉ dừng ở mức trao đổi một vài trạng thái hay dữ liệu đơn giản, mà là bên trong và bên ngoài tương tác với nhau một cách phức tạp.
Nếu muốn dần dần chuyển một thư viện viết bằng C sang Rust thì chẳng phải FFI là điều không thể tránh khỏi sao? Có lẽ sẽ phải thay từng phần nhỏ của chương trình bằng Rust và xử lý phần C còn lại bằng FFI, nên có phải ý bạn khi nói "giao tiếp với bên ngoài" là những công việc như vậy không? Nếu vậy thì tôi nghĩ việc tác giả bài gốc trở nên hoài nghi về Rust cũng là điều tự nhiên. Trừ khi thay toàn bộ mã cùng một lúc, nếu không sẽ không có được lợi ích của Rust, nên mới khuyến nghị Zig chăng.
^-^
Vì các phần
unsafeđược đánh dấu tường minh trong mã nguồn, nên tôi kỳ vọng sẽ hữu ích ở chỗ có thể xác định toàn bộ phạm vi ảnh hưởng của FFI, miễn là không dùng khốiunsafengay từ điểm vào của chương trình. Nhưng có vẻ điều này không để lại nhiều ấn tượng với tác giả.Ngay từ thời điểm dùng FFI, có thể xem như thiết kế an toàn đã không còn khả thi nữa.
Đúng vậy.
Đúng vậy, đã mạnh dạn viết rằng nó bị trát đầy
unsafemà cuối cùng vẫn nói là chưa được giải quyết sao...