Archivos Binarios en Rust, parte 1

8 min(s) Fecha: 2022-07-01

🦀 De regreso a los archivos .DAT 😄

A finales del siglo XX, aprendimos Turbo Pascal, Un lenguaje de programación de alto nivel desarrollado por el profesor Niklaus Wirth, que tiempos aquellos...

Estás diciendo que eres viejo, pero sin decir, que eres viejo... :)
-- un visitante del blog

cof, hmm, cof, cof Bueno, en aquella época se utilizaba este lenguaje, para aprender algoritmos, estructuras de datos, archivos (el tema que nos compete en esta entrada) y más.

Para el tema de manejo de archivos, la información se organizaba en registros, y se almacenaba en archivos de tipo binario usualmente con la extensión .DAT

Un ejemplo práctico en Pascal de un tipo de dato Registro:

type persona = record
   nombre: string[50];
   edad: integer;
end;

Volviendo al presente, en esta entrada usaremos Structs, Un struct, ó estructura, es un tipo de datos personalizado que permite empaquetar y nombrar múltiples valores relacionados, que conforman un grupo significativo.

Si estás familiarizado con un lenguaje de programación Orientado a Objetos, una estructura es como los atributos de datos de un objeto.

Texto tomado de The Rust Programming Language

En el caso de Rust, definimos un Struct como sigue:

struct Person {
    nombre: String,
    edad: u8,
}

Iniciando

Damos por entendido que temas relacionados como:

ya son de tu conocimiento, ó al menos, sabes de que tratan. En lo posible explicaremos brevemente para el ejemplo en curso, pero el uso de a profundidad de estos temas, dependerá de tí investigar.

Para este ejemplo, haremos lo siguiente:

En nuestro archivo Cargo.toml, agregamos las siguientes dependencias:

# Cargo.toml
...
[package]
name = "binary-files"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bincode = "1.3.3"
serde = { version = "1.0", features = ["derive"] }

bincode Es una librería que nos permite serializar/deserializar structs en bytes y viceversa, El tamaño del objeto codificado será igual o menor, que el tamaño que el objeto ocuparía en memoria en una aplicación Rust en ejecución, además expone una API de Lectura/Escritura que hace que funcione perfectamente con otras APIs basadas en flujos como archivos de Rust, flujos de red, etc... perfecto para nuestras necesidades en esta entrada.

En cristiano significa que...
-- un visitante del blog

... Que nos permite generar archivos binarios a partir de un Struct dado (ó arreglo de ellos), y después, al recuperar la información almacenada en el archivo, convertirla nuevamente a un struct.


Solicitando información al usuario

Definimos nuestro struct, con información básica(muy muy básica) de la persona. Usamos serde, para manejar nuestro struct de manera más eficiente.

use serde::{Deserialize, Serialize};

...

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
}

En la función main, pedimos la información al usuario:

fn main() {
    println!("nuevo usuario");
    println!();
    let name: String = get_input_default("nombre: ");
    let age: u8 = get_input_default("edad: ");
    println!();

    let people = Person { name, age };

    println!("nuestro usuario es: {:?}", people)
}

Nada nuevo bajo el sol, creamos una instancia de nuestro struct y la imprimimos por la consola.

La salida luce algo como:

❯ cargo run
nuevo usuario

nombre: diniremix
edad: 250

nuestro usuario es: Person { name: "diniremix", age: 250 }

y la función "get_input_default" ?
-- un visitante del blog

En esta entrada, se explica con más detalle, una función bastante útil y hemos querido reutilizarla para agilizar el proceso de pedir datos por la consola en nuestra aplicación.


Guardando el registro struct en un archivo binario

Crearemos una función llamada guardar_archivo, y hace lo siguiente:

fn guardar_archivo(people: &Person) {
    let mut f = File::create("usuario.dat").unwrap();

    let bytes = bincode::serialize(&people).unwrap();

    let result = f.write(bytes.as_slice()).expect("Ocurrio un error mientras se escribían los datos en el archivo");

    if f.sync_all().is_ok() {
        println!("archivo guardado");
    } else {
        println!("ocurrio un error al guardar el archivo");
    }
}

Modificamos la función main para invocar a la función guardar_archivo y enviarle la instancia de nuestro struct:

fn main() {
    println!("nuevo usuario");
    println!();
    let name: String = get_input_default("nombre: ");
    let age: u8 = get_input_default("edad: ");
    println!();

    let people = Person { name, age };
    //println!("nuestro usuario es: {:?}", people)
    
    guardar_archivo(&people);
}

La salida luce algo como:

❯ cargo run
nuevo usuario

nombre: diniremix
edad: 250

archivo guardado

y nuestro archivo usuario.dat, utilizando un editor hexadecimal para ver su contenido, usando la consola, luce así:

❯ xxd usuario.dat
00000000: 0900 0000 0000 0000 6469 6e69 7265 6d69  ........diniremi
00000010: 78fa                                     x.

Más info de xdd, por aquí


ohh!, y como recupero mi información?
-- un visitante del blog

Calma, veamos...


Recuperando datos del archivo binario

Crearemos una función llamada leer_archivo, y hace lo siguiente:

fn leer_archivo() {
    println!("leyendo archivo usuario.dat...",);
    let f = File::open("usuario.dat").expect("Unable to read the file");

    let mut buffer = BufReader::new(f);
    let mut data = Vec::new();

    buffer.read_to_end(&mut data).expect("Unable to read content");

    let people: Person = bincode::deserialize(&data).unwrap();

    println!("datos leidos: {:?}", &people);
}

si agreamos la llamada a la función leer_archivo en nuestra función main

// main.rs
fn main() {
    ...

    leer_archivo();
}

La salida luce algo como:

❯ cargo run
leyendo archivo usuario.dat...
datos leidos: Person { name: "diniremix", age: 250 }

Pequeños ajustes

nuestro archivo main.rs, tendrá el siguiente aspecto, despues de estos cambios.

// main.rs
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{self, BufReader, Read, Write};

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Person {
    name: String,
    age: u8,
}

fn leer_archivo(filename: &str) {
    println!("leyendo {}...", filename);
    let f = File::open(format!("{}", filename)).unwrap();

    let mut buffer = BufReader::new(f);
    let mut data = Vec::new();
    
    buffer.read_to_end(&mut data).expect("Unable to read content");

    let people: Person = bincode::deserialize(&data).unwrap();
    
    println!("datos leidos: {:?}", &people);
}

fn guardar_archivo(filename: &str, people: &Person) {
    let mut f = File::create(format!("{}", filename)).unwrap();

    let bytes = bincode::serialize(&people).unwrap();
    
    let result = f.write(bytes.as_slice()).expect("Error while writing to file");

    if f.sync_all().is_ok() {
        println!("archivo guardado");
    } else {
        println!("ocurrio un error al guardar el archivo");
    }
}

fn main() {
    println!("datos personales");
    println!("1. nuevo registro");
    println!("2. abrir registro");
    println!("3. salir");

    let opcion: u32 = get_input_default("opcion: ");

    match opcion {
        1 => {
            println!();
            println!("nuevo registro");
            let name: String = get_input_default("nombre: ");
            let age: u8 = get_input_default("edad: ");
            let filename: String = get_input_default("nombre para el archivo: ");
            println!();

            let people = Person { name, age };
            guardar_archivo(&filename, &people);
        }
        2 => {
            println!();
            println!("abrir registro");
            let filename: String = get_input_default("nombre del archivo: ");
            println!();
            leer_archivo(&filename);
        }

        3 => {
            println!("saliendo...");
        }
        _ => {
            println!();
            println!("opcion no valida, saliendo...");
        }
    }
}

Recuerda que la función get_input_default se explica con más detalle, por aquí.

al ejecutar nuestro programa (creando el nuevo registro struct)

❯ cargo run
datos personales
1. nuevo registro
2. abrir registro
3. salir
opcion: 1

nuevo registro
nombre: diniremix
edad: 250
nombre para el archivo: demo1.dat

archivo guardado

al ejecutar nuestro programa (leyendo el archivo)

❯ cargo run
datos personales
1. nuevo registro
2. abrir registro
3. salir
opcion: 2

abrir registro
nombre del archivo: demo1.dat

leyendo demo1.dat...
datos leidos: Person { name: "diniremix", age: 250 }

En la próxima entrada, agregaremos soporte para leer y escribir, arrays de registros structs y más!


Referencias