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