Rust 92 🦀 Calling Unsafe Functions and External C Libraries (FFI)
Rust’s Foreign Function Interface is the mechanism by which Rust code calls functions written in C, and by which C code calls functions written in Rust. The boundary is marked by extern blocks, and every call across it is unsafe because the compiler cannot verify that the foreign function upholds Rust’s memory safety invariants. The C side has no borrow checker, no ownership model, and no guarantee that a pointer is valid. The unsafe block is where the programmer asserts that the foreign function’s contract has been satisfied.
This chapter covers the two directions of the boundary: calling C from Rust, and exposing Rust to C. It examines the unsafe extern block that is required in the 2024 edition, the #[no_mangle] and extern "C" attributes that make a Rust function callable from C, the bindgen and cbindgen tools that generate bindings, and the memory management rules that apply when ownership crosses the language boundary.
Key point: The extern block is a promise that the function signature is correct. If the signature does not match the actual C function, the call is undefined behavior even if it compiles. In the 2024 edition, the unsafe keyword is required on the block to make this responsibility explicit.
Why FFI exists
The library reuse problem. A Rust project often needs functionality that exists in a mature C library: cryptographic primitives, database drivers, system APIs, or media codecs. Reimplementing those libraries in Rust is expensive and error-prone. FFI allows the Rust project to link against the C library and call its functions directly, with the Rust type system mapped onto the C types at the boundary.
The embedding problem. A C or C++ project may want to use a Rust library for a component where Rust’s safety guarantees are valuable. The Rust library exposes a C-compatible API, the C project links against it, and the call crosses the boundary in the opposite direction. The #[no_mangle] attribute and the extern "C" calling convention are what make the Rust function visible to the C linker.
The signature correctness problem. The compiler cannot verify that an extern block’s declaration matches the actual C function. If the Rust declaration says fn sqrt(x: f64) -> f64 and the C function actually takes an int, the call is undefined behavior. The unsafe extern block, introduced in Rust 1.82 and required in the 2024 edition, exists to make this responsibility visible at the declaration site .
a. Calling C Functions from Rust
The extern "C" block declares the C functions that the Rust code will call. The #[link] attribute specifies the native library to link against. The functions inside the block are unsafe by default because the compiler cannot verify their contracts.
// Declare the C function
unsafe extern "C" {
fn sqrt(x: f64) -> f64;
}
fn main() {
let x = 25.0;
// SAFETY: sqrt is a pure function; x is a valid f64.
let result = unsafe { sqrt(x) };
assert_eq!(result, 5.0);
}
The sqrt function from libm is safe to call with any f64, so the block can mark it as safe fn instead of unsafe fn. A function like strlen, which requires a valid pointer, remains unsafe fn because the pointer must be non-null and point to a nul-terminated string .
unsafe extern "C" {
// Safe: can be called with any f64
pub safe fn sqrt(x: f64) -> f64;
// Unsafe: requires a valid pointer
pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;
}
The libc crate provides the type definitions for the C types: c_int, c_char, c_void, and the rest. The std::os::raw module provides the same types, and the std::ffi module provides CString and CStr for string conversion.
b. Exposing Rust Functions to C
A Rust function that C will call needs three things: the #[no_mangle] attribute to prevent name mangling, the extern "C" calling convention to match C’s ABI, and C-compatible types for the parameters and return value .
use std::os::raw::c_int;
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
The #[no_mangle] attribute is what makes the function’s symbol name rust_add in the compiled library. Without it, the Rust compiler mangles the name into something like _ZN4mycrate8rust_add17h..., which the C linker cannot resolve.
The crate-type in Cargo.toml determines the output format. A cdylib produces a dynamic library (.so, .dylib, or .dll) that C can link against. A staticlib produces a static library (.a).
[lib]
crate-type = ["cdylib"]
The C header for the Rust function is a simple declaration. The cbindgen tool generates these headers automatically by analyzing the Rust source .
// myrust.h
int rust_add(int a, int b);
// main.c
#include "myrust.h"
int main() {
return rust_add(2, 3);
}
c. Binding Generation and Memory Management
bindgen generates Rust bindings from a C header. It uses libclang to parse the header and produces an extern "C" block with the correct types and a #[repr(C)] struct for each C struct . The generated bindings are usually included in a build.rs script and written to OUT_DIR.
// In build.rs
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");
bindings.write_to_file(out_path.join("bindings.rs")).unwrap();
cbindgen goes the other direction: it analyzes Rust code and generates a C header. It is configured with a cbindgen.toml file and can be run as a build step or a standalone command .
cbindgen --config cbindgen.toml --crate myrust --output myrust.h
Memory management across the boundary is the part that Rust’s ownership model cannot enforce. When Rust allocates memory and passes a pointer to C, the C code must eventually return that pointer to Rust for deallocation. The CString::into_raw method transfers ownership to the caller, and a corresponding free function must be exposed to reclaim it .
// Transfer ownership to C
#[no_mangle]
pub extern "C" fn create_greeting(name: *const c_char) -> *mut c_char {
// ... create the CString ...
greeting.into_raw()
}
// C must call this to free the memory
#[no_mangle]
pub extern "C" fn free_greeting(ptr: *mut c_char) {
if !ptr.is_null() {
// SAFETY: ptr was created by into_raw and not yet freed.
unsafe { drop(CString::from_raw(ptr)); }
}
}
The CStr type is the borrowed counterpart. CStr::from_ptr creates a CStr from a pointer, but the pointer must be valid and nul-terminated for the lifetime of the CStr. The to_str method validates UTF-8 and returns a Result .
Complete Example Session
// ============================================
// PART 1: CALLING A C FUNCTION
// ============================================
unsafe extern "C" {
fn sqrt(x: f64) -> f64;
}
fn main() {
let x = 25.0;
// SAFETY: sqrt accepts any f64.
println!("sqrt({}) = {}", x, unsafe { sqrt(x) });
}
// ============================================
// PART 2: UNSAFE EXTERN BLOCK (2024 EDITION)
// ============================================
unsafe extern "C" {
// Safe: any f64 is valid
pub safe fn sqrt(x: f64) -> f64;
// Unsafe: pointer must be valid
pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;
}
// ============================================
// PART 3: LINKING A NATIVE LIBRARY
// ============================================
#[link(name = "m")]
unsafe extern "C" {
fn cos(x: f64) -> f64;
}
// ============================================
// PART 4: EXPOSING RUST TO C
// ============================================
use std::os::raw::c_int;
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
// ============================================
// PART 5: CSTRING FOR PASSING STRINGS TO C
// ============================================
use std::ffi::CString;
fn pass_string() {
let c_string = CString::new("hello").expect("no nul bytes");
// c_string.as_ptr() is valid while c_string is alive
// pass to C function here
}
// ============================================
// PART 6: CSTR FOR RECEIVING STRINGS FROM C
// ============================================
use std::ffi::CStr;
fn receive_string(ptr: *const std::ffi::c_char) {
// SAFETY: ptr is valid and nul-terminated
let c_str = unsafe { CStr::from_ptr(ptr) };
let rust_str = c_str.to_str().expect("valid UTF-8");
println!("{}", rust_str);
}
// ============================================
// PART 7: TRANSFERRING OWNERSHIP TO C
// ============================================
#[no_mangle]
pub extern "C" fn create_greeting(name: *const std::ffi::c_char) -> *mut std::ffi::c_char {
let c_str = unsafe { CStr::from_ptr(name) };
let greeting = format!("Hello, {}!", c_str.to_str().unwrap());
CString::new(greeting).unwrap().into_raw()
}
// ============================================
// PART 8: RECLAIMING OWNERSHIP FROM C
// ============================================
#[no_mangle]
pub extern "C" fn free_greeting(ptr: *mut std::ffi::c_char) {
if !ptr.is_null() {
// SAFETY: ptr came from into_raw and has not been freed
unsafe { drop(CString::from_raw(ptr)); }
}
}
// ============================================
// PART 9: BINDGEN BUILD SCRIPT
// ============================================
// build.rs
// let bindings = bindgen::Builder::default()
// .header("wrapper.h")
// .generate()
// .expect("bindings");
// bindings.write_to_file(out_path.join("bindings.rs")).unwrap();
// ============================================
// PART 10: CDYLIB CRATE TYPE
// ============================================
// Cargo.toml
// [lib]
// crate-type = ["cdylib"]
The ten parts trace the boundary in both directions. Parts 1 through 3 show calling C from Rust. Parts 4 through 8 show exposing Rust to C and managing the string conversions and ownership transfers. Parts 9 and 10 show the tooling that automates the binding generation and the crate configuration.
Quick Reference
FFI Attributes and Keywords
| Item | Purpose |
|---|---|
extern "C" | C calling convention |
unsafe extern | Required in 2024 edition |
#[no_mangle] | Prevent name mangling |
#[link(name = "lib")] | Link native library |
pub extern "C" fn | Export function to C |
C Type Mapping
| C Type | Rust Type (std::os::raw) |
|---|---|
int | c_int |
char | c_char |
void | c_void |
char* | *const c_char / *mut c_char |
size_t | usize |
String Conversion
| Direction | Type | Method |
|---|---|---|
| Rust → C | CString | CString::new(s), .as_ptr() |
| C → Rust (borrowed) | CStr | CStr::from_ptr(ptr), .to_str() |
| Transfer to C | CString | .into_raw() |
| Reclaim from C | CString | CString::from_raw(ptr) |
Binding Tools
| Tool | Direction | Command |
|---|---|---|
bindgen | C header → Rust | bindgen wrapper.h |
cbindgen | Rust → C header | cbindgen --crate mylib |
Best Practices
✅ Do This:
// Mark the extern block as unsafe (2024 edition)
unsafe extern "C" { fn foreign(); } // ✅
// Write a SAFETY comment for every unsafe call
// SAFETY: the pointer is valid and nul-terminated. // ✅
let s = unsafe { CStr::from_ptr(ptr) }; // ✅
// Use CString for passing strings to C
let c = CString::new("hello").unwrap(); // ✅
// Provide a free function for memory transferred to C
CString::from_raw(ptr); // ✅
❌ Don’t Do This:
// Omit unsafe on the extern block (2024 edition)
extern "C" { fn foreign(); } // error in 2024 // ❌
// Assume the C function's signature is correct
unsafe extern "C" { fn wrong(x: i64); } // if C takes i32, UB // ❌
// Leak the CString by forgetting into_raw
let ptr = CString::new("x").unwrap().as_ptr(); // dangling // ❌
// Free a pointer not created by into_raw
CString::from_raw(some_other_ptr); // UB // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Linker error: undefined symbol | Missing #[no_mangle] or wrong crate-type | Add attribute, set cdylib |
| Segfault on string call | CString dropped before pointer used | Keep CString in scope |
| Garbage string from C | C string not nul-terminated | Validate with CStr::from_ptr |
| Memory leak | C never calls the free function | Document the ownership contract |
| Wrong ABI | Missing extern "C" on exported function | Add the attribute |
Real-World Examples
1. Call C sqrt
unsafe extern "C" { fn sqrt(x: f64) -> f64; }
unsafe { sqrt(25.0) };
2. Link libm
#[link(name = "m")]
unsafe extern "C" { fn cos(x: f64) -> f64; }
3. Export Add
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
4. CString to C
let c = CString::new("hello").unwrap();
unsafe { foreign(c.as_ptr()) };
5. CStr from C
let s = unsafe { CStr::from_ptr(ptr) };
let r = s.to_str().unwrap();
6. Transfer Ownership
let ptr = CString::new("x").unwrap().into_raw();
7. Reclaim Ownership
unsafe { drop(CString::from_raw(ptr)); }
8. cdylib Crate
[lib]
crate-type = ["cdylib"]
9. bindgen in build.rs
bindgen::Builder::default().header("wrapper.h").generate().unwrap();
10. cbindgen Header
cbindgen --crate mylib --output mylib.h
Visual
The FFI Boundary
┌──────────────────────────────────────────────────────────────┐
│ RUST C │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ extern "C" { │ │ int sqrt(int); │ │
│ │ fn sqrt(...); │◄────────►│ │ │
│ │ } │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ #[no_mangle] │ │ int add(int,int)│ │
│ │ extern "C" fn │─────────►│ │ │
│ │ add(...) │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ The boundary is unsafe because the compiler cannot │
│ verify the C side's contracts. │
│ │
└──────────────────────────────────────────────────────────────┘
Unsafe Extern Block
┌──────────────────────────────────────────────────────────────┐
│ unsafe extern "C" { │
│ pub safe fn sqrt(x: f64) -> f64; │
│ pub unsafe fn strlen(p: *const c_char) -> usize; │
│ } │
│ │
│ ┌─────────────────┐ │
│ │ safe fn │────► callable without unsafe │
│ │ │ (any f64 is valid) │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ unsafe fn │────► requires unsafe block │
│ │ │ (pointer must be valid) │
│ └─────────────────┘ │
│ │
│ The unsafe on the block asserts the signatures are correct. │
│ │
└──────────────────────────────────────────────────────────────┘
String Ownership
┌──────────────────────────────────────────────────────────────┐
│ RUST → C │
│ │
│ CString::new("hello") │
│ │ │
│ ├─ .as_ptr() ──► borrowed, valid while CString lives │
│ │ │
│ └─ .into_raw() ──► owned by C, must call free │
│ │
│ C → RUST │
│ │
│ ptr ──► CStr::from_ptr(ptr) ──► borrowed, valid while ptr │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ The C side does not know about Rust's lifetimes. ││
│ │ The programmer must ensure the pointer outlives its use.││
│ └─────────────────────────────────────────────────────────┘│
│ │
└──────────────────────────────────────────────────────────────┘
Binding Generation
┌──────────────────────────────────────────────────────────────┐
│ BINDGEN CBINDGEN │
│ │
│ C header ──► Rust bindings Rust code ──► C header │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ doggo.h │ │ lib.rs │ │
│ │ struct Doggo; │ │ #[no_mangle] │ │
│ │ void pet(Doggo*);│ │ extern "C" fn │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ bindings.rs │ │ mylib.h │ │
│ │ #[repr(C)] │ │ int pet(Doggo*);│ │
│ │ extern "C" { } │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Both tools automate the signature translation. │
│ The programmer still owns the memory safety proof. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| FFI declaration | unsafe extern "C" { } |
| 2024 edition requirement | unsafe on extern block is mandatory |
| Export to C | #[no_mangle] pub extern "C" fn |
| Crate type | cdylib for dynamic library |
| String to C | CString::new(), .as_ptr() |
| String from C | CStr::from_ptr(), .to_str() |
| Transfer ownership | CString::into_raw() |
| Reclaim ownership | CString::from_raw() |
| C → Rust bindings | bindgen |
| Rust → C header | cbindgen |
Key takeaways:
- The
externblock is a promise that the signature is correct. The compiler cannot verify that a C function’s declaration matches its implementation. In the 2024 edition, theunsafekeyword is required on the block to make this responsibility explicit. A wrong signature is undefined behavior even if the code compiles . #[no_mangle]andextern "C"are both required to export a Rust function to C. The first prevents name mangling; the second selects the C calling convention. Without either, the C linker cannot find the symbol or cannot call it correctly .- String conversion across the boundary requires
CStringandCStr. Rust strings are not nul-terminated and may contain interior nul bytes; C strings are nul-terminated and cannot.CStringowns the C-compatible allocation, andCStrborrows it. The pointer returned byas_ptr()is valid only while theCStringis alive . - Ownership transfer uses
into_rawandfrom_raw. When Rust gives a pointer to C, the C code must eventually return it for deallocation.CString::into_rawtransfers ownership, andCString::from_rawreclaims it. Forgetting the free function leaks memory; calling it on a pointer that was not created byinto_rawis undefined behavior . bindgenandcbindgenautomate the signature translation.bindgenparses C headers and generates Rustexternblocks.cbindgenanalyzes Rust code and generates C headers. Neither tool can verify the memory safety of the boundary; they only translate the types.
Remember: FFI is the boundary between Rust’s safety guarantees and the untyped, unverified world of C. The unsafe block is where the programmer accepts the responsibility that the compiler normally carries. The extern block declares what the foreign function expects; the unsafe block asserts that the declaration is correct and that the arguments satisfy it. String conversion, ownership transfer, and memory deallocation all cross the boundary without the compiler’s help. The tools—bindgen, cbindgen, libc, std::ffi—automate the mechanical parts, but they do not replace the proof. When the signature matches and the pointer is valid, the call is correct. When either is wrong, the code is undefined behavior that may compile and may run for years before it crashes.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!