Adrianistán

El blog de Adrián Arroyo


Bindings entre Rust y C/C++ con bindgen

- Adrián Arroyo Calle

Rust es un lenguaje con muchas posibilidades pero existe mucho código ya escrito en librerías de C y C++. Código que ha llevado mucho tiempo, que ha sido probado en miles de escenarios, software maduro y que no compensa reescribir. Afortunadamente podemos reutilizar ese código en nuestras aplicaciones Rust a través de bindings. Los bindings no son más que trozos de código que sirven de pegamento entre lenguajes. Esto es algo que se ha podido hacer desde siempre pero dependiendo de la librería podía llegar a ser muy tedioso. Afortunadamente tenemos bindgen, un programa que permite generar estos bindings de forma automática analizando el código de la librería de C o C++.



En este post veremos como usar SQLite desde Rust usando bindgen.

Instalando bindgen


En primer lugar necesitamos tener instalado Clang 3.9 o superior. En Ubuntu o Debian necesitamos estos paquetes:
sudo apt install llvm-3.9-dev libclang-3.9-dev clang-3.9

Para el resto de plataformas puedes descargar el binario desde la página de descargas de LLVM.

Bindgen permite dos modos de uso: línea de comandos o desde el código Rust. El más habitual es desde código Rust pero antes veremos el modo en línea de comandos.

Modo línea de comandos


Para bindings sencillos podemos usar el modo línea de comandos. Instalamos binden con Cargo:
cargo install bindgen

Su uso es muy sencillo:
bindgen /usr/include/sqlite3.h -o sqlite.rs

Simplemente indicamos el fichero de cabecera que queremos traducir y su correspondiente fichero de salida en Rust. Este fichero será el pegamento. Vamos a crear un programa que use este pegamento:
mod sqlite;

use sqlite::{sqlite3_open, sqlite3_exec, sqlite3_close, SQLITE_OK};
use std::ffi::CString;
use std::ptr::{null_mut,null};

fn main(){
let mut db = null_mut();
let database_name = CString::new("test.db").unwrap().into_raw();
let sql = CString::new("
CREATE TABLE contacts (name TEXT, tel TEXT);
INSERT INTO contacts VALUES ('Adrian','555-555-555');").unwrap().into_raw();
let mut error_msg = null_mut();
unsafe{
sqlite3_open(database_name,&mut db);
let rc = sqlite3_exec(db,sql,None,null_mut(),&mut error_msg);
if rc != SQLITE_OK as i32 {
let error = CString::from_raw(error_msg);
println!("ERROR: {}",error.into_string().unwrap());
}
sqlite3_close(db);
}
}

Como se puede apreciar, las llamadas al módulo de pegamento de hacen desde un bloque unsafe ya que se van a usar punteros al estilo C, de forma insegura. Hace tiempo escribí sobre ello así que voy a saltarme esa parte.

Compilamos enlazando de forma manual libsqlite3 de la siguiente forma:
rustc main.rs -lsqlite3

Si todo va bien, compilará aunque con numerosos warnings. En principio no son importantes.

Ahora si ejecutamos el programa resultante debería crear una base de datos nueva con una tabla contacts y los datos insertados.

¡Hemos conseguido llamar a una librería de C desde Rust y no hemos escrito ningún binding!

Build.rs


El sistema anterior funciona, pero no es lo más práctico, además no usa Cargo que es el sistema estándar de construcción de programas y crates un Rust. Lo habitual es dejar este proceso automatizado en el fichero build.rs que se ejecuta con Cargo.

Lo primero es añadir la siguiente línea al fichero Cargo.toml:
[build-requires]
bindgen = "0.26.3"

El siguiente paso consiste en crear un archivo cabecera de C que a su vez haga referencia a todos los archivos de cabecera que necesitamos. En el caso de SQLite es bastante simple.
#include <sqlite3.h>

Y lo llamamos wrapper.h

Ahora viene lo interesante. Dentro de build.rs creamos un programa que gracias a la API de bindgen haga lo mismo que la línea de comandos.
extern crate bindgen;

use std::env;
use std::path::PathBuf;

fn main() {
// indicamos al linker que necesitamos sqlite3
println!("cargo:rustc-link-lib=sqlite3");


let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");

// escribir los bindings en $OUT_DIR/bindings.rs
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}

El archivo build.rs debe estar en la misma carpeta que Cargo.toml para que funcione.

Finalmente para hacer accesible nuestros bindings creamos un módulo llamado sqlite.rs con el siguiente contenido.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

Lo único que hace es desactivar unos warnings molestos e incluir el texto de bindings.rs al completo.

Una vez hecho esto podemos usar desde el programa principal la librería de la misma forma que hemos visto antes.

Ahora podríamos usar estos bindings directamente en nuestro programa o rustizarlos (darles una capa segura alrededor e idiomática) y subirlo a Crates.io.

El código del post está en GitHub

Añadir comentario

Todos los comentarios están sujetos a moderación