How important is unsafe in function definition?

I find this read_volatile function (in core ptr) with attribute unsafe in function signature.

pub **unsafe** fn read_volatile<T>(src: *const T) -> T {

Imagine following function block. Will adding unsafe in function signature make any difference?

pub fn txbusy(instance: u8) -> bool {
    unsafe {
        
    }
}

You can't call unsafe functions in safe Rust. Though txbusy uses an unsafe block under the hood, it can still be called from safe Rust, which is like saying "no matter how you use this function, it will not cause UB," which is different from declaring it as an unsafe function, which is like saying "if you are not careful and hold up all necessary safety conditions when calling this function, it will cause UB." The reference contains a section about the unsafe keyword, where it can be used and what the consequences are: https://doc.rust-lang.org/reference/unsafe-keyword.html#the-unsafe-keyword

5 Likes

Yes. If you omit unsafe from a function that can cause UB if called incorrectly, then you just made an unsound API.

The effects of certain modifiers in a typed language are not necessarily manifest or occur at runtime. They also have logical consequences, as with unsafe.

3 Likes

when you implement the function, you can only do unsafe operations (like dereferenceing raw pointers, or calling other unsafe functions) inside a unsafe block or unsafe function. in this regards, you (the implementer) feel the same.

but unsafe is part of the type signature of functions, so when you call the function, the contract between the user and author is completely different. for example, suppose you must use unsafe because you need to read hardware registers using read_volatile():

use core::ptr::read_volatile;

// if you make your function also `unsafe`, you are saying to the user:
// you must ensure the precondition (e.g. rx buffer is not empty)
// if it goes wrong, it's your fault
unsafe fn read_rx_buffer() -> u8 {
    read_volatile(DEVICE_REGISTER_ADDRESS[0x10])
}

// if you make a safe `wrapper`, you are saying to the user:
// I have taken the responsibility to check the conditions.
// if unexpected things happen, it's a bug of my code
fn read_rx_buffer() -> Option<u8> {
    unsafe {
        let status = read_volatile(DEVICE_REGISTER_ADDRESS[0]);
        if status & 0x03 != 0 {
            Some(read_volatile(DEVICE_REGISTER_ADDRESS[0x10])
        } else {
            None
        }
    }
}
2 Likes

An unsafe function is a contract between the author and the caller of the function. The author promises the caller that the function is sound to call, if and only if its documented invariants are upheld, and the caller promises to maintain those invariants. The compiler acts as an arbiter of sorts by insisting that unsafe functions be only called inside unsafe blocks.

An unsafe block is a contract between the function author and the compiler. The author promises the compiler that soundness invariants are maintained, either by the author if the function is not unsafe or by the author and the caller together if it is. In return, the compiler lets the author do certain things whose soundness it cannot itself ensure.

Writing a function that has preconditions that affect soundness, but which is nevertheless not marked as unsafe, violates both of the above contracts.

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.