Skip to content

Rust FFI — Calling C Code from Rust

DodaTech Updated 2026-06-21 7 min read

Rust FFI (Foreign Function Interface) enables calling C libraries directly from Rust and exposing Rust functions with C ABI, providing seamless interop with the vast ecosystem of existing C code.

What You'll Learn

In this tutorial, you'll learn how Rust FFI works: declaring external C functions, linking C libraries, handling C strings, building safe wrappers around unsafe interfaces, and creating C-compatible Rust libraries for use from C.

Why It Matters

Most operating system APIs are C interfaces. Device drivers, system libraries (OpenSSL, libpcap, SQLite), and legacy codebases are written in C. Rust FFI lets you use these libraries directly without rewriting them, while providing safe Rust wrappers that prevent common C errors.

Real-World Use

The Linux kernel's Rust bindings use FFI to call C kernel APIs. Database drivers use FFI to link against libsqlite3 and libpq. Audio libraries use FFI to interface with ALSA and PulseAudio. Durga Antivirus Pro uses FFI to call the platform's native file system change notification API.

flowchart LR
    RS[Safe Rust] --> WRAP[Safe Rust Wrapper]
    WRAP --> UNSAFE[Unsafe FFI Bindings]
    UNSAFE --> C_LIB[C Library]
    C_LIB --> SYS[OS / Hardware]
    WRAP -->|manages| MEM[Memory / Lifetimes]
    UNSAFE -->|extern C| ABI[C ABI]
â„šī¸ Info

Prerequisites: Unsafe Rust, Smart Pointers, and knowledge of C programming basics.

Declaring External C Functions

Use extern "C" to declare C functions and call them from Rust.

use std::ffi::{CStr, CString};

extern "C" {
    fn strlen(s: *const i8) -> usize;
    fn puts(s: *const i8) -> i32;
    fn abs(x: i32) -> i32;
}

fn safe_strlen(s: &str) -> usize {
    let c_str = CString::new(s).expect("CString::new failed");
    unsafe { strlen(c_str.as_ptr()) }
}

fn main() {
    let len = safe_strlen("Hello, FFI!");
    println!("Length: {}", len);

    unsafe {
        println!("abs(-42) = {}", abs(-42));
    }
}

Expected output:

Length: 11
abs(-42) = 42

Linking to External Libraries

Use #[link] or build.rs to link C libraries.

// Link to libm (math library)
#[link(name = "m")]
extern "C" {
    fn sqrt(x: f64) -> f64;
    fn sin(x: f64) -> f64;
    fn cos(x: f64) -> f64;
}

fn main() {
    unsafe {
        let val = 2.0;
        println!("sqrt({}) = {}", val, sqrt(val));
        println!("sin({}) = {}", val, sin(val));
        println!("cos({}) = {}", val, cos(val));
    }
}

Expected output:

sqrt(2) = 1.4142135623730951
sin(2) = 0.9092974268256817
cos(2) = -0.4161468365471424

Safe Wrappers Around C APIs

The key pattern: encapsulate unsafe C calls in safe Rust functions.

use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::ptr;

// Simulated C library context
extern "C" {
    fn malloc(size: usize) -> *mut u8;
    fn free(ptr: *mut u8);
}

struct Buffer {
    ptr: *mut u8,
    size: usize,
}

impl Buffer {
    fn new(size: usize) -> Option<Self> {
        let ptr = unsafe { malloc(size) };
        if ptr.is_null() {
            None
        } else {
            Some(Buffer { ptr, size })
        }
    }

    fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
    }

    fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        unsafe { free(self.ptr) }
    }
}

fn main() {
    let mut buf = Buffer::new(64).expect("Allocation failed");
    let slice = buf.as_mut_slice();
    slice[0] = 42;
    slice[1] = 100;
    println!("Buffer[0] = {}, Buffer[1] = {}", slice[0], slice[1]);
    println!("Buffer len: {}", buf.as_slice().len());
}

Expected output:

Buffer[0] = 42, Buffer[1] = 100
Buffer len: 64

C Strings and Rust Strings

Converting between C strings and Rust strings requires careful null-terminator handling.

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

extern "C" {
    fn getenv(name: *const c_char) -> *mut c_char;
}

fn safe_getenv(name: &str) -> Option<String> {
    let c_name = CString::new(name).ok()?;
    let result = unsafe { getenv(c_name.as_ptr()) };
    if result.is_null() {
        None
    } else {
        let c_str = unsafe { CStr::from_ptr(result) };
        Some(c_str.to_string_lossy().into_owned())
    }
}

fn main() {
    match safe_getenv("HOME") {
        Some(val) => println!("HOME = {}", val),
        None => println!("HOME not set"),
    }

    match safe_getenv("NONEXISTENT_VAR") {
        Some(val) => println!("Found: {}", val),
        None => println!("Variable not found"),
    }
}

Expected output:

HOME = /home/user
Variable not found

Exposing Rust Functions to C

Rust functions can be called from C using extern "C" and #[no_mangle].

use std::ffi::CStr;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_greet(name: *const c_char) -> *mut c_char {
    let name_str = unsafe { CStr::from_ptr(name) }
        .to_string_lossy()
        .into_owned();
    let greeting = format!("Hello, {} from Rust!", name_str);
    std::ffi::CString::new(greeting)
        .unwrap()
        .into_raw()
}

// Simulated C caller
fn main() {
    let sum = unsafe { rust_add(40, 2) };
    println!("rust_add(40, 2) = {}", sum);

    let name = CString::new("C Programmer").unwrap();
    let greeting = unsafe { rust_greet(name.as_ptr()) };
    let greeting_str = unsafe { CStr::from_ptr(greeting) }
        .to_string_lossy()
        .into_owned();
    println!("{}", greeting_str);
    unsafe { let _ = CString::from_raw(greeting); }
}

Expected output:

rust_add(40, 2) = 42
Hello, C Programmer from Rust!

Common Mistakes

1. Forgetting Null Terminator for C Strings

Rust strings are not null-terminated. Always use CString to create null-terminated strings for C APIs.

2. Memory Leaks from C Allocations

C functions that return allocated memory must be freed. Use Rust's drop guards to ensure free() is called.

3. Incorrect ABI Specification

Use extern "C" for standard C ABI. Different platforms and compilers may use different calling conventions.

4. Passing Rust Slices to C

Rust slices contain a pointer and length. C expects *const T and a separate length. Use .as_ptr() and .len() explicitly.

5. Ignoring Errno

C functions set errno on error. Use std::io::Error::last_os_error() to capture it after C calls.

Practice Questions

1. What does extern "C" mean in Rust FFI? It declares that a function uses the C ABI (calling convention), allowing Rust to call C functions and vice versa with matching binary interfaces.

2. How do you handle C strings in Rust? Use CString to create null-terminated strings from Rust strings. Use CStr to read null-terminated strings from C. Always validate pointers before dereferencing.

3. Why must C strings be converted for Rust FFI? Rust strings are not null-terminated and can contain null bytes internally. C strings require null termination. CString handles this conversion and validates no internal null bytes.

4. What is #[no_mangle] and when is it used? #[no_mangle] prevents Rust from changing the function name during compilation (name mangling), keeping the name as-is for C code to link against. It is required for exported FFI functions.

5. Challenge: Write a safe Rust wrapper around a hypothetical C Database library that manages connection handles and prevents use-after-free through Rust's ownership system.

Mini Project: Safe Error Message Wrapper

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

extern "C" {
    fn strerror(errnum: i32) -> *mut c_char;
}

struct ErrorInfo {
    code: i32,
    message: String,
}

impl ErrorInfo {
    fn from_errno(code: i32) -> Self {
        let msg = if code != 0 {
            unsafe {
                let ptr = strerror(code);
                if ptr.is_null() {
                    "Unknown error".to_string()
                } else {
                    CStr::from_ptr(ptr).to_string_lossy().into_owned()
                }
            }
        } else {
            "No error".to_string()
        };
        ErrorInfo { code, message: msg }
    }
}

impl std::fmt::Display for ErrorInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Error {}: {}", self.code, self.message)
    }
}

fn main() {
    let errors = [0, 2, 5, 22];
    for &code in &errors {
        let info = ErrorInfo::from_errno(code);
        println!("{}", info);
    }
}

Expected output:

Error 0: No error
Error 2: No such file or directory
Error 5: Input/output error
Error 22: Invalid argument

FAQ

Is FFI safe in Rust?

FFI calls are inherently unsafe — the compiler cannot verify C function contracts. Always wrap FFI calls in safe Rust functions that validate inputs, check null pointers, and manage lifetimes.

What is a build.rs file?

build.rs is a build script that runs before your crate compiles. It is used to link C libraries, generate bindings (via bindgen), or compile C source files into your Rust crate.

How do I generate Rust FFI bindings automatically?

Use the bindgen crate, which parses C header files and generates corresponding extern "C" declarations and type definitions automatically.

Unsafe Rust
Embedded Rust
Rust Macros

What's Next

Learn Embedded Rust for microcontroller programming where direct hardware access via FFI is common, and Macros for automating FFI binding generation.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro