Rust FFIsystemsunsafe RustWebAssembly

Article & Tutorial

Unsafe Rust and FFI: A Practical Safety Guide

Contain unsafe operations behind checked interfaces, validate FFI invariants, and make ownership and ABI assumptions reviewable.

Unsafe Rust and FFI: A Practical Safety Guide featured illustration
Unsafe Rust and FFI: A Practical Safety Guide featured illustration
Contents

Unsafe Rust does not turn off the rest of the language. It permits a small set of operations whose correctness depends on invariants the compiler cannot prove. Foreign-function interfaces add another boundary: the Rust and foreign sides must agree about layout, ownership, lifetimes, nullability, panic behavior, and the calling convention.

Learning outcomes

  • decide whether an unsafe operation is necessary;
  • isolate unsafe code behind a narrow safe interface;
  • use CString and CStr for C-compatible text boundaries;
  • document and test pointer, layout, lifetime, and error invariants;
  • identify why an example is not automatically production-ready.

Prerequisites

Complete the ownership and borrowing guide. You should also be comfortable with Cargo builds, tests, and reading compiler diagnostics. Do not run FFI examples against an untrusted native library.

What unsafe permits

Rust’s unsafe features include dereferencing raw pointers, calling unsafe functions, accessing mutable statics, using unions, and implementing unsafe traits. The unsafe keyword does not prove that an operation is correct; it marks a review boundary where the programmer must uphold the contract.

Keep the unsafe block small

rust
use std::slice;

/// # Safety
/// `ptr` must be non-null, valid for reads of `len` bytes, and point to one
/// allocated object for the duration of this call.
unsafe fn view_bytes<'a>(ptr: *const u8, len: usize) -> &'a [u8] {
    // SAFETY: The caller documents and upholds the pointer and lifetime contract.
    unsafe { slice::from_raw_parts(ptr, len) }
}

This function is intentionally unsafe because the caller supplies facts Rust cannot verify. A safer wrapper should validate what it can, avoid returning a reference beyond the owner’s lifetime, and keep the unsafe operation in one reviewable location.

C-compatible strings

CString owns a nul-terminated string without interior nul bytes. CStr views a nul-terminated string borrowed from a foreign boundary.

rust
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

fn outbound(value: &str) -> Result<CString, std::ffi::NulError> {
    CString::new(value)
}

/// # Safety
/// `ptr` must be null or point to a valid, live, nul-terminated C string for
/// the entire lifetime `'a` of the returned borrow.
unsafe fn inbound<'a>(ptr: *const c_char) -> Option<&'a CStr> {
    if ptr.is_null() {
        return None;
    }

    // SAFETY: The foreign function contract must guarantee a valid nul-terminated string.
    Some(unsafe { CStr::from_ptr(ptr) })
}

The inbound function still has an unsafe contract: non-null is not enough. The pointer must be valid and properly terminated, and the returned borrow must not outlive the foreign allocation.

FFI design checklist

Diagram showing a safe Rust wrapper checking pointer, length, layout, ownership, and error invariants.
An unsafe block marks a contract that still requires human review.
Boundary questionRequired decision
Calling conventionMatch the declared ABI, commonly extern "C"
LayoutUse FFI-safe representations and verify alignment and sizes
OwnershipState who allocates, borrows, and frees every pointer
NullabilityModel nullable pointers explicitly and check them
StringsSpecify encoding and termination; reject invalid input
ErrorsDo not let unwinding cross an unsupported foreign boundary
ThreadsDocument Send/Sync and thread-affinity assumptions
VersioningTest the exact native library and supported ABI versions

Avoid passing Rust references, trait objects, or unspecified layouts across a C ABI. Prefer opaque handles and explicit lengths when the foreign contract needs them.

Safe wrappers are contracts

A wrapper is sound only if every safe caller receives the guarantees promised by its API. Validate lengths before pointer arithmetic, reject invalid encodings, preserve ownership rules, and convert foreign errors into a bounded Rust error. Unit tests should cover null pointers, empty input, maximum lengths, malformed text, foreign failure codes, and cleanup on every path.

WebAssembly and other targets

WebAssembly bindings add another ABI and memory model. Verify the chosen tool’s current generated interface, supported types, ownership convention, and panic behavior. Do not infer browser security, sandbox isolation, or portability from the presence of a .wasm file.

Common mistakes and troubleshooting

  • One large unsafe block: split it into operations with named invariants.
  • from_raw without an ownership plan: document exactly who will reclaim the allocation and when.
  • Pointer plus length overflow: validate the range before constructing a slice.
  • Rust panic across FFI: catch or prevent unwinding according to the ABI and library contract.
  • Layout guessed from a compiler: use explicit representations and verify the foreign declaration.
  • Safe wrapper with unchecked assumptions: make the assumptions part of the function contract and test them.

Security and production considerations

Treat every foreign call as an untrusted boundary. Review memory validity, integer conversion, resource exhaustion, thread safety, error handling, symbol resolution, supply-chain provenance, and release reproducibility. Fuzz parsers and boundary inputs where practical. This guide does not certify any FFI code as safe or suitable for production.

Summary

Use unsafe only where necessary, isolate it, document every invariant, and test the foreign contract independently. CString and CStr help with C strings, but they do not solve pointer validity, lifetime, ABI, or ownership by themselves.

Next step and technical review flags

Revisit Understanding Ownership and Borrowing before attempting an implementation. Verify each statement and example against the current Rust Reference, Rustonomicon, std::ffi documentation, platform ABI documentation, and the selected WebAssembly toolchain. Independent unsafe-code review is mandatory before approval.

Did this help?

Yes No

Join the discussion

0 Comments

Ask questions, add context, or share what worked.

Sign in to join the discussion

No comments yet. Start the discussion.

More from Melochip Editorial

Related articles

Stay in the loop. Get new Melochip articles

Receive Melochip article updates when new articles are available. Explicit opt-in only.

No pre-checked boxes. Unsubscribe support is part of the subscriber foundation.