---
title: "25.4 Calling C from Rust"
description: "Foreign function interface: declaring C functions with extern C, why every foreign call is unsafe, and a real libc example."
url: "https://learnrust.net/chapter-25/calling-c-from-rust/"
last_updated: "2026-06-13"
---

# 25.4 Calling C from Rust

The single most common reason to use `unsafe` in real Rust isn't clever data structures, it's talking to code written in other languages. Decades of essential software, operating system APIs, cryptography, compression, graphics, audio, are written in C, and rewriting all of it in Rust is neither possible nor desirable. So Rust can *call* C functions directly, through its **foreign function interface** (FFI). This pays off the loose thread from lesson [5.1](https://learnrust.net/chapter-5/constants-and-statics/), which mentioned "talking to other languages" as a future reason some things matter. Here it is. And because the C code on the other side has none of Rust's guarantees, every call across the boundary is `unsafe`.

## Declaring a foreign function

To call a C function, you first *declare* it in Rust so the compiler knows its name and signature. You don't write its body, it lives in some C library, you just describe its interface inside an `extern "C"` block:

```rust
extern "C" {
    fn abs(input: i32) -> i32;   // the C standard library's abs()
}

fn main() {
    let n = -42;
    unsafe {
        println!("abs({n}) = {}", abs(n));
    }
}
```

```
abs(-42) = 42
```

Read the pieces. `extern "C"` introduces a block of foreign function declarations, and the `"C"` specifies the **ABI**, the "application binary interface," the low-level convention for how arguments are passed and values returned. `"C"` is the near-universal lingua franca that almost every language can speak, which is why FFI is usually "C FFI" regardless of what language the other side is actually written in. Inside, `fn abs(input: i32) -> i32;` declares C's `abs` function: name, argument types, return type, but no body, because the body is in the C standard library, which the linker connects automatically.

Then the call: `abs(n)` must be inside `unsafe`. Calling *any* foreign function is one of the things that requires `unsafe`, and the reason is fundamental.

## Why every foreign call is unsafe

Think about what crossing the boundary means. The Rust compiler has checked *your* code, but it knows nothing about the C function on the other side. It can't verify that `abs` actually matches the signature you declared. It can't check that the C code respects ownership, or won't write past a buffer, or won't keep a pointer you passed and use it after you freed it. The C function might do *anything*, and Rust's guarantees stop at the boundary like a jurisdiction's border. So Rust requires `unsafe` on every foreign call as a blanket acknowledgment: "I'm leaving the zone the compiler protects, and I'm vouching that the foreign code behaves as its signature claims." You can't prove that statically, so you take responsibility for it.

This is the chapter's whole philosophy made concrete. The unsafety isn't Rust being paranoid; it's an honest admission that it cannot reason about code it didn't compile. The `unsafe` marks exactly where its knowledge ends.

## Passing data across: the C string example

Calling `abs` with an `i32` is easy because integers mean the same thing in both languages. Most real FFI is fiddlier, because Rust types and C types don't match. The classic mismatch is strings: a Rust `String` is a length-tracked UTF-8 buffer (chapter 18), while a C string is a raw pointer to bytes terminated by a zero byte, no length, no UTF-8 promise. To pass a string to C you must convert it to C's format, using `CString`:

```rust
use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn strlen(s: *const c_char) -> usize;   // C's strlen
}

fn main() {
    let rust_string = CString::new("hello, C").expect("no interior null byte");

    unsafe {
        let length = strlen(rust_string.as_ptr());
        println!("C says the length is {length}");
    }
}
```

```
C says the length is 8
```

`CString::new` builds a C-compatible, null-terminated string from a Rust string (it returns a `Result` because C strings can't contain a zero byte mid-string, so an interior null is an error). `as_ptr()` hands C a `*const c_char`, a raw pointer (lesson [25.2](https://learnrust.net/chapter-25/raw-pointers/)), because that's the only thing C understands, raw addresses. The `c_char` type is Rust's name for "C's `char`," from the `std::os::raw` module of C-compatible type aliases. The boundary forces you to think in C's terms: raw pointers, null terminators, matching integer widths. Getting any of it wrong, a mismatched type, a pointer that outlives its data, is undefined behavior the compiler can't catch, which is the whole reason the call is `unsafe`.

> **The signature is a promise only you can keep**
>
> When you write `fn strlen(s: *const c_char) -> usize;`, you are *asserting* that the real C `strlen` has exactly that signature. The compiler takes your word for it, it has no way to check the foreign function. If you get the types wrong (say you declare a parameter as `i32` when C expects a 64-bit value, or mix up the return type), the code compiles, links, and then corrupts memory or crashes at run time with no warning. FFI signatures are a contract you must get right by hand, against the C library's documentation or headers. This is the most error-prone part of FFI and a major reason to prefer audited wrapper crates (below) over hand-rolling declarations.

## In practice: bindgen and wrapper crates

Hand-writing `extern "C"` declarations for a whole C library would be tedious and dangerous, exactly the place to get a signature wrong. So the ecosystem has tooling. **`bindgen`** is a tool that reads a C library's header files and *generates* the matching Rust `extern` declarations automatically, eliminating the by-hand transcription and the errors that come with it. And for most popular C libraries, someone has already done this work and published a crate with a *safe* wrapper, the unsafe FFI confined behind a safe Rust API, exactly the lesson-[25.3](https://learnrust.net/chapter-25/unsafe-functions-and-safe-abstractions/) pattern at the language boundary. So in real projects you rarely write raw FFI yourself; you add a wrapper crate from crates.io and call safe functions, while the audited `unsafe` lives in the crate. Knowing how the boundary works (this lesson) is what lets you understand and trust those crates.

## Quiz time

**Question #1**

What does an `extern "C"` block do, and what is the `"C"` part?

<details class="solution">
<summary>Show solution</summary>

An `extern "C"` block *declares* foreign functions, their names, argument types, and return types, so Rust can call them; the bodies live in an external (usually C) library that the linker connects. The `"C"` specifies the ABI (application binary interface): the low-level convention for passing arguments and returning values. `"C"` is the near-universal standard nearly every language can speak, which is why FFI is done through it regardless of the other language.

</details>

**Question #2**

Why does calling a foreign (C) function always require `unsafe`?

<details class="solution">
<summary>Show solution</summary>

Because the Rust compiler can't verify anything about the foreign code: it can't confirm the function matches the signature you declared, respects ownership, stays within buffers, or doesn't misuse pointers you pass. Rust's guarantees stop at the language boundary. The `unsafe` is your acknowledgment that you're leaving the compiler-checked zone and vouching that the foreign code behaves as its declared signature claims, something you can't prove statically.

</details>

**Question #3**

Why can't you pass a Rust `String` directly to a C function, and what do you use instead?

<details class="solution">
<summary>Show solution</summary>

A Rust `String` is a length-tracked UTF-8 buffer, while a C string is a raw pointer to bytes terminated by a null (zero) byte, with no length and no UTF-8 requirement, different representations. You convert with `CString::new(...)` (which returns a `Result`, since C strings can't contain an interior null) and pass `.as_ptr()`, a `*const c_char` raw pointer, which is the only string form C understands. The boundary forces Rust data into C's representation.

</details>

You can now call into C. The reverse direction, letting C (or any language) call *your* Rust code, is the final FFI piece and the closing pitch of the chapter. The next lesson ([25.5](https://learnrust.net/chapter-25/calling-rust-from-c/)) covers exposing Rust functions with `#[no_mangle]` and `extern "C"`, so Rust can be embedded inside almost anything.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
