What is the real difference between `*const T` and `*mut T` raw pointers?

Hello!

I’d like too know what are the exact differences between *const T and *mut T types, what guarantees they provide and what invariants the programmer must maintain. i find documentation on the topic a bit thin:

  1. Non-exhaustive list of “properties to remember” in Rust book: https://doc.rust-lang.org/book/first-edition/raw-pointers.html
  2. A similar list in the second edition https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html
  3. A bit of important info about variance in nomicon: https://doc.rust-lang.org/nomicon/subtyping.html
  4. Nothing in the reference

Here is what I think is true about *const T and *mut T. Please correct me if I am wrong and confirm, if I am right.

  1. It’s not UB to cast between *const T and *mut T.
  2. *const T is variant and *mut T is invariant over T.
  3. 1) implies that 2) is the single real difference between them.

So if you are using raw pointers to implement a data structure (that is, not for FFI), you can pretty much use any of *const or *mut, provided that variance is correct. You may even have to use *const for “mutable” data if you need variance. The prime example of this is a Unique<T> type, which is logically an owning-raw pointer. It stores *const T to be variant over T, but give access to T via *mut.

If you are using raw pointers for FFI (as parameter and return types of extern “C” functions), then *const vs *mut is purely a question of documenting intent, and does not affect the generated code at all. However, documenting intent is important, because C and C++ have a rule that you can’t mutate a constant object.

17 Likes

Your understanding matches mine, for what it's worth.

If you are using raw pointers for FFI (as parameter and return types of extern “C” functions), then *const vs *mut is purely a question of documenting intent, and does not affect the generated code at all. However, documenting intent is important, because C and C++ have a rule that you can’t mutate a constant object.

This rule is somewhat reflected on the Rust side, in that &T will coerce to *const T but not to *mut T. The only safe way to get a *mut T from an immutable T is to cast through an intermediate *const T. (Though as you noted, the possibility of this cast means that this difference is only ergonomic.)

11 Likes

Now I have a case where I’m not quite sure which raw pointer to use.

I have a pointer that

  • needs to be passed over language boundaries
  • most likely involves interior mutability
  • is a “subtype” (as in the way you do sub-typing in C, not in Rust) of the actual passed value, as shown in the following snippet
#[repr(C)]
pub struct RawObject {
    f_ptr: unsafe extern "C" fn(*const/mut RawObject, i32) -> i32,
    // the actual pointed-to object has some more fields
}

Should I use *const RawObject or *mut RawObject when passing or storing such a pointer?

The rule of thumb I follow is whether the members of the structure will be mutated by the code that receives that argument. If any writes could occur, requiring a *mut RawObject makes that clear.

The code that receives the argument won’t mutate it, but the it might be mutated by the callback function it contains.

1 Like

Sounds like the callback deserves a *mut argument, then!

I wonder whether the convention here will eventually just become Option<ptr::NonNull<T>> and *const-vs-*mut will disappear entirely…

@scottmcm probably! I am not sure if you can use Option<NonNull> for ffi though?

You can in some cases! Things like the following are already fairly common:

extern "C" {
    pub fn foo(x: Option<&i32>);
}
1 Like

Argh, sanitaizer ate my NonNull :slight_smile:

I know that Option by itself is allowed in ffi, but what about NonNull?

1 Like

Ah, makes sense :slightly_smiling_face: Looks like it trips the lint, at least:

warning: `extern` block uses type `std::option::Option<std::ptr::NonNull<i32>>` which is not FFI-safe: enum has no representation hint
 --> src/lib.rs:5:19
  |
5 |     pub fn bar(x: Option<NonNull<i32>>);
  |                   ^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(improper_ctypes)] on by default
  = help: consider adding a #[repr(...)] attribute to this enum

Its docs do say that "Option<NonNull<T>> has the same size as *mut T", though, so it’d almost work…

Ah, there’s an issue about it:

Today I’ve stumbled on this PR, which suggest that using *const where *mut is expected might be unsound (at least, that’s what relnotes say?)

Is this really unsound though? In my current view of the world, this is not unsound (by itself). What matters is how did you get that pointer. If you originally had an &mut __m256i then using *const or *mut should be OK. If you originally had an &__m256i, then both are unsound.

cc @RalfJung :slight_smile:

3 Likes

My thoughts on that:

  • It takes quite a lot for an unsafe fn to be considered unsound!
  • Assuming that variance is the only difference, it’s irrelevant here because __m256i is not generic.
  • The fact that &T coerces to *const T does make the signature surprising and dangerous.

Unless there’s something peculiar about SIMD types that I don’t know about, I wouldn’t take that PR and its relnotes too literally; I feel the author may have used the word “unsound” too hastily.

2 Likes

(See also, apparently people really don’t like the idea of Option<NonNull<T>> for FFI.)

2 Likes

Is this really allowed?

Yes. Optional reference is ffi safe (documented here https://doc.rust-lang.org/nomicon/other-reprs.html) and i32 is ffi safe (currently undocumented https://github.com/rust-lang-nursery/nomicon/issues/23, but, for example, c_int is defined as i32 on some platforms: https://docs.rs/libc/0.2.47/libc/type.c_int.html).

2 Likes

I think you missed the other distinction between *const and *mut, which is that *p = ... works on one but not on the other. Personally, I find that to be very useful. I’m glad that it’s not UB to cast between them. The UB should happen when writing to memory that’s read-only, not when casting the pointer.

I agree. *const T and *mut T are equivalent in terms of UB.

However, as @ExpHP wrote

Using *const T could lead to people thinking they can pass in an &T, and that would be UB.

4 Likes

So may I ask a question.

*const UnsafeCell<T> is isomorphic to *mut T for sure. Are they just the same?

(if you turn &T into *const UnsafeCell<T> you also get UB, so use *const UnsafeCell<T> in signature is not that dangerous)

*const T and *mut T are both equivalent to usize from a safety point of view. This is easy to see when you can cast any random integer into a raw pointer using only safe code.