Pre-RFC: RArrow for Pointer Ergonomics

In unsafe Rust, there has long been a lack of ergonomics. With advancements such as raw_ref_op we are coming closer to a more ergonomic unsafe Rust.

Auto-deref does not operate on raw pointers. This is because we want a clear boundary between unsafe and safe. We want dereferencing of raw pointers to be explicit. There is a reason that auto-deref exists for references - ergonomics.

It is possible to have both a clear boundary and ergonomics for raw pointers. The problem stems from the affix kind of the asterisk operator - prefix. Because the dot operator has higher precedence, we are left with excess parentheses. Example:

(*(*(*pointer.add(5)).some_field).method_returning_pointer()).other_method()

What we need here is either a suffix operator, or an infix operator. With the already existing RArrow token, we could have

pointer.add(5)->some_field->method_returning_pointer()->other_method()

This is identical to C and C++, which is also great.

One common non-solution to this problem is "encapsulate your unsafe code". First off, it does not address the ergonomics. Second, it is not possible in domains with irreducible encapsulations. Most notable prevalent domains with irreducible encapsulations are:

  1. Non-trivial intrusive data structures.
  2. Interoperability with complex systems written in C.

Note that the proposed ergonomic operates on place expressions. It is important that it operates on place expressions, because it is the only way to completely eliminate excess parentheses. Suppose we used the Tilde token for obtaining pointers to fields. Then the example above would be written as:

(*(**pointer.add(5)~some_field).method_returning_pointer()).other_method()

Which does not solve the problem.

8 Likes

Some previous discussion:

2 Likes

One explicit postfix alternative to pointer->method_by_mutable_reference() is to use

pointer.as_mut().unwrap_unchecked().method_by_mutable_reference()

It has the added benefit of not hiding the null case, and making explicit that the method call converts the pointer to a reference (which can have significant effects; for example under the stacked borrows model, in case you keep around any references or pointers derived from that reference).


If you need to go from *const T or *mut T to T, thereā€™s already the .read() method (which doesnā€™t present the null case; with raw pointers in Rust null really isnā€™t handled particularly well & consistently now, is itā€¦).


Of course, .as_mut().unwrap_unchecked() is extremely verbose, on the other hand, you need to rule out null somehow, and if that is a check earlier in your code, and you want a mutable reference after all, maybe thatā€™s a place where you could have already created it.

Or you donā€™t want the intermediate mutable references either, in which case you have more on an argument for *const T and *mut T method receiver types.

If you want a mutable reference and the pointer is never null, maybe NonNull can - for once - even be more ergonomic, as that one allows just pointer.as_mut().method_by_mutable_reference().


I donā€™t actually know the types involved in your (dummy) example of

pointer
    .add(5)
    ->some_field
    ->method_returning_pointer()
    ->other_method()

(which is some implicitness I want to criticize, too; based on the type of the methods, there are many different things that could be happening here)

but assuming itā€™s something like

pointer: *mut Foo // pointing into a slice
struct Foo {
    // more fieldsā€¦
    some_field: *mut Bar,
}

impl Foo {
    fn method_returning_pointer(&mut self) -> *mut Baz;
}
impl Baz {
    fn other_method(&mut self);
}

we get, using the very verbose alternative outlined above, and the ~ syntax for fields:

pointer
    .add(5)
    ~some_field.read()
    .as_mut().unwrap_unchecked().method_returning_pointer()
    .as_mut().unwrap_unchecked().other_method()

Which indeed isnā€™t really nice (though the ~ operator is the least of my concerns as that one worked out well with the .read() IMO).

But if we turned all the *mut T into NonNull<T> (Iā€™m assuming that null-checks are missing because null cannot happen in the first place, so that sounds realistic to me) then itā€™s only

pointer
    .add(5)
    ~some_field.read()
    .as_mut().method_returning_pointer()
    .as_mut().other_method()

(NonNull::add is not stable yet)

Which I like, because every dereferencing became a postfix method explaining exactly what kind of access we have made to the target, either by-value .read() or by-reference with .as_mut() or .as_ref().

Or if, instead, we had method_returning_pointer and other_method designed to be used only with pointers anyways, and Rust decided to support (self: *mut Self) method receivers, then it could even look like

pointer
    .add(5)
    ~some_field.read()
    .method_returning_pointer()
    .other_method()
6 Likes

An as_mut_unchecked convenience method could be an acceptable compromise. Could of course be implemented by someone as a crate first to see if people want to use it.

But agreed that chains of method calls/field accesses of nullable pointers are a code smell, and too often thereā€™s just bound to be an unexpected null there somewhere, as seen all the time in Java and other languages with mandatory nullable references (hence null coalescing operators etc, or in Javaā€™s case just turning all potentially sus chains into Optional.flatMap chains instead). So some syntactical noisiness in in order.

2 Likes

In terms of syntax, another possible option would be postfix .*:

pointer.add(5).*.some_field.*.method_returning_pointer().*.other_method()

While it is true that this implicitly creates references, the same is true in today's syntax.

It's as_mut that is the odd one out here, I never understood why it treated null specially. These functions are now also occupying the space that would usually naturally fit the inverse of ptr::from_ref.

I do like that proposal... except for how verbose it still is. But maybe we should file an ACP, this is the one ingredient missing to have full method coverage for converting between references and raw pointers (and different pointee types and constness of raw pointers).

4 Likes

There's a parallel thread on the same proposal at