Introduction
To call Rust functions from C, you need two things: a stable symbol name and a C-compatible calling convention. Rust's name mangling produces symbols like _ZN7mycrate3add17h1234abcdE, which C cannot link against. The #[unsafe(no_mangle)] attribute (Edition 2024 syntax) and extern "C" fix this.
Key Concepts
- Name mangling: The compiler encodes type information into function names to support overloading and namespacing. C does not do this.
#[unsafe(no_mangle)]: Tells the compiler to use the exact function name as the symbol, without mangling. In Edition 2024, theunsafe()wrapper is required because disabling mangling can cause symbol collisions.extern "C": Specifies the C calling convention for the function.cdylib/staticlib: Crate types that produce shared or static libraries usable from C.
Real World Context
Any Rust library consumed by C, Python, Ruby, or other languages through FFI needs #[unsafe(no_mangle)] and extern "C". Projects like Firefox (Servo), Dropbox, and Discord expose Rust code to other languages this way.
Deep Dive
Basic Exported Function (Edition 2024)
In Edition 2024, #[no_mangle] must be wrapped in #[unsafe(...)] to acknowledge it is an unsafe operation:
rust#[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
The #[unsafe(no_mangle)] is required because:
- It exposes a global symbol that could collide with other symbols.
- It commits to a stable ABI that Rust normally does not guarantee.
Without #[unsafe(no_mangle)], the function name would be mangled:
rust// Without: symbol is _ZN7mycrate3add17h1234567890abcdefE // With: symbol is add
Building a C Library
Configure your Cargo.toml to output a C-compatible library:
toml[lib] crate-type = ["cdylib", "staticlib"] # cdylib = shared library (.so on Linux, .dylib on macOS, .dll on Windows) # staticlib = static library (.a on Unix, .lib on Windows)
cdylib is for shared/dynamic libraries. staticlib is for static linking.
Writing the C Header
C code needs a header file declaring the Rust functions:
c/* mylib.h */ #ifndef MYLIB_H #define MYLIB_H #include <stdint.h> int32_t add(int32_t a, int32_t b); #endif
For large APIs, use cbindgen to auto-generate headers.
cbindgen: Automatic Header Generation
The cbindgen crate generates C headers from your Rust source:
toml# Cargo.toml [build-dependencies] cbindgen = "0.26"
rust// build.rs fn main() { cbindgen::Builder::new() .with_crate(".") .generate() .expect("Unable to generate bindings") .write_to_file("include/mylib.h"); }
This reads your #[unsafe(no_mangle)] functions and produces a correct C header automatically.
Common Pitfalls
- Using
#[no_mangle]instead of#[unsafe(no_mangle)]in Edition 2024 — The bare form is a compile error. Always wrap inunsafe(). - Forgetting
pub— Withoutpub, the symbol may not be exported from the shared library. - Returning Rust-only types — Types like
String,Vec, orResulthave no C representation. Only return#[repr(C)]structs, primitive types, or raw pointers.
Best Practices
- Use
cbindgen— Hand-written headers get out of sync. Auto-generate them. - Prefix exported symbols — Use a library prefix like
mylib_addto avoid name collisions with other libraries. - Document the C API — Add comments in both the Rust source and the generated header explaining ownership and thread-safety guarantees.
Summary
- Edition 2024 requires
#[unsafe(no_mangle)]instead of#[no_mangle]. - Combine
#[unsafe(no_mangle)]withextern "C"to create C-callable functions. - Set
crate-type = ["cdylib"]in Cargo.toml for shared libraries. - Use
cbindgento auto-generate C headers. - Only return C-compatible types: primitives,
#[repr(C)]structs, and raw pointers.
Code Examples
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Concatenate two C strings. Caller must free the result with free_string.
#[unsafe(no_mangle)]
pub extern "C" fn concat_strings(
a: *const c_char,
b: *const c_char,
) -> *mut c_char {
let a_str = unsafe {
if a.is_null() { return std::ptr::null_mut(); }
// SAFETY: caller guarantees a is a valid C string
CStr::from_ptr(a).to_str().unwrap_or("")
};
let b_str = unsafe {
if b.is_null() { return std::ptr::null_mut(); }
// SAFETY: caller guarantees b is a valid C string
CStr::from_ptr(b).to_str().unwrap_or("")
};
let result = format!("{a_str}{b_str}");
CString::new(result)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut())
}
/// Free a string returned by concat_strings.
#[unsafe(no_mangle)]
pub extern "C" fn free_string(s: *mut c_char) {
if !s.is_null() {
// SAFETY: s was created by CString::into_raw in concat_strings
unsafe { drop(CString::from_raw(s)); }
}
}