Translate

viernes, 28 de julio de 2023

Copia y Clonación en Rust


Si bien la semántica de movimiento es la predeterminada, ciertos tipos se copian de forma predeterminada:


fn main() {

    let x = 42;

    let y = x;

    println!("x: {x}");

    println!("y: {y}");

}


Estos tipos implementan el rasgo Copy.

Puede optar por que tus propios tipos implementen la semántica de Copy:


#[derive(Copy, Clone, Debug)]

struct Point(i32, i32);


fn main() {

    let p1 = Point(3, 4);

    let p2 = p1;

    println!("p1: {p1:?}");

    println!("p2: {p2:?}");

}


  • Después de la asignación, tanto p1 como p2 poseen sus propios datos.
  • También podemos usar p1.clone() para copiar explícitamente los datos.


Copiar y clonar no es lo mismo:

  • Copiar se refiere a copias bit a bit de regiones de memoria y no funciona en objetos arbitrarios.
  • La copia no permite una lógica personalizada (a diferencia de los constructores de copias en C++).
  • La clonación es una operación más general y también permite un comportamiento personalizado al implementar el rasgo Clonar.
  • La copia no funciona en los tipos que implementan el rasgo Clone.

En el ejemplo anterior, intente lo siguiente:

  • Agregue un campo de cadena a la estructura Point. No se compilará porque String no es un tipo que implemente copia.
  • Quite Copiar del atributo de derivación. ¡El error del compilador ahora está en el println! para p1.

Derivar es una forma de generar código en Rust en tiempo de compilación. En este caso, se generan las implementaciones predeterminadas de los rasgos Copiar y Clonar.

lunes, 24 de julio de 2023

Movimientos en llamadas a funciones


Cuando pasa un valor a una función, el valor se asigna al parámetro de función. Esto transfiere la propiedad:


fn say_hello(name: String) {

    println!("Hello {name}")

}


fn main() {

    let name = String::from("Alice");

    say_hello(name);

    say_hello(name);

}

Si ejecutamos este programa va a lanzar un error : 

$ cargo run

   Compiling hello_cargo v0.1.0 

error[E0382]: use of moved value: `name`

  --> src/main.rs:15:15

   |

11 |     let name = String::from("Alice");

   |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait

12 |

13 |     say_hello(name);

   |               ---- value moved here

14 |

15 |     say_hello(name);

   |               ^^^^ value used here after move


For more information about this error, try `rustc --explain E0382`.

error: could not compile `hello_cargo` due to previous error

Pero que paso? Con la primera llamada a say_hello, main renuncia a la propiedad del nombre. Posteriormente, el nombre ya no se puede usar dentro de main.

La memoria de almacenamiento dinámico asignada para el nombre se liberará al final de la función say_hello.

main puede conservar la propiedad si pasa nombre como referencia (&nombre) y si say_hello acepta una referencia como parámetro:


fn say_hello(name: &String) {

    println!("Hello {name}")

}


fn main() {

    let name = String::from("Alice");

    say_hello(&name);

    say_hello(&name);

}


Alternativamente, main puede pasar un clon de name en la primera llamada (name.clone()). Así:


fn say_hello(name: String) {

    println!("Hello {name}")

}


fn main() {

    let name = String::from("Alice");

    say_hello(name.clone());

    say_hello(name.clone());

}

Rust hace que sea más difícil que C ++ crear copias sin darse cuenta al hacer que la semántica de movimiento sea la predeterminada y al obligar a los programadores a hacer clones explícitos.

martes, 18 de julio de 2023

Una lista clasificada de increíbles bibliotecas y herramientas de open source de Scala.


Encontre una lista de 380 proyectos open source de scala con un total de 370 000 estrellas agrupadas en 23 categorías. Todos los proyectos se clasifican según un puntaje de calidad del proyecto, que se calcula en función de varias métricas recopiladas automáticamente de GitHub y diferentes administradores de paquetes. 

Dejo link: https://github.com/stkeky/best-of-scala

El concepto de Ownership en Rust


En el post anterior dijimos "Rust logra esto modelando la propiedad o ownership explícitamente." Peeero no explique que es esto de la propiedad o ownership. 

Todos los enlaces de variables tienen un alcance donde son válidos y es un error usar una variable fuera de su alcance, por ejemplo :

struct Point(i32, i32);

fn main() {

    {

        let p = Point(3, 4);

        println!("x: {}", p.0);

    }

    println!("y: {}", p.1);

}

Si corremos este programa: 

$ cargo run

   Compiling hello_cargo v0.1.0 

error[E0425]: cannot find value `p` in this scope

 --> src/main.rs:8:23

  |

8 |     println!("y: {}", p.1);

  |                       ^ not found in this scope


For more information about this error, try `rustc --explain E0425`.

error: could not compile `hello_cargo` due to previous error


  • Al final del alcance, la variable se elimina y los datos se liberan.
  • Un destructor puede correr aquí para liberar recursos.
  • Decimos que la variable posee el valor.
Una asignación transferirá la propiedad entre las variables:

fn main() {
    let s1: String = String::from("Hello!");
    let s2: String = s1;
    println!("s2: {s2}");
    // println!("s1: {s1}");
}

Si corremos este programa funcionará : 

$ cargo run
   Compiling hello_cargo v0.1.0 
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/hello_cargo`
s2: Hello!

Pero si descomentamos esta linea // println!("s1: {s1}"); . Nos lanzará un error: 

$ cargo run
   Compiling hello_cargo v0.1.0 
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
2 |     let s1: String = String::from("Hello!");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2: String = s1;
  |                      -- value moved here
4 |     println!("s2: {s2}");
5 |     println!("s1: {s1}");
  |                    ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello_cargo` due to previous error

Que paso?
  • La asignación de s1 a s2 transfiere la propiedad.
  • Los datos se movieron de s1 y ya no se puede acceder a s1.
  • Cuando s1 sale del alcance, no pasa nada: no tiene propiedad.
  • Cuando s2 sale del alcance, los datos de la cadena se liberan.
  • Siempre hay exactamente un enlace de variable que posee un valor.
Esto es lo opuesto a los valores predeterminados en C++, que se copia por valor a menos que use std::move (¡y el constructor de movimiento o copia está definido!).

En Rust, la clonación de valores son explícitos (usando clon).


lunes, 17 de julio de 2023

Mover Strings en Rust

 


fn main() {

    let s1: String = String::from("Rust");

    let s2: String = s1;

}

Los datos del heap de s1 se reutilizan para s2.

Cuando s1 sale del alcance, no sucede nada (se ha movido).

Antes de mover a s2:



Después de mover a s2:


viernes, 14 de julio de 2023

Comparación de Gestión de memoria con Rust


Siguiendo con el post anterior. Una comparación aproximada de las técnicas de administración de memoria.

Ventajas de las diferentes técnicas de gestión de la memoria

  • Manual como C:
    • Sin sobrecarga de tiempo de ejecución.
  • Automático como Java:
    • Completamente automático.
    • Seguro y correcto.
  • Basado en el alcance como C++:
    • Parcialmente automático.
    • Sin sobrecarga de tiempo de ejecución.
  • Basado en el alcance aplicado por el compilador como Rust:
    • Aplicado por el compilador.
    • Sin sobrecarga de tiempo de ejecución.
    • Seguro y correcto.

Contras de las diferentes técnicas de gestión de memoria

  • Manual como C:
    • Use-After-Free
    • Liberaciones dobles.
    • Pérdidas de memoria en caso de error.
  • Automático como Java:
    • Las pausas del recolector de basura.
    • Retrasos en la liberación de memoria.
  • Basado en el alcance como C++:
    • Complejo, opt-in por el programador.
    • Potencial Use-After-Free
  • Reforzado por el compilador y basado en el alcance como Rust:
    • Cierta complejidad inicial.
    • Puede rechazar programas válidos.
Use-After-Free (UAF) es una vulnerabilidad relacionada con el uso incorrecto de la memoria dinámica durante la operación del programa. Si después de liberar una ubicación de memoria, un programa no borra el puntero a esa memoria, un atacante puede usar el error para piratear el programa.

jueves, 13 de julio de 2023

Manejo de memoria en Rust

 


Tradicionalmente, los Lenguajes se han dividido en dos grandes categorías:

  • Control total a través de la gestión manual de la memoria: C, C++, Pascal, …
  • Seguridad total a través de la gestión automática de la memoria en tiempo de ejecución: Java, Python, Go, Haskell, …

Rust ofrece una nueva mezcla:

Control total y seguridad a través de la aplicación en tiempo de compilación de la gestión correcta de la memoria.

Lo hace con un concepto de propiedad explícito.

Primero, actualicemos cómo funciona la administración de memoria.

Stack: 

  • Área continua de memoria para variables locales.
  • Los valores tienen tamaños fijos conocidos en tiempo de compilación.
  • Extremadamente rápido: simplemente mueva un puntero de pila.
  • Fácil de administrar: sigue las llamadas de función.
  • Gran recuerdo localidad.

Heap o monticulo : 

  • almacenamiento de valores fuera de las llamadas a funciones.
  • Los valores tienen tamaños dinámicos determinados en tiempo de ejecución.
  • Ligeramente más lento que la pila: se necesita algo de contabilidad.
  • No hay garantía de localidad de memoria.

La creación de una cadena coloca datos de tamaño fijo en la pila o stack y datos de tamaño dinámico en el monticulo o Heap :

fn main() {
    let s1 = String::from("Hello");
}



Un String está respaldado por un Vec, por lo que tiene una capacidad y longitud y puede crecer si es mutable a través de la reasignación en el Heap.

Podemos inspeccionar el diseño de la memoria con código inseguro. Sin embargo, debe señalar que esto es legítimamente inseguro.

fn main() {
    let mut s1 = String::from("Hello");
    s1.push(' ');
    s1.push_str("world");
    
    unsafe {
        let (capacity, ptr, len): (usize, usize, usize) = std::mem::transmute(s1);
        println!("ptr = {ptr:#x}, len = {len}, capacity = {capacity}");
    }
}

Veamos como son las formas que tienen de gestionar la memoria los lenguajes: 

Gestión de memoria manual

Usted mismo asigna y desasigna la memoria del heap. Se llama Gestión de memoria manual

Si no se hace con cuidado, esto puede provocar bloqueos, errores, vulnerabilidades de seguridad y pérdidas de memoria.

Ejemplo C, debe llamar a  free cada vez que asigno un puntero con malloc:

void foo(size_t n) {

    int* int_array = malloc(n * sizeof(int));

    //

    // ... lots of code

    //

    free(int_array);

}

Si la función termina entre malloc y free: el puntero se pierde y no podemos desasignar la memoria. Esto es un Memory leaked. 

Administración de memoria basada en alcance o ámbito

Los constructores y destructores te permiten engancharte a la vida útil de un objeto.

Al envolver un puntero en un objeto, puede liberar memoria cuando se destruye el objeto. El compilador garantiza que esto suceda, incluso si se genera una excepción.

Esto a menudo se denomina adquisición de recursos es inicialización (RAII) y le brinda indicadores inteligentes.

Un ejemplo de C++

void say_hello(std::unique_ptr<Person> person) {

  std::cout << "Hello " << person->name << std::endl;

}

  • El objeto std::unique_ptr se asigna en la pila y apunta a la memoria asignada en el heap.
  • Al final de say_hello, se ejecutará el destructor std::unique_ptr.
  • El destructor libera el objeto Person al que apunta.
Los constructores de movimientos especiales se usan cuando se pasa la propiedad a una función:

std::unique_ptr<Person> person = find_person("Carla");
say_hello(std::move(person));

Gestión automática de memoria

Una alternativa a la gestión de memoria manual y basada en el ámbito es la gestión de memoria automática:

  • El programador nunca asigna o desasigna memoria explícitamente.
  • Un recolector de basura encuentra memoria no utilizada y la desasigna para el programador.

Ejemplo Java, el objeto persona no se desasigna después de que sayHello devuelve:

void sayHello(Person person) {
  System.out.println("Hello " + person.getName());
}

Gestión de memoria en Rust

La gestión de la memoria en Rust es una combinación:

  • Seguro y correcto como Java, pero sin un recolector de basura.
  • Según la abstracción (o combinación de abstracciones) que elija, puede ser un único puntero único, referencia contada o atómicamente referencia contada.
  • Basado en el alcance como C++, pero el compilador impone una adherencia total.
  • Un usuario de Rust puede elegir la abstracción correcta para la situación, algunos incluso no tienen costo en tiempo de ejecución como C.
Rust logra esto modelando la propiedad o ownership explícitamente.

martes, 11 de julio de 2023

Ámbitos y sombreado


En la programación informática, el sombreado de variables o  variable shadowing se produce cuando una variable declarada dentro de un determinado ámbito (bloque de decisión, método o clase interna) tiene el mismo nombre que una variable declarada en un ámbito externo.

veamos esto en rust :


fn main() {

    let a = 10;

    println!("before: {a}");


    {

        let a = "hello";

        println!("inner scope: {a}");


        let a = true;

        println!("shadowed in inner scope: {a}");

    }


    println!("after: {a}");

}


$ cargo run

   Compiling hello_cargo v0.1.0 

    Finished dev [unoptimized + debuginfo] target(s) in 1.32s

     Running `target/debug/hello_cargo`

before: 10

inner scope: hello

shadowed in inner scope: true

after: 10


Definición: El sombreado es diferente de la mutación, porque después del sombreado, las ubicaciones de memoria de ambas variables existen al mismo tiempo. Ambos están disponibles con el mismo nombre, dependiendo de dónde lo use en el código.

Una variable de sombreado puede tener un tipo diferente.

El sombreado parece oscuro al principio, pero es conveniente para mantener los valores después de .unwrap().

El siguiente código demuestra por qué el compilador no puede simplemente reutilizar ubicaciones de memoria cuando oculta una variable inmutable en un ámbito, incluso si el tipo no cambia.


fn main() {

    let a = 1;

    let b = &a;

    let a = a + 1;

    println!("{a} {b}");

}

lunes, 10 de julio de 2023

Variables estáticas y constantes en Rust


Se puede declarar constantes en tiempo de compilación:


const DIGEST_SIZE: usize = 3;

const ZERO: Option<u8> = Some(42);


fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {

    let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE];

    for (idx, &b) in text.as_bytes().iter().enumerate() {

        digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);

    }

    digest

}


fn main() {

    let digest = compute_digest("Hello");

    println!("Digest: {digest:?}");

}


De acuerdo con Rust RFC Book, estos se insertan en el momento del uso.

También puede declarar variables estáticas:

static BANNER: &str = "Welcome to RustOS 3.14";


fn main() {

    println!("{BANNER}");

}



viernes, 7 de julio de 2023

Rust RFCs - RFC Book - Active RFC List


El proceso "RFC" (solicitud de comentarios) tiene como objetivo proporcionar una ruta coherente y controlada para los cambios en Rust (como nuevas funciones) para que todas las partes interesadas puedan confiar en la dirección del proyecto.

Muchos cambios, incluidas las correcciones de errores y las mejoras de la documentación, se pueden implementar y revisar a través del flujo de trabajo de solicitud de extracción normal de GitHub.

Sin embargo, algunos cambios son "sustanciales", y pedimos que se sometan a un pequeño proceso de diseño y produzcan un consenso entre la comunidad de Rust y los subequipos.

Si queremos solicitar un cambio o revisar los cambios existentes podemos hacerlo revisando esta documentación: https://rust-lang.github.io/rfcs/introduction.html

jueves, 6 de julio de 2023

Rust: Variables


Rust proporciona seguridad de tipos a través de tipos estáticos. Los enlaces de variables son inmutables de forma predeterminada:

fn main() {

    let x: i32 = 10;

    println!("x: {x}");

    // x = 20;

    // println!("x: {x}");

}


Rust verá cómo se usa la variable para determinar el tipo:


fn takes_u32(x: u32) {

    println!("u32: {x}");

}


fn takes_i8(y: i8) {

    println!("i8: {y}");

}


fn main() {

    let x = 10;

    let y = 20;


    takes_u32(x);

    takes_i8(y);

    // takes_u32(y);

}


cargo run

   Compiling hello_cargo v0.1.0 

    Finished dev [unoptimized + debuginfo] target(s) in 0.30s

     Running `target/debug/hello_cargo`

u32: 10

i8: 20


Pero eso no significa que se descuide el tipado estatico, si corremos el programa anterior descomentando la linea "    // takes_u32(y); " obtendremos: 

$ cargo run

   Compiling hello_cargo v0.1.0 

error[E0308]: mismatched types

  --> src/main.rs:15:15

   |

15 |     takes_u32(y);

   |     --------- ^ expected `u32`, found `i8`

   |     |

   |     arguments to this function are incorrect

   |

note: function defined here

  --> src/main.rs:1:4

   |

1  | fn takes_u32(x: u32) {

   |    ^^^^^^^^^ ------

help: you can convert an `i8` to a `u32` and panic if the converted value doesn't fit

   |

15 |     takes_u32(y.try_into().unwrap());

   |                ++++++++++++++++++++


For more information about this error, try `rustc --explain E0308`.

error: could not compile `hello_cargo` due to previous error


Es muy importante enfatizar que las variables declaradas así no son de algún tipo de dinámica "cualquier tipo" que pueda contener cualquier dato. El código máquina generado por tal declaración es idéntico a la declaración explícita de un tipo. El compilador hace el trabajo por nosotros y nos ayuda a escribir código más conciso.


El siguiente código le dice al compilador que copie en un determinado contenedor genérico sin que el código especifique explícitamente el tipo contenido, usando _ como marcador de posición:

fn main() {

    let mut v = Vec::new();

    v.push((10, false));

    v.push((20, true));

    println!("v: {v:?}");


    let vv = v.iter().collect::<std::collections::HashSet<_>>();

    println!("vv: {vv:?}");

}


collect se basa en FromIterator, que implementa HashSet.

lunes, 3 de julio de 2023

Vectores y bucles for en Rust


Vimos que una vector se puede declarar así:

let array = [10, 20, 30];

Puede imprimir una matriz con {:?}:

fn main() {
    let array = [10, 20, 30];
    println!("array: {array:?}");
}

Rust te permite iterar cosas como arreglos y rangos usando la palabra clave for:

fn main() {
    let array = [10, 20, 30];
    print!("Iterating over array:");
    for n in array {
        print!(" {n}");
    }
    println!();

    print!("Iterating over range:");
    for i in 0..3 {
        print!(" {}", array[i]);
    }
    println!();
}

Métodos en Rust


Los métodos son funciones asociadas a un tipo. El argumento self de un método es una instancia del tipo al que está asociado:

struct Rectangle {

    width: u32,

    height: u32,

}


impl Rectangle {

    fn area(&self) -> u32 {

        self.width * self.height

    }


    fn inc_width(&mut self, delta: u32) {

        self.width += delta;

    }

}


fn main() {

    let mut rect = Rectangle { width: 10, height: 5 };

    println!("old area: {}", rect.area());

    rect.inc_width(5);

    println!("new area: {}", rect.area());

}

$ cargo run

   Compiling hello_cargo v0.1.0

   Finished dev [unoptimized + debuginfo] target(s) in 0.28s

     Running `target/debug/hello_cargo`

old area: 50

new area: 75


Podemos definir un constructor : 


struct Rectangle {

    width: u32,

    height: u32,

}


impl Rectangle {


    fn new(width: u32, height: u32) -> Rectangle {

        Rectangle { width, height }

    }


    fn area(&self) -> u32 {

        self.width * self.height

    }


    fn inc_width(&mut self, delta: u32) {

        self.width += delta;

    }

}


fn main() {

    let mut rect = Rectangle::new(10, 5);

    println!("old area: {}", rect.area());

    rect.inc_width(5);

    println!("new area: {}", rect.area());

}



Rust: Conversiones implícitas


Rust no aplicará automáticamente conversiones implícitas entre tipos (a diferencia de C++). Puedes ver esto en un programa como este:

fn multiply(x: i16, y: i16) -> i16 {

    x * y

}

fn main() {

    let x: i8 = 15;

    let y: i16 = 1000;

    println!("{x} * {y} = {}", multiply(x, y));

}


Si corremos este código: 

$ cargo run

   Compiling hello_cargo v0.1.0 

error[E0308]: mismatched types

 --> src/main.rs:9:41

  |

9 |     println!("{x} * {y} = {}", multiply(x, y));

  |                                -------- ^ expected `i16`, found `i8`

  |                                |

  |                                arguments to this function are incorrect

  |

note: function defined here

 --> src/main.rs:1:4

  |

1 | fn multiply(x: i16, y: i16) -> i16 {

  |    ^^^^^^^^ ------

help: you can convert an `i8` to an `i16`

  |

9 |     println!("{x} * {y} = {}", multiply(x.into(), y));

  |                                          +++++++


For more information about this error, try `rustc --explain E0308`.

error: could not compile `hello_cargo` due to previous error


Vamos a hacerle caso y vamos a correr : rustc --explain E0308


El tipo esperado no coincide con el tipo recibido.


Ejemplos de código erróneo:

``

fn plus_one(x: i32) -> i32 {

    x + 1

}


plus_one("Not a number");

//       ^^^^^^^^^^^^^^ expected `i32`, found `&str`


if "Not a bool" {

// ^^^^^^^^^^^^ expected `bool`, found `&str`

}


let x: f32 = "Not a float";

//     ---   ^^^^^^^^^^^^^ expected `f32`, found `&str`

//     |

//     expected due to this

```

Este error ocurre cuando se usó una expresión en un lugar donde el compilador esperaba una expresión de un tipo diferente. Puede ocurrir en varios casos, siendo el más común cuando se llama a una función y se pasa un argumento que tiene un tipo diferente al tipo coincidente en la declaración de la función.


Genial! no cambio el tipo de forma implicita. 

Todos los tipos enteros de Rust implementan las características From<T> e Into<T> para permitirnos convertir entre ellos. El rasgo From<T> tiene un solo método from() y, de manera similar, el rasgo Into<T> tiene un solo método into(). La implementación de estos rasgos es la forma en que un tipo expresa que se puede convertir en otro tipo.

La biblioteca estándar tiene una implementación de From<i8> para i16, lo que significa que podemos convertir una variable x de tipo i8 en i16 llamando a i16::from(x). O, más simple, con x.into(), porque From<i8> para la implementación de i16 crea automáticamente una implementación de Into<i16> para i8.

Lo mismo se aplica a sus propias implementaciones From para sus propios tipos, por lo que es suficiente implementar solo From para obtener una implementación Into respectiva automáticamente.

Por lo tanto si hacemos .into() este código va funcionar : 


fn multiply(x: i16, y: i16) -> i16 {

    x * y

}


fn main() {

    let x: i8 = 15;

    let y: i16 = 1000;

    println!("{x} * {y} = {}", multiply(x.into(), y));

}


$ cargo run

   Compiling hello_cargo v0.1.0 

    Finished dev [unoptimized + debuginfo] target(s) in 0.83s

     Running `target/debug/hello_cargo`

15 * 1000 = 15000


Sobrecarga de funciones


Rust no soporta la sobrecarga:

  • Cada función tiene una sola implementación:
    • Siempre toma un número fijo de parámetros.
    • Siempre toma un único conjunto de tipos de parámetros.
  • Los valores predeterminados no son compatibles:
    • Todos los sitios de llamadas tienen el mismo número de argumentos.
    • Las macros a veces se utilizan como alternativa.
Sin embargo, los parámetros de función pueden ser genéricos:


fn pick_one<T>(a: T, b: T) -> T {

    if std::process::id() % 2 == 0 { a } else { b }

}


fn main() {

    println!("coin toss: {}", pick_one("heads", "tails"));

    println!("cash prize: {}", pick_one(500, 1000));

}