`as_ptr` vs `as_mut_ptr`

I’m in the midst of designing an API around raw pointers. I want to follow the example of the stdlib as much as possible. I want to know how to name the method(s) that extract a raw pointer out of a container. It looks like there are two different ways it’s done:

  1. Provide a single method, as_ptr() -> *mut T. The caller can cast/coerce to *const T if necessary.
  2. Provide both as_ptr() -> *const T and as_mut_ptr() -> *mut T.

Here are the 7 instances of as_ptr() I found in the stdlib. Some return *mut T. Others return *const T and are paired with a matching as_mut_ptr(), except for CStr which is asymmetrical.

Unfortunately, 4 of these return *const T and 3 return *mut T. It’s as close as you can get to a 50/50 split.

What’s the rationale here? Is this just an unfortunate historical inconsistency, or is there some common factor between the two groups that I’m not seeing, that should lead me to choose one approach over the other?

1 Like

The difference here is actually semantic!

If you have a &Cell/&RefCell, you still are allowed to mutate the inner value (as these are interior/shared mutability types). With &slice/&str/&CStr/&MaybeUninit, the reference truly is immutable and mutating the value behind it is UB plain and simple.

NonNull is the odd one out here, but it’s also a very small wrapper around *mut. Thus, &NonNull<_> is basically &*mut _ and getting the *mut out with as_ptr(&self) makes sense.

Use as_ptr(&self) to return a shared pointer, and as_mut_ptr(&mut self) to return a mutable pointer iff the shared pointer isn’t already mutable.

3 Likes

There’s also a third alternative of returning ptr::NonNull<T>: https://doc.rust-lang.org/std/boxed/struct.Box.html#method.into_raw_non_null

2 Likes

Oh, and the variance of *const and *mut is different. (Tbh, I just pray that if I get variance wrong, someone will notice or I’ll notice in testing, and I stay away from where it matters as much as possible)

I don’t think variance of a return type practically matters but I daren’t touch variance with a 39½ft pole.

Do not fear, let #[rustc_variance] guide you!

#![feature(rustc_attrs)]

#[rustc_variance]
struct Covariant<T>(fn() -> T);

#[rustc_variance]
struct Contravariant<T>(fn(T));

#[rustc_variance]
struct Invariant<T>(fn(T) -> T);

==>

error[E0208]: [+]
 --> src/lib.rs:4:1
  |
4 | struct Covariant<T>(fn() -> T);
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0208]: [-]
 --> src/lib.rs:7:1
  |
7 | struct Contravariant<T>(fn(T));
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0208]: [o]
  --> src/lib.rs:10:1
   |
10 | struct Invariant<T>(fn(T) -> T);
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 3 previous errors
5 Likes

There’s a simpler answer. The mut in a function name, per the rust guidelines, refers to whether self is &mut or &, so as_ptr is always as_ptr(&self) and as_mut_ptr is always as_mut_ptr(&mut self).

2 Likes

For NonNull it is as_ptr(self) though.

1 Like

FWIW, this thread prompted me to write https://github.com/rust-lang/rust/pull/60443 : please be aware that writing to a *const T pointer returned from as_ptr is UB! Just because you can cast it to *mut in safe code doesn’t mean you can actually use that pointer for writes.

That link is broken because the URL parser included the following : – here:

Technically, that should be to_ptr but too late now.

1 Like

Well, NonNull is Copy and ptr-sized; there’s little point in having &self methods.

Thanks for the replies! This turned out to be a great thread, I learned something new from just about every post :slight_smile:

2 Likes