Dù còn khá mới mẻ, Rust đã nhanh chóng leo lên vị trí cao trong danh sách các ngôn ngữ lập trình phổ biến. Vào tháng 7 năm 2019, Rust đứng ở vị trí thứ 33, nhưng đến tháng 7 năm 2020, nó đã tăng lên vị trí thứ 18 trên bảng xếp hạng TIOBE Programming Community Index. Từ năm 2016, Rust liên tục được bình chọn là ngôn ngữ "được yêu thích nhất" theo khảo sát của Stack Overflow.
Điều gì khiến Rust trở nên hấp dẫn đối với các lập trình viên? Ngay từ đầu, Rust đã được các nhà phát triển hứa hẹn về tính an toàn và có hiệu suất cao. Vậy Rust thực sự là một ngôn ngữ như thế nào? Có đúng với những gì mà đội phát triển đã hứa hẹn? Hãy cùng mình tìm hiểu trong bài viết sau đây nhé.
1. Rust là gì?
Rust là một ngôn ngữ lập trình cấp hệ thống (systems-level) tập trung vào sự an toàn và hiệu suất thực thi thông qua hệ thống quản lý bộ nhớ độc đáo, giúp ngăn chặn các lỗi phổ biến như tràn bộ đệm và xung đột dữ liệu mà không làm giảm khả năng kiểm soát ở cấp thấp.
Tên của ngôn ngữ lập trình Rust xuất phát từ nấm rỉ sét (rust fungi), một loại nấm có sức sống mạnh mẽ và khả năng tồn tại trong môi trường khắc nghiệt. Người sáng lập Rust, Graydon Hoare, đã chọn tên này với mong muốn tạo ra một ngôn ngữ mạnh mẽ, bền vững và có khả năng xử lý các vấn đề khó khăn trong lập trình hệ thống.
2. Các tính năng An toàn của Rust
Rust quản lý bộ nhớ và ngăn chặn các lỗi phổ biến liên quan đến bộ nhớ, như buffer overread
và data race
. Trong đó:
- Data Race: có nghĩa là "tranh chấp dữ liệu" hay "xung đột dữ liệu", đây là một tình huống trong lập trình đa luồng, khi có hai hoặc nhiều luồng cùng truy cập và thay đổi một vùng dữ liệu (VD: đồng thời cập nhật một biến đếm) mà không có cơ chế đồng bộ hóa phù hợp.
- Buffer Overread: có nghĩa là "đọc tràn bộ đệm" hoặc "đọc quá bộ đệm", lỗi xảy ra khi một chương trình cố gắng đọc dữ liệu ngoài phạm vi được cấp phát cho bộ đệm, dẫn đến việc truy cập các vùng bộ nhớ không mong muốn và có thể gây ra lỗi hoặc rò rỉ thông tin.
2.1 Ownership System
Rust sử dụng hệ thống sở hữu (Ownership System) để quản lý việc cấp phát và giải phóng bộ nhớ ngay trong quá trình biên dịch, giúp cải thiện hiệu suất khi chạy chương trình và ngăn chặn các lỗi như use after free
(cố gắng sử dụng bộ nhớ đã được giải phóng) và double free
(giải phóng bộ nhớ hai lần).
fn main() {
let x = String::from("Hello, Rust!"); // Bộ nhớ được cấp phát cho chuỗi "Hello, Rust!"
println!("{}", x); // Biến x sử dụng bộ nhớ này.
} // Khi x ra khỏi phạm vi sử dụng, bộ nhớ được giải phóng tự động.
Trong Rust, khái niệm "sở hữu" (ownership) xác định ai hoặc cái gì chịu trách nhiệm quản lý (cấp phát và giải phóng) bộ nhớ mà biến sử dụng. Khi biến tồn tại trong phạm vi của nó, biến là chủ sở hữu của bộ nhớ mà nó đang sử dụng. Bộ nhớ sẽ được cấp phát khi một biến được khai báo và sẽ được giải phóng khi biến đó không còn nằm trong phạm vi sử dụng (scope).
2.2 Borrowing System
Hệ thống sở hữu của Rust cho phép chỉ có một chủ sở hữu của bộ nhớ tại một thời điểm. Điều này có thể gây ra vấn đề khi chia sẻ giá trị giữa các hàm, cấu trúc, hoặc luồng. Để giải quyết vấn đề này, Rust sử dụng hệ thống mượn, với các quy tắc:
- Immutable reference: là tham chiếu đến một biến, cho phép đọc dữ liệu của biến đó nhưng không cho phép thay đổi dữ liệu. Bạn có thể có nhiều immutable references đến cùng một biến cùng lúc. Điều này có nghĩa là nhiều luồng hoặc nhiều phần khác nhau của chương trình có thể đọc dữ liệu từ cùng một biến mà không lo bị thay đổi bất ngờ.
- Mutable Reference: là tham chiếu đến một biến, cho phép cả đọc và thay đổi dữ liệu của biến đó. Tại một thời điểm, chỉ có thể có một mutable reference đến một biến cụ thể. Điều này đảm bảo rằng không có luồng hoặc phần nào khác của chương trình có thể gây ra xung đột bằng cách thay đổi dữ liệu trong khi nó đang được thay đổi bởi một mutable reference khác
Do chỉ có một mutable reference tại một thời điểm, Rust ngăn chặn các xung đột dữ liệu trong môi trường đa luồng (data races).
2.3 Automatic Bounds Checking
Rust có tính năng "kiểm tra ranh giới tự động" khi truy cập bộ đệm (buffer). Điều này có nghĩa là khi bạn cố gắng truy cập vào một vị trí trong bộ đệm, Rust sẽ đảm bảo rằng vị trí này nằm trong phạm vi hợp lệ. Nếu bạn cố gắng truy cập ra ngoài phạm vi này, chương trình sẽ gây ra một lỗi panic
và dừng lại thay vì tiếp tục hoạt động trong trạng thái không xác định, ngăn ngừa các lỗi như buffer overread
(đọc tràn bộ đệm) và buffer overflow
(tràn bộ đệm).
fn main() {
let buffer = [1, 2, 3, 4, 5]; // Mảng có 5 phần tử
// Cố gắng truy cập phần tử ngoài phạm vi của mảng
println!("Phần tử thứ 6: {}", buffer[5]);
}
Khi chạy chương trình trên, Rust sẽ đưa ra thông báo lỗi và dừng lại: thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:5:39
3. Các tính năng Hiệu suất cao của Rust
Các số liệu bên dưới cho thấy kết quả kiểm tra hiệu suất giữa 6 ngôn ngữ Rust, C, C++, Go, Java, Python với 3 thuật toán Bubble Sort và Monte Carlo Pi Estimation, Monte Carlo Pi Estimation SimpleRNG. Rust dẫn đầu về thời gian CPU ở 2 trên 3 thuật toán, chỉ thua một chút so với C và C++ ở thuật toán thứ 3.
Về vấn đề sử dụng bộ nhớ, Rust chỉ đứng sau C trong tất cả các thử nghiệm.
3.1 Zero Cost Abstraction
Rust áp dụng khái niệm zero cost abstraction để đơn giản hóa ngôn ngữ mà không làm giảm hiệu suất. Nói cách khác, các abstraction như generic, traits, và pattern matching trong Rust được thiết kế để không gây ra bất kỳ chi phí bổ sung nào vào thời gian chạy (runtime) so với việc viết code mà không sử dụng các abstraction này.
Trong Rust, bạn có thể viết các hàm generic, nhưng trong quá trình biên dịch, Rust sẽ tạo ra các phiên bản cụ thể của hàm này cho từng loại dữ liệu mà bạn sử dụng, đảm bảo không có chi phí runtime cho việc sử dụng generic.
fn max<T: Ord>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
let x = max(5, 10); // Phiên bản cho i32
let y = max(1.0, 3.5); // Phiên bản cho f64
println!("x = {}, y = {}", x, y);
}
Hàm max
là một hàm generic, có thể hoạt động với bất kỳ loại dữ liệu nào thỏa mãn trait Ord
. Khi biên dịch, Rust sẽ tạo ra hai phiên bản của hàm max
: một cho kiểu i32
và một cho kiểu f64
. Điều này đảm bảo rằng hiệu suất của code sử dụng generic không chậm hơn so với việc bạn viết các hàm riêng biệt cho mỗi loại.
3.2 Không có Garbage Collector
Rust là không có garbage collector nhờ vào hệ thống quản lý bộ nhớ tại thời điểm biên dịch. Garbage collection làm tăng chi phí trong thời gian chạy vì phải theo dõi và quản lý bộ nhớ để xác định khi nào có thể giải phóng bộ nhớ. Điều này làm tăng mức sử dụng bộ nhớ và CPU, đặc biệt là trong các thiết bị nhúng có tài nguyên hạn chế hoặc các phần mềm yêu cầu hiệu suất cao.
4. Mối liên hệ giữa Quản lý bộ nhớ và Bảo mật
Khoảng 70% các lỗi bảo mật, như được báo cáo bởi Microsoft Security Response Center, liên quan đến việc khai thác bộ nhớ như buffer overflows
(tràn bộ đệm) hoặc các sai sót trong quản lý bộ nhớ khác. Đảm bảo an toàn bộ nhớ có thể loại bỏ tới 70% lỗi bảo mật.
Các loại lỗi bộ nhớ phổ biến:
- Use after free: Khi bạn truy cập bộ nhớ đã giải phóng, nội dung của bộ nhớ đó có thể đã được thay thế hoặc sử dụng bởi một phần khác của chương trình. Kẻ tấn công có thể lợi dụng lỗ hổng này để đọc hoặc ghi dữ liệu vào bộ nhớ đó, dẫn đến rò rỉ thông tin hoặc thậm chí thực thi mã độc.
- Dereferencing a null pointer: Truy cập một con trỏ null thường dẫn đến sự cố (crash) cho chương trình, điều này có thể bị lợi dụng để thực hiện tấn công từ chối dịch vụ (DoS).
- Double free: Giải phóng bộ nhớ hai lần, kẻ tấn công có thể lợi dụng lỗ hổng này để thực hiện tấn công tấn công tràn bộ nhớ (heap overflow), thay đổi nội dung bộ nhớ và cuối cùng là chiếm quyền điều khiển hệ thống.
- Buffer overflow: Tràn bộ đệm, có thể cho phép kẻ tấn công ghi dữ liệu vào bộ nhớ ngoài phạm vi của mảng, gây hỏng dữ liệu quan trọng hoặc ghi đè địa chỉ trả về của hàm. Điều này có thể dẫn đến việc thực thi mã độc, từ đó kẻ tấn công có thể chiếm quyền điều khiển hệ thống.
- Buffer Overread: Đọc ngoài phạm vi của bộ đệm, có thể dẫn đến việc rò rỉ thông tin nhạy cảm từ bộ nhớ của chương trình. Một ví dụ nổi tiếng là lỗ hổng Heartbleed, nơi kẻ tấn công có thể đọc được dữ liệu nhạy cảm như khóa mật mã hoặc thông tin người dùng từ bộ nhớ của server.
- Data races: Xung đột dữ liệu, kẻ tấn công có thể lợi dụng điều này để gây ra các lỗi về dữ liệu, rò rỉ thông tin, hoặc leo thang đặc quyền. Ví dụ, với một ứng dụng web, data race có thể cho phép hai yêu cầu khác nhau ghi vào cùng một vùng bộ nhớ, dẫn đến hỏng dữ liệu hoặc tiết lộ thông tin người dùng.
Rust giải quyết được các vấn đề này bằng cách:
- Không cho phép quản lý bộ nhớ thủ công (manual memory management) ngoài khối mã
unsafe
, giảm thiểu các lỗi nhưuse after free
vàdouble free
. - Rust không có con trỏ null mà thay vào đó sử dụng loại
Option
để đại diện cho các giá trị có thể chưa được khởi tạo. - Rust kiểm tra ranh giới bộ đệm tự động, ngăn chặn các lỗi
buffer overflow
vàbuffer overread
. - Rust ngăn chặn
data races
bằng cách chỉ cho phép một luồng truy cập bộ nhớ tại một thời điểm hoặc nhiều luồng đọc mà không có luồng nào được phép ghi.
5. Kết luận
Rust đã được bình chọn là ngôn ngữ lập trình "được yêu thích nhất" trên Stack Overflow trong sáu năm liên tiếp (từ 2016). Sự "yêu thích" này là hoàn toàn có cơ sở, do Rust được thiết kế với mục tiêu chính là nâng cao hiệu suất và mức độ an toàn của ứng dụng, đặc biệt trong lập trình đa luồng.
Rust nổi bật với khả năng đảm bảo an toàn bộ nhớ mà không cần sử dụng garbage collector, và được đánh giá là ngôn ngữ an toàn nhất so với các ngôn ngữ như C, C++, Java, Go, và Python. Rust cũng đạt được hiệu suất cao, điều này đặc biệt ấn tượng vì hầu hết các ngôn ngữ khác thường phải thỏa hiệp giữa an toàn và hiệu suất.
Các bài viết liên quan tại Blog 200Lab:
Bài viết liên quan
Hướng dẫn tích hợp Redux và React Query trong dự án React Vite
Nov 22, 2024 • 8 min read
Giới thiệu Kiến trúc Backend for Frontend (BFF)
Nov 16, 2024 • 10 min read
Flask là gì? Hướng dẫn tạo Ứng dụng Web với Flask
Nov 15, 2024 • 7 min read
Webhook là gì? So sánh Webhook và API
Nov 15, 2024 • 8 min read
Spring Boot là gì? Hướng dẫn Khởi tạo Project Spring Boot với Docker
Nov 14, 2024 • 6 min read
Two-Factor Authentication (2FA) là gì? Vì sao chỉ Mật khẩu thôi là chưa đủ?
Nov 13, 2024 • 7 min read