Ir al contenido

FFI en Rust: cómo llamar a C desde Rust

FFI son las siglas de Foreign Function Interface (interfaz de funciones externas). Es la forma en que Rust habla con código escrito en otro lenguaje. Casi siempre, ese otro lenguaje es C, o algo que sigue las reglas de C, porque C es el “idioma común” que entienden casi todos los lenguajes y sistemas operativos.

Piensa en dos personas que no comparten idioma materno, pero que hablan un poco de inglés. C es ese idioma compartido. Con FFI puedes usar desde Rust una biblioteca de C ya madura, o escribir una biblioteca rápida en Rust que puedan llamar programas en Python, Node.js o C. Esta página se apoya en Unsafe Rust, así que léela primero si los bloques unsafe son nuevos para ti.

La biblioteca estándar de C siempre está enlazada en los programas de Rust en Linux, macOS y Windows, así que sus funciones son perfectas para una primera prueba. Para llamar a una, la declaras dentro de un bloque extern "C": su nombre, sus parámetros y su tipo de retorno. Después la llamas dentro de unsafe:

use std::ffi::c_int;
unsafe extern "C" {
// Esta es la función de C `int abs(int x);` escrita en Rust.
fn abs(x: c_int) -> c_int;
}
fn main() {
let n = unsafe { abs(-42) };
println!("abs(-42) = {n}");
}

Salida:

abs(-42) = 42
▶ Pruébalo tú mismo

Fíjate en algunos detalles:

  • "C" indica la ABI: las reglas sobre cómo se pasan los argumentos y cómo se devuelven los valores. Las reglas de C son las que todo el mundo acepta.
  • c_int es el nombre que Rust le da al int de C. El módulo std::ffi tiene tipos como c_int, c_long y c_char, para que tu declaración coincida exactamente con C en cualquier plataforma.
  • Escribes unsafe extern "C". En la edición 2024 ese unsafe es obligatorio, porque estás prometiendo que la declaración es correcta. El código más antiguo (y el de la edición 2021) suele escribir simplemente extern "C" { ... }. La versión con unsafe funciona en ambas ediciones, así que esta página la usa en todas partes.

Por qué cada llamada es unsafe (y cómo esconderlo)

Sección titulada «Por qué cada llamada es unsafe (y cómo esconderlo)»

El compilador de Rust no puede leer código C. No tiene ni idea de si abs recibe de verdad un int, de si puede fallar o de si se guarda un puntero que le diste. Por eso cada llamada a una función externa es unsafe: tú asumes la responsabilidad de usarla correctamente.

La buena noticia es que solo tienes que pensarlo a fondo una vez. El patrón habitual es escribir un pequeño envoltorio seguro (safe wrapper): una función normal de Rust que comprueba las reglas y luego hace la llamada unsafe. Todos los demás llaman simplemente al envoltorio.

Aquí tienes un ejemplo real. En C, abs(INT_MIN) es comportamiento indefinido (undefined behavior), porque el resultado es demasiado grande para caber en un int. Nuestro envoltorio comprueba ese caso primero:

use std::ffi::c_int;
unsafe extern "C" {
fn abs(x: c_int) -> c_int;
}
// Un envoltorio seguro: quien lo llama no necesita `unsafe`.
fn safe_abs(x: i32) -> Option<i32> {
// abs(i32::MIN) no tiene un resultado válido en C, así que lo rechazamos.
if x == i32::MIN {
return None;
}
// SAFETY: x no es i32::MIN, así que abs tiene un resultado válido.
Some(unsafe { abs(x) })
}
fn main() {
println!("{:?}", safe_abs(-42));
println!("{:?}", safe_abs(i32::MIN));
}

Salida:

Some(42)
None
▶ Pruébalo tú mismo

El comentario // SAFETY: es una costumbre de Rust que vale la pena copiar. Explica por qué este bloque unsafe concreto es correcto, para que quien lea el código después no tenga que adivinarlo. Se suele escribir con la palabra SAFETY en inglés, tal cual.

Si una función de C de verdad no tiene ninguna regla que cumplir para ninguna entrada, puedes marcarla como safe fn dentro del bloque unsafe extern, y entonces llamarla sin unsafe. abs no es una de esas funciones (acuérdate de INT_MIN), así que la dejamos como unsafe y comprobamos la entrada nosotros mismos.

Las cadenas de texto son donde viven la mayoría de los errores de FFI, porque Rust y C las guardan de forma distinta:

  • Un String / &str de Rust conoce su longitud y puede contener cualquier byte, incluido el 0.
  • Una cadena de C es solo un puntero a bytes. La cadena termina en el primer byte 0 (llamado NUL). No se guarda ninguna longitud.

Así que no puedes pasarle un &str a C directamente. Usa CString (en inglés) para crear una copia propia terminada en NUL, y .as_ptr() para obtener el *const c_char que C espera:

use std::ffi::{c_char, CString};
unsafe extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn c_strlen(text: &str) -> Option<usize> {
// CString::new añade el NUL al final por nosotros.
let c_text = CString::new(text).ok()?;
// SAFETY: c_text es una cadena válida terminada en NUL y vive hasta el final de esta función.
Some(unsafe { strlen(c_text.as_ptr()) })
}
fn main() {
let hello = CString::new("hello").unwrap();
println!("{:?}", hello.as_bytes_with_nul());
println!("{:?}", c_strlen("hello"));
println!("{:?}", c_strlen("héllo"));
match CString::new("hel\0lo") {
Ok(_) => println!("ok"),
Err(e) => println!("error: {e}"),
}
}

Salida:

[104, 101, 108, 108, 111, 0]
Some(5)
Some(6)
error: nul byte found in provided data at position: 3
▶ Pruébalo tú mismo

Aquí dejamos las cadenas en inglés a propósito: el resultado depende de los bytes exactos de cada texto. El mensaje “nul byte found in provided data at position: 3” lo genera Rust y dice que encontró un byte NUL en la posición 3.

Hay dos sorpresas, y las dos son importantes:

  1. strlen("héllo") da 6, no 5. C cuenta bytes, y la é ocupa dos bytes en UTF-8.
  2. CString::new falla si tu texto contiene un byte 0. C creería que la cadena termina ahí y cortaría "lo" sin avisar. Rust se niega en lugar de dejar que eso pase; por eso CString::new devuelve un Result.

En la otra dirección, cuando C te entrega un char *, envuélvelo en CStr (en inglés). CStr es a CString lo que &str es a String: una vista prestada. La función strchr de C devuelve un puntero al primer carácter que coincide, lo que viene muy bien para una demostración:

use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" {
fn strchr(s: *const c_char, c: c_int) -> *mut c_char;
}
fn find_from(text: &CStr, ch: u8) -> Option<String> {
let ptr = unsafe { strchr(text.as_ptr(), ch as c_int) };
if ptr.is_null() {
return None; // C dice "no encontrado" con un puntero nulo
}
// SAFETY: ptr apunta dentro de `text`, que sigue vivo y termina en NUL.
let found = unsafe { CStr::from_ptr(ptr) };
Some(found.to_string_lossy().into_owned())
}
fn main() {
let text = CString::new("hola, mundo").unwrap();
println!("{:?}", find_from(&text, b','));
println!("{:?}", find_from(&text, b'z'));
// Un literal c"..." es un &CStr con el NUL ya añadido.
println!("{:?}", find_from(c"rust-lang.org", b'.'));
}

Salida:

Some(", mundo")
None
Some(".org")
▶ Pruébalo tú mismo

to_string_lossy() convierte los bytes de C en texto de Rust. Reemplaza cualquier secuencia UTF-8 inválida por �, porque las cadenas de C no prometen ser UTF-8. Usa .to_str() si prefieres recibir un error.

Rust tiene permitido reordenar los campos de un struct para ahorrar espacio. C nunca lo hace: los campos quedan en memoria en el orden en que los escribiste, con relleno (padding) entre ellos. Por ejemplo, un struct con campos u8, u32, u8 ocupa hoy 8 bytes con la disposición de Rust (Rust junta los dos u8), pero 12 bytes con la disposición de C. Si C y Rust no están de acuerdo sobre la disposición en memoria, C lee los bytes equivocados, y nada te avisa.

Así que, cuando un struct cruza la frontera, dile a Rust que use la disposición de C con #[repr(C)]. La función div de C devuelve un struct div_t con dos campos int, quot y rem:

use std::ffi::c_int;
// Coincide con el de C: typedef struct { int quot; int rem; } div_t;
#[repr(C)]
#[derive(Debug)]
struct DivT {
quot: c_int,
rem: c_int,
}
unsafe extern "C" {
fn div(numerator: c_int, denominator: c_int) -> DivT;
}
fn main() {
let result = unsafe { div(17, 5) };
println!("{result:?}");
println!("17 / 5 = {} y sobran {}", result.quot, result.rem);
}

Salida:

DivT { quot: 3, rem: 2 }
17 / 5 = 3 y sobran 2
▶ Pruébalo tú mismo

FFI funciona en las dos direcciones. Escribe extern "C" fn (sin bloque) y obtienes una función de Rust que sigue las reglas de llamada de C, así que el código C puede llamarla. La función qsort de C recibe una función de comparación como argumento, y podemos darle una escrita en Rust:

use std::ffi::{c_int, c_void};
unsafe extern "C" {
fn qsort(
base: *mut c_void,
count: usize,
size: usize,
compare: extern "C" fn(*const c_void, *const c_void) -> c_int,
);
}
// Una función de Rust a la que llamará C.
extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int {
// SAFETY: qsort solo nos pasa punteros a elementos de nuestro array de i32.
let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) };
a.cmp(&b) as c_int // Less = -1, Equal = 0, Greater = 1
}
fn sort_with_c(numbers: &mut [i32]) {
unsafe {
qsort(
numbers.as_mut_ptr() as *mut c_void,
numbers.len(),
std::mem::size_of::<i32>(),
compare_i32,
);
}
}
fn main() {
let mut numbers = [42, 7, 19, -3, 0];
sort_with_c(&mut numbers);
println!("{:?}", numbers);
}

Salida:

[-3, 0, 7, 19, 42]
▶ Pruébalo tú mismo

(En código real simplemente llamarías a sort(). Esto solo sirve para mostrar a C llamando de vuelta a Rust).

Cuando un programa en C necesita encontrar tu función por su nombre, añade #[no_mangle]. Normalmente Rust “deforma” (mangles) los nombres en algo como _ZN4main11add_numbers17h...E, para que distintos crates nunca choquen entre sí. #[no_mangle] conserva el nombre simple add_numbers:

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

Nota sobre la edición 2024: los ejemplos del Playground de este sitio usan la edición 2021, donde #[no_mangle] está bien. En la edición 2024 (la que usa por defecto un proyecto nuevo creado con cargo new), hay que escribirlo #[unsafe(no_mangle)], porque dos funciones con el mismo nombre sin deformar romperían el programa. La nueva forma también funciona en la edición 2021.

Para crear una biblioteca a la que C pueda enlazarse, pon el tipo de crate cdylib en Cargo.toml:

[lib]
crate-type = ["cdylib"]
// src/lib.rs (edición 2024)
#[unsafe(no_mangle)]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}

cargo build --release genera target/release/libmathlib.so (.dylib en macOS, .dll en Windows) para un crate llamado mathlib. Después, un programa en C declara la función y se enlaza con la biblioteca:

#include <stdio.h>
#include <stdint.h>
int32_t add_numbers(int32_t a, int32_t b);
int main(void) {
printf("2 + 3 = %d\n", add_numbers(2, 3));
return 0;
}
Terminal window
$ cc main.c -L target/release -lmathlib -o main
$ LD_LIBRARY_PATH=target/release ./main
2 + 3 = 5

Escribir esa cabecera de C a mano cansa enseguida. cbindgen (en inglés) la genera a partir de tu código Rust.

Compilar tu propio código C con build.rs y cc

Sección titulada «Compilar tu propio código C con build.rs y cc»

Hasta ahora solo hemos usado funciones que ya estaban enlazadas. Para usar tu propio archivo de C, deja que Cargo lo compile. Un archivo llamado build.rs en la raíz del proyecto es un script de compilación (build script): Cargo lo ejecuta antes de compilar tu crate. El crate cc (en inglés) lo reduce a unas pocas líneas.

Terminal window
cargo new hello-c
cd hello-c
cargo add --build cc
src/greet.c
#include <stdio.h>
int add_ints(int a, int b) {
return a + b;
}
void greet(const char *name) {
printf("¡Hola desde C, %s!\n", name);
fflush(stdout);
}
build.rs
fn main() {
// Compila src/greet.c como biblioteca estática y la enlaza.
cc::Build::new().file("src/greet.c").compile("greet");
// Vuelve a ejecutar este script solo cuando cambie el archivo de C.
println!("cargo::rerun-if-changed=src/greet.c");
}
src/main.rs
use std::ffi::{c_char, c_int, CString};
unsafe extern "C" {
fn add_ints(a: c_int, b: c_int) -> c_int;
fn greet(name: *const c_char);
}
fn main() {
let sum = unsafe { add_ints(2, 3) };
println!("2 + 3 = {sum}");
let name = CString::new("Ferris").unwrap();
unsafe { greet(name.as_ptr()) };
}
Terminal window
$ cargo run -q
2 + 3 = 5
¡Hola desde C, Ferris!

Necesitas tener instalado un compilador de C (gcc, clang o MSVC en Windows); cc lo encuentra por ti.

Para una biblioteca de C grande, con cientos de funciones, escribir cada declaración extern a mano es lento y es fácil equivocarse. bindgen (en inglés) lee los archivos de cabecera de C y genera las declaraciones de Rust por ti, normalmente también desde build.rs. La mayoría de los crates -sys de crates.io (como openssl-sys) se construyen así.

Como C es el idioma compartido, las mismas ideas permiten que Rust hable con muchos otros lenguajes. Para estos casos casi nunca escribes código extern "C" a mano: un crate hace el trabajo unsafe y te da una API segura y cómoda:

  • Python: PyO3 (en inglés) te permite escribir módulos de Python en Rust (y llamar a Python desde Rust). Su herramienta compañera, maturin, los compila y los publica como paquetes normales de pip. Bibliotecas como Polars y pydantic-core lo usan.
  • Node.js: napi-rs (en inglés) crea complementos nativos de Node.js en Rust, y genera los tipos de TypeScript por ti.
  • Proyectos en C y C++: cdylib más cbindgen (en inglés), como viste arriba.
  • El navegador: no es FFI en el sentido de C, pero wasm-bindgen cumple un papel parecido. Consulta WebAssembly.

Llamar a una función externa fuera de unsafe.

unsafe extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
let n = abs(-7);
println!("{n}");
}
error[E0133]: call to unsafe function `abs` is unsafe and requires unsafe function or block
--> src/main.rs:6:13
|
6 | let n = abs(-7);
| ^^^^^^^ call to unsafe function

El compilador dice que llamar a la función unsafe abs requiere una función o un bloque unsafe. Solución: envuelve la llamada en unsafe { ... } o, mejor aún, en una función envoltorio segura, como viste arriba.

Un puntero a un CString que ya no existe. Esto compila, pero es un error de verdad:

use std::ffi::{c_char, CString};
unsafe extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn main() {
let ptr = CString::new("hello").unwrap().as_ptr();
let len = unsafe { strlen(ptr) };
println!("{len}");
}
warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement
--> src/main.rs:8:46
|
8 | let ptr = CString::new("hello").unwrap().as_ptr();
| ------------------------------ ^^^^^^ pointer created here
| |
| this `CString` is dropped at end of statement
|
= help: bind the `CString` to a variable such that it outlives the pointer returned by `as_ptr`

El aviso dice que esto crea un puntero colgante (dangling pointer), porque el CString temporal se libera al final de la sentencia. El CString es un valor temporal, así que se libera en el ;, y ptr queda apuntando a memoria liberada. Solución: guarda el CString en su propia variable (let s = CString::new("hello").unwrap();) y llama a s.as_ptr() mientras s siga viva. Nunca ignores este aviso.

extern "C" sin unsafe en un proyecto de la edición 2024. El código copiado de tutoriales antiguos falla así:

error: extern blocks must be unsafe
--> src/main.rs:3:1
|
3 | / extern "C" {
4 | | fn add_ints(a: c_int, b: c_int) -> c_int;
5 | | fn greet(name: *const c_char);
6 | | }
| |_^

El compilador exige que los bloques extern sean unsafe. Solución: escribe unsafe extern "C" { ... }.

Olvidar #[repr(C)]. No hay ningún error: C simplemente lee basura. Pon #[repr(C)] en todos los structs y enums que crucen la frontera.

  • Declara las funciones de C en un bloque unsafe extern "C" { ... }, usando los tipos de C de std::ffi, como c_int y c_char.
  • Cada llamada externa es unsafe. Envuélvela una sola vez en una función segura de Rust que compruebe las reglas, con un comentario // SAFETY:.
  • Las cadenas de C terminan en un byte NUL. Usa CString para enviar texto a C y CStr para leer texto de C. Mantén vivo el CString mientras C use el puntero.
  • Usa #[repr(C)] en los structs que compartas con C, porque Rust puede reordenar los campos.
  • extern "C" fn más #[no_mangle] (#[unsafe(no_mangle)] en la edición 2024) permite que C llame a Rust; cdylib crea una biblioteca para ello.
  • build.rs + cc compila tu código C; bindgen genera las declaraciones; PyO3 y napi-rs conectan Rust con Python y Node.js.

Comprueba lo aprendido

1. ¿Por qué es unsafe cada llamada a una función de un bloque extern "C"?

2. ¿Qué pasa con CString::new("hel\0lo")?

3. ¿Por qué los structs que se comparten con C necesitan #[repr(C)]?

4. En un crate de la edición 2024, ¿cómo se escribe el atributo que conserva el nombre simple de una función para C?

Puntuación: 0 / 4