Unsafe Rust

Tất cả mã nguồn mà chúng ta đã thảo luận cho đến nay đều có các đảm bảo về an toàn bộ nhớ của Rust được thực thi tại thời điểm biên dịch. Tuy nhiên, Rust có một ngôn ngữ thứ hai ẩn bên trong nó không thực thi các đảm bảo an toàn bộ nhớ này: nó được gọi là unsafe Rust và hoạt động giống như Rust thông thường, nhưng cung cấp cho chúng ta các siêu năng lực bổ sung.

Unsafe Rust tồn tại bởi vì, về bản chất, phân tích tĩnh là bảo thủ. Khi trình biên dịch cố gắng xác định liệu mã có duy trì các đảm bảo hay không, tốt hơn là nó từ chối một số chương trình hợp lệ còn hơn là chấp nhận một số chương trình không hợp lệ. Mặc dù mã có thể ổn, nhưng nếu trình biên dịch Rust không có đủ thông tin để tự tin, nó sẽ từ chối mã. Trong những trường hợp này, bạn có thể sử dụng mã unsafe để nói với trình biên dịch rằng, "Hãy tin tôi, tôi biết mình đang làm gì." Tuy nhiên, hãy cảnh giác, rằng bạn sử dụng unsafe Rust với rủi ro của riêng mình: nếu bạn sử dụng mã unsafe không chính xác, các vấn đề có thể xảy ra do không an toàn bộ nhớ, chẳng hạn như giải tham chiếu con trỏ null.

Một lý do khác khiến Rust có một phiên bản khác là unsafe là vì phần cứng máy tính cơ bản vốn không an toàn. Nếu Rust không cho phép bạn thực hiện các hoạt động không an toàn, bạn không thể thực hiện một số tác vụ nhất định. Rust cần cho phép bạn thực hiện lập trình hệ thống cấp thấp, chẳng hạn như tương tác trực tiếp với hệ điều hành hoặc thậm chí viết hệ điều hành của riêng bạn. Làm việc với lập trình hệ thống cấp thấp là một trong những mục tiêu của ngôn ngữ này. Hãy khám phá những gì chúng ta có thể làm với unsafe Rust và cách thực hiện.

Siêu Năng Lực Unsafe

Để chuyển sang unsafe Rust, sử dụng từ khóa unsafe và sau đó bắt đầu một khối mới chứa mã unsafe. Bạn có thể thực hiện năm hành động trong unsafe Rust mà bạn không thể thực hiện trong safe Rust, mà chúng ta gọi là siêu năng lực unsafe. Những siêu năng lực đó bao gồm khả năng:

  • Giải tham chiếu một con trỏ thô
  • Gọi một hàm hoặc phương thức unsafe
  • Truy cập hoặc sửa đổi một biến tĩnh có thể thay đổi
  • Triển khai một trait unsafe
  • Truy cập các trường của một union

Điều quan trọng cần hiểu là unsafe không tắt trình kiểm tra mượn hoặc vô hiệu hóa bất kỳ kiểm tra an toàn nào khác của Rust: nếu bạn sử dụng một tham chiếu trong mã unsafe, nó vẫn sẽ được kiểm tra. Từ khóa unsafe chỉ cung cấp cho bạn quyền truy cập vào năm tính năng này, sau đó không được trình biên dịch kiểm tra về an toàn bộ nhớ. Bạn vẫn sẽ có một mức độ an toàn nhất định bên trong một khối unsafe.

Ngoài ra, unsafe không có nghĩa là mã bên trong khối nhất thiết phải nguy hiểm hoặc chắc chắn sẽ có vấn đề về an toàn bộ nhớ: ý định là với tư cách là lập trình viên, bạn sẽ đảm bảo rằng mã bên trong khối unsafe sẽ truy cập bộ nhớ theo cách hợp lệ.

Con người có thể mắc lỗi và lỗi sẽ xảy ra, nhưng bằng cách yêu cầu năm hoạt động unsafe này phải nằm trong các khối được chú thích với unsafe, bạn sẽ biết rằng bất kỳ lỗi nào liên quan đến an toàn bộ nhớ phải nằm trong một khối unsafe. Giữ các khối unsafe nhỏ; bạn sẽ biết ơn sau này khi điều tra các lỗi bộ nhớ.

Để cô lập mã unsafe càng nhiều càng tốt, tốt nhất là bao bọc mã đó trong một trừu tượng an toàn và cung cấp một API an toàn, điều mà chúng ta sẽ thảo luận sau trong chương khi chúng ta xem xét các hàm và phương thức unsafe. Các phần của thư viện tiêu chuẩn được triển khai như các trừu tượng an toàn trên mã unsafe đã được kiểm tra. Bao bọc mã unsafe trong một trừu tượng an toàn ngăn việc sử dụng unsafe rò rỉ ra tất cả các nơi mà bạn hoặc người dùng của bạn có thể muốn sử dụng chức năng được triển khai với mã unsafe, bởi vì việc sử dụng một trừu tượng an toàn là an toàn.

Hãy xem xét từng siêu năng lực unsafe một. Chúng ta cũng sẽ xem xét một số trừu tượng cung cấp giao diện an toàn cho mã unsafe.

Giải Tham Chiếu một Con Trỏ Thô

Trong "Tham Chiếu Đang Treo" ở Chương 4, chúng ta đã đề cập rằng trình biên dịch đảm bảo các tham chiếu luôn hợp lệ. Unsafe Rust có hai loại mới được gọi là con trỏ thô tương tự như tham chiếu. Giống như với các tham chiếu, con trỏ thô có thể bất biến hoặc có thể thay đổi và được viết là *const T*mut T, tương ứng. Dấu hoa thị không phải là toán tử giải tham chiếu; nó là một phần của tên kiểu. Trong bối cảnh của con trỏ thô, bất biến có nghĩa là con trỏ không thể được gán trực tiếp sau khi được giải tham chiếu.

Khác với tham chiếu và con trỏ thông minh, con trỏ thô:

  • Được phép bỏ qua các quy tắc mượn bằng cách có cả con trỏ bất biến và có thể thay đổi hoặc nhiều con trỏ có thể thay đổi đến cùng một vị trí
  • Không được đảm bảo trỏ đến bộ nhớ hợp lệ
  • Được phép là null
  • Không triển khai bất kỳ quá trình dọn dẹp tự động nào

Bằng cách chọn không để Rust thực thi các đảm bảo này, bạn có thể từ bỏ an toàn được đảm bảo để đổi lấy hiệu suất cao hơn hoặc khả năng giao tiếp với ngôn ngữ hoặc phần cứng khác nơi mà các đảm bảo của Rust không áp dụng.

Listing 20-1 cho thấy cách tạo một con trỏ bất biến và một con trỏ có thể thay đổi.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}

Lưu ý rằng chúng ta không đưa từ khóa unsafe vào mã này. Chúng ta có thể tạo con trỏ thô trong mã an toàn; chúng ta chỉ không thể giải tham chiếu con trỏ thô bên ngoài một khối unsafe, như bạn sẽ thấy sau đây.

Chúng ta đã tạo con trỏ thô bằng cách sử dụng các toán tử mượn thô: &raw const num tạo một con trỏ thô bất biến *const i32, và &raw mut num tạo một con trỏ thô có thể thay đổi *mut i32. Bởi vì chúng ta tạo chúng trực tiếp từ một biến cục bộ, chúng ta biết các con trỏ thô cụ thể này là hợp lệ, nhưng chúng ta không thể đưa ra giả định đó về bất kỳ con trỏ thô nào.

Để minh họa điều này, tiếp theo chúng ta sẽ tạo một con trỏ thô mà tính hợp lệ của nó chúng ta không thể chắc chắn, sử dụng as để ép kiểu một giá trị thay vì sử dụng các toán tử mượn thô. Listing 20-2 cho thấy cách tạo một con trỏ thô đến một vị trí bộ nhớ tùy ý. Việc cố gắng sử dụng bộ nhớ tùy ý là không xác định: có thể có dữ liệu tại địa chỉ đó hoặc có thể không, trình biên dịch có thể tối ưu hóa mã để không có truy cập bộ nhớ, hoặc chương trình có thể kết thúc với lỗi phân đoạn. Thông thường, không có lý do tốt để viết mã như thế này, đặc biệt là trong các trường hợp mà bạn có thể sử dụng toán tử mượn thô thay thế, nhưng điều đó là có thể.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}

Nhớ rằng chúng ta có thể tạo con trỏ thô trong mã an toàn, nhưng chúng ta không thể giải tham chiếu con trỏ thô và đọc dữ liệu đang được trỏ đến. Trong Listing 20-3, chúng ta sử dụng toán tử giải tham chiếu * trên một con trỏ thô đòi hỏi một khối unsafe.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}

Việc tạo một con trỏ không gây hại gì; chỉ khi chúng ta cố gắng truy cập giá trị mà nó trỏ đến, chúng ta mới có thể phải đối mặt với một giá trị không hợp lệ.

Cũng lưu ý rằng trong Listing 20-1 và 20-3, chúng ta đã tạo các con trỏ thô *const i32*mut i32 đều trỏ đến cùng một vị trí bộ nhớ, nơi num được lưu trữ. Nếu thay vào đó, chúng ta cố gắng tạo một tham chiếu bất biến và một tham chiếu có thể thay đổi đến num, mã sẽ không biên dịch được vì các quy tắc sở hữu của Rust không cho phép một tham chiếu có thể thay đổi cùng lúc với bất kỳ tham chiếu bất biến nào. Với con trỏ thô, chúng ta có thể tạo một con trỏ có thể thay đổi và một con trỏ bất biến đến cùng một vị trí và thay đổi dữ liệu thông qua con trỏ có thể thay đổi, có khả năng tạo ra cuộc đua dữ liệu. Hãy cẩn thận!

Với tất cả những nguy hiểm này, tại sao bạn lại sử dụng con trỏ thô? Một trường hợp sử dụng chính là khi giao tiếp với mã C, như bạn sẽ thấy trong phần tiếp theo, "Gọi Hàm hoặc Phương Thức Unsafe." Một trường hợp khác là khi xây dựng các trừu tượng an toàn mà trình kiểm tra mượn không hiểu. Chúng ta sẽ giới thiệu các hàm unsafe và sau đó xem xét một ví dụ về một trừu tượng an toàn sử dụng mã unsafe.

Gọi Hàm hoặc Phương Thức Unsafe

Loại hoạt động thứ hai mà bạn có thể thực hiện trong một khối unsafe là gọi các hàm unsafe. Các hàm và phương thức unsafe trông giống như các hàm và phương thức thông thường, nhưng chúng có thêm unsafe trước phần còn lại của định nghĩa. Từ khóa unsafe trong bối cảnh này chỉ ra rằng hàm có các yêu cầu mà chúng ta cần duy trì khi gọi hàm này, bởi vì Rust không thể đảm bảo rằng chúng ta đã đáp ứng các yêu cầu này. Bằng cách gọi một hàm unsafe trong một khối unsafe, chúng ta đang nói rằng chúng ta đã đọc tài liệu của hàm này và chúng ta chịu trách nhiệm duy trì các hợp đồng của hàm.

Đây là một hàm unsafe có tên dangerous không làm gì trong thân hàm:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

Chúng ta phải gọi hàm dangerous trong một khối unsafe riêng biệt. Nếu chúng ta cố gắng gọi dangerous mà không có khối unsafe, chúng ta sẽ gặp lỗi:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Với khối unsafe, chúng ta đang khẳng định với Rust rằng chúng ta đã đọc tài liệu của hàm, chúng ta hiểu cách sử dụng nó đúng cách và chúng ta đã xác minh rằng chúng ta đang thực hiện hợp đồng của hàm.

Để thực hiện các hoạt động unsafe trong thân của một hàm unsafe, bạn vẫn cần sử dụng một khối unsafe, giống như trong một hàm thông thường, và trình biên dịch sẽ cảnh báo bạn nếu bạn quên. Điều này giúp giữ cho các khối unsafe càng nhỏ càng tốt, vì các hoạt động unsafe có thể không cần thiết trên toàn bộ thân hàm.

Tạo một Trừu Tượng An Toàn trên Mã Unsafe

Chỉ vì một hàm chứa mã unsafe không có nghĩa là chúng ta cần đánh dấu toàn bộ hàm là unsafe. Trên thực tế, việc bao bọc mã unsafe trong một hàm an toàn là một trừu tượng phổ biến. Ví dụ, hãy nghiên cứu hàm split_at_mut từ thư viện tiêu chuẩn, hàm này yêu cầu một số mã unsafe. Chúng ta sẽ khám phá cách chúng ta có thể triển khai nó. Phương thức an toàn này được định nghĩa trên các slice có thể thay đổi: nó lấy một slice và tạo ra hai slice bằng cách chia slice tại chỉ số được đưa vào dưới dạng đối số. Listing 20-4 cho thấy cách sử dụng split_at_mut.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}

Chúng ta không thể triển khai hàm này chỉ bằng Rust an toàn. Một nỗ lực có thể trông giống như Listing 20-5, nhưng sẽ không biên dịch được. Để đơn giản, chúng ta sẽ triển khai split_at_mut dưới dạng một hàm thay vì một phương thức và chỉ cho các slice của giá trị i32 thay vì cho một kiểu chung T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

Hàm này đầu tiên lấy tổng độ dài của slice. Sau đó, nó khẳng định rằng chỉ số được đưa vào dưới dạng tham số nằm trong slice bằng cách kiểm tra xem nó có nhỏ hơn hoặc bằng độ dài hay không. Sự khẳng định có nghĩa là nếu chúng ta truyền một chỉ số lớn hơn độ dài để chia slice, hàm sẽ panic trước khi nó cố gắng sử dụng chỉ số đó.

Sau đó, chúng ta trả về hai slice có thể thay đổi trong một tuple: một từ đầu slice ban đầu đến chỉ số mid và một từ mid đến cuối slice.

Khi chúng ta cố gắng biên dịch mã trong Listing 20-5, chúng ta sẽ gặp lỗi.

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Trình kiểm tra mượn của Rust không thể hiểu rằng chúng ta đang mượn các phần khác nhau của slice; nó chỉ biết rằng chúng ta đang mượn từ cùng một slice hai lần. Việc mượn các phần khác nhau của một slice về cơ bản là ổn vì hai slice không chồng chéo nhau, nhưng Rust không đủ thông minh để biết điều này. Khi chúng ta biết mã là ổn, nhưng Rust không biết, đó là lúc chúng ta cần sử dụng mã unsafe.

Listing 20-6 cho thấy cách sử dụng khối unsafe, con trỏ thô và một số lệnh gọi đến các hàm unsafe để làm cho việc triển khai split_at_mut hoạt động.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

Nhớ lại từ "Kiểu Slice" trong Chương 4 rằng slice là một con trỏ đến một số dữ liệu và độ dài của slice. Chúng ta sử dụng phương thức len để lấy độ dài của một slice và phương thức as_mut_ptr để truy cập con trỏ thô của một slice. Trong trường hợp này, vì chúng ta có một slice có thể thay đổi cho các giá trị i32, as_mut_ptr trả về một con trỏ thô với kiểu *mut i32, mà chúng ta đã lưu trữ trong biến ptr.

Chúng ta giữ khẳng định rằng chỉ số mid nằm trong slice. Sau đó, chúng ta đi đến mã unsafe: hàm slice::from_raw_parts_mut nhận một con trỏ thô và một độ dài, và nó tạo ra một slice. Chúng ta sử dụng nó để tạo một slice bắt đầu từ ptr và dài mid phần tử. Sau đó, chúng ta gọi phương thức add trên ptr với mid là đối số để có được một con trỏ thô bắt đầu tại mid, và chúng ta tạo một slice sử dụng con trỏ đó và số phần tử còn lại sau mid làm độ dài.

Hàm slice::from_raw_parts_mut là unsafe vì nó nhận một con trỏ thô và phải tin rằng con trỏ này là hợp lệ. Phương thức add trên các con trỏ thô cũng là unsafe vì nó phải tin rằng vị trí offset cũng là một con trỏ hợp lệ. Do đó, chúng ta phải đặt một khối unsafe xung quanh các lệnh gọi của chúng ta đến slice::from_raw_parts_mutadd để chúng ta có thể gọi chúng. Bằng cách xem xét mã và thêm khẳng định rằng mid phải nhỏ hơn hoặc bằng len, chúng ta có thể nói rằng tất cả các con trỏ thô được sử dụng trong khối unsafe sẽ là các con trỏ hợp lệ đến dữ liệu trong slice. Đây là một cách sử dụng unsafe có thể chấp nhận và thích hợp.

Lưu ý rằng chúng ta không cần phải đánh dấu hàm split_at_mut kết quả là unsafe, và chúng ta có thể gọi hàm này từ Rust an toàn. Chúng ta đã tạo một trừu tượng an toàn cho mã unsafe với một triển khai của hàm sử dụng mã unsafe theo cách an toàn, bởi vì nó chỉ tạo các con trỏ hợp lệ từ dữ liệu mà hàm này có quyền truy cập.

Ngược lại, việc sử dụng slice::from_raw_parts_mut trong Listing 20-7 có thể sẽ gây crash khi slice được sử dụng. Mã này lấy một vị trí bộ nhớ tùy ý và tạo một slice dài 10.000 phần tử.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

Chúng ta không sở hữu bộ nhớ tại vị trí tùy ý này, và không có đảm bảo rằng slice mà mã này tạo ra chứa các giá trị i32 hợp lệ. Cố gắng sử dụng values như thể nó là một slice hợp lệ dẫn đến hành vi không xác định.

Sử dụng Hàm extern để Gọi Mã Bên Ngoài

Đôi khi, mã Rust của bạn có thể cần tương tác với mã được viết bằng một ngôn ngữ khác. Đối với điều này, Rust có từ khóa extern tạo điều kiện cho việc tạo và sử dụng một Giao Diện Hàm Ngoại (FFI). FFI là một cách để một ngôn ngữ lập trình định nghĩa các hàm và cho phép một ngôn ngữ lập trình khác (ngoại) gọi các hàm đó.

Listing 20-8 minh họa cách thiết lập tích hợp với hàm abs từ thư viện tiêu chuẩn C. Các hàm được khai báo trong các khối extern thường không an toàn khi gọi từ mã Rust, vì vậy các khối extern cũng phải được đánh dấu là unsafe. Lý do là vì các ngôn ngữ khác không thực thi các quy tắc và đảm bảo của Rust, và Rust không thể kiểm tra chúng, vì vậy trách nhiệm thuộc về lập trình viên để đảm bảo an toàn.

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

Trong khối unsafe extern "C", chúng ta liệt kê tên và chữ ký của các hàm bên ngoài từ một ngôn ngữ khác mà chúng ta muốn gọi. Phần "C" định nghĩa giao diện nhị phân ứng dụng (ABI) mà hàm bên ngoài sử dụng: ABI định nghĩa cách gọi hàm ở cấp độ assembly. ABI "C" là phổ biến nhất và tuân theo ABI của ngôn ngữ lập trình C. Thông tin về tất cả các ABI mà Rust hỗ trợ có sẵn trong Tài liệu tham khảo Rust.

Mỗi mục được khai báo trong một khối unsafe extern đều được ngầm hiểu là unsafe. Tuy nhiên, một số hàm FFI thực sự an toàn để gọi. Ví dụ, hàm abs từ thư viện tiêu chuẩn của C không có bất kỳ cân nhắc an toàn bộ nhớ nào và chúng ta biết nó có thể được gọi với bất kỳ giá trị i32 nào. Trong những trường hợp như thế này, chúng ta có thể sử dụng từ khóa safe để nói rằng hàm cụ thể này an toàn để gọi ngay cả khi nó nằm trong một khối unsafe extern. Sau khi chúng ta thực hiện thay đổi đó, việc gọi nó không còn yêu cầu một khối unsafe, như thể hiện trong Listing 20-9.

unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}

Việc đánh dấu một hàm là safe không tự nhiên làm cho nó an toàn! Thay vào đó, nó giống như một lời hứa mà bạn đang tạo với Rust rằng nó an toàn. Vẫn là trách nhiệm của bạn để đảm bảo lời hứa đó được giữ!

Gọi Hàm Rust từ Các Ngôn Ngữ Khác

Chúng ta cũng có thể sử dụng extern để tạo một giao diện cho phép các ngôn ngữ khác gọi các hàm Rust. Thay vì tạo một khối extern hoàn chỉnh, chúng ta thêm từ khóa extern và chỉ định ABI để sử dụng ngay trước từ khóa fn cho hàm liên quan. Chúng ta cũng cần thêm chú thích #[unsafe(no_mangle)] để nói với trình biên dịch Rust không làm rối tên của hàm này. Làm rối là khi một trình biên dịch thay đổi tên mà chúng ta đã đặt cho một hàm thành một tên khác chứa thêm thông tin cho các phần khác của quá trình biên dịch sử dụng nhưng ít dễ đọc hơn đối với con người. Mỗi trình biên dịch ngôn ngữ lập trình làm rối tên hơi khác nhau, vì vậy để một hàm Rust có thể được đặt tên bởi các ngôn ngữ khác, chúng ta phải vô hiệu hóa việc làm rối tên của trình biên dịch Rust. Điều này là không an toàn vì có thể có xung đột tên giữa các thư viện mà không có cơ chế làm rối tên tích hợp, vì vậy đó là trách nhiệm của chúng ta để đảm bảo tên mà chúng ta chọn là an toàn để xuất mà không làm rối.

Trong ví dụ sau, chúng ta làm cho hàm call_from_c có thể truy cập từ mã C, sau khi nó được biên dịch thành một thư viện chia sẻ và được liên kết từ C:

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
}

Cách sử dụng extern này chỉ yêu cầu unsafe trong thuộc tính, không phải trên khối extern.

Truy Cập hoặc Sửa Đổi một Biến Tĩnh Có Thể Thay Đổi

Trong sách này, chúng ta vẫn chưa nói về các biến toàn cục, mà Rust có hỗ trợ nhưng có thể gây ra vấn đề với các quy tắc sở hữu của Rust. Nếu hai luồng đang truy cập cùng một biến toàn cục có thể thay đổi, nó có thể gây ra đua dữ liệu.

Trong Rust, các biến toàn cục được gọi là biến tĩnh. Listing 20-10 cho thấy một ví dụ về khai báo và sử dụng một biến tĩnh với một slice chuỗi làm giá trị.

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {HELLO_WORLD}");
}

Các biến tĩnh tương tự như các hằng số, mà chúng ta đã thảo luận trong "Hằng Số" trong Chương 3. Tên của các biến tĩnh là SCREAMING_SNAKE_CASE theo quy ước. Các biến tĩnh chỉ có thể lưu trữ các tham chiếu với thời gian sống 'static, có nghĩa là trình biên dịch Rust có thể tính toán thời gian sống và chúng ta không cần phải chú thích nó một cách rõ ràng. Truy cập một biến tĩnh bất biến là an toàn.

Một sự khác biệt tinh tế giữa các hằng số và các biến tĩnh bất biến là các giá trị trong một biến tĩnh có một địa chỉ cố định trong bộ nhớ. Việc sử dụng giá trị sẽ luôn truy cập cùng một dữ liệu. Các hằng số, mặt khác, được phép nhân bản dữ liệu của chúng bất cứ khi nào chúng được sử dụng. Một sự khác biệt khác là các biến tĩnh có thể có thể thay đổi. Truy cập và sửa đổi các biến tĩnh có thể thay đổi là không an toàn. Listing 20-11 cho thấy cách khai báo, truy cập và sửa đổi một biến tĩnh có thể thay đổi có tên là COUNTER.

static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}

Giống như với các biến thông thường, chúng ta chỉ định khả năng thay đổi bằng từ khóa mut. Bất kỳ mã nào đọc hoặc ghi từ COUNTER phải nằm trong một khối unsafe. Mã này biên dịch và in COUNTER: 3 như chúng ta mong đợi vì nó là đơn luồng. Việc có nhiều luồng truy cập COUNTER có thể dẫn đến đua dữ liệu, vì vậy nó là hành vi không xác định. Do đó, chúng ta cần đánh dấu toàn bộ hàm là unsafe, và ghi chú giới hạn an toàn, để bất kỳ ai gọi hàm đều biết những gì họ được và không được phép làm một cách an toàn.

Bất cứ khi nào chúng ta viết một hàm unsafe, theo quy ước, chúng ta nên viết một bình luận bắt đầu bằng SAFETY và giải thích những gì người gọi cần làm để gọi hàm một cách an toàn. Tương tự, bất cứ khi nào chúng ta thực hiện một hoạt động unsafe, theo quy ước, chúng ta nên viết một bình luận bắt đầu bằng SAFETY để giải thích cách các quy tắc an toàn được duy trì.

Ngoài ra, trình biên dịch sẽ không cho phép bạn tạo các tham chiếu đến một biến tĩnh có thể thay đổi. Bạn chỉ có thể truy cập nó thông qua một con trỏ thô, được tạo bằng một trong các toán tử mượn thô. Điều đó bao gồm cả trong các trường hợp khi tham chiếu được tạo một cách vô hình, như khi nó được sử dụng trong println! trong mã này. Yêu cầu rằng các tham chiếu đến các biến tĩnh có thể thay đổi chỉ có thể được tạo thông qua các con trỏ thô giúp làm cho các yêu cầu an toàn để sử dụng chúng trở nên rõ ràng hơn.

Với dữ liệu có thể thay đổi mà có thể truy cập toàn cục, khó để đảm bảo rằng không có đua dữ liệu, đó là lý do tại sao Rust coi các biến tĩnh có thể thay đổi là không an toàn. Khi có thể, tốt hơn là sử dụng các kỹ thuật đồng thời và các con trỏ thông minh an toàn với luồng mà chúng ta đã thảo luận trong Chương 16 để trình biên dịch kiểm tra rằng việc truy cập dữ liệu từ các luồng khác nhau được thực hiện một cách an toàn.

Triển Khai một Trait Unsafe

Chúng ta có thể sử dụng unsafe để triển khai một trait unsafe. Một trait là unsafe khi ít nhất một trong các phương thức của nó có một bất biến mà trình biên dịch không thể xác minh. Chúng ta khai báo rằng một trait là unsafe bằng cách thêm từ khóa unsafe trước trait và đánh dấu việc triển khai trait cũng là unsafe, như thể hiện trong Listing 20-12.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}

Bằng cách sử dụng unsafe impl, chúng ta đang hứa rằng chúng ta sẽ duy trì các bất biến mà trình biên dịch không thể xác minh.

Ví dụ, nhớ lại các trait đánh dấu SyncSend mà chúng ta đã thảo luận trong "Khả năng mở rộng đồng thời với các Trait SyncSend" trong Chương 16: trình biên dịch triển khai các trait này tự động nếu các kiểu của chúng ta được tạo thành hoàn toàn từ các kiểu khác triển khai SendSync. Nếu chúng ta triển khai một kiểu chứa một kiểu không triển khai Send hoặc Sync, chẳng hạn như con trỏ thô, và chúng ta muốn đánh dấu kiểu đó là Send hoặc Sync, chúng ta phải sử dụng unsafe. Rust không thể xác minh rằng kiểu của chúng ta duy trì các đảm bảo rằng nó có thể được gửi an toàn qua các luồng hoặc truy cập từ nhiều luồng; do đó, chúng ta cần thực hiện các kiểm tra đó thủ công và chỉ ra như vậy với unsafe.

Truy Cập các Trường của một Union

Hành động cuối cùng chỉ hoạt động với unsafe là truy cập các trường của một union. Một union tương tự như một struct, nhưng chỉ một trường được khai báo được sử dụng trong một phiên bản cụ thể tại một thời điểm. Unions chủ yếu được sử dụng để giao tiếp với các unions trong mã C. Truy cập các trường của union là unsafe vì Rust không thể đảm bảo kiểu của dữ liệu hiện đang được lưu trữ trong phiên bản union. Bạn có thể tìm hiểu thêm về unions trong Tài liệu tham khảo Rust.

Sử Dụng Miri để Kiểm Tra Mã Unsafe

Khi viết mã unsafe, bạn có thể muốn kiểm tra rằng những gì bạn đã viết thực sự là an toàn và chính xác. Một trong những cách tốt nhất để làm điều đó là sử dụng Miri, một công cụ Rust chính thức để phát hiện hành vi không xác định. Trong khi trình kiểm tra mượn là một công cụ tĩnh hoạt động tại thời điểm biên dịch, Miri là một công cụ động hoạt động tại thời điểm chạy. Nó kiểm tra mã của bạn bằng cách chạy chương trình của bạn, hoặc bộ kiểm tra của nó, và phát hiện khi bạn vi phạm các quy tắc mà nó hiểu về cách Rust nên hoạt động.

Sử dụng Miri yêu cầu một bản dựng nightly của Rust (mà chúng ta nói thêm trong Phụ lục G: Rust được tạo ra như thế nào và "Rust Nightly"). Bạn có thể cài đặt cả phiên bản nightly của Rust và công cụ Miri bằng cách nhập rustup +nightly component add miri. Điều này không thay đổi phiên bản Rust mà dự án của bạn sử dụng; nó chỉ thêm công cụ vào hệ thống của bạn để bạn có thể sử dụng nó khi muốn. Bạn có thể chạy Miri trên một dự án bằng cách nhập cargo +nightly miri run hoặc cargo +nightly miri test.

Ví dụ về việc điều này có thể hữu ích như thế nào, hãy xem điều gì xảy ra khi chúng ta chạy nó với Listing 20-11.

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
COUNTER: 3

Miri chính xác cảnh báo chúng ta rằng chúng ta có các tham chiếu chia sẻ đến dữ liệu có thể thay đổi. Ở đây, Miri chỉ đưa ra cảnh báo vì điều này không được đảm bảo là hành vi không xác định trong trường hợp này, và nó không nói cho chúng ta cách khắc phục vấn đề. nhưng ít nhất chúng ta biết có rủi ro về hành vi không xác định và có thể suy nghĩ về cách làm cho mã an toàn. Trong một số trường hợp, Miri cũng có thể phát hiện các lỗi rõ ràng—các mẫu mã chắc chắn là sai—và đưa ra các khuyến nghị về cách khắc phục các lỗi đó.

Miri không bắt tất cả mọi thứ mà bạn có thể làm sai khi viết mã unsafe. Miri là một công cụ phân tích động, vì vậy nó chỉ bắt các vấn đề với mã thực sự được chạy. Điều đó có nghĩa là bạn sẽ cần sử dụng nó kết hợp với các kỹ thuật kiểm tra tốt để tăng sự tự tin về mã unsafe mà bạn đã viết. Miri cũng không bao gồm mọi cách có thể mà mã của bạn có thể không đúng.

Nói cách khác: Nếu Miri phát hiện một vấn đề, bạn biết có một lỗi, nhưng chỉ vì Miri không bắt được lỗi không có nghĩa là không có vấn đề. Tuy nhiên, nó có thể bắt được nhiều lỗi. Hãy thử chạy nó trên các ví dụ khác về mã unsafe trong chương này và xem nó nói gì!

Bạn có thể tìm hiểu thêm về Miri tại kho lưu trữ GitHub của nó.

Khi Nào Nên Sử Dụng Mã Unsafe

Việc sử dụng unsafe để sử dụng một trong năm siêu năng lực vừa thảo luận không sai hoặc thậm chí không bị coi là xấu, nhưng nó phức tạp hơn để có được mã unsafe chính xác vì trình biên dịch không thể giúp duy trì an toàn bộ nhớ. Khi bạn có lý do để sử dụng mã unsafe, bạn có thể làm như vậy, và việc có chú thích unsafe rõ ràng giúp dễ dàng theo dõi nguồn gốc của các vấn đề khi chúng xảy ra. Bất cứ khi nào bạn viết mã unsafe, bạn có thể sử dụng Miri để giúp bạn tự tin hơn rằng mã bạn đã viết tuân theo các quy tắc của Rust.

Để khám phá sâu hơn về cách làm việc hiệu quả với unsafe Rust, hãy đọc hướng dẫn chính thức của Rust về chủ đề này, Rustonomicon.