Hacker News new | ask | show | jobs
by mk12 498 days ago
It’s harder to write correct unsafe Rust than correct Zig because (1) Rust uses references all over the place, but when writing unsafe code you must scrupulously avoid “producing” an invalid reference (even if you never deference it), and (2) there’s lots of syntax noise which obscures what the code is doing (though &raw is a step in the right direction).
2 comments

For Rust references, rules[0] are not hard to follow:

The pointer must be properly aligned.

It must be non-null.

It must be “dereferenceable”: a pointer is dereferenceable if the memory range of the given size starting at the pointer is entirely contained within the bounds of that allocated object. Note that in Rust, every (stack-allocated) variable is considered a separate allocated object.

The pointer must point to a valid value of type T.

When creating a mutable reference, then while this reference exists, the memory it points to must not get accessed (read or written) through any other pointer or reference not derived from this reference.

When creating a shared reference, then while this reference exists, the memory it points to must not get mutated (except inside UnsafeCell).

[0]: https://doc.rust-lang.org/stable/core/ptr/index.html#pointer...

One reason it’s harder to follow is you can’t have references to uninitialized memory. In Zig pointers to uninitialized memory are fine as long as you don’t dereference them. That’s also true of Rust’s raw pointers, but most Rust code uses references, so it can’t be reused in unsafe contexts.

As a concrete example, I previously used Vec in unsafe code that dealt with uninitialized memory. This should be fine because Vec is basically a raw pointer and two integers. But later they changed the docs to say that this was undefined behavior (essentially, Vec reserves the right to “produce” a reference whenever it wants, even if you avoid calling methods that would do that). So my code that previously followed the rules now appeared to violate them.

> you can’t have references to uninitialized memory.

The method is available[0] in nightly Rust

    pub const unsafe fn as_uninit_ref<'a>(self) -> Option<&'a MaybeUninit<T>>
    where
        T: Sized,
    {
        // SAFETY: the caller must guarantee that `self` meets all the
        // requirements for a reference.
        if self.is_null() { None } else { Some(unsafe { &*(self as *const MaybeUninit<T>) }) }
    }
[0]: https://doc.rust-lang.org/stable/core/primitive.pointer.html...
> Rust uses references all over the place

This is mostly a concern with the Rust stdlib, though. And it's in principle fixable, by writing new varieties of those stdlib functions that take raw-pointer or &UnsafeCell<...> arguments, and delegating the "safe" varieties to those.

There's compiler-level traits like `Iterator` and `Future` which enforce references. If wanting to do intrusive pointers into them, one risks creating overlapping references: https://github.com/tokio-rs/tokio/issues/3399
References to UnsafeCell<> should still be safe, because the only thing you can do with an &UnsafeCell<T> is extract a possibly-mutating raw pointer to T. (UnsafeCell<> is also special in that, like Cell<>, you can mutate through it without incurring UB.)