---
title: 'The Foreign Function Interface'
source: 'https://academia.sh/en/courses/rust-advanced/foreign-function-interface'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains that the compiler never sees the body of a function called through an extern "C" declaration, and trusts only the declared signature'
    - "Proves, by comparing the result of two foreign calls with Rust's own counterpart, that trusting only the signature produces the correct result"
    - "Explains with an example that the compiler cannot catch an incorrectly written declaration, and that correctness is the writer's responsibility"
---

# The Foreign Function Interface

Calling a C function whose body the compiler never sees, trusting only its declared signature; that the result of two foreign calls matches Rust's own counterpart is shown by running code, and that the correctness of the declaration is the writer's responsibility is explained.

The previous lesson showed the guarantee delegated to an `unsafe` function's signature:
`get_second_item`'s body was written in Rust, the compiler could read that body, it just
did not test the precondition on its own. This lesson carries the same delegation one step
further: calling a function whose body was not written in Rust, one the compiler never
sees at all. There, the contract is no longer a precondition sentence, only a signature —
the number of parameters, the parameter types, the return type.

This boundary exists for a practical reason: rewriting every function a program needs in
its own language is not mandatory. Calls provided by the operating system, long-used
numeric and text-processing libraries, hardware-specific drivers — a large share of these
are written in C, and C's binary compatibility convention serves as a shared boundary that
belongs to no single language. Rust does not reinvent this boundary; it offers a form that
can speak C's calling convention directly and communicates with the other side without
touching its body.

## Calling by Trusting Only the Signature

Calling a C library function from Rust does not require seeing that function's source, or
even its having been compiled on the same machine — common C functions are already linked
into the operating system's own standard library. The Rust program only writes a
declaration: the function's name, the types of the parameters it takes, the type of the
value it returns. This declaration is placed inside an `extern "C"` block; the `"C"` word
says which binary compatibility convention (in what order the parameters are carried on
the stack or in registers) the call will follow — Rust's own functions can be compiled
with a different convention, and speaking to a C library requires this convention to be
explicitly switched to C's.

Once the declaration is written, no extra compile step or link step is required: the
compiler searches the system library it always links the program against, for these two
names, and finds them. Parameter types are not made up by hand either; the `std::os::raw`
module defines names like `c_int` and `c_char` that correspond to the width of C's `int`
and `char` types on this platform — these share the same memory layout as Rust's own `i32`
or `u8` types, but the naming makes it readable from the source that the declaration
matches the C side.

The boundary does not operate in only this direction. A Rust function can also be opened
outward with a signature that C can call; in that case the compiler leaves that function's
name unchanged so the linker can find it under the name C expects. This lesson measures
only the direction from Rust to C — `abs` and `strlen` are both provided by the system,
the side the Rust program calls. The reverse direction rests on the same contract
convention, only which side writes the declaration in the source changes.

The declaration itself and calling the declared function do not carry the same status.
Writing an `extern "C"` block resembles constructing the raw pointer measured in the
previous lesson: the compiler only records a name and a signature, it has not yet crossed
to the other side. The danger begins when there is a genuine jump to where that name
points — which is why only the call requires an `unsafe` block, the declaration itself does
not.

```rust
// declaration.rs — the declaration itself does not require unsafe, calling it does
use std::os::raw::c_int;

extern "C" {
    fn abs(x: c_int) -> c_int;
}

fn main() {
    // Carrying the function as a pointer does not call it; unsafe is not required.
    let fn_ptr: unsafe extern "C" fn(c_int) -> c_int = abs;
    println!("declaration compiled, not called: {}", fn_ptr as usize != 0);
}
```

```
declaration compiled, not called: true
```

This run never called `abs`; it only carried the declared function into a variable and
confirmed that this variable was not empty (did not point to address zero) — the real
address itself was never printed anywhere. **LL6.** Compilation gave no warning on this
line because the declared function was genuinely used, only not called; an unused
declaration would have been flagged as dead code by the compiler. The declaration itself
not requiring `unsafe` is another example of the distinction the language draws between
saying "this name points somewhere" and saying "I am jumping there now" — the same
distinction as between constructing a raw pointer and reading it in the previous lesson.

## Measuring Two Foreign Calls

The run below calls two foreign functions: `abs`, which takes an absolute value, and
`strlen`, which counts the length of a byte array. Neither has a body inside this program
— only a declaration. The results are compared against Rust counterparts that do the same
job.

**LL7.** The number whose absolute value is taken is `-7`; the string whose length is
measured is `"merhaba"` (seven bytes, carrying no multi-byte characters).

```rust
// foreign.rs — the result of two foreign calls and their rust counterpart
use std::ffi::CString;
use std::os::raw::{c_char, c_int};

extern "C" {
    fn abs(x: c_int) -> c_int;
    fn strlen(s: *const c_char) -> usize;
}

fn main() {
    let abs_value = unsafe { abs(-7) };
    let text = CString::new("merhaba").unwrap();
    let length = unsafe { strlen(text.as_ptr()) };

    println!("foreign abs: {} (rust counterpart {})", abs_value, (-7i32).abs());
    println!("foreign length: {} (rust counterpart {})", length, "merhaba".len());
}
```

```
foreign abs: 7 (rust counterpart 7)
foreign length: 7 (rust counterpart 7)
```

Both calls sit inside an `unsafe` block — but for a different reason than the `unsafe` in
the previous lesson. There, the danger was a read spilling past the slice's bounds; here,
the danger is that the compiler knows nothing at all about the other side. The source code
of `abs` and `strlen` is nowhere in this program; the compiler produced the call without
ever seeing them, trusting only the two-line declaration above. **LL8.** The result
matched exactly on both lines: the foreign `abs` call returned `7`, and Rust's own `abs`
method also returned `7`; the foreign `strlen` call returned `7`, and Rust's `len` method
also returned `7`. The match is not a coincidence — it is the result of the declaration
matching the real C-side signature exactly.

The `CString::new` call here is a deliberate step: Rust's own string type does not carry a
zero byte at the end and keeps its length in a separate counter; C's string convention, by
contrast, is a byte array that ends with a zero byte. Calling `strlen` directly with a
Rust string would lead to a type mismatch at a boundary where the two conventions differ;
`CString` performs the translation between them, building a byte array that ends with a
zero byte, and passes only that array's starting address to `strlen`.

## Separate Rules on the Two Sides of the Boundary

In this lesson, only single integers and byte arrays cross the boundary; if a struct
crossed the boundary, a further rule would come into play. Rust's own struct layout engine
can reorder fields however it likes during compilation — a freedom that does not change
the program's behavior, only affects its layout in memory. The C side, by contrast,
guarantees that it places a struct's fields one after another in declared order, following
alignment rules. Even if both sides agree on a struct carrying the same names, if one
reorders the fields while the other keeps the order fixed, the same memory block is read
with two different interpretations. This lesson does not pass a struct, only an integer
and a pointer; this part of the boundary is not measured here, it is only mentioned from
the source as something that exists.

`abs` and `strlen` were chosen because both only read memory — neither allocates a new
memory block and hands it over to the Rust side. This shows one of the most important
restrictions at the FFI boundary: if a C function allocated memory inside itself and
returned a pointer, who releases that memory, and with which allocator, would become a
separate matter of contract. Rust's own growable array and C's `malloc` function are not
required to use the same allocator; trying to release a memory block allocated on the C
side with Rust's own release rule mixes up the two separate allocators' ledgers, and that
produces undefined behaviour. This lesson does not run that situation — `abs` returns an
integer, `strlen` returns a length, neither allocates new memory — but it is mentioned to
make this part of the boundary's existence visible from the source.

Not every part of the boundary falls into the **handed to the writer** or **outside the
language** class; the `CString::new` call does not belong to either. C's
zero-byte-terminated string convention means a string cannot carry a zero byte inside it —
the string ends the moment a zero byte is seen. `CString::new` tests at run time whether
the byte array it is given carries a zero byte inside it, and if it does, stops the call
not with a panic but with an error value.

```rust
// zero_byte.rs — CString::new checks for a zero byte inside the body
use std::ffi::CString;

fn main() {
    let clean = CString::new("merhaba");
    let broken = CString::new("me\0rhaba");

    println!("clean string built: {}", clean.is_ok());
    println!("zero byte inside the body rejected: {}", broken.is_err());
}
```

```
clean string built: true
zero byte inside the body rejected: true
```

**LL9.** This is a different spot from the delegated part of the boundary in the `strlen`
call: here the checking is done not by the compiler but at run time, but it is still
**done** — the `Result` type gives the caller no permission to ignore the error. Not all of
the boundary is handed to the writer; an input that does not conform to C's own format
rule is stopped before it ever reaches the foreign call.

The way errors are reported also differs on the two sides of the boundary. In Rust, an
operation's failure is usually carried by a `Result` value, and the compiler checks
whether this value has been handled. C has no such type; a function's failure is usually
marked by a special return value (for instance, a negative number or zero) or by a
separate global state. An `extern "C"` declaration does not remove this difference — if the
declared return type is `c_int`, knowing whether that value is an error code or an
ordinary result still depends on the caller reading the C side's documentation; the
compiler cannot derive this distinction from the type, because the type is the same for
both.

Character encoding carries a separate trap concerning what the number `strlen` returns
means. Both Rust's own `len` method and `strlen` count the same thing: the string's
**byte** length — not how many characters the string visually consists of. Because the
`"merhaba"` used in this lesson consists only of single-byte characters, this distinction
did not show up; in a string carrying a character encoded with more than one byte, the two
numbers separate from each other.

```rust
// byte_char.rs — separating byte length from character count
use std::ffi::CString;
use std::os::raw::c_char;

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

fn main() {
    let text = "señor";
    let bytes = CString::new(text).unwrap();
    let foreign_length = unsafe { strlen(bytes.as_ptr()) };

    println!("rust byte length: {}", text.len());
    println!("rust character count: {}", text.chars().count());
    println!("foreign strlen: {}", foreign_length);
}
```

```
rust byte length: 6
rust character count: 5
foreign strlen: 6
```

**LL10.** `text.len()` and the foreign `strlen` call matched exactly here too (`6`) — both
count bytes. What comes apart is the character count found with the `chars().count()`
method (`5`): the `ñ` letter in the string, though written as a single character in the
Rust source, takes two bytes in UTF-8 encoding. A C side that relies on `strlen` always
reads this number as bytes; a caller that mistakes it for a character count has proceeded
with a wrong assumption on a string that is not single-byte. The compiler cannot catch this
wrong assumption either — the return type is `usize`, and `usize` does not say whether it
carries a byte count or a character count.

## The Limiting Measurement

Both calls working correctly depended on the declaration having been written correctly —
and the compiler never tested this correctness at any point. `abs`'s real C signature
takes a single integer and returns a single integer; because the declaration wrote this
correctly, the call worked correctly. If the declaration had been written wrongly — for
instance, if `abs` had been declared to take two parameters or return a floating-point
number — the compiler would not have caught it. The compiler has no access to the other
side's body; it only looks at the declaration written and produces a call that matches
that declaration. The result of a wrong declaration is not run in this lesson, because its
result is specific to this machine and falls into the undefined behaviour class — values
on the stack can be interpreted as the wrong type, the program can produce a wrong number,
or it can crash.

This is where the **handed to the writer** and **outside the language** classes intersect.
Checking, as in the previous lesson, was handed to the writer — but what is handed over
here is not a precondition sentence, it is a signature carrying no guarantee at all about
the other side's correctness. The other side itself was written outside Rust, compiled
outside Rust's compilation process, and already linked by the system; the compiler's only
knowledge of it is the two lines the programmer wrote. If the declaration is correct, the
boundary between the two languages becomes invisible — the run above showed exactly this.
If the declaration is wrong, it is not the compiler but only an observation at run time
that can reveal where the error is.

The breadth of this boundary is measured not by a single wrong declaration but by every
field of the declaration. If the parameter count is wrong, the call shifts the positions
it reads from the stack or registers, and every parameter after it carries a wrong value
too. If a parameter type is not the correct width — for instance, if the real C signature
expects a four-byte integer while the declaration writes an eight-byte integer — the
caller ends up sending extra bytes the other side never reads, or sending too few. If the
return type is wrong, the bytes the call returns are interpreted according to the wrong
type. What all three cases share is that the compiler rejects none of them: an
`extern "C"` block is a syntactically valid declaration, and whether it matches the real
signature on the C side is not compilation's concern.

## Summary

- An `extern "C"` block declares a function whose body is written outside Rust using only
  its signature; the compiler never sees that body, and its guarantee stays entirely
  **outside the language**
- Common C library functions are already linked on the system; once the declaration is
  written, no extra compile or link step is required
- The result of two foreign calls (`abs`, `strlen`) matched Rust's own counterpart exactly
  — the match is the result of the declaration having been written correctly, matching the
  real C signature
- `CString` closes the convention gap between Rust's length-counted string and C's
  zero-byte-terminated array with an explicit conversion at the boundary
- If the declaration had been written wrongly, the compiler could not have caught it;
  correctness is entirely the writer's responsibility — this is the point where the
  **handed to the writer** and **outside the language** classes intersect
- Not every part of the boundary is handed over: `CString::new` tests at run time whether
  there is a zero byte inside the body, and if there is, stops with an error value — this
  check, even though it happens at run time, still **holds**

## Next Step

In this lesson, the boundary was between two languages; as long as the declaration was
correct, which machine it ran on did not matter — the `abs` and `strlen` calls produced the
same result no matter which processor or operating system the program ran on, because both
were linked to the system's own standard library. The next lesson reverses this
independence: the same source needing a different body depending on the **target** it runs
on. There, the guarantee will not rest on a declaration but on the constraints the target
itself gives — and that constraint will be written not in an external library's signature,
as in this lesson, but conditionally, in the source itself.
