Generar bindings: bindgen y cbindgen en vez de declarar a mano
Declarar tres funciones a mano se aguanta. Una biblioteca real —SQLite, libclang, una API del sistema— tiene cientos de funciones, structs, enums y constantes, y cada firma mal transcrita es un UB esperando. Dos herramientas automatizan la frontera: bindgen lee cabeceras C y emite declaraciones Rust; cbindgen lee tu Rust y emite una cabecera C. Generar, no transcribir. Pero ninguna decide por ti la seguridad ni la propiedad: eso es el wrapper de nivel 40.
Declarar abs y strlen a mano es didáctico. Ponerse a transcribir SQLite —cientos de funciones, decenas de struct, constantes, enum, macros— a mano es una forma lenta de fabricar undefined behavior: cada firma copiada mal, cada c_int donde iba c_long, cada campo en el orden equivocado es una corrupción silenciosa que el compilador no puede detectar porque, como ya sabes, no ve el otro lado. La respuesta profesional no es “tener más cuidado”: es no escribir las declaraciones a mano en absoluto. Dos herramientas automatizan cada dirección de la frontera. bindgen lee las cabeceras .h de C y emite las declaraciones Rust equivalentes —los bloques unsafe extern, los #[repr(C)], los alias, las constantes—. cbindgen hace lo inverso: lee tu API Rust exportada y produce la cabecera .h que un consumidor de C necesita. El lema del nivel es: generar, no transcribir.
- Generar declaraciones Rust desde cabeceras C con
bindgen. - Integrar
bindgenen unbuild.rspara regenerar en cada compilación. - Producir una cabecera C desde tu API Rust con
cbindgen. - Entender qué automatizan y qué no pueden decidir por ti: la seguridad y la propiedad.
bindgen: de las cabeceras C a Rust
El patrón canónico es el crate -sys: un crate cuya única misión es exponer las declaraciones crudas de una biblioteca C. bindgen se declara como dependencia de compilación y se invoca desde build.rs, que escribe el resultado en OUT_DIR:
[build-dependencies]
bindgen = "0.70"
// build.rs
use std::{env, path::PathBuf};
fn main() {
println!("cargo:rustc-link-lib=foo"); // enlaza libfoo
let bindings = bindgen::Builder::default()
.header("wrapper.h") // cabecera que incluye lo que quieres
.generate()
.expect("no se pudieron generar los bindings");
let salida = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings.write_to_file(salida.join("bindings.rs")).unwrap();
}
El fichero wrapper.h no es más que un #include de las cabeceras que te interesan. Y el crate incorpora el resultado con una macro include!:
// lib.rs
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
bindgen usa libclang para parsear C de verdad —no una heurística— y emite el unsafe extern "C", los struct con #[repr(C)], los alias de tipo y las constantes, todo ajustado a la plataforma de destino. Generarlo en build.rs garantiza que los anchos y el layout se recalculan en cada máquina donde compilas.
cbindgen: de Rust a la cabecera C
La dirección inversa, la que corona la lección anterior. Escribiste una biblioteca Rust con funciones #[unsafe(no_mangle)] pub extern "C"; cbindgen la escanea y genera el .h con los prototipos, los struct y los enum correspondientes:
cbindgen --config cbindgen.toml --crate mi_crate --output mi_crate.h
También puede invocarse desde build.rs para regenerar la cabecera en cada compilación. Con ese .h en la mano, cualquier programa de C o C++ puede consumir tu biblioteca Rust sin que tú escribas el prototipo a mano ni corras el riesgo de que la cabecera se desincronice de la firma real.
flowchart TB H[Cabecera C punto h] -->|bindgen mas libclang| R[bindings.rs con unsafe extern C] R --> W[Wrapper seguro escrito a mano] A[API Rust con extern C] -->|cbindgen| G[Cabecera C generada] G --> Cc[Consumidor en C o C plus plus] style H fill:#fab387,color:#11111b style R fill:#89b4fa,color:#11111b style W fill:#a6e3a1,color:#11111b style G fill:#cba6f7,color:#11111b
Lo que la herramienta no decide
Aquí está el punto que separa usar bindgen de entenderlo. Lo que genera es una traducción cruda y uno-a-uno: el crate -sys. Reproduce la sintaxis de las firmas con fidelidad mecánica, pero no conoce la semántica. No sabe quién libera cada puntero, cuáles pueden ser nulos, qué función exige que llames antes a otra, ni qué invariantes debe cumplir un struct. Todo eso vive en la documentación y en la cabeza de quien escribió la biblioteca, no en los tipos de C. Por eso las declaraciones generadas son todas unsafe: la herramienta te da la ABI correcta, no la seguridad.
De ahí la convención de dos capas que verás en todo el ecosistema:
crate foo-sys
Los bindings crudos que genera bindgen. Firmas unsafe, punteros por todas partes, uno-a-uno con la cabecera C. Nadie programa contra esto directamente si puede evitarlo.
crate foo
El wrapper seguro, escrito a mano encima del -sys. Traduce punteros a referencias y Result, envuelve recursos en tipos con Drop, esconde el unsafe. Es lo que el nivel 40 te enseña a construir.
Separar foo-sys de foo divide dos trabajos que no se parecen: traducir la ABI (mecánico, automatizable, lo hace bindgen) y diseñar una API segura (creativo, exige juicio, lo haces tú). El crate -sys puede regenerarse y compartirse entre múltiples wrappers rivales; el wrapper puede evolucionar sin volver a tocar la ABI. Cuando veas libsqlite3-sys y rusqlite, estás viendo exactamente esta división.
Afinar bindgen: no traduzcas la biblioteca entera
Por defecto bindgen traduce todo lo que alcanza desde la cabecera, y eso arrastra media libc por culpa de los #include transitivos. En la práctica se acota con listas de permitidos y se refinan los tipos generados:
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.allowlist_function("foo_.*") // solo lo que empieza por foo_
.allowlist_type("Foo.*")
.newtype_enum("FooStatus") // enum C como newtype con constantes
.derive_default(true) // deriva Default donde sea posible
.generate()
.unwrap();
allowlist_function y allowlist_type recortan la superficie a lo que de verdad usas; newtype_enum evita el peligro de tratar un enum de C como exhaustivo cuando C puede devolver un valor fuera del conjunto declarado. Afinar bindgen no es cosmética: un binding más pequeño es menos superficie unsafe que auditar.
Es fácil creer que bindgen “resuelve” la FFI, y en cierto sentido resuelve mucho: elimina de un plumazo toda una clase de errores —el c_long tecleado como c_int, el campo en el orden equivocado, la constante mal copiada— que ninguna revisión humana atrapa con fiabilidad. Pero conviene ver con precisión qué clase de error elimina y cuál no. bindgen elimina el error clerical: la infidelidad entre la cabecera y su transcripción. Lo que no puede tocar es el error semántico, porque para eso tendría que saber algo que la cabecera no dice. Un prototipo de C char* obtener_nombre(Usuario* u) le dice a la herramienta que hay un puntero de entrada y un puntero de salida; no le dice si el puntero devuelto lo debes liberar tú o pertenece al objeto, si puede ser nulo, si sigue siendo válido tras modificar u, ni si u puede ser nulo. Esa información —la propiedad, la nulabilidad, las precondiciones, la aliasing— es justo la que constituye la seguridad, y no está codificada en los tipos de C, así que ninguna herramienta la puede extraer. El resultado es una traslación sutil pero total de dónde vive el riesgo: sin bindgen, tu error probable era transcribir mal una firma; con bindgen, tu binding es tan correcto como la cabecera de la que parte y tan seguro como el wrapper que le pongas encima. La máquina se ha quedado con el trabajo mecánico —traducir sintaxis— y te ha dejado, destilado y sin distracciones, el único trabajo que de verdad exigía un humano: suministrar el significado que C omitió. Por eso generar bindings no es el final de la FFI sino su comienzo bien planteado: te entrega una base fiel sobre la que ejercer el juicio que ninguna herramienta puede automatizar.
bindgen traduce cabeceras C a declaraciones Rust (unsafe extern, #[repr(C)], alias, constantes) usando libclang; se integra en build.rs escribiendo a OUT_DIR e incluyéndose con include!, y así recalcula el layout por plataforma. cbindgen hace lo inverso: genera un .h desde tu API Rust exportada. Lo generado es la ABI, no la seguridad: son bindings crudos y unsafe, el crate foo-sys. Encima se escribe a mano el wrapper seguro, el crate foo, que aporta la propiedad, la nulabilidad y las precondiciones que C no codifica. Automatizar borra el error clerical, no el semántico.
- Crea un crate
-sysconbindgencomobuild-dependencyy unwrapper.hque incluya una cabecera del sistema (por ejemplomath.h). Inspecciona elbindings.rsgenerado enOUT_DIR. - Explica por qué generar en
build.rsconinclude!(concat!(env!("OUT_DIR"), ...))es superior a comitear unbindings.rsfijo al repositorio. - Con
cbindgen, genera la cabecera.hde la funciónsumarque exportaste en la lección anterior. Comprueba que el prototipo coincide con la firma Rust. - Toma un prototipo C como
char* obtener_nombre(Usuario*)y enumera tres decisiones semánticas (propiedad, nulabilidad, validez) quebindgenno puede tomar por ti. - Explica la convención
foo-sysfrente afooy qué responsabilidad vive en cada capa. Da un ejemplo real del ecosistema.