In addition, it's often considered permissible for C APIs to exhibit undefined behavior if their API contracts are violated. This means that a bug in Rust code that calls into a C API incorrectly can (indirectly) cause undefined behavior. For this reason, all FFI calls are marked unsafe in Rust.
The typical approach to using C libraries from Rust is to create a "safe wrapper" around the unsafe FFI calls that uses Rust's type system and lifetimes to enforce the safety invariants. Of course it's possible to mess up this wrapper and have accidental undefined behavior, but you're much less likely to do so through a safe wrapper than if you use the unsafe FFI calls directly (or use C or Zig) for a couple reasons:
- Writing the safe wrapper forces you to sit down and think about safety invariants instead of glossing over it.
- Once you're done, the compiler will check the safety invariants for you every time you use the API -- no chance of making a mistake or forgetting to read a comment.
[0]: This could be avoided/mitigated with some kind of lightweight in-process sandboxing (e.g. Intel MPK + seccomp) to prevent C libraries from accessing memory that they don't own or performing syscalls they shouldn't. There's some academic research on this (and I experimented with it myself for a masters thesis project), but it generally requires some (minimal) performance overhead and code changes at language boundaries.