It sounds to me that you didn't get the right understanding.
For the right understanding you must kwow that the operations used to execude Rust code (or anything on a computer), CPU assembly opcodes, deal with memory in ways that can corrupt its contents if not done in proper ways. Languages like C that compile to assembly and can produce assembly code that does such corruption are said to have the ability to write programs that trigger "undefined behaviour" (UB). Undefined behaviour means, the compiler doesn't warrant any particular outcome, which means a program having parts with undefined behaviour stops working as intended. The memory could get corrupted, leading to any kinds of results that don't make sense, or the process being terminated by the OS as misbehaving. It can also mean that the compiler compiling the program starts making assumptions about the data handled by the provided code because e.g. "the data must have this length since the program accesses it at that length, so if the programmer thinks that this is correct, then the length must be assured". Of course when the programmer actually did it wrong then the compiler drew the wrong conclusions, as well, and the resulting assembly opcodes actually being executed stop making sense.
So, it's essential to use CPU opcodes in ways that yield a controlled, predictable outcome. When using a language like C, it means, use C in ways that never trigger undefined behaviour. It's an exercise in thinking and constraint.
Safe Rust does not have that problem: the compiler checks that what the provided program does makes sense, and rejects the program otherwise (compilation error). Safe Rust does not allow for any programs with undefined behaviour--it does the work of checking invariants that need to be upheld so that the produced opcodes never trigger UB. This is nice but can be limiting, since the compiler is not as smart as a human.
But unsafe Rust does behave like C in this regard: it does allow to write programs that trigger UB, if the programmer does not do it right. unsafe is providing means to program in a way that the Rust compiler could not ensure never triggers UB. This is nice since it allows the smart humans to do things that the compiler doesn't understand would not actually trigger UB. But it also means that if the human makes an error, UB can happen.
These can be pretty simple things such as: you have a sequence of bytes in a variable s: &[u8], and you know it represents a string in UTF-8. You can use str::from_utf8(s). This runs code that verifies all the bytes that they are, indeed, UTF-8 characters; you get an Option back, and have to handle that (maybe by using .expect(..)). This is a little bit costly. So if you are really sure as a programmer, you could instead write unsafe { str::from_utf8_unchecked(s) } and add a comment like // This is safe because this data came originally from a String and hence is guaranteed in UTF-8. This is a tad faster, but if you got it wrong and the bytes actually can contain other sequences than proper UTF-8 characters, then your program can exhibit UB, which would be bad.
So now you write a function foos_to_string, that you do not declare to be unsafe, since you believe that no UB can be triggered by the caller using it:
impl Foo {
// Write a human-readable representation of Foo to `output`. UTF-8 is used.
fn write_to(&self, output: impl Write) -> Result<(), std::io::Error> {
...
}
}
pub fn foos_to_string(foos: &[Foo]) -> String {
let mut buffer: Vec<u8> = Vec::new();
for foo in foos {
foo.write_to(&mut buffer).expect("writing to buffer won't fail");
buffer.push(b'\n');
}
unsafe {
// Safe because we're only writing UTF-8 bytes to `buffer`:
// `Foo::write_to` guarantees to only write UTF-8, and we only
// add ASCII-newlines
String::from_utf8_unchecked(buffer)
}
}
It is wrapping unsafe in a safe way (we hope). There is (we hope) no way that calling foos_to_string can produce UB.
When you use the crate providing foos_to_string you are trusting that its developers got it right.
OTOH, from_utf8_unchecked is marked unsafe: it indicates that it is the responsibility of the caller to uphold the requirements necessary to avoid UB. In this case, you have to trust yourself as the developer, primarily.
Marking a function as unsafe means, "this function does things that can trigger UB if you do not follow the preconditions for the correct use of the function."
So unsafe is used by developers to tell the user "Use it as we say, and it's safe".
Better worded, it is "Use it as we say, and it will not trigger UB".
("Safe", in the context of Rust, tends to mean whether it cannot be mis-used, which is not the case here. It's not it that is safe; it is your use of it that is safe if you follow the preconditions.)
And the user decides to trust or not. (Also within our own library.)
The user has to trust their own capability to correctly follow the preconditions that avoid UB.
OK, the user also has to trust the library developers to document the preconditions correctly; but that is probably not what you meant.
The safe wrapper instead means they decided it's safe, and we can use it as normal code.
A safe wrapper doesn't simply "decide that what the wrapped function does is safe, hence we now declare it safe"--this would be totally bogus. The criterium for a wrapper to be safe is when it cannot be used in a way that triggers UB. If the wrapper does use unsafe functions internally, then the only way for the wrapper to become safe is to follow all of the preconditions in its use of the unsafe functions in its implementation. In the foos_to_string function above that was that the bytes were guaranteed to be UTF-8 because only functions that generated UTF-8 were writing into the buffer.
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.
I'm not 100% sure what you mean here. What do you mean by "costly"? "Costly" as in "causes security problems"? "Costly" as in "runs more slowly"?
But when calling functions that are not marked unsafe, we always hope that its developers didn't make a human reasoning error (i.e. bug) when deciding that their own calling of an unsafe function or facility was safe due to their program logic.
So, as Rust users we think that it is "not a big problem" because such bugs should be rare (at least rarer than in C/C++, since in Rust those kinds of bugs are only caused by program parts in unsafe sections, which makes the relative amount of code smaller, and makes it easier to audit for problems).