Danh sách bài viết

Bài 283: Raw Pointer: *const T, *mut T

Bài 283 của series Rust Cơ Bản — raw pointer *const T và *mut T, tương đương pointer trong C. Reference &T và &mut T trong Rust luôn đi kèm lifetime và bị borrow checker quản chặt — đảm bảo an toàn nhưng cũng giới hạn khả năng biểu đạt. Khi cần liên thông với C, xây low-level data structure như linked list, hay thực thi atomic operation theo cách thư viện chuẩn không cho, bạn cần raw pointer: *const T chỉ đọc, *mut T có thể ghi. Raw pointer bỏ qua lifetime, bỏ qua alias rule, có thể null, có thể dangling, có thể unaligned. Bài này giới thiệu cách tạo raw pointer (không cần unsafe), khi nào cần dùng, và các API tiêu chuẩn để chuyển đổi qua lại giữa raw pointer và type sở hữu như Box hay Vec — chuẩn bị nền cho bài 284 sẽ học cách dereference an toàn.

10/06/2026
9 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu raw pointer là gì và khác biệt cốt lõi với reference &T, &mut T: không lifetime, không alias check, không bị borrow checker quản.
  • Phân biệt hai loại *const T (read-only) và *mut T (mutable) — chỉ là quy ước cho người đọc, runtime không phân biệt.
  • Tạo raw pointer từ reference qua syntax &x as *const T&mut y as *mut T.
  • Nắm nguyên tắc quan trọng: tạo raw pointer là an toàn (không cần unsafe); chỉ dereference mới bắt buộc unsafe.
  • Hiểu raw pointer có thể null, dangling, hoặc unaligned — và đó là responsibility của programmer.
  • Biết 3 use case chính: FFI với C, low-level data structure (linked list, intrusive collection), atomic operation thấp.
  • Dùng API tiêu chuẩn để chuyển đổi: Box::into_raw, Box::from_raw, Vec::as_ptr, std::ptr::null().
2

Raw Pointer Là Gì

Raw pointer là một số nguyên kích thước bằng usize đại diện cho địa chỉ memory — giống hệt pointer trong C/C++. Trong Rust, raw pointer có hai dạng:

  • *const T — pointer trỏ tới T, ngữ nghĩa "read-only".
  • *mut T — pointer trỏ tới T, ngữ nghĩa "có thể ghi".

So với reference &T&mut T, raw pointer thiếu hầu hết bảo đảm Rust thường cung cấp:

  • Không lifetime. Compiler không track xem dữ liệu pointer trỏ tới còn sống không.
  • Không alias check. Nhiều *mut T cùng trỏ một địa chỉ — borrow checker không phàn nàn.
  • Có thể null, dangling, unaligned. Reference Rust luôn non-null và aligned; raw pointer không bắt buộc.

Đổi lại, raw pointer biểu đạt được pattern mà reference không làm nổi: gọi hàm C, chia sẻ buffer nhiều owner, tự quản lifetime trong cấu trúc dữ liệu phức tạp. Vì bỏ check, raw pointer cũng là cách dễ gây undefined behavior nhất — Rustonomicon dành chương riêng cảnh báo.

3

Hai Loại: *const T Và *mut T

Cú pháp khá lạ với người đến từ C: * đứng trước const/mut, sau đó tới type. Đọc trái sang phải: "pointer tới const T" hoặc "pointer tới mut T".

let p1: *const i32;   // pointer read-only tới i32
let p2: *mut i32;     // pointer mutable tới i32
let p3: *const u8;    // pointer tới byte (phổ biến trong FFI)
let p4: *mut [u8];    // pointer tới slice (fat pointer: ptr + len)

Khác biệt giữa *const T*mut T chủ yếu là tài liệu — runtime hai loại cùng layout, cùng size usize, cast qua lại tự do (p as *mut T). Compiler chỉ phân biệt ở vài chỗ rõ ràng như std::ptr::write yêu cầu *mut T.

Convention: định đọc dùng *const T, định ghi dùng *mut T. FFI binding tới C: const char**const c_char, char**mut c_char; void**mut c_void.

4

Tạo Raw Pointer Từ Reference

Cách phổ biến nhất: ép một reference thường thành raw pointer bằng as.

fn main() {
    let x: i32 = 42;
    let p_const: *const i32 = &x as *const i32;

    let mut y: i32 = 100;
    let p_mut: *mut i32 = &mut y as *mut i32;

    println!("p_const trỏ tới địa chỉ: {:p}", p_const);
    println!("p_mut   trỏ tới địa chỉ: {:p}", p_mut);
}

Hai dòng tạo pointer này không hề có khối unsafe — Rust cho phép tạo raw pointer trong safe code, vì chỉ tạo thôi chưa truy cập memory, chưa có gì nguy hiểm. Lưu ý &mut y yêu cầu y phải mut; &x as *const T không.

Cũng có thể short-hand bỏ phần annotation rõ ràng:

let x = 42_i32;
let p: *const i32 = &x;   // coerce ngầm từ &i32 sang *const i32

Điểm thú vị: cùng một địa chỉ có thể có nhiều raw pointer *mut trỏ đến, không vi phạm gì:

fn main() {
    let mut value = 10_i32;
    let r = &mut value as *mut i32;
    let s = &mut value as *mut i32;   // OK với raw pointer

    // Hai raw pointer cùng trỏ vào địa chỉ của `value` — không alias check.
    println!("r = {:p}, s = {:p}", r, s);
    assert_eq!(r, s);
}

Cùng pattern nếu thay *mut i32 bằng &mut i32 sẽ compile error: "cannot borrow `value` as mutable more than once at a time". Raw pointer mở cánh cửa này — và đặt trách nhiệm tránh data race lên programmer.

5

Không Cần unsafe Để Tạo, Chỉ Cần Để Deref

Đây là phân chia trách nhiệm quan trọng của Rust:

  • Tạo raw pointer — an toàn, không cần unsafe. Vì pointer chỉ là một số; chưa đụng vào memory.
  • Cast giữa raw pointer — an toàn. p as *mut T, p as *const U, p as usize đều OK.
  • So sánh raw pointer (p1 == p2) — an toàn. Chỉ so sánh số.
  • Dereference (*p) hoặc gọi method qua pointer ((*p).field) — BẮT BUỘC unsafe. Vì lúc này thật sự đọc/ghi memory; pointer có thể invalid.
fn main() {
    let x: i32 = 42;
    let p: *const i32 = &x;

    // Tạo, cast, in địa chỉ — safe
    let q: *const u8 = p as *const u8;
    println!("p = {:p}, q = {:p}", p, q);

    // Dereference — bắt buộc unsafe
    let v = unsafe { *p };
    println!("value = {}", v);
}

Triết lý: tách cầm địa chỉ khỏi đọc/ghi qua địa chỉ. Cầm không gây hại — có thể log, lưu struct, truyền qua FFI. Chỉ khi đọc/ghi, programmer mới phải cam kết "pointer này hợp lệ" qua khối unsafe. Bài 284 đi sâu phần deref.

6

Null, Dangling, Unaligned

Raw pointer có thể ở 3 trạng thái không hợp lệ mà reference không bao giờ ở:

Null pointer — trỏ tới địa chỉ 0. Rust cung cấp helper trong std::ptr:

use std::ptr;

fn main() {
    let p_null: *const i32 = ptr::null();
    let p_null_mut: *mut i32 = ptr::null_mut();

    println!("p_null is null? {}", p_null.is_null());      // true
    println!("p_null_mut is null? {}", p_null_mut.is_null()); // true
}

Method .is_null() chạy được cả *const*mut, không cần unsafe — chỉ so sánh số với 0. Dereference null pointer là UB; luôn check trước nếu pointer có thể null.

Dangling pointer — trỏ tới memory đã giải phóng. Ví dụ kinh điển:

fn dangling() -> *const i32 {
    let local = 42_i32;
    &local as *const i32
    // `local` drop khi function return — pointer dangling, deref về sau là UB.
}

Đây là use-after-return — bug C/C++ thường gặp. Rust không chặn được khi ra khỏi safe code; quy ước: hạn chế tối đa khu vực dùng raw pointer, đóng gói sau abstraction an toàn.

Unaligned pointer — địa chỉ không chia hết cho align_of::<T>(). Deref unaligned là UB; cần truy cập có chủ đích thì dùng std::ptr::read_unaligned.

7

Khi Nào Cần Raw Pointer

Application Rust thông thường gần như không chạm raw pointer. Bốn use case ngoại lệ:

  • FFI với C/C++. Hàm C nhận con trỏ — binding Rust phải dùng raw pointer. Ví dụ libc::strlen: fn(*const c_char) -> size_t. CStr/CString trong std wrap quanh để cung cấp interface an toàn hơn.
  • Low-level data structure. Linked list, intrusive collection, B-tree tự cài — borrow checker không biểu đạt được "child trỏ ngược parent". std::collections::LinkedList bên trong dùng raw pointer.
  • Atomic và lock-free. AtomicPtr thao tác trực tiếp với raw pointer để tránh overhead Arc/Mutex.
  • Custom allocator. Trait GlobalAlloc nhận và trả *mut u8.

Nguyên tắc: nếu có thể dùng &/&mut hoặc smart pointer, hãy dùng. Raw pointer là phương án cuối — đóng gói chặt trong abstraction, không lộ public API. tokio, hyper, serde đều có raw pointer ở core nhưng người dùng không thấy.

8

Conversion Tiêu Chuẩn

Standard library cung cấp helper an toàn để chuyển đổi giữa smart pointer (sở hữu memory) và raw pointer:

Box::into_raw / Box::from_raw — round-trip:

fn main() {
    let boxed: Box<i32> = Box::new(99);

    // into_raw: chuyển ownership từ Box ra raw pointer.
    // Box KHÔNG còn drop value — programmer chịu trách nhiệm free.
    let raw: *mut i32 = Box::into_raw(boxed);
    println!("raw = {:p}", raw);

    // from_raw: chuyển raw pointer ngược lại thành Box.
    // unsafe vì compiler không biết pointer có thật sự từ Box::into_raw không.
    let boxed_again: Box<i32> = unsafe { Box::from_raw(raw) };
    println!("value = {}", *boxed_again);
    // boxed_again drop ở đây — free đúng lúc.
}

Pattern phổ biến khi truyền Rust object qua FFI: into_raw tạo opaque pointer cho C giữ; sau C trả lại, Rust from_raw recover ownership và drop.

Vec::as_ptr — cho FFI nhận buffer:

fn main() {
    let buffer: Vec<u8> = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]; // "Hello"

    let ptr: *const u8 = buffer.as_ptr();
    let len: usize = buffer.len();

    // ptr + len có thể truyền cho hàm C nhận `const uint8_t*` và `size_t`.
    println!("ptr = {:p}, len = {}", ptr, len);

    // Vec vẫn sở hữu memory — không được drop trong khi C còn dùng ptr.
    // Tương đương cho ghi: buffer.as_mut_ptr() trả *mut u8.
}

Lưu ý: as_ptr chỉ borrow pointer, không chuyển ownership. Vec phải sống đủ lâu cho C dùng xong. Nếu cần chuyển hẳn ownership ra C: Vec::into_raw_parts (unstable) hoặc Box<[T]>::into_raw.

9

Tổng Kết

  • Raw pointer *const T*mut T là tương đương Rust của pointer C: chỉ là một số usize chỉ địa chỉ memory.
  • Khác reference: không lifetime, không alias check, có thể null/dangling/unaligned. Compiler không quản — programmer chịu trách nhiệm.
  • Hai loại chỉ khác về intent: *const "read-only", *mut "writable". Runtime giống nhau, cast tự do.
  • Tạo raw pointer (&x as *const T, &mut y as *mut T) là safe — không cần khối unsafe. Cast, so sánh, in địa chỉ cũng safe.
  • Dereference (*p) hoặc truy cập field ((*p).f) bắt buộc unsafe — đó là lúc thật sự đụng vào memory có thể invalid.
  • Nhiều *mut cùng trỏ một địa chỉ là OK — borrow checker không phàn nàn (đổi lại programmer phải tự tránh data race).
  • std::ptr::null(), std::ptr::null_mut() tạo null pointer; .is_null() check an toàn.
  • 4 use case chính: FFI với C, low-level data structure, atomic operation, custom allocator. Application thông thường không cần.
  • Conversion tiêu chuẩn: Box::into_raw/Box::from_raw round-trip ownership, Vec::as_ptr/as_mut_ptr mượn pointer cho FFI.
  • Bài 284 sẽ học chi tiết dereference, kiểm tra null/alignment, và viết khối unsafe minimal đúng cách.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Liệt kê 3 khác biệt cốt lõi giữa &mut T*mut T. Tại sao raw pointer cho phép nhiều *mut cùng trỏ một địa chỉ trong khi &mut thì không?
  2. Đoạn code sau có compile không? Vì sao? let x = 42; let p = &x as *const i32; println!("{:p}", p);
  3. Tạo raw pointer không cần unsafe nhưng dereference thì cần. Triết lý đằng sau sự tách biệt này là gì?
  4. Bạn viết một hàm trả về *const i32 từ một biến local. Compiler không báo lỗi nhưng dereference pointer này sau khi function return là UB. Loại bug này tên gì, và vì sao Rust không chặn?
  5. Trong tình huống nào bạn cần dùng Box::into_raw? Sau khi dùng Box::into_raw, nếu không gọi Box::from_raw ngược lại, hậu quả là gì?
  6. Phân biệt Vec::as_ptr với Box::into_raw về mặt ownership. Khi truyền buffer cho hàm C, dùng cái nào và cần lưu ý gì để tránh use-after-free?
Đáp án
  1. 3 khác biệt: (a) &mut T có lifetime, raw pointer không; (b) &mut T đảm bảo non-null và aligned, raw pointer có thể null/dangling/unaligned; (c) &mut T exclusive (chỉ 1 tại một thời điểm), raw pointer không alias check. Borrow checker enforce rule "1 &mut hoặc N &" trên reference để đảm bảo data race không xảy ra. Raw pointer là escape hatch — bỏ rule này để biểu đạt pattern low-level (linked list, FFI), đổi lại programmer cam kết tự tránh race qua synchronization (mutex, atomic).
  2. Có, compile và chạy bình thường. Tạo raw pointer từ reference qua as *const T là safe operation — không cần unsafe. Print địa chỉ qua {:p} cũng safe. Chỉ khi dereference (*p) mới cần unsafe.
  3. Triết lý: tách khả năng cầm địa chỉ ra khỏi khả năng đọc/ghi qua địa chỉ. Cầm pointer chưa gây hại — chỉ là một số; có thể serialize, log, lưu struct, truyền qua FFI cho C giữ mà không đụng memory. Chỉ khi thật sự đọc/ghi (deref), pointer mới có khả năng invalid và gây UB. Đặt unsafe tại deref-site giúp khoanh vùng audit chính xác: muốn tìm chỗ có thể UB, grep unsafe; còn raw pointer chạy quanh code base không nguy hiểm tự thân.
  4. Tên: use-after-return (subtype của use-after-free) — pointer trỏ tới stack frame đã pop. Rust không chặn vì raw pointer thiết kế đúng nghĩa là "thoát khỏi borrow checker" — nếu compiler enforce lifetime trên raw pointer thì raw pointer mất đi mục đích tồn tại (FFI, low-level interop với code không hiểu lifetime Rust). Trách nhiệm tránh dangling chuyển sang programmer; lý do encapsulate raw pointer trong abstraction an toàn càng sớm càng tốt.
  5. Khi cần chuyển ownership của heap-allocated object ra khỏi sự quản lý của Rust: truyền object cho C giữ (FFI pattern opaque pointer), hoặc tự cài cấu trúc dữ liệu cần manual lifetime. Nếu không gọi Box::from_raw ngược lại, memory không bao giờ được free — leak. Box ban đầu đã "từ bỏ" ownership ở into_raw, không còn drop value khi out of scope. Phải có cơ chế (Rust gọi lại from_raw, hoặc C side gọi free function được expose) để recover ownership.
  6. Vec::as_ptr mượn pointer — Vec vẫn sở hữu memory, sẽ drop khi out of scope. Box::into_raw chuyển ownership — Box không drop nữa, programmer chịu free. Khi truyền buffer cho hàm C: nếu C dùng synchronous (đọc xong trả về ngay), dùng as_ptr + len đơn giản hơn — chỉ cần đảm bảo Vec sống tới hết call. Nếu C giữ pointer async (callback sau), phải đảm bảo Vec không drop giữa chừng — hoặc dùng Box::<[u8]>::into_raw để chuyển ownership hẳn. Bug điển hình: let p = vec![1u8,2,3].as_ptr(); ffi_call(p); — Vec drop ngay sau dòng as_ptr, pointer dangling khi ffi_call chạy. Fix: bind Vec vào let binding sống đủ lâu, hoặc clone vào Box.
11

Bài Tiếp Theo

Bài 284: Dereferencing Raw Pointer — học cách viết khối unsafe { *p } đúng cách: kiểm tra null trước khi deref, đảm bảo alignment, hiểu UB nào xảy ra khi pointer invalid, và pattern minimal-unsafe (đóng gói deref trong scope nhỏ nhất có thể). Tiếp đó bài 285 sẽ vào unsafe fnunsafe trait — cơ chế đánh dấu function/trait có precondition compiler không kiểm được.