- Về quản lý bộ nhớ, Zig cung cấp cách tiếp cận đơn giản và trực quan hơn Rust
- Borrow checker của Rust rất mạnh, nhưng với việc phát triển các công cụ CLI nhỏ, nó lại gây ra độ phức tạp quá mức và gánh nặng cho lập trình viên
- Quản lý bộ nhớ thủ công của Zig vẫn có thể đạt được độ an toàn bộ nhớ hiệu quả chỉ với công cụ phù hợp và một chút kỷ luật từ lập trình viên
- Độ an toàn của chương trình không chỉ là an toàn bộ nhớ, mà còn phụ thuộc vào nhiều yếu tố như hành vi có thể dự đoán được, hiệu năng có thể kiểm soát, bảo vệ dữ liệu
- Rust phù hợp với các hệ thống quy mô lớn, nhưng với các công cụ CLI nhỏ và thực dụng thì Zig có lợi thế hơn về năng suất phát triển và khả năng bảo trì
Tổng quan
Gần đây, khi tạo các công cụ CLI, tôi ưu tiên chọn Zig hơn là Rust
Nền tảng của quản lý bộ nhớ: stack và heap
- Stack là vùng bộ nhớ nhanh có kích thước cố định, lưu trữ dữ liệu rất tạm thời như tham số hàm, biến cục bộ, địa chỉ trả về
- Heap là vùng dùng cho cấp phát bộ nhớ động, được sử dụng khi dữ liệu có vòng đời dài hơn hoặc kích thước chỉ được xác định lúc chạy
- Stack có cấu trúc đơn giản nhưng không gian hạn chế, còn heap thì cần quan tâm nhiều hơn đến tốc độ và phân mảnh
Borrow Checker của Rust
- Borrow checker của Rust đảm bảo an toàn bộ nhớ tại thời điểm biên dịch
- Nó áp đặt các quy tắc về tham chiếu, quyền sở hữu và vòng đời (lifetime) để ngăn chặn trước các lỗi như dereference con trỏ null hay dangling pointer
- Tuy nhiên, an toàn bộ nhớ chỉ được kiểm tra theo tiêu chí thời điểm biên dịch, chứ không thể loại bỏ hoàn toàn sai sót của người dùng hay các vấn đề thiết kế sở hữu phức tạp
Trường hợp thực tế: Notes CLI của riêng tôi
- Khi tôi định viết một CLI quản lý ghi chú cá nhân bằng Rust, borrow checker khiến tôi phải vất vả thiết kế lại cấu trúc
- Ngược lại, trong Zig, chỉ cần dùng allocator là có thể tạo chỉ mục dựa trên con trỏ và tự do thay đổi/xóa dễ dàng hơn nhiều
- Borrow checker của Rust có mục tiêu rất rõ ràng, nhưng với Zig, chỉ cần kiến thức nền tảng về quản lý bộ nhớ và một chút kỷ luật là vẫn có thể đạt được mức hiệu quả và an toàn cao
Khi an toàn bộ nhớ không phải là toàn bộ khái niệm an toàn của công cụ CLI
- Độ an toàn thực sự của một sản phẩm còn bao gồm nhiều yếu tố như hành vi có thể dự đoán được, phản hồi có ý nghĩa khi xảy ra lỗi, bảo vệ dữ liệu nhạy cảm, khả năng chống chịu trước tấn công
- Dù là Rust hay Zig, nếu không đáp ứng được các điều kiện ngoài an toàn bộ nhớ thì cũng khó có thể gọi là “an toàn”
- Ví dụ, nếu CLI lặng lẽ ghi đè dữ liệu khi có lỗi hoặc thiết lập sai quyền truy cập tệp, người dùng có thể gặp vấn đề nghiêm trọng
-
Độ an toàn của công cụ CLI
- Hành vi có thể dự đoán được: cần đảm bảo hành vi nhất quán và rõ ràng ngay cả khi đầu vào sai hoặc xuất hiện tình huống bất ngờ
- Ngăn crash và hỏng dữ liệu: cần xử lý lỗi một cách êm ái, đồng thời tránh làm hỏng dữ liệu hoặc phát sinh crash không được thông báo
- Quản lý hiệu năng: ngay cả khi xử lý lượng dữ liệu lớn cũng không được để xảy ra tiêu hao tài nguyên quá mức hoặc giảm độ phản hồi
- Bảo vệ thông tin nhạy cảm: cần chú ý đến tệp tạm và thiết lập quyền truy cập
- Khả năng chống chịu trước tấn công: cần vững vàng trước các vấn đề như kiểm tra đầu vào, tràn bộ nhớ, tấn công chèn lệnh
Điểm mạnh và giới hạn của Rust Borrow Checker
-
Điểm mạnh
- Ngăn data race và tham chiếu trùng lặp: trình biên dịch đảm bảo quy ước một tham chiếu mutable duy nhất hoặc nhiều tham chiếu immutable
- Bảo đảm mạnh ở thời điểm biên dịch: phần lớn lỗi liên quan đến bộ nhớ bị chặn trước khi chạy
- Phát hiện lỗi sớm: đây là lợi thế lớn trong các dịch vụ thương mại hoặc hệ thống đồng thời
-
Giới hạn và bất tiện
- Gánh nặng nhận thức: ngay cả với tác vụ CLI nhỏ cũng buộc phải cân nhắc quyền sở hữu/vòng đời/quản lý tham chiếu
- Boilerplate và méo mó cấu trúc: các wrapper như Rc, RefCell, việc lạm dụng clone hay thiết kế lại cấu trúc khiến người ta tập trung vào việc “làm hài lòng trình biên dịch” thay vì “giải quyết vấn đề”
- Bất lực trước lỗi logic/trạng thái: nó chỉ đảm bảo quy tắc bộ nhớ, chứ không đảm bảo tính dự đoán, lỗi logic hay tính toàn vẹn dữ liệu
- Độ phức tạp ở các edge case: xung đột lifetime dễ phát sinh trong cache, trạng thái toàn cục, mutable index
- Kết quả là, trong các dự án CLI nhỏ, borrow checker của Rust trở thành một “loại thuế tinh thần” đối với lập trình viên và có thể khiến mọi thứ phức tạp hơn mức thực sự cần thiết
Cách tiếp cận của Zig với an toàn và sự đơn giản
- Zig dựa trên các kiểm tra an toàn có thể lựa chọn và quản lý bộ nhớ thủ công
- Nó tích hợp sẵn khái niệm allocator, giúp hiện thực hóa việc sử dụng bộ nhớ có cấu trúc và có thể dự đoán được
- Bạn cũng có thể tự tạo allocator tùy biến để chỉ định cách quản lý bộ nhớ phù hợp với đặc tính dự án của mình
- Nhờ cú pháp
defercủa Zig, việc tự động giải phóng và dọn dẹp tài nguyên khi kết thúc phạm vi cũng trực quan hơn nhiều - Khác với Rust, Zig nhấn mạnh trách nhiệm của lập trình viên, nên cần có kỷ luật, nhưng nếu thiết kế cấu trúc tốt thì việc đạt được và duy trì an toàn bộ nhớ vẫn khá dễ dàng
- Zig có mã nguồn gọn gàng hơn, và việc thay đổi cấu trúc như con trỏ, danh sách, chỉ mục cũng đơn giản hơn Rust rất nhiều
- Không bị ràng buộc như Rust, vẫn có thể hiện thực mã an toàn và hiệu quả ở cùng một mức độ
- Ngoài ra, tính năng comptime của Zig còn hỗ trợ rất nhiều cho việc thực thi mã, kiểm thử và tối ưu hóa tại thời điểm biên dịch
Tầm quan trọng của trải nghiệm lập trình viên (Developer Ergonomics)
- Trải nghiệm lập trình viên (ergonomics) là yếu tố bao trùm từ cú pháp ngôn ngữ, tooling, tài liệu cho đến cộng đồng
- Rust nhờ những quy tắc cực kỳ nghiêm ngặt mà cuối cùng có thể đảm bảo an toàn bộ nhớ, nhưng các quy tắc quá nặng và ceremony lại làm giảm năng suất
- Zig nhấn mạnh thiết kế do lập trình viên dẫn dắt, nên việc viết/sửa/hiểu mã trở nên dễ và nhanh hơn
- Zig với mã trực quan, vòng lặp cải tiến nhanh, gánh nặng nhận thức thấp giúp lập trình viên tập trung giải quyết vấn đề thay vì phải vật lộn với công cụ
- Zig tin tưởng lập trình viên và trao cho họ công cụ cùng quyền lựa chọn phù hợp, trong khi Rust đôi khi có thể tạo cảm giác quá giám sát và hạn chế
- Thay vì chỉ “bảo vệ lập trình viên khỏi sai lầm”, một môi trường thân thiện với lập trình viên là môi trường đảm bảo cơ hội để họ tự học hỏi và trưởng thành thông qua chính những sai lầm của mình
Kết luận
- Với các lĩnh vực mà ưu thế của Rust được phát huy tối đa như hệ thống lớn, đa luồng, chạy dài hạn, thì Rust vẫn là lựa chọn tốt nhất
- Tuy nhiên, với các công cụ CLI nhỏ nhưng thiết thực, sự gọn nhẹ, đơn giản, tốc độ triển khai và bảo trì của Zig phù hợp hơn
- An toàn bộ nhớ chỉ là một mảnh của bức tranh an toàn; những yếu tố thiết yếu với công cụ CLI như hành vi có thể dự đoán được, khả năng bảo trì, độ vững chắc… lại dễ đạt được hơn trong Zig
- Cuối cùng, điều quan trọng không phải là “ngôn ngữ tốt hơn”, mà là lựa chọn công cụ phù hợp với quy trình làm việc và đặc tính dự án của chính mình
- Zig là ngôn ngữ cực kỳ phù hợp để phát triển các công cụ nhỏ, nơi hội tụ “an toàn bộ nhớ + chi phí nhận thức thấp + tính thân thiện với lập trình viên/năng suất”
3 bình luận
Có vẻ như hệ sinh thái của nó vẫn chưa ổn định bằng Rust.
Vì Zig ở các phiên bản mới khá thường xuyên có breaking change... nên có vẻ dù là dự án nhỏ thì tốt nhất vẫn nên gắn CI vào để tiếp tục duy trì, quản lý.
Ý kiến Hacker News
Ưu điểm của Zig là cho phép tiếp tục suy nghĩ như một lập trình viên C, nhưng ở một mức độ nào đó, đây đơn giản cũng là vấn đề quen tay
Những lập trình viên đã đủ quen với Rust thì không còn phải "đấu" với borrow checker nữa, vì họ đã suy nghĩ theo cấu trúc mã như vậy rồi
Kiểu tiếp cận như “object soup” trong Rust không hoạt động tốt, nhưng thực ra tôi không nghĩ đó vốn là cách dễ hơn về bản chất, chỉ là vì chúng ta quen nên thấy nó dễ hơn thôi
Nếu chấp nhận tiền đề rằng ergonomics (trải nghiệm sử dụng) rất khó đo lường hay định lượng, thì những tranh luận như trên sẽ cứ mãi mơ hồ
Câu chuyện “đấu với borrow checker” bắt nguồn từ thời người ta mới chỉ hiểu lifetime theo kiểu lexical trong Rust
Theo kinh nghiệm của tôi, các lập trình viên Rust dày dạn thường rải Arc khắp nơi và dùng nó gần như garbage collection tự động
Tôi cũng đã thấy nhiều dự án Rust mã nguồn mở mà ngay cả các lập trình viên Rust có kinh nghiệm cũng dùng Arc, Clone, Copy... khắp nơi
Điểm mạnh của Zig là vẫn có thể phát triển theo kiểu quen thuộc như C, đồng thời tận dụng được các tính năng an toàn từ ngôn ngữ và tooling
Tôi hầu như không đồng ý với nội dung bài gốc
Rust cũng giống C hay Zig ở chỗ buộc bạn phải suy nghĩ về lifetime, ownership và borrow scope, khác biệt là có hay không có sự hỗ trợ từ compiler
Dù thông minh đến đâu, con người vẫn sẽ mắc lỗi khi mệt mỏi hoặc mất tập trung, và biết thừa nhận điều đó mới là khôn ngoan
Không gian các chương trình mà compiler Rust coi là an toàn vẫn chưa đủ rộng, nên nó khá thường xuyên từ chối cả những chương trình hoàn toàn hợp lý
Ví dụ: nếu trong struct Foo,
barvàbazđều là chuỗi, thì khi lấy tham chiếu biến đổi tớibarrồi muốn lấy tham chiếu bất biến tớibaz, mã sẽ không compile, và trong tình huống như vậy ta buộc phải lách cấu trúc mã một cách gượng épNói ngược lại, chính việc phải đổi mã sang một thiết kế tốt thứ hai hoặc thứ ba chỉ để “tránh những trường hợp thực tế vẫn ổn nhưng lại bị từ chối compile” đã là một gánh nặng lớn
Ví dụ trên thực sự là một trường hợp rất hay, tôi muốn hỏi liệu có thể dùng nó trong blog hay bài viết của mình không
Nhìn đoạn mã trên xong tôi lại càng bớt tự tin vào lập luận của mình
Chúng ta cần nhớ rằng không phải mọi chương trình đều nhất thiết phải “an toàn” đến mức đó
Nhiều người trong chúng ta lớn lên cùng những phần mềm Unsafe mà vẫn rất yêu thích, như Star Fox 64, MS Paint, FruityLoops...
Tôi từng đọc rằng Andrew Kelley, người tạo ra Zig, đã làm Zig vì thiếu một môi trường phù hợp để phát triển phần mềm sản xuất âm nhạc (DAW), và tôi nghĩ Zig khá hợp với kiểu phần mềm sáng tạo như vậy
Ai nhạy cảm với lỗi bộ nhớ thì cứ dùng Rust
Tôi còn tin rằng Super Mario World thú vị hơn chính vì các lỗi bộ nhớ của nó
“An toàn” là cách nói rút gọn của “chương trình của tôi hoạt động đúng như dự định”
unsafeTôi hơi bối rối, không biết có phải mọi người nghĩ ý kiến của tôi tệ vì nó hàm ý rằng an toàn bộ nhớ là không quan trọng không
Tôi thấy tiếc vì giá trị của borrow checker đã bị đánh giá thấp
Borrow checker của Rust đảm bảo ở thời điểm compile rằng sẽ không có truy cập bộ nhớ không hợp lệ
Dĩ nhiên, cái giá là đôi khi phải đổi cấu trúc mã để phù hợp với các quy tắc của compiler
Khi tự dùng Rust riêng, tôi chưa từng cảm thấy các lifetime annotation là sai, chúng chỉ giống như một việc lặt vặt hơi phiền nhưng rồi cũng nhanh chóng thành quen
Chừng nào chưa dùng
unsafe, trong Rust không thể có chuyện hai thread cùng lúc ghi vào cùng một vùng nhớTôi không đồng cảm với ý “vì sao Zig lại thực dụng hơn trong CLI tool”, vì Rust vẫn có lợi thế rõ ràng trong việc ngăn CVE
Trên thực tế, phần lớn công việc của tôi vẫn làm tốt bằng ngôn ngữ GC, còn khi đóng góp cho ngôn ngữ khác thì Rust, Zig hay C/C++ tôi đều dùng được
CLI tool có gì đặc biệt đâu?
Câu nói rằng không dùng
unsafethì hai thread không thể cùng ghi vào cùng một vùng nhớ thật ra không hoàn toàn rạch ròi như vậyTôi đồng ý rằng việc hiện thực backlinks trong Rust quá phức tạp
Có thể làm được bằng
Rc,Weak,RefCell,.borrow()..., nhưng không hề dễNếu là chương trình chạy ngắn thì arena allocation cũng là một cách hay, có lẽ đây chính là kiểu CLI tool đang được nhắc tới
Rust thực sự phát huy sức mạnh ở các ứng dụng lớn, đa luồng, chạy thời gian dài
Tôi từng thực sự viết một client metaverse lớn bằng Rust, chạy hàng chục thread suốt 24 giờ mà không hề có rò rỉ bộ nhớ hay crash
Nếu làm điều tương tự bằng C++, sẽ cần đội QA và các công cụ như Valgrind là bắt buộc, còn ngôn ngữ scripting thì quá chậm về hiệu năng
Tôi cũng từng làm một máy bay mô phỏng vật lý bằng Rust, có tính cả độ cong Trái Đất và sai lệch trọng lực
Zig đúng là hấp dẫn, nhưng D vẫn còn đó, và cá nhân tôi thấy D mới là thứ thay thế C/C++ mà tôi muốn
Cú pháp Zig hơi có gì đó không hợp gu, còn Rust thì đã trở thành trung tâm của hệ sinh thái rồi
Go cũng chiếm thị phần lớn trong nhiều toolchain, và trong lĩnh vực AI thì được dùng nhiều chỉ sau Python
Trước thời Rust từng có tranh luận Go vs. D, và tôi thậm chí đã mua cả sách D rồi cuối cùng vẫn chuyển sang Go
int64cũng trực quan hơnD tốt đấy, nhưng chưa có killer app đủ để phổ biến rộng rãi
Tôi không thực sự hiểu vì sao phải cố dùng Rust hay Zig để viết CLI tool
Nút thắt cổ chai là I/O chứ không phải GC chậm
Trừ khi là game, DB hay loại ứng dụng ngốn bộ nhớ, còn không thì vấn đề GC không phải trọng tâm
Tôi muốn nhấn mạnh rằng thay vì tranh cãi về an toàn bộ nhớ, đáng để suy nghĩ hơn là vì sao phải chọn ngôn ngữ không cần GC
Nếu lý do là “vì không có GC thì vui hơn”, vậy như thế đã đủ rồi, chẳng cần tranh luận gì thêm
Thời gian startup tức thì, gần như không có độ trễ khi chạy, là một lợi ích rất lớn
Viết CLI bằng Go là một trải nghiệm rất ổn, dù bản thân tôi không thích ngôn ngữ Go lắm
Tôi ưu tiên số một những ngôn ngữ có sum type, pattern matching và hỗ trợ async
Về ý cho rằng phát triển không có GC chỉ liên quan đến game
Tranh luận về GC có phần giống hiệu ứng bandwagon
Tôi đã làm một note tool đơn giản bằng borrow/referencing tích hợp sẵn của Rust, và nó không phức tạp như tôi tưởng
Nếu hình dung cấu trúc kiểu lưu chỉ số trong danh sách
notesrồi nối bằng map, thì gần như không có khác biệt về tốc độ mà cũng chẳng có bất lợi về an toànNếu có nhầm chỉ số thì cũng chỉ thành lỗi out-of-bounds, vẫn tốt hơn rất nhiều so với việc ghi đè lên bộ nhớ kernel
Ngay cả lúc debug bằng
printfcũng dễ và trực quan hơn nhiềuRaw pointer hay reference thường chỉ nên dùng ở những nơi thực sự cần thiết như allocator hay async runtime, còn với logic thông thường thì cách làm dựa trên index phù hợp hơn
Đây cũng là lý do nổi tiếng khiến async trong Rust không dùng được self-referential struct và phát sinh các vấn đề quanh
PinCon trỏ tới giá trị lưu trong
vecsẽ mất hiệu lực nếu xảy rareallochay tình huống tương tự, và khi đó Miri sẽ báo lỗi ngayNếu tôi là một lập trình viên C++ đang tìm ngôn ngữ an toàn, tôi sẽ thấy Swift là lựa chọn phù hợp nhất
Một ngôn ngữ quen thuộc hoặc giống thứ mình đã biết sẽ giúp thích nghi nhanh hơn
Swift gần đây cũng tăng cường hỗ trợ đa nền tảng, và có nhiều người đang làm trong ủy ban chuẩn C++ tham gia
Tuy vậy, do gắn nhiều với Apple và thiếu framework UI native phổ biến, nên việc mở rộng sang phe ngoài Apple có vẻ vẫn chậm hơn
Tôi hy vọng Swift sẽ trở nên phổ biến hơn nữa
Nếu có tài liệu nào so sánh Swift với Zig/C thì rất mong được gợi ý
Có ý rằng Zig cũng có thể tạo ra phần mềm an toàn bộ nhớ nếu đủ cẩn thận, nhưng thật ra C cũng cho kết quả tương tự nếu dùng có kỷ luật ở một mức nhất định
Vấn đề là chính cái “một chút tiết chế/kỷ luật” ấy ngoài đời thực thường không đủ, nên lỗi mới phát sinh
Zig còn xử lý thêm các vấn đề dưới đây
comptime, và thời gian build nhanh hơn C++/Rust hàng chục lần cũng là điểm mạnhNếu suốt hơn 50 năm mà C vẫn thất bại với vấn đề kỷ luật này, thì đó là chuyện còn khó hơn cả “con đường của Thiếu Lâm”