FFI & C Interop
Rust's Foreign Function Interface lets you reuse decades of battle-tested C libraries and expose fast Rust code to other languages — all through the shared C ABI.
Learn FFI & C Interop in our free Rust course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Rust course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll call C from Rust, export Rust functions to C, convert strings across the boundary, and share C-compatible structs — learning where unsafe is required and why.
What You'll Learn in This Lesson
1️⃣ Calling C from Rust
An extern "C" block declares foreign functions using the C calling convention. Their bodies live in a C library you link against. Because Rust can't verify foreign code, every call must sit inside an unsafe block.
The declared signature must match the C function exactly — a mismatch is undefined behaviour, not a compile error, which is part of why this is unsafe .
2️⃣ Calling Rust from C
To make a Rust function callable from C, give it the C ABI with extern "C" and keep its symbol name with #[no_mangle] so the linker can find it under the expected name.
Without #[no_mangle] , Rust mangles the symbol and C could never link to add by name.
3️⃣ Strings and C-Compatible Structs
Rust strings are UTF-8 and not NUL-terminated; C strings are. Convert with CString (owned, to send) and CStr (borrowed, to receive). Whoever allocates a buffer must free it.
For structs, add #[repr(C)] so Rust lays out the fields exactly as C expects. The libc crate supplies the C types, and bindgen can generate these declarations from a header.
Mini-Challenge: Export square to C
Write a C-callable function and the C declaration that would invoke it.
Common Errors (and the fix)
- "call to unsafe function is unsafe" — Wrap the foreign call in an unsafe block.
- C can't find the symbol — Add #[no_mangle] and pub extern "C" to the exported function.
- Garbled struct fields — Add #[repr(C)] so the layout matches C's.
- Crashes passing strings — Use CString / CStr , and free buffers on the side that allocated them.
Pro Tips
- 💡 Wrap unsafe in a safe API: validate inputs once, then keep the rest of your code in safe Rust.
- 💡 Let bindgen do the typing: generate bindings from C headers instead of hand-writing extern blocks.
- 💡 Be explicit about ownership: document and provide a free function for any pointer you hand to C.
📋 Quick Reference — FFI
Goal
Rust Syntax
Declare C fn
extern "C" { fn abs(i: c_int) -> c_int; }
Export to C
#[no_mangle] pub extern "C" fn f()
Foreign call
unsafe { abs(-1) }
Owned C string
CString::new(s)
Borrow C string
CStr::from_ptr(ptr)
C struct layout
#[repr(C)] struct P { .. }
Frequently Asked Questions
🎉 Lesson Complete — and You've Finished the Rust Course!
- ✅ extern "C" bridges Rust and C through the shared C ABI
- ✅ #[no_mangle] exports Rust functions C can link against
- ✅ Foreign calls and raw pointers live in unsafe ; CString / CStr bridge strings
- ✅ #[repr(C)] , libc , and bindgen make struct and library interop practical
- 🎓 Congratulations — you've completed the entire Rust course! From ownership and borrowing through generics, traits, async, web servers, databases, and now FFI, you've covered the full toolkit for writing fast, safe, production-grade Rust. Go build something great.
Practice quiz
What does FFI stand for?
- Foreign Function Interface
- Fast Function Inlining
- File Format Interop
- Free Form Input
Answer: Foreign Function Interface. FFI, the Foreign Function Interface, lets Rust call and be called by other languages, usually C.
What does an extern "C" block declare?
- Inline assembly
- A new Rust module
- Functions using the C calling convention / ABI
- A C compiler
Answer: Functions using the C calling convention / ABI. extern "C" tells Rust to use the C ABI for the listed functions.
Why is #[no_mangle] used on a Rust function exported to C?
- To mark it unsafe
- To keep its symbol name unchanged so C can link to it
- To make it faster
- To inline it
Answer: To keep its symbol name unchanged so C can link to it. #[no_mangle] disables name mangling so the symbol matches what C expects.
Why must calls to foreign C functions be inside unsafe?
- unsafe makes it faster
- It is only a style choice
- C is slow
- The compiler cannot verify the foreign code upholds Rust's guarantees
Answer: The compiler cannot verify the foreign code upholds Rust's guarantees. Rust cannot check the safety of foreign code, so you take responsibility with unsafe.
Which type represents an owned, NUL-terminated C string in Rust?
- CString
- String
- OsString
- Vec<char>
Answer: CString. std::ffi::CString owns a NUL-terminated byte buffer suitable for C.
Which type borrows a NUL-terminated C string (a *const c_char)?
- str
- CStr
- CString
- String
Answer: CStr. std::ffi::CStr is the borrowed view over an existing C string.
What does #[repr(C)] on a struct guarantee?
- It cannot be mutated
- It is copied
- It is faster
- C-compatible field layout and ordering
Answer: C-compatible field layout and ordering. #[repr(C)] gives the struct the same memory layout C uses, so it can cross the boundary.
What is the libc crate typically used for?
- Garbage collection
- Async runtimes
- Bindings to standard C library types and functions
- Web servers
Answer: Bindings to standard C library types and functions. libc provides Rust bindings to platform C library types and functions like c_int and malloc.
What does the bindgen tool do?
- Compiles Rust to C
- Generates Rust FFI bindings from C headers
- Runs benchmarks
- Formats code
Answer: Generates Rust FFI bindings from C headers. bindgen reads C header files and generates the corresponding Rust extern declarations.
When passing a Rust pointer across FFI, which is true?
- Raw pointers carry no lifetime or aliasing guarantees, so you uphold them manually
- Rust frees it automatically in C
- The borrow checker tracks it across the boundary
- Lifetimes are enforced in C
Answer: Raw pointers carry no lifetime or aliasing guarantees, so you uphold them manually. Raw pointers drop Rust's safety guarantees; correct use across FFI is the programmer's responsibility.
Continue this course
- Previous: Database Access with sqlx
- Next: Serialization with Serde