The extern crate confusion
You are trying to call a C function from Rust. You find a tutorial from 2016 that starts with extern crate libc;. You type it in, and the compiler yells at you. Or worse, you manage to compile it, but the linker fails because you missed the actual function declaration.
The extern crate syntax has a different job now. It is not for FFI. It is for bringing Rust crates into scope. For talking to C, C++, or any other language, you need extern blocks. The keyword extern appears in both, but they are unrelated features. extern crate imports a Rust module. extern blocks declare foreign functions.
Mixing them up is the most common mistake when starting FFI. The compiler will not help you here. If you write extern crate my_c_lib;, Rust looks for a Rust crate named my_c_lib. It does not look for a C library. You need to switch gears and use the correct tool.
What an extern block actually does
An extern block is a declaration, not a definition. It tells the compiler about functions that live outside your Rust code. You provide the signature: the name, the arguments, and the return type. You do not provide the body.
Think of it like a blueprint for a machine part that exists in the next room. You draw the shape and label the inputs and outputs so you know how to connect to it. You don't build the part. The part is already built, somewhere else.
The block also specifies the ABI, or Application Binary Interface. The ABI defines how arguments are passed to the function and how the return value comes back. Does the caller put arguments in registers or on the stack? Does the callee clean up the stack? The string inside extern "..." answers these questions. "C" is the standard C calling convention. "system" adapts to the platform's default, which matters on Windows.
You also need to tell the linker where to find the compiled code. That's the job of the #[link] attribute. It attaches metadata to the extern block, pointing the linker to the library file.
Minimal example: calling a math function
Here is the simplest case. You want to call sin from the C math library. The library is usually named libm on Unix systems, which the linker knows as m.
// #[link] tells the linker to include the 'm' library.
// This is usually libm.so or libm.dylib.
#[link(name = "m")]
extern "C" {
// Declare the foreign function signature.
// No body is provided. The compiler trusts this exists.
fn sin(x: f64) -> f64;
}
fn main() {
// SAFETY: sin is a standard C library function.
// It takes a float and returns a float. No pointers involved.
// The math library is linked via #[link].
let result = unsafe { sin(0.0) };
println!("sin(0.0) = {}", result);
}
The unsafe block is mandatory. Calling any function declared in an extern block is unsafe. The compiler cannot verify the C side. It cannot check if the function exists, if the signature matches, or if the function dereferences a null pointer. You must wrap the call in unsafe to acknowledge that responsibility.
If you forget the unsafe block, the compiler rejects the code with an error about calling an unsafe function. You cannot call foreign functions from safe Rust.
Convention aside: always put #[link] on the extern block, not on the function inside. This keeps the linkage metadata grouped with the declarations. It also allows you to declare multiple functions from the same library without repeating the attribute.
Walkthrough: compile, link, run
When you compile this code, three things happen.
First, the compiler processes the extern "C" block. It records the symbol name sin and its signature. It generates a reference to that symbol in the object file. It does not check if sin exists. It assumes you are telling the truth.
Second, the linker runs. It sees the #[link(name = "m")] attribute. It searches for a library named m. It finds libm and resolves the sin symbol. If the library is missing or the symbol is not found, the linker fails with an "undefined reference" error. This is a link-time error, not a compile-time error.
Third, at runtime, the unsafe block allows the call. The CPU jumps to the address of sin. The arguments are passed according to the C ABI. The function executes in C land. The return value comes back to Rust.
The unsafe keyword does not disable checks. It only gates operations that the compiler cannot verify. Inside the unsafe block, you can still use safe Rust. The unsafety comes from the fact that the foreign function might violate Rust's invariants. It might write to memory you own, or return a dangling pointer.
Realistic example: pointers and C types
Real FFI rarely involves just floats. You usually pass pointers, structs, or strings. C types also differ from Rust types. An int in C is not always 32 bits. A long varies by platform. Rust has fixed-width types like i32 and i64. Using the wrong size causes undefined behavior.
Use the types from std::os::raw. These types match the C types on the current platform. c_int matches int. c_long matches long. c_char matches char.
use std::os::raw::{c_int, c_char};
// Link against a hypothetical library 'mylib'.
#[link(name = "mylib")]
extern "C" {
// C function that processes a buffer and returns a status code.
// The pointer is const because the C function promises not to modify data.
fn process_buffer(data: *const c_char, len: c_int) -> c_int;
}
/// Wraps the unsafe FFI call in a safe Rust function.
/// This is the standard pattern: expose a safe API, hide the unsafe.
fn process_safe(data: &[u8]) -> i32 {
// SAFETY: process_buffer expects a valid pointer and length.
// We provide a slice's pointer and its length.
// The C function promises not to read past `len`.
// The slice must remain valid for the duration of the call.
let result = unsafe {
process_buffer(data.as_ptr() as *const c_char, data.len() as c_int)
};
result as i32
}
fn main() {
let buffer = b"Hello, C!";
let status = process_safe(buffer);
println!("Status: {}", status);
}
The wrapper function process_safe is safe. It takes a slice, which guarantees the data is valid and has a length. It converts the slice to a pointer and length, then calls the unsafe function. The caller of process_safe does not need unsafe. This is the goal of FFI: build a safe abstraction over the unsafe foreign code.
Convention aside: cast as_ptr() to *const c_char explicitly. The types are different, and the cast makes the conversion clear to readers. Some linters warn about implicit casts between pointer types.
Pitfall: never return a reference from an FFI call that points into C-owned memory. Rust does not know when C frees that memory. If you return &str from a C function, the reference might dangle the moment the C side drops the buffer. Always copy the data into a Rust String or Vec before returning it.
Pitfalls and compiler errors
FFI is where Rust's safety guarantees end and your responsibility begins. The compiler helps with syntax and types, but it cannot check the C side.
If you declare a function with the wrong signature, the compiler will not catch it. If the C function expects an int and you pass an i64, the arguments will be misaligned in registers or on the stack. The C function reads garbage. This is undefined behavior. The program might crash, or it might silently corrupt data.
Use std::os::raw types to match C types. If you are unsure about a type size, check the C header or use bindgen to generate the declarations.
If you forget to link the library, the compiler succeeds but the linker fails. The error message mentions an undefined reference. Add the correct #[link] attribute or ensure the library is in the linker's search path.
If you try to call the function without unsafe, the compiler rejects you with an error about unsafe function calls. You must wrap the call in an unsafe block.
If you pass a pointer to data that Rust drops, you get a dangling pointer. Rust drops data when the owner goes out of scope. If C holds a pointer to that data, it becomes invalid. The C function might read freed memory. This is undefined behavior. Ensure the Rust data lives as long as C needs it.
Convention aside: write a // SAFETY comment for every unsafe block. List the invariants you checked. "The pointer is valid." "The length matches." "The data is not mutated." This comment is a proof. If you cannot write the proof, you do not have a safe wrapper. Treat the comment as a contract with future readers, including yourself.
The compiler cannot check the C side. Your // SAFETY comment is the only contract you have. Write it like a lawyer.
Decision: when to use what
Use extern blocks when you are declaring a small number of C functions and want full control over the linkage. Use bindgen when you are wrapping a large C library with many headers and structs; it generates the extern blocks and type definitions for you. Use the cc crate when you need to compile C source files as part of your Rust build; it handles the compiler flags and linking automatically. Reach for extern crate only when you are importing another Rust crate; it does nothing for FFI.