Comprehensive Rust
Comprehensive Rust
Martin Geisler
Índice
2 Usando Cargo 19
2.1 El ecosistema de Rust . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Código de ejemplo en esta formación . . . . . . . . . . . . . . . . . . . . . . 20
2.3 Ejecutar código de forma local con Cargo . . . . . . . . . . . . . . . . . . . . 21
I Día 1: mañana 23
3 Te damos la bienvenida al Día 1 24
4 Hola, Mundo 26
4.1 ¿Qué es Rust? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.2 Ventajas de Rust . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.3 Playground . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5 Tipos y valores 29
5.1 Hola, Mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.2 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.3 Valores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.4 Aritmética . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.5 Inferencia de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.6 Ejercicio: Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.6.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1
6.4.1 Ámbitos y Shadowing . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.5 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.6 Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.7 Ejercicio: secuencia de Collatz . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.7.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
II Día 1: Tarde 41
7 Te damos la bienvenida 42
8 Tuplas y arrays 43
8.1 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
8.2 Tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
8.3 Iteración de Arreglos (Arrays) . . . . . . . . . . . . . . . . . . . . . . . . . . 44
8.4 Patrones y Desestructuración . . . . . . . . . . . . . . . . . . . . . . . . . . 44
8.5 Ejercicio: arrays anidados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
8.5.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
9 Referencias 48
9.1 Enums compartidas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
9.2 Referencias exclusivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
9.3 Slices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
9.4 Cadenas de texto (Strings) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
9.5 Ejercicio: geometría . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
9.5.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
12 Correspondencia de Patrones 65
12.1 Correspondencia de Valores . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
12.2 Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
12.3 Enumeraciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
12.4 Control de Flujo Let . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
12.5 Ejercicio: evaluación de expresiones . . . . . . . . . . . . . . . . . . . . . . 70
12.5.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
13 Métodos y Traits 75
13.1 Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2
13.2 Traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
13.2.1 Implementación de Traits . . . . . . . . . . . . . . . . . . . . . . . . 77
13.2.2 Supertraits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
13.2.3 Tipos de datos asociados . . . . . . . . . . . . . . . . . . . . . . . . . 79
13.3 Derivación de Traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
13.4 Ejercicio: trait de registro . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
13.4.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
IV Día 2: tarde 82
14 Te damos la bienvenida 83
15 Genéricos 84
15.1 Funciones genéricas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
15.2 Tipos de Datos Genéricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
15.3 Traits Genéricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
15.4 Trait Bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
15.5 impl Trait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
15.6 dyn Trait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
15.7 Ejercicio: min genérico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
15.7.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3
19.2 Métodos de Gestión de Memoria . . . . . . . . . . . . . . . . . . . . . . . . . 112
19.3 Ownership . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
19.4 Semántica de movimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
19.5 Trait Clone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
19.6 Tipos Copy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
19.7 El Trait Drop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
19.8 Ejercicio: Constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
19.8.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
25 Iteradores 158
25.1 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
25.2 IntoIterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
25.3 FromIterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
25.4 Ejercicio: Encadenamiento de métodos del iterador . . . . . . . . . . . . . . 161
25.4.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
26 Módulos 164
26.1 Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
26.2 Jerarquía del sistema de archivos . . . . . . . . . . . . . . . . . . . . . . . . 165
26.3 Visibilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
4
26.4 use, super, self . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
26.5 Ejercicio: Módulos para una biblioteca GUI . . . . . . . . . . . . . . . . . . . 168
26.5.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
27 Probando 175
27.1 Pruebas Unitarias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
27.2 Otros tipos de pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
27.3 Lints de compiladores y Clippy . . . . . . . . . . . . . . . . . . . . . . . . . 177
27.4 Ejercicio: Algoritmo de Luhn . . . . . . . . . . . . . . . . . . . . . . . . . . 177
27.4.1 Solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
IX Android 207
31 Te Damos la Bienvenida a Rust en Android 208
32 Configurar 209
34 AIDL 213
34.1 Tutorial de Servicio de Cumpleaños . . . . . . . . . . . . . . . . . . . . . . . 213
34.1.1 Interfaces de AIDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
34.1.2 Generated Service API . . . . . . . . . . . . . . . . . . . . . . . . . . 214
34.1.3 Implementación del servicio . . . . . . . . . . . . . . . . . . . . . . . 214
5
34.1.4 Servidor de AIDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
34.1.5 Despliegue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
34.1.6 Cliente de AIDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
34.1.7 Cambio de API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
34.1.8 Updating Client and Service . . . . . . . . . . . . . . . . . . . . . . . 218
34.2 Working With AIDL Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
34.2.1 Tipos Primitivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
34.2.2 Tipos Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
34.2.3 Enviando Objectos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
34.2.4 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
34.2.5 Enviando Archívos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
37 Interoperabilidad 229
37.1 Interoperabilidad con C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
37.1.1 Uso de Bindgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
37.1.2 Llamar a Rust . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
37.2 Con C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
37.2.1 El Modulo Puente (Bridge) . . . . . . . . . . . . . . . . . . . . . . . . 233
37.2.2 Declaraciones Bridge en Rust . . . . . . . . . . . . . . . . . . . . . . . 234
37.2.3 C++ generado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
37.2.4 Declaraciones Bridge en C++ . . . . . . . . . . . . . . . . . . . . . . . 235
37.2.5 Tipos de datos compartidos . . . . . . . . . . . . . . . . . . . . . . . . 236
37.2.6 Enums compartidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
37.2.7 Manejo de Errores en Rust . . . . . . . . . . . . . . . . . . . . . . . . 237
37.2.8 Manejo de Errores en C++ . . . . . . . . . . . . . . . . . . . . . . . . 238
37.2.9 Tipos adicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
37.2.10Building in Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
37.2.11Building in Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
37.2.12Building in Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
37.3 Interoperabilidad con Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
38 Ejercicios 242
X Chromium 243
39 Te Damos la Bienvenida a Rust en Chromium 244
40 Configurar 245
6
43.2 Depender de código de Rust desde Chromium C++ . . . . . . . . . . . . . . . 252
43.3 Visual Studio Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
43.4 Ejercicio de reglas de compilación . . . . . . . . . . . . . . . . . . . . . . . . 253
44 Probando 255
44.1 Biblioteca rust_gtest_interop . . . . . . . . . . . . . . . . . . . . . . . . 256
44.2 Reglas GN para pruebas de Rust . . . . . . . . . . . . . . . . . . . . . . . . . 256
44.3 Macro chromium::import! . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
44.4 Ejercicio de pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
50 no_std 277
50.1 Un programa no_std mínimo . . . . . . . . . . . . . . . . . . . . . . . . . . 278
50.2 alloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
51 Microcontroladores 280
51.1 MMIO sin procesar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
51.2 Crates de Acceso Periférico . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
51.3 Crates HAL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
51.4 Crates de compatibilidad de placa . . . . . . . . . . . . . . . . . . . . . . . . 284
51.5 El patrón de tipo de estado . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
51.6 embedded-hal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
7
51.7 probe-rs y cargo-embed . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
51.7.1 Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
51.8 Otros proyectos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
52 Ejercicios 288
52.1 Brújula . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
52.2 Rust Bare Metal: Ejercicio de la Mañana . . . . . . . . . . . . . . . . . . . . 290
55 Android 313
55.1 vmbase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
56 Ejercicios 315
56.1 Controlador RTC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
56.2 Rust Bare Metal: Tarde . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
58 Hilos 341
58.1 Hilos Simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
58.2 Hilos con ámbito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
59 Canales 344
59.1 Transmisores y Receptores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
8
59.2 Canales sin límites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
59.3 Canales delimitados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
62 Ejercicios 353
62.1 La cena de los filósofos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
62.2 Comprobador de enlaces multihilo . . . . . . . . . . . . . . . . . . . . . . . 354
62.3 Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
66 Inconvenientes 373
66.1 Bloqueo del ejecutor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
66.2 Pin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
66.3 Traits asíncronos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
66.4 Cancelación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
67 Ejercicios 381
67.1 La Cena de Filósofos --- Async . . . . . . . . . . . . . . . . . . . . . . . . . . 381
67.2 Aplicación de chat de difusión . . . . . . . . . . . . . . . . . . . . . . . . . . 382
67.3 Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
XV Conclusiones 390
68 ¡Gracias! 391
69 Glosario 392
9
70 Otros recursos de Rust 396
71 Créditos 398
10
Te damos la bienvenida a
Comprehensive Rust
Este es un curso gratuito de Rust desarrollado por el equipo de Android en Google. El curso
abarca el espectro completo de Rust, desde la sintaxis básica hasta temas avanzados como
genéricos y el manejo de errores.
La última versión del curso se puede encontrar en [Link]
comprehensive-rust/. Si lo estás leyendo en otro lugar, consulta allí para obtener
actualizaciones.
Este curso está disponible en otros idiomas. Seleccione su idioma preferido en la
esquina superior a la derecha, o navega a la página de [Tradduciones](Translations](running-
the-course/[Link]) para una lista de todas las traducciones disponibles.
Este curso tambien está disponible como un PDF.
El objetivo del curso es enseñarte Rust. Suponemos que no sabes nada sobre Rust y esperamos
lograr lo siguiente:
• Darte un entendimiento comprensivo de la sintaxis y lenguaje Rust.
• Permitirte modificar programas de Rust y escribir otros nuevos.
• Enseñarte idiomática propia de Rust.
Llamamos a los cuatro primeros días del curso Fundamentos de Rust.
Basándonos en esto, te invitamos a profundizar en uno o más temas especializados:
• Android: un curso de medio día sobre el uso de Rust en el desarrollo de la plataforma
Android (AOSP). En él se incluye la interoperabilidad con C, C++ y Java.
• Chromium: una clase de medio día sobre el uso de Rust dentro del navegador Chromium.
Incluye interoperabilidad con C++ y como incorporar bibliotecas de tercer partido
(”crates”) en Chromium.
• Bare Metal: una clase de un día sobre el uso de Rust para el desarrollo bare-metal
(insertado). Se tratarán tanto los microcontroladores como los procesadores de
aplicaciones.
• Concurrencia: una clase de un día sobre concurrencia en Rust. Abordaremos tanto
la concurrencia clásica (programación interrumpible mediante hilos y exclusiones
mutuas), como la concurrencia async / await (multitarea cooperativa mediante traits
future).
11
Objetivos que no trataremos
Rust es un lenguaje muy amplio y no podremos abarcarlo todo en unos pocos días. Algunos
de los objetivos que no se plantean en este curso son los siguientes:
• Aprender a desarrollar macros: consulta el capítulo 19.5 del Libro de Rust y Rust by
Example.
Suposiciones
El curso presupone que ya sabes programar. Rust es un lenguaje estáticamente tipado y,
a veces, haremos comparaciones con C y C++ para explicarlo mejor o contrastar nuestro
enfoque.
Si sabes programar en un lenguaje dinámicamente tipado, como Python o JavaScript, podrás
seguir el ritmo sin problema.
Este es un ejemplo de una nota del orador. Las utilizaremos para añadir información adicional
a las diapositivas. Puede tratarse de puntos clave que el instructor debería tratar, así como de
respuestas a preguntas frecuentes que surgen en clase.
12
Capítulo 1
13
que varias personas tienen el mismo problema, coméntalo delante de la clase y ofrece
una solución. Por ejemplo, enséñales dónde encontrar la información importante en la
biblioteca estándar.
Eso es todo. ¡Buena suerte con el curso, y esperamos que te diviertas tanto como nosotros!
Después, envíanos un comentario para que podamos seguir mejorando el curso. Estaremos
encantados de que nos cuentes qué aspectos destacarías y qué se puede mejorar. Tus alumnos
también pueden enviarnos sus sugerencias!
Fundamentos de Rust
Los primeros cuatro días forman los Fundamentos de Rust. ¡Los días son muy intensos y
cubrimos mucho terreno!
Horario del curso:
• Día 1 por la mañana (2 horas y 5 minutos, incluidos los descansos)
Sección Duración
Te damos la bienvenida 5 minutos
Hola, Mundo 15 minutos
Tipos y valores 40 minutos
Básicos de Control de Flujo 40 minutos
Sección Duración
Tuplas y arrays 35 minutos
Referencias 55 minutos
Tipos definidos por el usuario 50 minutos
Sección Duración
Te damos la bienvenida 3 minutos
Correspondencia de Patrones 1 hora
Métodos y Traits 50 minutos
14
Sección Duración
Genéricos 45 minutos
Tipos de la Biblioteca Estándar 1 hora
Traits de la biblioteca estándar 1 hora y 10 minutos
Sección Duración
Te damos la bienvenida 3 minutos
Manejo de Memoria 1 hora
Punteros inteligentes 55 minutos
Sección Duración
Préstamos (Borrowing) 55 minutos
Duraciones de vida 50 minutos
Sección Duración
Te damos la bienvenida 3 minutos
Iteradores 45 minutos
Módulos 40 minutos
Probando 45 minutos
Sección Duración
Manejo de Errores 1 hora
Unsafe Rust 1 hora y 5 minutos
Rust en Android
Rust en Android es un curso de medio día sobre el uso de Rust para el desarrollo de la
plataforma Android. En él se incluye la interoperabilidad con C, C++ y Java.
Necesitarás conseguir el AOSP. Descarga el repositorio del curso en el mismo ordenador y
mueve el directorio src/android/ a la raíz del AOSP. De esta forma, el sistema de compilación
de Android verá los archivos [Link] en src/android/.
15
Asegúrate que adb sync funciona con tu emulador o en un dispositivo físico y haz pre-build
en todos los ejemplos de Android usando src/android/build_all.sh. Lee el script para
ver los comandos que corren y asegúrate que funcionan cuando lo corres a mano.
Rust en Chromium
Rust en Chromium es una clase en profundidad de medio día sobre el uso de Rust como
parte del navegador Chromium. Incluye el uso de Rust en el sistema de compilación gn de
Chromium e incorpora bibliotecas de terceros (”crates”) e interoperabilidad en C++.
Deberás poder compilar Chromium: [recomendamos] una compilación de depuración
de componentes (../chromium/[Link]) por cuestiones de velocidad, pero cualquier
compilación funcionará de forma correcta. Asegúrate de que puedes ejecutar el navegador
Chromium que has compilado.
Bare Metal Rust es una clase de un día sobre cómo usar Rust para el desarrollo bare-metal
(insertado). Se tratarán tanto microcontroladores como procesadores de aplicaciones.
Para la parte de los microcontroladores, necesitarás comprar con antelación la segunda
versión de la placa programable BBC micro:bit. Todo el mundo deberá instalar una serie de
paquetes, tal como se describe en la página de bienvenida.
Concurrencia en Rust
Sección Duración
Hilos 30 minutos
Canales 20 minutos
Send y Sync 15 minutos
Estado compartido 30 minutos
Ejercicios 1 hora y 10 minutos
16
Sección Duración
Conceptos básicos de Async 30 minutos
Canales y Control de Flujo 20 minutos
Inconvenientes 55 minutos
Ejercicios 1 hora y 10 minutos
Formato
El curso está diseñado para ser muy interactivo, por lo que te recomendamos que dejes que
las preguntas guíen el aprendizaje de Rust.
1.3 Traducciones
El curso se ha traducido a otros idiomas gracias a grupo de maravillosos voluntarios:
• Portugués Brasileño por @rastringer, @hugojacob, @joaovicmendes y @henrif75.
• Chino (simplificado) por @suetfei, @wnghl, @anlunx, @kongy, @noahdragon,
@superwhd, @SketchK y @nodmp.
• Chino (tradicional) por @hueich, @victorhsieh, @mingyc, @kuanhungchen y
@johnathan79717.
• Japonés por @CoinEZ-JPN, @momotaro1105, @HidenoriKobayashi y @kantasv.
• Coreano por @keispace, @jiyongp, @jooyunghan, y @namhyung.
• Español por @deavid.
• Ucranio por @git-user-cpp, @yaremam, y @reta.
Cambia el idioma con el seleccionador situado en la esquina superior derecha.
Traducciones Incompletas
Hay muchas traducciones todavía en curso. A continuación, incluimos enlaces a las
traducciones más actualizadas:
• Árabe por @younies
• Bengalí por @raselmandol.
• Francés por @KookaS, @vcaen, y @AdrienBaudemont.
• Alemán por @Throvn y @ronaldfw.
• Italiano por @henrythebuilder y @detro.
17
La lista completa de traducciones con su estado corriente también esta disponible a partir de
su ultima actualización o sincronizado a la versión mas reciente del curso.
Si quieres ayudar en esta iniciativa, consulta nuestras instrucciones para empezar. Las
traducciones se coordinan en la herramienta de seguimiento de incidencias.
18
Capítulo 2
Usando Cargo
Cuando empieces a informarte sobre Rust, conocerás Cargo, la herramienta estándar que
se utiliza en el ecosistema de Rust para crear y ejecutar sus aplicaciones. En este artículo, te
ofrecemos una breve descripción de lo que es Cargo, cómo se integra en el ecosistema más
amplio y cómo encaja en esta formación.
Instalación
Sigue las instrucciones que se indican en [Link]
Esto te dará la herramienta de compilación Cargo (cargo) y el compilador Rust (rustc).
También obtendrás rustup, una utilidad de línea de comandos que puedes utilizar para
instalar diferentes versiones del compilador.
Después de instalar Rust, debes configurar tu editor o IDE para utilizar Rust. La mayoría de
los editores lo hacen con rust-analyzer, que ofrece funciones de autocompletado y salto a la
definición para VS Code, Emacs y Vim/Neovim, entre otros. También hay disponible otro IDE
denominado RustRover.
• En Debian o Ubuntu, también puedes instalar Cargo, el código fuente de Rust y el
formateador de Rust a través de apt. Sin embargo, solo podrás conseguir una versión
de Rust obsoleta que podría dar lugar a comportamientos inesperados. El comando es
el siguiente:
sudo apt install cargo rust-src rustfmt
• En macOS, puedes usar Homebrew para instalar Rust, pero esto podría proveer una
versión anticuada. Por lo tanto, es recomendad instalar Rust del sitio oficial.
19
• cargo: herramienta de compilación y gestión de dependencias de Rust. Cargo sabe cómo
descargar dependencias, que normalmente se alojan en [Link] y las transfiere
a rustc al crear el proyecto. Cargo también incorpora un ejecutor de pruebas que se
utiliza para realizar pruebas unitarias.
• rustup: el instalador y actualizador de cadenas de herramientas de Rust. Esta
herramienta se utiliza para instalar y actualizar rustc y cargo cuando se lanzan
nuevas versiones de Rust. Además, rustup también puede descargar documentación
de la biblioteca estándar. Puedes tener varias versiones de Rust instaladas a la vez y
rustup te permitirá cambiar de una a otra según lo necesites.
Puntos clave:
• Rust cuenta con un programa de lanzamiento rápido en el que se publica una nueva
versión cada seis semanas. Las nuevas versiones mantienen la retrocompatibilidad con
las versiones anteriores, además de habilitar nuevas funciones.
• Hay tres canales de lanzamiento: ”stable”, ”beta” y ”nightly”.
• Las funciones nuevas se prueban en ”nightly”, y ”beta” es lo que se convierte en ”estable”
cada seis semanas.
• Las dependencias también pueden resolverse desde [registros] alternativos, git, carpetas,
etc.
• Rust también tiene varias [ediciones]: la más actual es Rust 2021. Las ediciones anteriores
son Rust 2015 y Rust 2018.
– Las ediciones pueden introducir cambios de incompatibilidad con versiones
anteriores en el lenguaje.
– Para evitar que se rompa el código, las ediciones son opcionales: selecciona la
edición para tu crate a través del archivo [Link].
– Para evitar la división del ecosistema, los compiladores de Rust pueden mezclar el
código escrito para distintas ediciones.
– Hay que mencionar que es bastante raro utilizar el compilador directamente y no
a través de cargo (la mayoría de los usuarios nunca lo hacen).
– Vale la pena mencionar que Cargo en sí es una herramienta extremadamente
poderosa e integral. Es capaz de hacer muchas cosas avanzadas y no limitadas a:
20
Se recomienda instalar Cargo, ya que facilitará la realización de los ejercicios. El último día
realizaremos un ejercicio más largo en el que se mostrará cómo trabajar con dependencias, y
para eso se necesita Cargo.
Los bloques de código de este curso son totalmente interactivos:
fn main() {
println!("¡Edítame!");
}
Puedes usar Ctrl + Enter para ejecutar el código cuando el cursor esté en el cuadro de texto.
La mayoría de los códigos de ejemplo se pueden editar, como se muestra arriba, pero hay
algunos que no se pueden editar por varios motivos:
• Los playgrounds insertados no pueden ejecutar pruebas unitarias. Copia y pega el código
y ábrelo en la página del playground para mostrar pruebas unitarias.
• Los playgrounds insertados pierden su estado en cuanto sales e de la página. Por este
motivo, los alumnos deben resolver los ejercicios con una versión local de Rust o a
través del playground.
21
fn main() {
println!("¡Edítame!");
}
5. Usa cargo run para hacer build y ejecutar tu binario actualizado:
$ cargo run
Compiling exercise v0.1.0 (/home/mgeisler/tmp/exercise)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/exercise`
Edit me!
6. Comprueba que no haya errores en el proyecto con cargo check. Compílalo sin
ejecutarlo con cargo build. Encontrarás la salida en target/debug/ para una
versión de depuración normal. Usa cargo build --release para generar una
compilación de lanzamiento optimizada en target/release/.
7. Edita [Link] para añadir dependencias a tu proyecto. Cuando ejecutes comandos
cargo, se descargarán y compilarán automáticamente las dependencias que falten.
Anima a los participantes de la clase a instalar Cargo y utilizar un editor local. Les facilitará
mucho las cosas, ya que dispondrán de un entorno de desarrollo normal.
22
Parte I
Día 1: mañana
23
Capítulo 3
Horario
Contando con los descansos de 10 minutos, la duración prevista de la sesión es de unas 2
horas y 5 minutos. Esta sesión contiene:
Sección Duración
Te damos la bienvenida 5 minutos
Hola, Mundo 15 minutos
Tipos y valores 40 minutos
Básicos de Control de Flujo 40 minutos
24
El objetivo del primer día es mostrar los aspectos ”básicos” de Rust que podrían tener
paralelismos inmediatos con otros lenguajes de programación. A lo largo del curso se
estudiarán los aspectos más avanzados de Rust.
Si estás impartiendo el curso en un aula, este un buen lugar para repasar el calendario. Debes
tener en cuenta que hay un ejercicio al final de cada parte, seguido de una pausa. Organiza
las sesiones de forma que se explique la solución del ejercicio después de la pausa. Las horas
que se indican son una sugerencia para que el curso se ciña al horario establecido. ¡No dudes
en modificar el calendario y hacer los cambios que consideres necesarios!
25
Capítulo 4
Hola, Mundo
Diapositiva Duración
¿Qué es Rust? 10 minutos
Ventajas de Rust 3 minutos
Playground 2 minutos
26
4.2 Ventajas de Rust
Estas son algunas de las ventajas competitivas de Rust:
• Seguridad de la memoria durante el tiempo de compilación: se evitan clases completas de
errores de memoria durante el tiempo de compilación
– No hay variables no inicializadas.
– No hay errores double free.
– No hay errores use-after-free.
– No hay punteros NULL.
– No se olvidan las exclusiones mutuas bloqueadas.
– No hay condiciones de carrera de datos entre hilos.
– No se invalidan los iteradores.
• No hay comportamientos indefinidos en el tiempo de ejecución: es decir, una instrucción
de Rust nunca queda sin especificar
– Se comprueban los límites de acceso a los arrays.
– Se define el desbordamiento de enteros (panic o wrap-around).
• Características de los lenguajes modernos: es tan expresivo y ergonómico como los
lenguajes de nivel superior
– Enumeraciones (Enums) y coincidencia de patrones.
– Genéricos.
– Sin overhead de FFI.
– Abstracciones sin coste.
– Excelentes errores de compilación.
– Gestor de dependencias integrado.
– Asistencia integrada para pruebas.
– Compatibilidad excelente con el protocolo del servidor de lenguaje.
This slide should take about 3 minutes.
No le dediques mucho tiempo a este punto. Todos estos aspectos se tratarán de forma más
detallada más adelante.
Asegúrate de preguntar a la clase en qué lenguajes tienen experiencia. Dependiendo de la
respuesta puedes destacar diferentes características de Rust:
• Experiencia con C o C++: Rust elimina una clase completa de errores de runtime mediante
el borrow checker. Obtienes un rendimiento similar al de C y C++, pero no tienes
problemas de seguridad en la memoria. Además, obtienes un lenguaje moderno con
elementos como la coincidencia de patrones y la gestión de dependencias integrado.
• Experiencia con Java, Go, Python, JavaScript, etc.: Consigues la misma seguridad de
memoria que en éstos lenguajes, además de una experiencia similar a la de un lenguaje
de alto nivel. También consigues un rendimiento rápido y predecible como en C y C++
(sin recolector de memoria residual), así como acceso a hardware de bajo nivel (si lo
necesitas).
27
4.3 Playground
El playground de Rust ofrece una forma sencilla de ejecutar programas cortos de Rust y es la
base de los ejemplos y ejercicios de este curso. Prueba a ejecutar el programa ”hello-world”
con el que empieza. Incluye algunas funciones útiles:
• En ”Tools”, usa la opción rustfmt para dar formato al código de forma ”estándar”.
• Rust cuenta con dos ”perfiles” principales para generar código: Debug (comprobaciones
adicionales del tiempo de ejecución, menor optimización) y Release (menos
comprobaciones del tiempo de ejecución y mayor optimización). Puedes acceder a ellos
haciendo clic en ”Debug”, en la parte superior.
• Si te interesa, utiliza la opción ”ASM” en ”...” para ver el código de ensamblado que se
ha generado.
This slide should take about 2 minutes.
Cuando sea la hora del descanso, anima a los asistentes a abrir el playground para que
experimenten un poco. Hazles saber que pueden mantener la pestaña abierta y probar cosas
durante el resto del curso. Resulta especialmente útil para los participantes con un nivel
avanzado que quieran obtener más información sobre las optimizaciones o el ensamblaje
generado de Rust.
28
Capítulo 5
Tipos y valores
Diapositiva Duración
Hola, Mundo 5 minutos
Variables 5 minutos
Valores 5 minutos
Aritmética 3 minutos
Inferencia de tipos 3 minutos
Ejercicio: Fibonacci 15 minutos
29
• Rust es moderno y totalmente compatible con sistemas como Unicode.
• Rust utiliza macros en situaciones en las que se desea un número variable de argumentos
(sin sobrecarga de funciones).
• Que las macros sean 'higiénicas' significa que no capturan accidentalmenteidentificadores
del ámbito en el que se utilizan. En realidad, las macros de Rust solo son parcialmente
higiénicas.
• Rust es un lenguaje multiparadigma. Por ejemplo, cuenta con funciones de programación
orientadas a objetos y, aunque no es un lenguaje funcional, incluye una serie de
conceptos funcionales.
5.2 Variables
Rust ofrece seguridad de tipos mediante tipado estático. Los enlaces a variables son hechos
con let:
fn main() {
let x: i32 = 10;
println!("x: {x}");
// x = 20;
// println!("x: {x}");
}
This slide should take about 5 minutes.
• Elimina el comentario de x = 20 para demostrar que las variables son inmutables de
forma predeterminada. Añade la palabra clave mut para que se puedan hacer cambios.
• En este ejemplo, i32 es el tipo de la variable. Se debe conocer durante el tiempo de
compilación, pero la inferencia de tipos (véase más adelante) permite al programador
omitirla en muchos casos.
5.3 Valores
A continuación, se muestran algunos tipos integrados básicos, así como la sintaxis de los
valores literales de cada tipo.
Tipos Literales
Enteros i8, i16, i32, i64, i128, -10, 0, 1_000, 123_i64
con signo isize
Enteros u8, u16, u32, u64, u128, 0, 123, 10_u16
sin signo usize
Números f32, f64 3.14, -10.0e20, 2_f32
de coma
flotante
Valores char 'a', 'α', '∞'
escalares
Unicode
Booleanos bool true, false
30
Los tipos tienen la siguiente anchura:
• iN, uN, and fN son N bits de capacidad,
• isize y usize tienen el ancho de un puntero,
• char tiene un tamaño de 32 bits,
• bool tiene 8 bits de ancho.
This slide should take about 5 minutes.
Hay algunas sintaxis que no se han mostrado anteriormente:
• Todos guiones bajos en los números pueden no utilizarse, ya que solo sirven para facilitar
la lectura. Por lo tanto, 1_000 se puede escribir como 1000 (o 10_00), y 123_i64 se
puede escribir como 123i64.
5.4 Aritmética
fn interproduct(a: i32, b: i32, c: i32) -> i32 {
return a * b + b * c + c * a;
}
fn main() {
println!("resultado: {}", interproduct(120, 100, 248));
}
This slide should take about 3 minutes.
Es la primera vez que vemos una función distinta a main, pero el significado debería quedar
claro: utiliza tres números enteros y devuelve uno. Más adelante, hablaremos sobre las
funciones con más profundidad.
La aritmética es muy similar a otros idiomas, al igual que su precedencia.
¿Qué pasa con el desbordamiento de enteros? En C y C++, el desbordamiento de números
enteros con signo no está definido, y podría tener diferentes resultados en diferentes
plataformas o compiladores. En Rust sí está definido.
Cambia el i32 a i16 para observar un desbordamiento de un número entero, lo que da error
(pánico) en una versión de depuración, pero lo envuelve en una compilación de lanzamiento.
Hay otras opciones disponibles, como el desbordamiento, la saturación y el acarreo, a las
que se accede mediante la sintaxis del método, por ejemplo, (a * b).saturating_add(b
* c).saturating_add(c * a).
De hecho, el compilador detectará si existe un desbordamiento de expresiones constantes,
por ello el ejemplo requiere una función independiente.
31
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);
}
This slide should take about 3 minutes.
Esta diapositiva muestra cómo el compilador de Rust infiere tipos basándose en restricciones
proporcionadas por declaraciones y usos de variables.
Es muy importante subrayar que las variables que se declaran así no son de un ”tipo
cualquiera” dinámico 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.
Cuando ningún elemento restringe el tipo de un literal entero, Rust lo define de forma
predeterminada como i32. A veces aparece como {integer} en los mensajes de error. Del
mismo modo, los literales de punto flotante se definen como f64 de forma predeterminada.
fn main() {
let x = 3.14;
let y = 20;
assert_eq!(x, y);
// ERROR: no hay implementación para `{float} == {integer}`
}
fn main() {
32
let n = 20;
println!("fib({n}) = {}", fib(n));
}
5.6.1 Solución
fn fib(n: u32) -> u32 {
if n < 2 {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
fn main() {
let n = 20;
println!("fib({n}) = {}", fib(n));
}
33
Capítulo 6
Diapositiva Duración
Expresiones if 4 minutos
Bucles 5 minutos
break y continue 4 minutos
Bloques y ámbitos 5 minutos
Funciones 3 minutos
Macros 2 minutos
Ejercicio: secuencia de Collatz 15 minutos
6.1 Expresiones if
Puedes usar expresiones if de la misma forma que en otros lenguajes:
fn main() {
let x = 10;
if x == 0 {
println!("cero!");
} else if x < 100 {
println!("muy grande");
} else {
println!("enorme");
}
}
Además, puedes utilizar if como expresión. La última expresión de cada bloque se convierte
en el valor de la expresión if:
fn main() {
let x = 10;
let size = if x < 20 { "pequeño" } else { "grande" };
println!("tamaño del número: {}", size);
}
34
This slide should take about 4 minutes.
Dado que if es una expresión y debe tener un tipo concreto, ambos de sus bloques de ramas
deben tener el mismo tipo. En el segundo ejemplo, muestra lo que sucede al añadir ; después
de "small".
Cuando se utiliza if en una expresión, esta debe tener un ; para separarla de la siguiente
instrucción. Elimina ; antes de println! para ver el error del compilador.
6.2 Bucles
Hay tres palabras clave de bucle en Rust: while, loop y for:
Bucles while
La palabra clave while es muy similar a la de otros lenguajes y ejecuta el cuerpo del bucle
mientras que la condición sea valida.
fn main() {
let mut x = 200;
while x >= 10 {
x = x / 2;
}
println!("x final: {x}");
}
6.2.1 for
El bucle for itera sobre rangos de valores o las entradas de una colección:
fn main() {
for x in 1..5 {
println!("x: {x}");
}
6.2.2 loop
El bucle loop repite hasta encontrar un break.
fn main() {
let mut i = 0;
loop {
35
i += 1;
println!("{i}");
if i > 100 {
break;
}
}
}
6.3.1 Etiquetas
De forma opcional, tanto continue como break pueden utilizar un argumento de etiqueta
para interrumpir los bucles anidados:
fn main() {
let s = [[5, 6, 7], [8, 9, 10], [21, 15, 32]];
let mut elements_searched = 0;
let target_value = 10;
'outer: for i in 0..=2 {
for j in 0..=2 {
elements_searched += 1;
if s[i][j] == target_value {
break 'outer;
}
}
}
print!("elementos travesados: {elements_searched}");
}
• Ten en cuenta que loop es la única construcción de bucle que devuelve un valor no
trivial. Esto se debe a que es inevitable que se introduzca al menos una vez (a diferencia
de los bucles while y for).
36
6.4 Bloques y ámbitos
Bloques
En Rust, un bloque contiene una secuencia de expresiones rodeados por llaves {}. Cada
bloque tiene el tipo y valor de la última expresión del bloque:
fn main() {
let z = 13;
let x = {
let y = 10;
println!("y: {y}");
z - y
};
println!("x: {x}");
}
Si la última expresión termina con ;, el tipo y el valor resultante será ().
This slide and its sub-slides should take about 5 minutes.
• Puedes mostrar cómo cambia el valor del bloque cambiando su última línea. Por ejemplo,
añade o quita un punto y coma, o utiliza la expresión return.
let a = true;
println!("sombreado en el ámbito interno: {a}");
}
println!("después: {a}");
}
• Para demostrar que el ámbito de una variable está limitado, añade una b en el bloque
interno del último ejemplo y, a continuación, intenta acceder a ella desde fuera de ese
bloque.
• Definición: Shadowing (sombreado) es distinto de la mutación, ya que después de
sombrear las ubicaciones de memoria de las dos variables existen al mismo tiempo.
Ambas están disponibles bajo el mismo nombre, en función de dónde se utiliza en el
código.
• Una variable sombreada puede tener un tipo diferente.
• Al principio, el sombreado no es fácil, pero resulta útil para conservar valores después
de .unwrap().
37
6.5 Funciones
fn gcd(a: u32, b: u32) -> u32 {
if b > 0 {
gcd(b, a % b)
} else {
a
}
}
fn main() {
println!("gcd: {}", gcd(143, 52));
}
This slide should take about 3 minutes.
• Los parámetros de declaración van seguidos de un tipo (al contrario que en algunos
lenguajes de programación) y, a continuación, de un tipo de resultado devuelto.
• La última expresión del cuerpo de una función (o de cualquier bloque) se convierte en
el valor devuelto. Basta con omitir el carácter ; al final de la expresión. La palabra clave
return puede ser utilizado para devolver valores antes del fin de la función, pero la
sintaxis de ”valor desnudo” es idiomático al fin de una función.
• Algunas funciones no devuelven ningún valor, devuelven el ”tipo unitario”, (). El
compilador deducirá esto si se omite el tipo de retorno -> ().
• El sobrecargo de funciones no existe en Rust -- cada función tiene una única
implementación.
– Siempre toma un número fijo de parámetros. No se admiten argumentos
predeterminados. Las macros se pueden utilizar para admitir funciones variádicas.
– Siempre se utiliza un solo conjunto de tipos de parámetros. Estos tipos pueden ser
genéricos, lo cual discutiremos mas tarde.
6.6 Macros
Las macros se amplían a código de Rust durante la compilación y pueden adoptar un número
variable de argumentos. Se distinguen por utilizar un símbolo ! al final. La biblioteca estándar
de Rust incluye una serie de macros útiles.
• println!(format, ..) imprime una linea a la salida estándar (”standard output”),
aplicando el formato descrito en std::fmt.
• format!(format, ..) funciona igual que println!, pero devuelve el resultado en
forma de cadena.
• dbg!(expression) registra el valor de la expresión y lo devuelve.
• todo!() marca un fragmento de código como no implementado todavía. Si se ejecuta,
activará un error pánico.
• unreachable!() marca un fragmento de código como inaccesible. Si se ejecuta, activará
un error pánico.
fn factorial(n: u32) -> u32 {
let mut product = 1;
for i in 1..=n {
product *= dbg!(i);
}
38
product
}
fn main() {
let n = 4;
println!("{n}! = {}", factorial(n));
}
This slide should take about 2 minutes.
El objetivo de esta sección es mostrar que existen estos elementos útiles y cómo usarlos. Por
qué se definen como macros y a qué se expanden no es muy importante.
En el curso no se imparte la definición de macros, pero en una sección posterior se describirá
el uso de las macros de derivación.
fn main() {
todo!("Implementar esto")
}
39
6.7.1 Solución
/// Determina la longitud de la secuencia de Collatz que empieza por `n`.
fn collatz_length(mut n: i32) -> u32 {
let mut len = 1;
while n > 1 {
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
len += 1;
}
len
}
#[test]
fn test_collatz_length() {
assert_eq!(collatz_length(11), 15);
}
fn main() {
println!("Longitud: {}", collatz_length(11));
}
40
Parte II
Día 1: Tarde
41
Capítulo 7
Te damos la bienvenida
Sección Duración
Tuplas y arrays 35 minutos
Referencias 55 minutos
Tipos definidos por el usuario 50 minutos
42
Capítulo 8
Tuplas y arrays
Diapositiva Duración
Arrays 5 minutos
Tuplas 5 minutos
Iteración de Arreglos (Arrays) 3 minutos
Patrones y Desestructuración 5 minutos
Ejercicio: arrays anidados 15 minutos
8.1 Arrays
fn main() {
let mut a: [i8; 10] = [42; 10];
a[5] = 0;
println!("a: {a:?}");
}
This slide should take about 5 minutes.
• Un valor del tipo array [T; N] contiene N (una constante en tiempo de compilación)
elementos del mismo tipo T. Ten en cuenta que la longitud del array es _parte de su tipo),
lo que significa que [u8; 3] y [u8; 4] se consideran dos tipos diferentes. Los slices,
que tienen un tamaño determinado al tiempo de ejecución, serán discutidos mas tarde.
• Prueba a acceder a un array que esté fuera de los límites. Los accesos a los arrays se
comprueban en el tiempo de ejecución. Rust suele optimizar estas comprobaciones y se
pueden evitar utilizando Rust inseguro.
• Podemos usar literales para asignar valores a arrays.
• El macro de impresión println! solicita la implementación de depuración con el
parámetro de formato ?: {} ofrece la salida predeterminada y {:?} ofrece la salida
de depuración. Tipos como números enteros y cadenas implementan la salida de
depuración. Esto significan que tenemos que usar la salida de depuración en este caso.
43
• Si se añade #, por ejemplo {a:#?}, se da formato al texto para facilitar la lectura.
8.2 Tuplas
fn main() {
let t: (i8, bool) = (7, true);
println!("t.0: {}", t.0);
println!("t.1: {}", t.1);
}
This slide should take about 5 minutes.
• Al igual que los arrays, las tuplas tienen una longitud fija.
• Las tuplas agrupan valores de diferentes tipos en un tipo compuesto.
• Se puede acceder a los campos de una tupla por el punto y el índice del valor, por ejemplo,
t.0, t.1.
• La tupla vacía () es llamado el ”tipo de unidad” y significa la ausencia de un valor de
retorno, parecido a void en otros lenguajes.
44
Rust también provee la coincidencia de patrones para destructurar un valor en sus partes
constituyentes:
fn print_tuple(tuple: (i32, i32)) {
let (left, right) = tuple;
println!("left: {left}, right: {right}");
}
This slide should take about 5 minutes.
• Los patrones usados aquí son ”irrefutables”, es decir que el compilador puede
estáticamente verificar que el valor a la derecha del = tiene la misma estructura que el
patrón.
• Un nombre de variable es un patrón irrefutable que siempre coincide con cualquier
valor, así que también podemos usar let para declarar una sola variable.
• Los patrones también se pueden usar en los condicionales, dejando que la comparación
de igualdad y el desestructuramiento ocurren al mismo tiempo. Esta forma de
coincidencia de patrones sera discutido mas a fondo mas tarde.
• Edita los ejemplos anteriores para enseñar el error de compilador cuando el patrón no
coincide con el valor.
#[test]
fn test_transpose() {
let matrix = [
[101, 102, 103], //
[201, 202, 203],
[301, 302, 303],
];
let transposed = transpose(matrix);
45
assert_eq!(
transposed,
[
[101, 201, 301], //
[102, 202, 302],
[103, 203, 303],
]
);
}
fn main() {
let matrix = [
[101, 102, 103], // <-- el comentario hace que rustfmt añade una nueva línea
[201, 202, 203],
[301, 302, 303],
];
8.5.1 Solución
fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
let mut result = [[0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
result[j][i] = matrix[i][j];
}
}
result
}
#[test]
fn test_transpose() {
let matrix = [
[101, 102, 103], //
[201, 202, 203],
[301, 302, 303],
];
let transposed = transpose(matrix);
assert_eq!(
transposed,
[
[101, 201, 301], //
[102, 202, 302],
[103, 203, 303],
]
);
}
46
fn main() {
let matrix = [
[101, 102, 103], // <-- el comentario hace que rustfmt añade una nueva línea
[201, 202, 203],
[301, 302, 303],
];
47
Capítulo 9
Referencias
Diapositiva Duración
Enums compartidas 10 minutos
Referencias exclusivas 10 minutos
Slices: &[T] 10 minutos
Cadenas de texto (Strings) 10 minutos
Ejercicio: geometría 15 minutos
48
• Se dice que una referencia ”toma prestado” el valor al que hace referencia. Este es un
buen modelo para los estudiantes que no están familiarizados con los punteros, ya que
el código puede usar la referencia para acceder al valor, pero este sigue ”perteneciendo”
a la variable original. En el curso hablaremos con más profundidad sobre la propiedad
el tercer día.
• Las referencias se implementan como punteros y una ventaja clave es que pueden
ser mucho más pequeñas del elemento al que apuntan. Los participantes que estén
familiarizados con C o C++ reconocerán las referencias como punteros. A lo largo
del curso, hablaremos sobre cómo Rust evita los errores de seguridad en la memoria
derivados del uso de punteros sin formato.
• Rust no crea referencias automáticamente, & siempre es obligatorio.
• Rust realizará una desreferencia automática en algunos casos, en especial al invocar
métodos (prueba ref_x.count_ones()). No hay necesidad para un operador -> como
en C++.
• En este ejemplo, r es mutable para que se pueda reasignar (r = &b). Debes tener en
cuenta que se vuelve a enlazar r para que haga referencia a otro elemento. Es distinto
de C++, donde la asignación a una referencia modifica el valor referenciado.
• Una referencia compartida no permite modificar el valor al que hace referencia, incluso
aunque el valor sea mutable. Prueba con *r = 'X'.
• Rust hace un seguimiento del tiempo de vida de todas las referencias para asegurarse
de que duran lo suficiente. En Rust seguro no se dan referencias colgantes. x_axis
devolvería una referencia a point, pero point se desasignará cuando se devuelva la
función, por lo que no se compilará.
• Más adelante hablaremos de los préstamos cuando lleguemos a la parte de propiedad.
49
9.3 Slices
Un slice ofrece una visión de una colección más amplia:
fn main() {
let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60];
println!("a: {a:?}");
println!("s: {s:?}");
}
• Los slices toman prestados datos del tipo slice.
• Pregunta: ¿Qué ocurre si se modifica a[3] justo antes de imprimir s?
This slide should take about 10 minutes.
• Creamos un slice tomando prestado a y especificando entre paréntesis los índices de
inicio y de fin.
• Si el slice comienza en el índice 0, la sintaxis de rango de Rust nos permite eliminar el
índice inicial, lo que significa que &a[0..[Link]()] y &a[..[Link]()] son idénticos.
• Lo mismo ocurre con el último índice, por lo que &a[2..[Link]()] y &a[2..] son
idénticos.
• Para crear fácilmente un slice del array completo, podemos usar &a[..].
• s es una referencia a un slice de i32s. Ten en cuenta que el tipo de s (&[i32]) ya no
menciona la longitud del array. Esto nos permite realizar cálculos en slices de diferentes
tamaños.
• Los slices siempre tienen préstamos de otros objetos. En este ejemplo, a tiene que
permanecer ”vivo” (en el ámbito) al menos durante el tiempo que dure el slice.
• La cuestión sobre la modificación de a[3] puede suscitar un debate interesante, pero la
respuesta es que, por razones de seguridad de memoria, no se puede hacer mediante a
en este punto de la ejecución, pero sí se pueden leer los datos de a y s de forma segura.
Funciona antes de crear el slice y después de println, cuando el slice ya no se utiliza.
50
let s3: &str = &s2[[Link]() - [Link]()..];
println!("s3: {s3}");
}
This slide should take about 10 minutes.
• &str introduce un slice de cadena, que es una referencia inmutable a los datos de
cadena codificados en UTF-8 y almacenados en un bloque de memoria. Los literales de
cadena ("Hello") se almacenan en el binario del programa.
• El tipo String de Rust es un envoltorio que rodea a un vector de bytes. Como sucede
con Vec<T>, tiene propietario.
• Al igual que ocurre con muchos otros tipos, String::from() crea una cadena a partir
de un literal de cadena. String::new() crea una cadena vacía a la que se pueden
añadir datos de cadena mediante los métodos push() y push_str().
• La macro format!() es una forma práctica de generar una cadena propia a partir de
valores dinámicos. Acepta la misma especificación de formato que println!().
• Puedes tomar prestados slices &str de String a través de & y, de forma opcional, la
selección de intervalos. Si seleccionas un intervalo de bytes que no esté alineado con
los límites de caracteres, la expresión activará un pánico. El iterador chars itera sobre
los caracteres y se aconseja esta opción a intentar definir los límites de los caracteres
correctamente.
• Para los programadores de C++: piensa en &str como el const char* de C++, pero
uno que siempre apunta a una cadena válida en la memoria. El String de Rust es
parecido a std::string de C++ (la diferencia principal es que solo puede contener
bytes codificados en UTF-8 y nunca utilizará una optimización de cadena pequeña).
• Los literales de cadenas de bytes te permiten crear un valor &[u8] directamente:
fn main() {
println!("{:?}", b"abc");
println!("{:?}", &[97, 98, 99]);
}
• Las cadenas sin formato te permiten crear un valor &str con los escapes inhabilitados:
r"\n" == "\\n". Puedes insertar comillas dobles con la misma cantidad de # a cada
lado de ellas:
fn main() {
println!(r#"<a href="[Link]">link</a>"#);
println!("<a href=\"[Link]\">link</a>");
}
51
fn magnitude(...) -> f64 {
todo!()
}
fn normalize(...) {
todo!()
}
fn main() {
println!("Magnitud de un vector unitario: {}", magnitude(&[0.0, 1.0, 0.0]));
9.5.1 Solución
/// Calcula la magnitud del vector dado.
fn magnitude(vector: &[f64; 3]) -> f64 {
let mut mag_squared = 0.0;
for coord in vector {
mag_squared += coord * coord;
}
mag_squared.sqrt()
}
fn main() {
println!("Magnitud de un vector unitario: {}", magnitude(&[0.0, 1.0, 0.0]));
52
}
53
Capítulo 10
Diapositiva Duración
Estructuras con nombre 10 minutos
Estructuras de tuplas 10 minutos
Enumeraciones 5 minutos
Estático 5 minutos
Aliases de tipo 2 minutos
Ejercicio: eventos de ascensor 15 minutos
fn describe(person: &Person) {
println!("{} tiene {} años", [Link], [Link]);
}
fn main() {
let mut peter = Person { name: String::from("Peter"), age: 27 };
describe(&peter);
[Link] = 28;
describe(&peter);
54
describe(&avery);
fn main() {
let p = Point(17, 23);
println!("({}, {})", p.0, p.1);
}
Esto se suele utilizar para envoltorios de campo único (denominados newtypes):
struct PoundsOfForce(f64);
struct Newtons(f64);
fn set_thruster_force(force: Newtons) {
// ...
}
fn main() {
let force = compute_thruster_force();
set_thruster_force(force);
}
55
This slide should take about 10 minutes.
• Los newtypes son una buena forma de codificar información adicional sobre el valor
de un tipo primitivo, por ejemplo:
– El número se mide en algunas unidades: Newtons en el ejemplo anterior.
– El valor ha pasado alguna validación cuando se ha creado, por lo que ya no
tendrás que volver a validarlo cada vez que lo uses: PhoneNumber(String) u
OddNumber(u32).
• Demuestra cómo se añade un valor f64 a un tipo Newtons accediendo al campo único
del newtype.
– Por lo general, a Rust no le gustan los elementos no explícitos, como el
desenvolvimiento automático o, por ejemplo, el uso de booleanos como enteros.
– El día 3 (genéricos), se explicará la sobrecarga del operador.
• El ejemplo es una sutil referencia al fracaso de la sonda Mars Climate Orbiter.
10.3 Enumeraciones
La palabra clave enum permite crear un tipo que tiene diferentes variantes:
#[derive(Debug)]
enum Direction {
Left,
Right,
}
#[derive(Debug)]
enum PlayerMove {
Pass, // Variante simple
Run(Direction), // Variante de tupla
Teleport { x: u32, y: u32 }, // Variante de struct
}
fn main() {
let m: PlayerMove = PlayerMove::Run(Direction::Left);
println!("En este turno: {:?}", m);
}
This slide should take about 5 minutes.
Puntos Clave:
• Las enumeraciones te permiten colectar un conjunto de valores en un solo tipo.
• Direction es un tipo con variantes. Hay dos valores de Direction: Direction::Left
y Direction::Right.
• PlayerMove es un tipo con tres variantes. Además de las cargas útiles, Rust almacenará
un discriminante para saber qué variante se encuentra en un valor PlayerMove en el
tiempo de ejecución.
• Este es un buen momento para comparar las estructuras y las enumeraciones:
– En ambas puedes tener una versión sencilla sin campos (estructura unitaria) o una
versión con distintos tipos de campos (variantes con carga útil).
– Incluso podrías implementar las distintas variantes de una enumeración con
estructuras diferentes, pero entonces no serían del mismo tipo como lo serían
56
si estuvieran todas definidas en una enumeración.
• Rust usa muy poco espacio para almacenar el discriminante.
– Si es necesario, almacena un número entero del tamaño más pequeño requerido
– Si los valores de la variante permitidos no cubren todos los patrones de bits,
se utilizarán patrones de bits no válidos para codificar el discriminante (la
”optimización de nicho”). Por ejemplo, Option<&u8> almacena un puntero en un
número entero o NULL para la variante None.
– Puedes controlar el discriminante si es necesario (por ejemplo, para asegurar la
compatibilidad con C):
#[repr(u32)]
enum Bar {
A, // 0
B = 10000,
C, // 10001
}
fn main() {
println!("A: {}", Bar::A as u32);
println!("B: {}", Bar::B as u32);
println!("C: {}", Bar::C as u32);
}
Sin repr, el tipo discriminante ocupa 2 bytes, debido a que 10001 se cabe en 2 bytes.
Más información
Rust cuenta con varias optimizaciones que puede utilizar para hacer que las enums ocupen
menos espacio.
• Optimización de puntero nulo: para algunos tipos, Rust asegura que size_of::<T>()
es igual a size_of::<Option<T> >().
Fragmento de código de ejemplo si quieres mostrar cómo puede ser la representación bit
a bit en la práctica. Es importante tener en cuenta que el compilador no ofrece garantías
con respecto a esta representación, por lo tanto es totalmente inseguro.
use std::mem::transmute;
macro_rules! dbg_bits {
($e:expr, $bit_type:ty) => {
println!("- {}: {:#x}", stringify!($e), transmute::<_, $bit_type>($e));
};
}
fn main() {
unsafe {
println!("bool:");
dbg_bits!(false, u8);
dbg_bits!(true, u8);
println!("Option<bool>:");
dbg_bits!(None::<bool>, u8);
dbg_bits!(Some(false), u8);
57
dbg_bits!(Some(true), u8);
println!("Option<Option<bool>>:");
dbg_bits!(Some(Some(false)), u8);
dbg_bits!(Some(Some(true)), u8);
dbg_bits!(Some(None::<bool>), u8);
dbg_bits!(None::<Option<bool>>, u8);
println!("Option<&i32>:");
dbg_bits!(None::<&i32>, usize);
dbg_bits!(Some(&0i32), usize);
}
}
10.4 static
Las variables estáticas vivirán durante toda la ejecución del programa y, por lo tanto, no se
moverán:
static BANNER: &str = "Bienvenide a RustOS 3.14";
fn main() {
println!("{BANNER}");
}
Tal y como se indica en el libro Rust RFC Book, estas no son insertadas y tienen una ubicación
de memoria real asociada. Esto resulta útil para código insertado y no seguro. Además, la
variable continúa durante toda la ejecución del programa. Cuando un valor de ámbito global
no tiene ningún motivo para necesitar identidad de objeto, se suele preferir const.
This slide should take about 5 minutes.
• Por su parte, static se parece a una variable global mutable en C++.
• static proporciona la identidad del objeto: una dirección en la memoria y en el estado
que requieren los tipos con mutabilidad interior, como Mutex<T>.
Más información
Dado que se puede acceder a las variables static desde cualquier hilo, deben ser Sync.
Mutabilidad interior es posible a través de un Mutex, atómico o parecido.
Datos locales al hilo se pueden crear con el macro std::thread_local.
10.5 const
Las variables constantes se evalúan en tiempo de compilación y sus valores se insertan donde
sean utilizados:
const DIGEST_SIZE: usize = 3;
const ZERO: Option<u8> = Some(42);
58
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:?}");
}
Según el libro Rust RFC Book, se insertan cuando se utilizan.
Sólo se pueden llamar a las funciones marcadas como const en tiempo de compilación para
generar valores const. Sin embargo, las funciones const se pueden llamar en runtime.
• Menciona que const se comporta semánticamente de forma similar a constexpr de
C++
• No es muy habitual que se necesite una constante evaluada en runtime, pero es útil y
más seguro que usar una estática.
59
#[derive(Debug)]
/// Un evento en el sistema de ascensores al que debe reaccionar el controlador.
enum Event {
// TAREAS: añadir variantes obligatorias
}
fn main() {
println!(
"Un pasajero de la planta baja ha pulsado el botón para ir hacia arriba: {:?}",
lobby_call_button_pressed(0, Direction::Up)
);
println!("El ascensor ha llegado a la planta baja: {:?}", car_arrived(0));
println!("Las puertas del ascensor se han abierto: {:?}", car_door_opened());
println!(
"Un pasajero ha pulsado el botón de la tercera planta: {:?}",
car_floor_button_pressed(3)
);
println!("Las puertas del ascensor se han cerrado: {:?}", car_door_closed());
println!("El ascensor ha llegado a la tercera planta: {:?}", car_arrived(3));
60
}
10.7.1 Solución
#[derive(Debug)]
/// Un evento en el sistema de ascensores al que debe reaccionar el controlador.
enum Event {
/// Se ha pulsado un botón.
ButtonPressed(Button),
61
fn car_door_closed() -> Event {
Event::CarDoorClosed
}
fn main() {
println!(
"Un pasajero de la planta baja ha pulsado el botón para ir hacia arriba: {:?}",
lobby_call_button_pressed(0, Direction::Up)
);
println!("El ascensor ha llegado a la planta baja: {:?}", car_arrived(0));
println!("Las puertas del ascensor se han abierto: {:?}", car_door_opened());
println!(
"Un pasajero ha pulsado el botón de la tercera planta: {:?}",
car_floor_button_pressed(3)
);
println!("Las puertas del ascensor se han cerrado: {:?}", car_door_closed());
println!("El ascensor ha llegado a la tercera planta: {:?}", car_arrived(3));
}
62
Parte III
Día 2: Mañana
63
Capítulo 11
Ahora que ya sabemos bastante sobre Rust, continuaremos con un enfoque en el sistema de
tipos de Rust:
• Coincidencia de Patrones: desestructuración de enums, structs y arrays.
• Métodos: asociar funciones a tipos.
• Traits: comportamientos que comparten varios tipos.
• Genéricos: parametrizar tipos en otros tipos.
• Tipos y traits de bibliotecas estándar: un recorrido por la amplia biblioteca estándar de
Rust.
Horario
Contando con los descansos de 10 minutos, la duración prevista de la sesión es de unas 2
horas y 10 minutos. Contiene:
Sección Duración
Te damos la bienvenida 3 minutos
Correspondencia de Patrones 1 hora
Métodos y Traits 50 minutos
64
Capítulo 12
Correspondencia de Patrones
Diapositiva Duración
Correspondencia de Valores 10 minutos
Desestructurando Structs 4 minutos
Desestructurando Enums 4 minutos
Control de Flujo Let 10 minutos
Ejercicio: evaluación de expresiones 30 minutos
65
ejemplo anterior, el tipo es ().
Una variable del patrón (en este ejemplo, key) creará un enlace que se puede usar dentro del
brazo de coincidencia.
Un protección de coincidencia hace que la expresión coincida únicamente si se cumple la
condición.
This slide should take about 10 minutes.
Puntos Clave:
• Puedes señalar cómo se usan algunos caracteres concretos en un patrón
– | como or
– .. puede ampliarse tanto como sea necesario
– 1..=5 representa un rango inclusivo
– _ es un comodín
• Las guardas de coincidencia, como característica sintáctica independiente, son
importantes y necesarios cuando queremos expresar de forma concisa ideas más
complejas de lo que permitirían los patrones por sí solos.
• No son lo mismo que una expresión if independiente dentro del brazo de coincidencias.
Una expresión if dentro del bloque de ramas (después de =>) se produce tras seleccionar
el brazo de coincidencias. Si no se cumple la condición if dentro de ese bloque, no se
tienen en cuenta otros brazos de la expresión match original.
• La condición definida en el guarda se aplica a todas las expresiones de un patrón con
un |.
12.2 Structs
Al igual que las tuplas, las estructuras se pueden desestructurar con la coincidencia:
struct Foo {
x: (u32, u32),
y: u32,
}
#[rustfmt::skip]
fn main() {
let foo = Foo { x: (1, 2), y: 3 };
match foo {
Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"),
Foo { y: 2, x: i } => println!("y = 2, x = {i:?}"),
Foo { y, .. } => println!("y = {y}, se han ignorado otros campos"),
}
}
This slide should take about 4 minutes.
• Cambia los valores literales de foo para que coincidan con los demás patrones.
• Añade un campo nuevo a Foo y realiza los cambios necesarios en el patrón.
66
• La diferencia entre una captura y una expresión constante puede ser difícil de detectar.
Prueba a cambiar el 2 del segundo brazo por una variable y observa que no funciona.
Cámbialo a const y verás que vuelve a funcionar.
12.3 Enumeraciones
Al igual que las tuplas, las enumeraciones también se pueden desestructurar con la
coincidencia:
Los patrones también se pueden usar para enlazar variables a partes de los valores. Así es
como se inspecciona la estructura de tus tipos. Empecemos con un tipo enum sencillo:
enum Result {
Ok(i32),
Err(String),
}
fn main() {
let n = 100;
match divide_in_two(n) {
Result::Ok(half) => println!("{n} dividido entre dos es {half}"),
Result::Err(msg) => println!("se ha producido un error: {msg}"),
}
}
Aquí hemos utilizado los brazos para desestructurar el valor de Result. En el primer brazo,
half está vinculado al valor que hay dentro de la variante Ok. En el segundo, msg está
vinculado al mensaje de error.
This slide should take about 4 minutes.
• La expresión if/else devuelve una enumeración que más tarde se descomprime con
match.
• Puedes probar a añadir una tercera variante a la definición de la enumeración y mostrar
los errores al ejecutar el código. Señala los lugares en los que tu código está ahora
incompleto y explica cómo el compilador intenta dar sugerencias.
• Solo se puede acceder a los valores de las variantes de enumeración una vez que
coincidan con el patrón.
• Demuestra lo que pasa cuando la búsqueda no es exhaustiva. Ten en cuenta la ventaja
que ofrece el compilador de Rust al confirmar que se gestionan todos los casos.
• Guarda el resultado de divide_in_two en la variable result y hazlo coincidir
mediante match en un bucle. No se compilará porque se utilizará msg cuando coincida.
Para solucionarlo, haz coincidir &result en lugar de result. De esta forma, msg
se convertirá en una referencia y no se utilizará. Esta ”ergonomía de coincidencia”
67
apareció en Rust 2018. Si quieres que sea compatible con las versiones anteriores de
Rust, sustituye msg por ref msg en el patrón.
Expresiones if let
La [expresión if let][([Link]
expressions) te permite ejecutar código diferente en función de si un valor coincide con
un patrón:
use std::time::Duration;
fn sleep_for(secs: f32) {
if let Ok(dur) = Duration::try_from_secs_f32(secs) {
std::thread::sleep(dur);
println!("Horas de sueño: {:?}", dur);
}
}
fn main() {
sleep_for(-10.0);
sleep_for(0.8);
}
68
};
fn main() {
println!("resultado: {:?}", hex_or_die_trying(Some(String::from("foo"))));
}
Al igual que con if let, hay una variante while let que prueba repetidamente un valor
con respecto a un patrón:
fn main() {
let mut name = String::from("Comprehensive Rust ");
while let Some(c) = [Link]() {
println!("character: {c}");
}
// (There are more efficient ways to reverse a string!)
}
Aquí, String::pop devolverá Some(c) hasta que la cadena este vacía, cuando empezara a
devolver None. while let nos permite seguir iterando a través de todos los elementos.
This slide should take about 10 minutes.
if-let
• A diferencia de match, if let no tiene que cubrir todas las ramas, pudiendo así
conseguir que sea más conciso que match.
• Un uso habitual consiste en gestionar valores Some al trabajar con Option.
• A diferencia de match, if let no admite cláusulas guardia para la coincidencia de
patrones.
let-else
Las instrucciones if-let se pueden apilar, tal y como se muestra. La construcción let-else
permite aplanar este código anidado. Reescribe esta rara versión para que los participantes
puedan ver la transformación.
La versión reescrita es la siguiente:
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let Some(s) = maybe_string else {
return Err(String::from("se ha obtenido None"));
};
69
let Some(digit) = first_byte_char.to_digit(16) else {
return Err(String::from("no es un dígito hexadecimal"));
};
return Ok(digit);
}
while-let
• Señala que el bucle while let seguirá funcionando siempre que el valor coincida con
el patrón.
• Puedes reescribir el bucle while let como un ciclo infinito con una instrucción if
que rompe el bucle si [Link]() no devuelve un valor para desenvolver. while let
proporciona azúcar sintáctico para la situación anterior.
70
/// Una expresión en forma de árbol.
#[derive(Debug)]
enum Expression {
/// Operación en dos subexpresiones.
Op { op: Operation, left: Box<Expression>, right: Box<Expression> },
#[test]
fn test_value() {
assert_eq!(eval(Expression::Value(19)), Ok(19));
}
#[test]
fn test_sum() {
assert_eq!(
eval(Expression::Op {
op: Operation::Add,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(20)),
}),
Ok(30)
);
}
#[test]
fn test_recursion() {
let term1 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(9)),
};
let term2 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(3)),
right: Box::new(Expression::Value(4)),
}),
right: Box::new(Expression::Value(5)),
};
assert_eq!(
eval(Expression::Op {
op: Operation::Add,
left: Box::new(term1),
71
right: Box::new(term2),
}),
Ok(85)
);
}
#[test]
fn test_error() {
assert_eq!(
eval(Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
}),
Err(String::from("división entre cero"))
);
}
12.5.1 Solución
/// Operación que se puede llevar a cabo en dos subexpresiones.
#[derive(Debug)]
enum Operation {
Add,
Sub,
Mul,
Div,
}
72
Operation::Add => left + right,
Operation::Sub => left - right,
Operation::Mul => left * right,
Operation::Div => {
if right == 0 {
return Err(String::from("división entre cero"));
} else {
left / right
}
}
})
}
Expression::Value(v) => Ok(v),
}
}
#[test]
fn test_value() {
assert_eq!(eval(Expression::Value(19)), Ok(19));
}
#[test]
fn test_sum() {
assert_eq!(
eval(Expression::Op {
op: Operation::Add,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(20)),
}),
Ok(30)
);
}
#[test]
fn test_recursion() {
let term1 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(9)),
};
let term2 = Expression::Op {
op: Operation::Mul,
left: Box::new(Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(3)),
right: Box::new(Expression::Value(4)),
}),
right: Box::new(Expression::Value(5)),
};
assert_eq!(
eval(Expression::Op {
73
op: Operation::Add,
left: Box::new(term1),
right: Box::new(term2),
}),
Ok(85)
);
}
#[test]
fn test_error() {
assert_eq!(
eval(Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
}),
Err(String::from("división entre cero"))
);
}
fn main() {
let expr = Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(20)),
right: Box::new(Expression::Value(10)),
};
println!("expr: {:?}", expr);
println!("resultado: {:?}", eval(expr));
}
74
Capítulo 13
Métodos y Traits
Diapositiva Duración
Métodos 10 minutos
Traits 15 minutos
Derivación de Traits 3 minutos
Ejercicio: registro genérico 20 minutos
13.1 Métodos
Rust te permite asociar funciones a los nuevos tipos. Para ello, usa un bloque impl:
#[derive(Debug)]
struct Race {
name: String,
laps: Vec<i32>,
}
impl Race {
// No hay receptor, método estático
fn new(name: &str) -> Self {
Self { name: String::from(name), laps: Vec::new() }
}
75
println!("Vuelta {idx}: {lap} s");
}
}
fn main() {
let mut race = Race::new("Gran Premio de Mónaco");
race.add_lap(70);
race.add_lap(68);
race.print_laps();
race.add_lap(71);
race.print_laps();
[Link]();
// race.add_lap(42);
}
El argumento self denomina el ”receiver” (receptor) - el objeto sobre cual el método actuará.
Hay varios receivers comunes para un método:
• &self: toma prestado el objeto del llamador utilizando una referencia compartida e
inmutable. El objeto se puede volver a utilizar después.
• &mut self: toma prestado el objeto del llamador mediante una referencia única y
mutable. El objeto se puede volver a utilizar después.
• self: asume el ownership del objeto y lo aleja del llamador. El método se convierte en el
propietario del objeto. El objeto se eliminará (es decir, se anulará la asignación) cuando
el método devuelva un resultado, a menos que se transmita su ownership de forma
explícita. El ownership completa no implica automáticamente una mutabilidad.
• mut self: igual que lo anterior, pero el método puede mutar el objeto.
• Sin receptor: se convierte en un método estático de la estructura. Normalmente se utiliza
para crear constructores que se suelen denominar new.
This slide should take about 8 minutes.
Puntos Clave:
• Puede resultar útil presentar los métodos comparándolos con funciones.
– Se llama a los métodos en una instancia de un tipo (como un estructura o una
enumeración) y el primer parámetro representa la instancia como self.
– Los desarrolladores pueden optar por utilizar métodos para aprovechar la sintaxis
de los receptores de métodos y para ayudar a mantenerlos más organizados.
Mediante el uso de métodos podemos mantener todo el código de implementación
en un lugar predecible.
• Señala el uso de la palabra clave self, el receptor de un método.
– Indica que se trata de un término abreviado de self: Self y tal vez muestra cómo
se podría utilizar también el nombre de la estructura.
– Explica que Self es un tipo de alias para el tipo en el que está el bloque impl y que
se puede usar en cualquier parte del bloque.
76
– Ten en cuenta que se puede usar self como otras estructuras y que la notación de
puntos puede utilizarse para referirse a campos concretos.
– Puede ser un buen momento para mostrar la diferencia entre &self y self
modificando el código e intentando ejecutar finish dos veces.
– Además de las variantes self, también hay tipos de envoltorios especiales que
pueden ser tipos de receptor, como Box<Self>.
13.2 Traits
Rust te permite abstraer sobre tipos con traits. Son similares a las interfaces:
trait Pet {
/// Devuelve una frase de esta mascota.
fn talk(&self) -> String;
fn greet(&self) {
println!("¡Eres una monada! ¿Cómo te llamas? {}", [Link]());
}
}
struct Dog {
name: String,
age: i8,
}
fn main() {
let fido = Dog { name: String::from("Fido"), age: 5 };
[Link]();
}
77
• Para implementar Trait para un tipo Type, utiliza un bloque impl Trait for Type
{ .. }.
• A diferencia de los interfases de Go, tener los métodos adecuados no es suficiente: un
tipo Cat con un método talk() no satisface Pet automáticamente al menos que este
en un bloque impl Pet.
• Los traits pueden especificar implementaciones predeterminadas de algunos métodos.
Implementaciones predeterminadas pueden usar todos los métodos de un trait (incluso
los métodos que los usuarios deben implementar ellos mismos). En este caso, greet es
predeterminado y utiliza talk.
13.2.2 Supertraits
Un trait puede requerir que los tipos que lo implementan también implementen otros traits,
llamados supertraits. Aquí, cualquier tipo implementando Pet también debe implementar
Animal.
trait Animal {
fn leg_count(&self) -> u32;
}
struct Dog(String);
fn main() {
let puppy = Dog(String::from("Rex"));
println!("{} tiene {} piernas", [Link](), puppy.leg_count());
}
Algunas veces esto es llamado ”herencia de traits”, pero los estudiantes no deben esperar que
esto se comporte como la herencia OO (object-oriented). Solo especifica un requerimiento
adicional sobre las implementaciones de un trait.
78
13.2.3 Tipos de datos asociados
Tipos asociados son tipos guarda-espacio que han sido proveídos por la implementación del
trait.
#[derive(Debug)]
struct Meters(i32);
#[derive(Debug)]
struct MetersSquared(i32);
trait Multiply {
type Output;
fn multiply(&self, other: &Self) -> Self::Output;
}
fn main() {
println!("{:?}", Meters(10).multiply(&Meters(20)));
}
• Tipos asociados también son llamados ”tipos de salida”. La observación clave es que el
implementador, no el ejecutor, escoge este tipo.
• Muchos traits de la biblioteca estándar tienen tipos asociados, incluyendo operadores
aritméticos y Iterator.
fn main() {
let p1 = Player::default(); // El trait predeterminado añade el constructor `default
let mut p2 = [Link](); // El trait clonado añade el método `clone`.
[Link] = String::from("EldurScrollz");
// El trait Debug permite que sea compatible con imprimir con `{:?}`.
println!("{:?} contra {:?}", p1, p2);
}
This slide should take about 3 minutes.
79
La derivación se implementa con macros y muchos crates ofrecen macros de derivación
útiles para añadir funciones. Por ejemplo, serde puede derivar la compatibilidad con la
serialización para una struct con #[derive(Serialize)].
struct StderrLogger;
fn main() {
let l = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
do_things(&l);
}
13.4.1 Solución
use std::fmt::Display;
80
/// Registra un mensaje con el nivel de verbosidad determinado.
fn log(&self, verbosity: u8, message: impl Display);
}
struct StderrLogger;
/// Registra solo los mensajes que cumplan el nivel de verbosidad determinado.
struct VerbosityFilter {
max_verbosity: u8,
inner: StderrLogger,
}
fn main() {
let l = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
do_things(&l);
}
81
Parte IV
Día 2: tarde
82
Capítulo 14
Te damos la bienvenida
Incluyendo descansos de 10 minutos, esta sesión debería durar unas 3 horas y 15 minutos.
Contiene:
Sección Duración
Genéricos 45 minutos
Tipos de la Biblioteca Estándar 1 hora
Traits de la biblioteca estándar 1 hora y 10 minutos
83
Capítulo 15
Genéricos
Diapositiva Duración
Funciones genéricas 5 minutos
Tipos de Datos Genéricos 10 minutos
Trait Bounds 10 minutos
impl Trait 5 minutos
dyn Trait 5 minutos
Ejercicio: min genérico 10 minutos
fn main() {
println!("número elegido: {:?}", pick(97, 222, 333));
println!("tupla elegida: {:?}", pick(28, ("perro", 1), ("gato", 2)));
}
This slide should take about 5 minutes.
• Rust infiere un tipo para T en función de los tipos de los argumentos y del valor devuelto.
• Es similar a las plantillas de C++, pero Rust compila de forma parcial la función genérica
de forma inmediata, por lo que debe ser válida para todos los tipos que coincidan con
84
las restricciones. Por ejemplo, prueba a modificar pick para que devuelva even + odd
si n == 0. Aunque solo se use la instanciación pick con números enteros, Rust seguirá
considerando que no es válida. En cambio, C++ lo permitiría.
• Código genérico es convertido en código no genérico basada en los sitios de ejecución
.Se trata de una abstracción sin coste: se obtiene exactamente el mismo resultado que si
se hubiesen programado de forma manual las estructuras de datos sin la abstracción.
impl<T> Point<T> {
fn coords(&self) -> (&T, &T) {
(&self.x, &self.y)
}
fn set_x(&mut self, x: T) {
self.x = x;
}
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
println!("{integer:?} y {float:?}");
println!("coordenadas: {:?}", [Link]());
}
This slide should take about 10 minutes.
• P: ¿Por qué T se especifica dos veces en impl<T> Point<T> {}? ¿No es redundante?
– Esto se debe a que es una sección de implementación genérica para un tipo genérico.
Son genéricos de forma independiente.
– Significa que estos métodos están definidos para cualquier T.
– Es posible escribir impl Point<u32> { .. }.
* Point sigue siendo genérico y puedes usar Point<f64>, pero los métodos de
este bloque solo estarán disponibles para Point<u32>.
• Prueba a declarar una nueva variable let p = Punto { x: 5, y: 10.0 };.
Actualiza el código para permitir que haya puntos que tengan elementos de diferentes
tipos con dos variables de tipo, por ejemplo, T y U.
85
15.3 Traits Genéricos
Los traits también pueden ser genéricos, como los tipos y las funciones. Los parámetros de
un trait obtienen tipos concretos cuando es usado.
#[derive(Debug)]
struct Foo(String);
fn main() {
let from_int = Foo::from(123);
let from_bool = Foo::from(true);
println!("{from_int:?}, {from_bool:?}");
}
• El trait From sera discutido mas tarde, pero su definición en la documentación std es
simple.
• Las implementaciones del trait no necesitan cubrir todos los parámetros de tipo
posibles. Aquí, Foo::from("hello") no compilaría porque no hay una implementación
From<&str> para Foo.
• Tipos genéricos toman tipos como entradas, mientras tipos asociados son tipos de salida.
Un trait puede tener varias implementaciones para diferentes tipos de entrada.
• De hecho, Rust requiere que a lo más solo una implementación de un trait coincida
con cualquier tipo T. A diferencia de otros lenguajes, Rust no tiene una heurística para
escoger la coincidencia ”más especifica”. Hay trabajo corriente para implementar esta
heurística, llamado especialización.
// struct NotClonable;
86
fn main() {
let foo = String::from("foo");
let pair = duplicate(foo);
println!("{pair:?}");
}
This slide should take about 8 minutes.
• Prueba a crear un NonClonable y pásalo a duplicable.
• Si se necesitan varios traits, usa + para unirlos.
• Muestra una cláusula where para que los alumnos la encuentren al leer el código.
fn duplicate<T>(a: T) -> (T, T)
where
T: Clone,
{
([Link](), [Link]())
}
– Despeja la firma de la función si tienes muchos parámetros.
– Tiene funciones adicionales para que sea más potente.
* Si alguien pregunta, la función adicional es que el tipo que está a la izquierda
de ”:” puede ser arbitrario, como Option<T>.
• Ten en cuenta que Rust (todavía) no admite especialización. Por ejemplo, dado el
duplicate, original, no es válido añadir un duplicate(a: u32) especializado.
fn main() {
let many = add_42_millions(42_i8);
println!("{many}");
let many_more = add_42_millions(10_000_000);
println!("{many_more}");
let debuggable = pair_of(27);
println!("depurable: {debuggable:?}");
}
87
This slide should take about 5 minutes.
impl Trait te deja trabajar con tipos que no puedes nombrar. El significado de impl Trait
es un poco diferente dependiendo de su posición.
• En el caso de los parámetros, impl Trait es como un parámetro genérico anónimo
con un límite de trait.
• En el caso de un tipo de resultado devuelto, significa que este es un tipo concreto que
implementa el trait, sin nombrar el tipo. Esto puede ser útil cuando no quieres exponer
el tipo concreto en una API pública.
La inferencia es más complicada en la posición de retorno. Una función que devuelve
impl Foo elige el tipo concreto que devuelve, sin escribirlo en el código fuente.
Una función que devuelve un tipo genérico como collect<B>() -> B puede
devolver cualquier tipo que cumpla B, y es posible que el llamador tenga que
elegir uno, como con let x: Vec<_> = [Link]() o con la sintaxis turbofish,
[Link]::<Vec<_>>().
¿Cuál es el tipo de debuggable? Prueba con let debuggable: () = .. para ver lo que
muestra el mensaje de error.
trait Pet {
fn talk(&self) -> String;
}
88
println!("Hola, quien eres? {}", [Link]());
}
fn main() {
let cat = Cat { lives: 9 };
let dog = Dog { name: String::from("Fido"), age: 5 };
generic(&cat);
generic(&dog);
dynamic(&cat);
dynamic(&dog);
}
This slide should take about 5 minutes.
• Genéricos, incluyendo impl Trait, utilizan monomorphización para crear una
instancia especializada de la funcionan para cada tipo con el cual el genérico es
instanciando. Esto significa que llamar un método de trait dentro de una función
generica todavía usa despacho estático, ya que el compilador tiene todo la información
necesaria para determinar el tipo cuya implementación debería de usar.
• dyn Trait utiliza despacho dinámico con una tabla virtual de metodos (vtable).
Esto significa que solo hay una sola version de fn dynamic que es utilizado
independientemente del tipo de Pet que es proveído.
• Cuando uno usa dyn Trait, el objeto trait necesita estar detrás algún tipo de indirección.
En este caso es una referencia, pero tipos de puntador inteligentes como Box también
pueden ser usados (demostraremos este durante el día 3).
• Durante el tiempo de ejecución, un &dyn Pet es representado como un ”puntador
gordo”, es decir un par de dos puntadores: Un puntador apunta al objeto concreto que
implementa Pet, y el otra apunta al vtable para la implementación del trait para ese tipo.
Cuando uno llama el método talk sobre &dyn Pet, el compilador busca el puntador de
función para talk en el vtable y ejecuta la función, pasando el puntador al Dog o Cat a
esa función. El compilador no necesita saber el tipo concreto del Pet para hacer esto.
• Un dyn Trait es considerado ser ”tipo-borrado”, ya que no tenemos información sobre
el tipo concreto del objeto al tiempo de compilación.
89
fn main() {
assert_eq!(min(0, 10), 0);
assert_eq!(min(500, 123), 123);
15.7.1 Solución
use std::cmp::Ordering;
fn main() {
assert_eq!(min(0, 10), 0);
assert_eq!(min(500, 123), 123);
90
Capítulo 16
Diapositiva Duración
Biblioteca estándar 3 minutos
Documentación 5 minutos
Option 10 minutos
Result 5 minutos
String 5 minutos
Vec (Vectores) 5 minutos
HashMap 5 minutos
Ejercicio: Contador 20 minutos
Dedica un tiempo a revisar las páginas de la documentación de cada una de las diapositivas
de esta sección para destacar algunos de los métodos que más se usan.
16.2 Documentación
Rust incluye una amplia documentación. Por ejemplo:
91
• Todos los detalles sobre bucles.
• Tipos primitivos como u8.
• Tipos de la biblioteca estándar como Option o BinaryHeap.
De hecho, puedes documentar tu propio código:
/// Determina si el primer argumento es divisible por el segundo argumento.
///
/// Si el segundo es cero, el resultado será false.
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
if rhs == 0 {
return false;
}
lhs % rhs == 0
}
El contenido se trata como Markdown. Todos los crates de la biblioteca de Rust publicados
se documentan automáticamente en [Link] mediante la herramienta rustdoc. Es propio
documentar todos los elementos públicos de una API usando este patrón.
Para documentar un elemento desde dentro (por ejemplo, dentro de un módulo), utiliza //!
o /*! .. */, denominado como ”comentarios internos del documento”:
//! Este módulo contiene funciones relacionadas con la divisibilidad de números enteros.
This slide should take about 5 minutes.
• Muestra a los alumnos los documentos generados para el crate rand en [Link]
rand.
16.3 Option
Ya hemos visto algunos usos de Option<T>. Almacena un valor de tipo T o nada. Por ejemplo,
String::find devuelve un Option<usize>.
fn main() {
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = [Link]('é');
println!("buscar {position:?} devuelto");
assert_eq!([Link](), 14);
position = [Link]('Z');
println!("buscar {position:?} devuelto");
assert_eq!([Link]("No se ha encontrado el carácter"), 0);
}
This slide should take about 10 minutes.
• Option se usa en muchos contextos, no solo en la biblioteca estándar.
• unwrap devolverá el valor en un elemento Option o un error pánico. expect funciona
de forma similar, pero muestra un mensaje de error.
– Puedes obtener un pánico en None, pero no puedes olvidarte ”de forma accidental”
de seleccionar None.
– Es habitual usar unwrap/expect por todas partes, pero el código de producción
suele gestionar None de una forma más adecuada.
92
• La ”optimización de nicho” significa que Option<T> a menudo tiene el mismo tamaño
en memoria que T.
16.4 Result
Result es parecido a Option, pero indica si una operación se ha completado de forma
correcta o ha fallado, cada una con un tipo diferente. Es genérico: Result<T, E> donde T es
usado en el variante Ok y E en el variante Err.
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("[Link]");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Querido diario: {contents} ({bytes} bytes)");
} else {
println!("No se ha podido leer el contenido del archivo");
}
}
Err(err) => {
println!("No se ha podido abrir el diario: {err}");
}
}
}
This slide should take about 5 minutes.
• Al igual que con Option, el valor correcto se encuentra dentro de Result, lo que obliga
al desarrollador a extraerlo de forma explícita. Esto fomenta la comprobación de errores.
En el caso de que nunca se produzca un error, se puede llamar a unwrap() o a expect(),
y esto también es una señal de la intención del desarrollador.
• La documentación sobre Result es una lectura recomendada. Aunque no se vea durante
este curso, merece la pena mencionarlo. Contiene muchos métodos y funciones prácticos
que ayudan a seguir un estilo de programación funcional.
• Result es el tipo estándar para implementar la gestión de errores, tal y como veremos
el día 4.
16.5 String
String es el búfer de cadena ampliable UTF-8 estándar:
fn main() {
let mut s1 = String::new();
s1.push_str("Hola");
println!("s1: longitud = {}, capacidad = {}", [Link](), [Link]());
93
s2.push_str(&s1);
[Link]('!');
println!("s2: longitud= {}, capacidad = {}", [Link](), [Link]());
94
[Link](9999);
println!("v2: longitud= {}, capacidad = {}", [Link](), [Link]());
16.7 HashMap
Mapa hash estándar con protección frente a ataques HashDoS:
use std::collections::HashMap;
fn main() {
let mut page_counts = HashMap::new();
page_counts.insert("Las aventuras de Huckleberry Finn", 207);
page_counts.insert("Los cuentos de los hermanos Grimm", 751);
page_counts.insert("Orgullo y prejuicio", 303);
if !page_counts.contains_key("Los miserables") {
println!(
"Tenemos información acerca de {} libros, pero no de Los miserables.",
page_counts.len()
);
95
}
for book in ["Orgullo y prejuicio", "Las aventuras de Alicia en el país de las marav
match page_counts.get(book) {
Some(count) => println!("{book}: {count} páginas"),
None => println!("{book} es desconocido."),
}
}
println!("{page_counts:#?}");
}
This slide should take about 5 minutes.
• HashMap no se ha explicado en el preludio y debe conocerse.
• Prueba las siguientes líneas de código. La primera línea comprobará si un libro está
incluido en el hashmap y, si no, devolverá un valor alternativo. La segunda línea
insertará el valor alternativo en el hashmap si el libro no se encuentra.
let pc1 = page_counts
.get("Harry Potter y la piedra filosofal")
.unwrap_or(&336);
let pc2 = page_counts
.entry("Los juegos del hambre".to_string())
.or_insert(374);
• A diferencia de vec!, por desgracia no hay ninguna macro estándar de hashmap!.
– Sin embargo, desde la versión 1.56 de Rust, HashMap implementa [From<[(K, V);
N]>]([Link]
From%3C%5B(K,+V);+N%5D%3E-for-HashMap%3CK,+V,+RandomState%, que nos
permite inicializar fácilmente un mapa hash a partir de un array literal:
let page_counts = HashMap::from([
("Harry Potter y la piedra filosofal".to_string(), 336),
("Los juegos del hambre".to_string(), 374),
]);
• HashMap también se puede crear a partir de cualquier Iterator que genere tuplas de
pares clave-valor.
• Mostraremos HashMap<String, i32> y evitaremos utilizar &str para que los ejemplos
sean más sencillos. Por supuesto, se pueden usar las referencias en las colecciones, pero
pueden dar problemas con el borrow checker.
– Prueba a eliminar to_string() del ejemplo anterior para ver si aún sigue
compilando. ¿Dónde crees que podríamos encontrar problemas?
• Este tipo tiene varios tipos de devolución ”específicos del método”, como std::collections::hash_map::K
96
Estos tipos a menudo aparecen en las búsquedas de la documentación de Rust. Muestra
a los estudiantes la documentación de este tipo y el enlace útil de vuelta al método keys.
/// Counter cuenta el número de veces que se ha visto cada valor de tipo T.
struct Counter {
values: HashMap<u32, u64>,
}
impl Counter {
/// Crea un nuevo Counter.
fn new() -> Self {
Counter {
values: HashMap::new(),
}
}
fn main() {
let mut ctr = Counter::new();
[Link](13);
[Link](14);
[Link](16);
97
[Link](14);
[Link](14);
[Link](11);
for i in 10..20 {
println!("se han visto {} valores iguales a {}", ctr.times_seen(i), i);
}
16.8.1 Solución
use std::collections::HashMap;
use std::hash::Hash;
/// Counter cuenta el número de veces que se ha visto cada valor de tipo T.
struct Counter<T> {
values: HashMap<T, u64>,
}
fn main() {
let mut ctr = Counter::new();
[Link](13);
[Link](14);
[Link](16);
[Link](14);
[Link](14);
[Link](11);
98
for i in 10..20 {
println!("se han visto {} valores iguales a {}", ctr.times_seen(i), i);
}
99
Capítulo 17
Diapositiva Duración
Comparaciones 5 minutos
Operadores 5 minutos
From e Into 5 minutos
Probando 5 minutos
Read y Write 5 minutos
Default, sintaxis de actualización de structs 5 minutos
Cierres 10 minutos
Ejercicio: ROT13 30 minutos
Al igual que con los tipos de biblioteca estándar, dedica tiempo a revisar la documentación
de cada trait.
Esta parte es larga, por lo que recomendamos tomar un descanso al llegar a la mitad.
17.1 Comparaciones
Estos traits permiten comparar valores. Se pueden derivar todos los traits de los tipos que
contengan campos que implementen estos traits.
PartialEq y Eq
PartialEq es una relación de equivalencia parcial, con el método requerido eq y el método
proporcionado ne. Los operadores == y != llamarán a estos métodos.
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
100
[Link] == [Link]
}
}
Eq es una relación de equivalencia completa (reflexiva, simétrica y transitiva) e implica
PartialEq. Las funciones que requieren una equivalencia total usan Eq como límite del
trait.
PartialOrd y Ord
PartialOrd define un orden parcial, con un método partial_cmp. Se usa para implementar
los operadores <, <=, >= y >.
use std::cmp::Ordering;
#[derive(Eq, PartialEq)]
struct Citation {
author: String,
year: u32,
}
impl PartialOrd for Citation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match [Link].partial_cmp(&[Link]) {
Some(Ordering::Equal) => [Link].partial_cmp(&[Link]),
author_ord => author_ord,
}
}
}
Ord es un orden total en el que cmp devuelve Ordering.
This slide should take about 5 minutes.
PartialEq se puede implementar entre diferentes tipos, pero Eq no, ya que es reflexivo:
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq<u32> for Key {
fn eq(&self, other: &u32) -> bool {
[Link] == *other
}
}
En la práctica, es habitual derivar estos traits, aunque no se suelen implementar.
17.2 Operadores
La sobrecarga de operadores se implementa mediante traits en std::ops:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
101
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: 100, y: 200 };
println!("{:?} + {:?} = {:?}", p1, p2, p1 + p2);
}
This slide should take about 5 minutes.
Cuestiones de debate:
• ¿En qué situaciones sería útil implementar Add para &Point?
– Respuesta: Add:add consume a self. Si el tipo T para el que se sobrecarga el
operador no es Copy, deberías plantearte también sobrecargar el operador para
&T. Así se evita la clonación innecesaria en el sitio de la llamada.
• ¿Por qué Output es un tipo asociado? ¿Se podría convertir en un parámetro tipo del
método?
– Respuesta corta: el llamador controla los parámetros tipo de la función, pero los
tipos asociados (como Output) son controlados por el implementador de un trait.
• Se podría implementar Add para dos tipos distintos; por ejemplo, impl Add<(i32,
i32)> for Point añadiría una tupla a un Point.
102
This slide should take about 5 minutes.
• Por eso se suele implementar solo From, ya que el tipo ya habrá implementado también
Into.
• Cuando se declara un tipo de entrada de argumento de función como ”cualquier
elemento que se pueda convertir en String”, la regla es la contraria y se debe usar Into.
La función aceptará tipos que implementen From y aquellos que solo implementen
Into.
17.4 Probando
Rust no tiene conversiones de tipo implícitas, pero admite conversiones explícitas con as. Por
lo general, se definen según la semántica de C.
fn main() {
let value: i64 = 1000;
println!("ya que u16: {}", value as u16);
println!("ya que i16: {}", value as i16);
println!("ya que u8: {}", value as u8);
}
Los resultados de as se definen siempre en Rust y son coherentes en todas las plataformas. Es
posible que no coincida con tu idea de cambiar el signo o convertirlo a otro de menor tamaño.
Consulta los documentos y/o pregunta si tienes cualquier duda.
La conversión con as es una herramienta relativamente precisa y fácil de usar de forma
incorrecta. Puede ser una fuente de pequeños errores, ya que los futuros trabajos de
mantenimiento cambian los tipos que se usan o los intervalos de valores de los tipos. Las
conversiones se utilizan únicamente cuando se quiere indicar un truncamiento incondicional
(por ejemplo, seleccionando los 32 bits inferiores de un u64 con as u32, independientemente
del elemento que se encontrase en los bits altos).
En el caso de las conversiones que no sean falibles (por ejemplo, u32 a u64), se recomienda
utilizar From o Into en lugar de as para confirmar que la conversión es precisamente infalible.
En el caso de las conversiones falibles, TryFrom y TryInto están disponibles cuando necesitas
gestionar conversiones que se ajustan de forma diferente a las que no lo hacen.
This slide should take about 5 minutes.
Plantéate hacer una pausa después de esta diapositiva.
as es similar a una conversión estática de C++. En general, se desaconseja el uso de as en
los casos en los que puedan perderse datos, o al menos se recomienda dejar un comentario
explicativo.
Esto es habitual al convertir números enteros a usize para usarlos como índice.
103
let buf_reader = BufReader::new(reader);
buf_reader.lines().count()
}
#[derive(Debug)]
struct Implemented(String);
fn main() {
104
let default_struct = Derived::default();
println!("{default_struct:#?}");
let almost_default_struct =
Derived { y: "Ya está configurado.".into(), ..Derived::default() };
println!("{almost_default_struct:#?}");
17.7 Cierres
Los cierres o expresiones lambda tienen tipos que no pueden nombrarse. Sin embargo,
implementan traits especiales Fn, FnMut y FnOnce:
fn apply_with_log(func: impl FnOnce(i32) -> i32, input: i32) -> i32 {
println!("Llamado función sobre {input}");
func(input)
}
fn main() {
let add_3 = |x| x + 3;
println!("add_3: {}", apply_with_log(add_3, 10));
println!("add_3: {}", apply_with_log(add_3, 20));
105
This slide should take about 10 minutes.
Un Fn (por ejemplo, add_3) no consume ni modifica los valores capturados, o quizá no captura
nada en absoluto. Se puede llamar varias veces al mismo tiempo.
Un FnMut (por ejemplo, accumulate) puede modificar los valores capturados. Se puede
llamar varias veces, pero no de forma simultánea.
Si tienes un FnOnce (por ejemplo, multiply_sum), solo puedes llamarlo una vez. Puede
consumir valores capturados.
FnMut es un subtipo de FnOnce, mientras que Fn es un subtipo de FnMut y FnOnce. Es decir,
puedes utilizar un FnMut siempre que se llame a un FnOnce, y puedes usar un Fn siempre
que se llame a un FnMut o a un FnOnce.
Cuando defines una función que admite un closure, debes usar FnOnce si es posible (es decir,
se llama una vez) o, en su defecto, FnMut. En último lugar estaría Fn. De esta forma, se ofrece
la máxima flexibilidad al llamador.
Por el contrario, cuando tienes un cierre (closure), lo más flexible que puedes tener es Fn (se
puede transmitir en todas partes), a continuación FnMut y, por último, FnOnce.
El compilador también infiere Copy (por ejemplo, add_3) y Clone (por ejemplo,
multiply_sum), dependiendo de lo que capture el cierre.
De forma predeterminada, los cierres capturan, si pueden, por referencia. La palabra clave
move hace que capturen por valor.
fn make_greeter(prefix: String) -> impl Fn(&str) {
return move |name| println!("{} {}", prefix, name);
}
fn main() {
let hi = make_greeter("¿Qué".to_string());
hi("Greg");
}
fn main() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
106
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
println!("{}", result);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn joke() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
assert_eq!(&result, "To get to the other side!");
}
#[test]
fn binary() {
let input: Vec<u8> = (0..=255u8).collect();
let mut rot = RotDecoder::<&[u8]> { input: input.as_ref(), rot: 13 };
let mut buf = [0u8; 256];
assert_eq!([Link](&mut buf).unwrap(), 256);
for i in 0..=255 {
if input[i] != buf[i] {
assert!(input[i].is_ascii_alphabetic());
assert!(buf[i].is_ascii_alphabetic());
}
}
}
}
¿Qué ocurre si encadenas dos instancias RotDecoder y cada una de ellas rota 13 posiciones?
17.8.1 Solución
use std::io::Read;
107
}
}
Ok(size)
}
}
fn main() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
println!("{}", result);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn joke() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
assert_eq!(&result, "To get to the other side!");
}
#[test]
fn binary() {
let input: Vec<u8> = (0..=255u8).collect();
let mut rot = RotDecoder::<&[u8]> { input: input.as_ref(), rot: 13 };
let mut buf = [0u8; 256];
assert_eq!([Link](&mut buf).unwrap(), 256);
for i in 0..=255 {
if input[i] != buf[i] {
assert!(input[i].is_ascii_alphabetic());
assert!(buf[i].is_ascii_alphabetic());
}
}
}
}
108
Parte V
Día 3: Mañana
109
Capítulo 18
Horario
Contando con los descansos de 10 minutos, la duración prevista de la sesión es de unas 2
horas y 20 minutos. Contiene:
Sección Duración
Te damos la bienvenida 3 minutos
Manejo de Memoria 1 hora
Punteros inteligentes 55 minutos
110
Capítulo 19
Manejo de Memoria
Diapositiva Duración
Revisión de la memoria de programas 5 minutos
Métodos de Gestión de Memoria 10 minutos
Ownership 5 minutos
Semántica de movimiento 5 minutos
Trait Clone 2 minutos
Tipos Copy 5 minutos
Trait Drop 10 minutos
Ejercicio: Constructores 20 minutos
Ejemplo
Al crear un String, los metadatos de tamaño fijo se colocan en la stack y los datos de tamaño
dinámico (la cadena real) en el heap:
111
fn main() {
let s1 = String::from("Hola");
}
Stack
.- - - - - - - - - - - - - -. Heap
: : .- - - - - - - - - - - - - - - -.
: s1 : : :
: +-----------+-------+ : : :
: | capacity | 5 | : : +----+----+----+----+----+ :
: | ptr | o-+---+-----+-->| H | e | l | l | o | :
: | len | 5 | : : +----+----+----+----+----+ :
: +-----------+-------+ : : :
: : : :
`- - - - - - - - - - - - - -' `- - - - - - - - - - - - - - - -'
This slide should take about 5 minutes.
• Menciona que un String está respaldado por un Vec, por lo que tiene capacidad y
longitud y, si es mutable, puede crecer mediante reasignación en el heap.
• Si los alumnos lo preguntan, puedes mencionar que la memoria subyacente recibe una
asignación de heap mediante el Asignador del Sistema y que se pueden implementar
asignadores personalizados mediante el Allocator API.
Más información
Podemos inspeccionar la disposición de la memoria con código unsafe. Sin embargo, debes
señalar que esto no es seguro.
fn main() {
let mut s1 = String::from("Hola");
[Link](' ');
s1.push_str("mundo");
// ¡NO HAGÁIS ESTO EN CASA! Solo con fines educativos.
// La cadena no proporciona garantías sobre su diseño, por lo que podría desencadena
// un comportamiento indefinido.
unsafe {
let (capacity, ptr, len): (usize, usize, usize) = std::mem::transmute(s1);
println!("capacidad = {capacity}, ptr = {ptr:#x}, len = {len}");
}
}
112
– Un sistema de tiempo de ejecución asegura que la memoria no se libera hasta que
ya no se pueda hacer referencia a ella.
– Normalmente se implementa con un contador de referencias, la recolección de
elementos no utilizados o RAII.
Rust ofrece una mezcla de ambas:
Control completo y seguridad completa gracias a que el compilador se encarga del
manejo correcto de la memoria.
Para ello, se utiliza un concepto de ownership (propiedad) explícito.
This slide should take about 10 minutes.
El objetivo de esta diapositiva es ayudar a los estudiantes de otros lenguajes a entender mejor
Rust.
• C debe gestionar el montículo de forma manual con malloc y free. Entre los errores
habituales se incluyen olvidarse de llamar a free, llamarlo varias veces para el mismo
puntero o desreferenciar un puntero después de que se haya liberado la memoria a la
que apunta.
• C++ tiene herramientas como los punteros inteligentes (unique_ptr y shared_ptr) que
aprovechan las garantías del lenguaje sobre la llamada a destructores para garantizar
que la memoria se libere cuando se devuelva una función. Sin embargo, es fácil hacer
un uso inadecuado de estas herramientas y crear errores similares a los de C.
• Java, Go y Python utilizan el recolector de elementos no utilizados para identificar la
memoria a la que ya no se puede acceder y descartarla. Esto asegura que se pueda
desreferenciar cualquier puntero, de forma que se eliminan los errores use-after-free y
otros tipos de errores. Sin embargo, el recolector de elementos no utilizados tiene un
coste de tiempo de ejecución y es difícil ajustarlo adecuadamente.
El modelo de propiedad y préstamo de Rust puede, en muchos casos, permitir obtener el
rendimiento de C, con operaciones asignadas y libres donde se necesiten y sin coste. También
proporciona herramientas similares a los punteros inteligentes de C++. Si es necesario,
hay disponibles otras opciones, como el recuento de referencias, e incluso hay crates de
terceros que admiten la recolección de elementos no utilizados del tiempo de ejecución (estos
elementos no se tratan en esta clase).
19.3 Ownership
Todos los enlaces a variables tienen un ámbito donde son válidos y se produce un error
cuando se usan fuera de él:
struct Point(i32, i32);
fn main() {
{
let p = Point(3, 4);
println!("x: {}", p.0);
}
println!("y: {}", p.1);
}
113
Decimos que el valor pertenece a la variable. Cada valor en Rust tiene exactamente un dueño
en todo tiempo.
Al final del ámbito, la variable se elimina y los datos se liberan. Un destructor puede correr
en este momento para librar recursos.
This slide should take about 5 minutes.
Los participantes que estén familiarizados con las implementaciones de recolección de
elementos no utilizados sabrán que este tipo de recolector comienza con un conjunto de
”raíces” para buscar toda la memoria disponible. El principio de ”propietario único” de Rust
es una idea similar.
114
: : | `- - - - - - - - - - - - - - - - - - -'
: s2 : |
: +-----------+-------+ : |
: | ptr | o---+---+--'
: | len | 6 | :
: | capacity | 6 | :
: +-----------+-------+ :
: :
`- - - - - - - - - - - - - -'
Cuando pasas un valor a una función, el valor se asigna al parámetro de la función. Esta
acción transfiere el ownership:
fn say_hello(name: String) {
println!("Hola {name}")
}
fn main() {
let name = String::from("Alice");
say_hello(name);
// say_hello(name);
}
This slide should take about 5 minutes.
• Menciona que es lo contrario de los valores predeterminados de C++, que se copian
por valor, a menos que utilices std::move (y que el constructor de movimiento esté
definido).
• Es únicamente el ownership el que se mueve. Si se genera algún código máquina para
manipular los datos en sí, se trata de una cuestión de optimización, y esas copias se
optimizan de forma agresiva.
• Los valores simples (como los enteros) se pueden marcar como Copy (consulta las
diapositivas posteriores).
• En Rust, la clonación es explícita (usando clone).
En el ejemplo de say_hello:
• Con la primera llamada a say_hello, main deja de tener el ownership de name. Después,
ya no se podrá usar name dentro de main.
• La memoria de heap asignada a name se liberará al final de la función say_hello.
• main podrá conservar el _ownership_ si pasaname como referencia
(&name) y si say_hello‘ acepta una referencia como parámetro.
• Por otro lado, main puede pasar un clon de name en la primera llamada ([Link]()).
• Rust hace que resulte más difícil que en C++ crear copias por error al definir la semántica
de movimiento como predeterminada y al obligar a los programadores a clonar sólo de
forma explícita.
Más información
Copias Defensivas en C++ Moderno
La versión moderna de C++ soluciona este problema de forma diferente:
115
std::string s1 = "Cpp";
std::string s2 = s1; // Duplica los datos en s1.
• Los datos de la stack de s1 se duplican y s2 obtiene su propia copia independiente.
• Cuando s1 y s2 salen del ámbito, cada uno libera su propia memoria.
Antes de la asignación de copias:
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - -.
: : : :
: s1 : : :
: +-----------+-------+ : : +----+----+----+ :
: | ptr | o---+---+--+--+-->| C | p | p | :
: | len | 3 | : : +----+----+----+ :
: | capacity | 3 | : : :
: +-----------+-------+ : : :
: : `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'
Después de la asignación de copia:
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - -.
: : : :
: s1 : : :
: +-----------+-------+ : : +----+----+----+ :
: | ptr | o---+---+--+--+-->| C | p | p | :
: | len | 3 | : : +----+----+----+ :
: | capacity | 3 | : : :
: +-----------+-------+ : : :
: : : :
: s2 : : :
: +-----------+-------+ : : +----+----+----+ :
: | ptr | o---+---+-----+-->| C | p | p | :
: | len | 3 | : : +----+----+----+ :
: | capacity | 3 | : : :
: +-----------+-------+ : : :
: : `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'
Puntos clave:
• C++ ha tomado una decisión algo distinta a Rust. Puesto que = copia los datos, los datos
de cadena deben clonarse. De lo contrario, obtendríamos un error double free si alguna
de las cadenas saliera fuera del ámbito.
• C++ también tiene std::move, que se usa para indicar cuándo se puede mover un
valor. Si el ejemplo hubiera sido s2 = std::move(s1), no se llevaría a cabo ninguna
asignación de montículo. Después del movimiento, s1 tendría un estado válido, pero no
especificado. A diferencia de Rust, el programador puede seguir utilizando s1.
• A diferencia de Rust, en C++ se puede ejecutar código arbitrario con = según el tipo que
se vaya a copiar o mover.
116
19.5 Trait Clone
Cuando queramos hacer una copia de un valor, podemos hacerlo con el trait Clone.
fn say_hello(name: String) {
println!("Hola {name}")
}
fn main() {
let name = String::from("Alice");
say_hello([Link]());
say_hello(name);
}
This slide should take about 2 minutes.
• La función de Clone es poder encontrar fácilmente dónde se producen las asignaciones
al heap. Busca a .clone() y algunos otros como vec! o Box::new.
• Es habitual ”clonar para salir” de los problemas con el verificador de préstamos y volver
más tarde para optimizar esos clones.
• clone generalmente realiza una copia a fondo del valor. Por ejemplo, si clonas un array,
todos los elementos del array tambien son clonados.
• El comportamiento de clone es definido por el usuario, entonces puede realizar lógica
personalizada de clonación si es necesario.
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 tienen sus propios datos.
117
• También podemos utilizar [Link]() para copiar los datos de forma explícita.
This slide should take about 5 minutes.
Copiar y clonar no es lo mismo:
• Copiar hace referencia a las copias bit a bit de regiones de memoria y no funciona en
cualquier objeto.
• Copiar no permite lógica personalizada (a diferencia de los constructores de copias de
C++).
• Clonar es una operación más general y que permite un comportamiento personalizado
implementando el trait Clone.
• Copiar no funciona en los tipos que implementan el trait Drop.
En el ejemplo anterior, prueba lo siguiente:
• Añade un campo String a struct Point. No se compilará porque String no es de
tipo Copy.
• Elimina Copy del atributo derive. El error del compilador se encuentra ahora en
println! para p1.
• Demuestra que funciona si clonas p1.
Más información
• Referencias compartidas son Copy/Clone, pero referencias mutables no lo son. Esto es
porque Rust requiere que las referencias mutables sean exclusivas. Esto significa que es
valido hacer una copia de una referencia compartida, pero hacer lo mismo para una
referencia mutable violaría las reglas de préstamo de Rust.
fn main() {
let a = Droppable { name: "a" };
{
let b = Droppable { name: "b" };
{
let c = Droppable { name: "c" };
let d = Droppable { name: "d" };
println!("Saliendo del bloque B");
118
}
println!("Saliendo del bloque A");
}
drop(a);
println!("Saliendo de la página principal");
}
This slide should take about 8 minutes.
• Ten en cuenta que std::mem::drop no es igual que std::ops::Drop::drop.
• Los valores se suprimen automáticamente cuando salen del ámbito.
• Cuando se elimina un valor, si implementa std::ops::Drop, se llamará a su
implementación Drop::drop.
• También se suprimirán todos sus campos, independientemente de si implementa o no
Drop.
• std::mem::drop es solo una función vacía que toma cualquier valor. Es importante
saber que toma la propiedad del valor, por lo que se descarta al final de su ámbito. Se
trata de una forma sencilla de suprimir los valores de forma explícita antes que si se
salen de su ámbito.
– Esta acción puede ser útil para los objetos que trabajan con drop, como liberando
bloqueos, cerrando archivos, etc.
Cuestiones de debate:
• ¿Por qué Drop::drop no acepta self?
– Respuesta corta: si lo hiciera, std::mem::drop sería llamado al final del bloque, lo
que daría como resultado otra llamada a Drop::drop, ¡y un desbordamiento del
stack!
• Prueba a sustituir drop(a) por [Link]().
#[derive(Clone, Debug)]
struct Dependency {
name: String,
version_expression: String,
}
119
struct Package {
name: String,
version: String,
authors: Vec<String>,
dependencies: Vec<Dependency>,
language: Option<Language>,
}
impl Package {
/// Devuelve una representación de este paquete como una dependencia para usarla
/// en la compilación de otros paquetes.
fn as_dependency(&self) -> Dependency {
todo!("1")
}
}
impl PackageBuilder {
fn new(name: impl Into<String>) -> Self {
todo!("2")
}
fn main() {
120
let base64 = PackageBuilder::new("base64").version("0.13").build();
println!("base64: {base64:?}");
let log =
PackageBuilder::new("log").version("0.4").language(Language::Rust).build();
println!("registro: {log:?}");
let serde = PackageBuilder::new("serde")
.authors(vec!["djmitche".into()])
.version(String::from("4.0"))
.dependency(base64.as_dependency())
.dependency(log.as_dependency())
.build();
println!("serde: {serde:?}");
}
19.8.1 Solución
#[derive(Debug)]
enum Language {
Rust,
Java,
Perl,
}
#[derive(Clone, Debug)]
struct Dependency {
name: String,
version_expression: String,
}
impl Package {
/// Devuelve una representación de este paquete como una dependencia para usarla
/// en la compilación de otros paquetes.
fn as_dependency(&self) -> Dependency {
Dependency {
name: [Link](),
version_expression: [Link](),
}
}
}
121
struct PackageBuilder(Package);
impl PackageBuilder {
fn new(name: impl Into<String>) -> Self {
Self(Package {
name: [Link](),
version: "0.1".into(),
authors: vec![],
dependencies: vec![],
language: None,
})
}
fn main() {
let base64 = PackageBuilder::new("base64").version("0.13").build();
println!("base64: {base64:?}");
let log =
PackageBuilder::new("log").version("0.4").language(Language::Rust).build();
println!("registro: {log:?}");
let serde = PackageBuilder::new("serde")
.authors(vec!["djmitche".into()])
.version(String::from("4.0"))
122
.dependency(base64.as_dependency())
.dependency(log.as_dependency())
.build();
println!("serde: {serde:?}");
}
123
Capítulo 20
Punteros inteligentes
Diapositiva Duración
Box 10 minutos
Rc 5 minutos
Objetos Trait Poseídos 10 minutos
Ejercicio: Árbol binario 30 minutos
20.1 Box<T>
Box es un puntero propio de datos en el heap:
fn main() {
let five = Box::new(5);
println!("cinco: {}", *five);
}
Stack Heap
.- - - - - - -. .- - - - - - -.
: : : :
: five : : :
: +-----+ : : +-----+ :
: | o---|---+-----+-->| 5 | :
: +-----+ : : +-----+ :
: : : :
: : : :
`- - - - - - -' `- - - - - - -'
Box<T> implementa Deref<Target = T>, lo que significa que puedes llamar a métodos
desde T directamente en un Box<T>.
Los tipos de datos recursivos o los tipos de datos con tamaños dinámicos deben utilizar un
Box:
124
#[derive(Debug)]
enum List<T> {
/// Una lista no vacía: el primer elemento y el resto de la lista.
Element(T, Box<List<T>>),
/// Una lista vacía.
Nil,
}
fn main() {
let list: List<i32> =
List::Element(1, Box::new(List::Element(2, Box::new(List::Nil))));
println!("{list:?}");
}
Stack Heap
.- - - - - - - - - - - - - - . .- - - - - - - - - - - - - - - - - - - - - - - - -.
: : : :
: list : : :
: +---------+----+----+ : : +---------+----+----+ +------+----+----+ :
: | Element | 1 | o--+----+-----+--->| Element | 2 | o--+--->| Nil | // | // | :
: +---------+----+----+ : : +---------+----+----+ +------+----+----+ :
: : : :
: : : :
'- - - - - - - - - - - - - - ' '- - - - - - - - - - - - - - - - - - - - - - - - -'
This slide should take about 8 minutes.
• Box es igual que std::unique_ptr en C++, salvo que está asegurado que no será nulo.
• Un Box puede resultar útil en los siguientes casos:
– tiene un tipo cuyo tamaño no se conoce durante la compilación, pero el compilador
de Rust quiere saber el tamaño exacto.
– quieres transferir la propiedad (”ownership”) de una gran cantidad de datos. Para
evitar que se copien grandes cantidades de datos en el stack, almacena los datos
del heap en un Box para que solo se mueva el puntero.
• Si no utilizamos Box e intentamos insertar un List directamente dentro de List, el
compilador no podría calcular un tamaño fijo de la estructura en la memoria (List
tendría un tamaño infinito).
• Box resuelve este problema, ya que tiene el mismo tamaño que un puntero normal y
solo apunta al siguiente elemento de la List en el heap.
• Elimina Box de la definición de la lista y muestra el error del compilador. El mensaje
”recursivo con indirección” es una sugerencia de que debes usar un Box o referencia de
algún tipo en lugar de almacenar un valor directamente.
Más información
Optimización de la Memoria
Aunque Box se parece a std::unique_ptr en C++, no puede ser vació o nulo. Esto hace
Box uno de los tipos que permite que el compilador optimice el almacenaje de ciertas
125
enumeraciones.
Por ejemplo, Option<Box<T>> tiene el mismo tamaño que Box<T>, ya que el compilador usa
el valor nulo para discriminar variantes en vez de usar una etiqueta explicita (”Null Pointer
Optimization”):
use std::mem::size_of_val;
struct Item(String);
fn main() {
let just_box: Box<Item> = Box::new(Item("Solo box".into()));
let optional_box: Option<Box<Item>> =
Some(Box::new(Item("Box opcional".into())));
let none: Option<Box<Item>> = None;
assert_eq!(size_of_val(&just_box), size_of_val(&optional_box));
assert_eq!(size_of_val(&just_box), size_of_val(&none));
20.2 Rc
Rc es un puntero compartido de referencia contada. Utilízalo cuando necesites hacer
referencia a los mismos datos desde varios lugares:
use std::rc::Rc;
fn main() {
let a = Rc::new(10);
let b = Rc::clone(&a);
println!("a: {a}");
println!("b: {b}");
}
• Consulta Arc y Mutex si te encuentras en un contexto multihilo.
• Puedes degradar un puntero compartido en un puntero Weak para crear ciclos que se
abandonarán.
This slide should take about 5 minutes.
• El recuento de Rc asegura que el valor que contiene sea válido mientras haya referencias.
• Rc en Rust es como std::shared_ptr en C++.
• Rc::clone es simple: crea un puntero en la misma asignación y aumenta el recuento
de referencias. No hace clones completos y, por lo general, se puede ignorar cuando se
buscan problemas de rendimiento en el código.
• make_mut clona el valor interno si es necesario (”copiar al escribir”) y devuelve una
referencia mutable.
• Comprueba el recuento de referencias con Rc::strong_count.
126
• Rc::downgrade ofrece un objeto de referencia contada debil para crear ciclos que que
se borraran propiamente (probablemente en combinación con RefCell).
trait Pet {
fn talk(&self) -> String;
}
fn main() {
let pets: Vec<Box<dyn Pet>> = vec![
Box::new(Cat { lives: 9 }),
Box::new(Dog { name: String::from("Fido"), age: 5 }),
];
for pet in pets {
println!("Hola, quien eres? {}", [Link]());
}
}
Diseño de la memoria después de asignar pets:
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - - - - - -.
: : : :
: "pets: Vec<dyn Pet>" : : "data: Cat" +----+----+----+----+ :
: +-----------+-------+ : : +-------+-------+ | F | i | d | o | :
: | ptr | o---+---+--. : | lives | 9 | +----+----+----+----+ :
127
: | len | 2 | : | : +-------+-------+ ^ :
: | capacity | 2 | : | : ^ | :
: +-----------+-------+ : | : | '-------. :
: : | : | data:"Dog"| :
: : | : | +-------+--|-------+ :
`- - - - - - - - - - - - - -' | : +---|-+-----+ | name | o, 4, 4 | :
`--+-->| o o | o o-|----->| age | 5 | :
: +-|---+-|---+ +-------+----------+ :
: | | :
`- - -| - - |- - - - - - - - - - - - - - - - -'
| |
| | "Program text"
.- - -| - - |- - - - - - - - - - - - - - - - -.
: | | vtable :
: | | +----------------------+ :
: | `----->| "<Dog as Pet>::talk" | :
: | +----------------------+ :
: | vtable :
: | +----------------------+ :
: '----------->| "<Cat as Pet>::talk" | :
: +----------------------+ :
: :
'- - - - - - - - - - - - - - - - - - - - - - -'
This slide should take about 10 minutes.
• Los tipos que implementan un trait pueden tener diferentes tamaños. Esto hace
imposible tener elementos como Vec<dyn Pet> en el ejemplo anterior.
• dyn Pet es una forma de indicar al compilador un tipo de tamaño dinámico que
implementa Pet.
• En este ejemplo, pets es alocado sobre el stack y los datos del vector sobre el heap. Los
dos elementos del vector son punteros gordos:
– Un puntero gordo es un puntero de tamaño doble. Tiene dos componentes: un
puntero al objeto y un puntero a la tabla virtual de métodos (vtable) para la
implementación de Pet de ese objeto.
– Los datos para el Dog llamado Fido son los campos name y age. El Cat tiene un
campo lives.
• Compara estas salidas en el ejemplo anterior:
println!("{} {}", std::mem::size_of::<Dog>(), std::mem::size_of::<Cat>());
println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>());
println!("{}", std::mem::size_of::<&dyn Pet>());
println!("{}", std::mem::size_of::<Box<dyn Pet>>());
128
Implementa los siguientes tipos para superar las pruebas correspondientes.
Ejercicio adicional: implementar un iterador sobre un árbol binario que devuelva los valores
en orden.
/// Un nodo del árbol binario.
#[derive(Debug)]
struct Node<T: Ord> {
value: T,
left: Subtree<T>,
right: Subtree<T>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn len() {
let mut tree = BinaryTree::new();
129
assert_eq!([Link](), 0);
[Link](2);
assert_eq!([Link](), 1);
[Link](1);
assert_eq!([Link](), 2);
[Link](2); // No es un elemento único.
assert_eq!([Link](), 2);
}
#[test]
fn has() {
let mut tree = BinaryTree::new();
fn check_has(tree: &BinaryTree<i32>, exp: &[bool]) {
let got: Vec<bool> =
(0..[Link]()).map(|i| [Link](&(i as i32))).collect();
assert_eq!(&got, exp);
}
#[test]
fn unbalanced() {
let mut tree = BinaryTree::new();
for i in 0..100 {
[Link](i);
}
assert_eq!([Link](), 100);
assert!([Link](&50));
}
}
20.4.1 Solución
use std::cmp::Ordering;
130
/// Un subárbol posiblemente vacío.
#[derive(Debug)]
struct Subtree<T: Ord>(Option<Box<Node<T>>>);
131
Ordering::Less => [Link](value),
Ordering::Equal => true,
Ordering::Greater => [Link](value),
},
}
}
fn main() {
let mut tree = BinaryTree::new();
[Link]("foo");
assert_eq!([Link](), 1);
[Link]("bar");
assert!([Link](&"foo"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn len() {
let mut tree = BinaryTree::new();
assert_eq!([Link](), 0);
[Link](2);
assert_eq!([Link](), 1);
[Link](1);
assert_eq!([Link](), 2);
[Link](2); // No es un elemento único.
assert_eq!([Link](), 2);
}
#[test]
fn has() {
let mut tree = BinaryTree::new();
fn check_has(tree: &BinaryTree<i32>, exp: &[bool]) {
let got: Vec<bool> =
(0..[Link]()).map(|i| [Link](&(i as i32))).collect();
132
assert_eq!(&got, exp);
}
#[test]
fn unbalanced() {
let mut tree = BinaryTree::new();
for i in 0..100 {
[Link](i);
}
assert_eq!([Link](), 100);
assert!([Link](&50));
}
}
133
Parte VI
Día 3: Tarde
134
Capítulo 21
Te damos la bienvenida
Contando con los descansos de 10 minutos, la duración prevista de la sesión es de una horas
y 55 minutos. Contiene:
Sección Duración
Préstamos (Borrowing) 55 minutos
Duraciones de vida 50 minutos
135
Capítulo 22
Préstamos (Borrowing)
Diapositiva Duración
Emprestar (borrow) un valor 10 minutos
Verificación de Préstamos 10 minutos
Errores de Préstamo 3 minutos
Mutabilidad Interior 10 minutos
Ejercicio: Estadísticas de Salud 20 minutos
fn main() {
let p1 = Point(3, 4);
let p2 = Point(10, 20);
let p3 = add(&p1, &p2);
println!("{p1:?} + {p2:?} = {p3:?}");
}
• La función add toma prestados dos puntos y devuelve uno nuevo.
• El llamador conserva el ownership de las entradas.
This slide should take about 10 minutes.
136
En esta diapositiva se repasará el material de las referencias desde día 1 y se ampliará un
poco para incluir los argumentos de las funciones y los valores devueltos.
Más información
Notas sobre la devolución de resultados de la stack:
• Demuestra que la instrucción de retorno de add es barato porque el compilador puede
eliminar la operación de copia. Cambia el código anterior para imprimir las direcciones
de la stack y ejecutarlas en el Playground o consulta el ensamblador en Godbolt. En el
nivel de optimización ”DEBUG”, las direcciones deberían cambiar. Sin embargo, deberían
mantenerse igual modificar la configuración ”RELEASE”:
#[derive(Debug)]
struct Point(i32, i32);
pub fn main() {
let p1 = Point(3, 4);
let p2 = Point(10, 20);
let p3 = add(&p1, &p2);
println!("&p3.0: {:p}", &p3.0);
println!("{p1:?} + {p2:?} = {p3:?}");
}
• El compilador Rust puede hacer enlineamiento automático que puede ser deshabilitado
al nivel de una función con #[inline(never)].
• Una vez deshabilitado, la dirección impresa cambiara en todos los niveles de
optimización. Mirando en Godbolt o Playground, uno puede ver que en este caso el
valor de retorno dependen del ABI, e.g. en amd64 los dos i32 que constituyen el punto
son regresados en 2 registros (eax y edx).
{
let c: &mut i32 = &mut a;
137
*c = 20;
}
println!("a: {a}");
println!("b: {b}");
}
This slide should take about 10 minutes.
• Ten en cuenta que el requisito es que las referencias que están en conflicto no se
encuentren en el mismo punto. No importa en el lugar en el que se desreferencie la
referencia.
• El código anterior no se compila porque a se toma prestada como mutable (a través de
c) y como inmutable (a través de b) al mismo tiempo.
• Mueve la instrucción println! de b antes del ámbito que introduce c para que el código
compile.
• Después de ese cambio, el compilador se da cuenta de que b solo se usa antes del nuevo
préstamo mutable de a a través de c. Se trata de una función del verificador de préstamos
denominada ”tiempo de vida no léxico”.
• La restricción de referencia exclusiva es bastante sólida. Rust la utiliza para asegurarse
de que no se produzcan data races. Rust también se basa en esta restricción para
optimizar el código. Por ejemplo, el valor de una referencia compartida se puede
almacenar en caché de forma segura en un registro durante el tiempo de vida de
dicha referencia.
• El verificador de préstamos está diseñado para adaptarse a muchos patrones comunes,
como tomar referencias exclusivas en diferentes campos de un struct al mismo tiempo.
Sin embargo, hay algunas situaciones en las que ”no lo entiende del todo”, lo que suele
dar lugar a ”conflictos con el comprobador de préstamos.”
138
• En ambos casos, añadir elementos a la colección puede invalidar referencias pre-
existentes a los elementos de la colección si es necesario realizar reasignación.
RefCell
use std::cell::RefCell;
fn main() {
// Nota que `cell` NO es declarado como mutable.
let cell = RefCell::new(5);
{
let mut cell_ref = cell.borrow_mut();
*cell_ref = 123;
println!("{cell:?}");
}
Cell
Cell envuelve un valor y permite obtenerlo o definirlo, incluso con una referencia compartida
a Cell. Sin embargo, no permite obtener referencias al valor. Como no hay referencias, las
reglas de préstamos no pueden quebrantarse.
use std::cell::Cell;
fn main() {
// Nota que `cell` NO es declarado como mutable.
let cell = Cell::new(5);
[Link](123);
println!("{}", [Link]());
}
This slide should take about 10 minutes.
139
Lo más importante de esta diapositiva es que Rust ofrece formas seguras de modificar los
datos subyacentes a una referencia compartida. Hay varias formas de garantizar la seguridad,
como RefCell y Cell.
• RefCell implementa las reglas de préstamos habituales de Rust (varias referencias
compartidas o una única referencia exclusiva) con una comprobación del tiempo de
ejecución. En este caso, todos los préstamos son muy cortos y nunca se solapan, por lo
que las comprobaciones siempre se llevan a cabo de forma correcta.
– El bloque extra en el ejemplo RefCell existe para terminar el préstamo creado por
la llamada a borrow_mut antes de que imprimimos cal celda. Intentando imprimir
una celda RefCell solo enseña el mensaje "{borrowed}".
• Cell es un medio más sencillo de garantizar la seguridad: tiene un método set que
utiliza &self. No es necesario comprobar el tiempo de ejecución, pero sí es necesario
transferir los valores, lo que puede tener su propio coste.
• Ambos RefCell y Cell son !Sync, que significa que &RefCell y &Cell no pueden ser
pasados entre hilos. Esto previene que dos hilos intenten acceder la celda al mismo
tiempo.
#![allow(dead_code)]
pub struct User {
name: String,
age: u32,
height: f32,
visit_count: usize,
last_blood_pressure: Option<(u32, u32)>,
}
140
visit_count: u32,
height_change: f32,
blood_pressure_change: Option<(i32, i32)>,
}
impl User {
pub fn new(name: String, age: u32, height: f32) -> Self {
Self { name, age, height, visit_count: 0, last_blood_pressure: None }
}
fn main() {
let bob = User::new(String::from("Bob"), 32, 155.2);
println!("Me llamo {} y tengo {} años", [Link], [Link]);
}
#[test]
fn test_visit() {
let mut bob = User::new(String::from("Bob"), 32, 155.2);
assert_eq!(bob.visit_count, 0);
let report =
bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (120, 80) });
assert_eq!(report.patient_name, "Bob");
assert_eq!(report.visit_count, 1);
assert_eq!(report.blood_pressure_change, None);
let report =
bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (115, 76) });
assert_eq!(report.visit_count, 2);
assert_eq!(report.blood_pressure_change, Some((-5, -4)));
}
22.5.1 Solución
#![allow(dead_code)]
pub struct User {
name: String,
age: u32,
height: f32,
visit_count: usize,
last_blood_pressure: Option<(u32, u32)>,
}
141
blood_pressure: (u32, u32),
}
impl User {
pub fn new(name: String, age: u32, height: f32) -> Self {
Self { name, age, height, visit_count: 0, last_blood_pressure: None }
}
fn main() {
let bob = User::new(String::from("Bob"), 32, 155.2);
println!("Me llamo {} y tengo {} años", [Link], [Link]);
}
#[test]
fn test_visit() {
let mut bob = User::new(String::from("Bob"), 32, 155.2);
assert_eq!(bob.visit_count, 0);
let report =
bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (120, 80) });
assert_eq!(report.patient_name, "Bob");
assert_eq!(report.visit_count, 1);
assert_eq!(report.blood_pressure_change, None);
let report =
142
bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (115, 76) });
assert_eq!(report.visit_count, 2);
assert_eq!(report.blood_pressure_change, Some((-5, -4)));
}
143
Capítulo 23
Duraciones de vida
Diapositiva Duración
Anotaciones de duración de vida 10 minutos
Elisión de duración de vida 5 minutos
Duraciones de vida de estructuras (structs) 5 minutos
Ejercicio: Análisis de Protobuf 30 minutos
144
}
fn main() {
let p1: Point = Point(10, 10);
let p2: Point = Point(20, 20);
let p3 = left_most(&p1, &p2); // ¿Cuál es el tiempo de vida de p3?
println!("p3: {p3:?}");
}
This slide should take about 10 minutes.
En este ejemplo, el compilador no conoce el tiempo de vida que se debe inferir para p3. Al
examinar el cuerpo de la función, se puede suponer con seguridad que el tiempo de vida de
p3 es menor quep1 y p2. Sin embargo, como sucede con los tipos, Rust requiere anotaciones
explícitas de los tiempos de vida en los argumentos de las funciones y los valores devueltos.
Añade 'a correctamente a left_most:
fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point {
Por tanto, ”dado p1 y p2, que superan el tiempo de vida de 'a, el valor devuelto tiene una
duración de al menos 'a.
De forma habitual, los tiempos de vida se pueden omitir, tal como se describe en la siguiente
diapositiva.
145
nearest = Some((p, cab_distance(p, query)));
};
}
[Link](|(p, _)| p)
}
fn main() {
println!(
"{:?}",
nearest(
&[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1),],
&Point(0, 2)
)
);
}
This slide should take about 5 minutes.
En este ejemplo, cab_distance se ha suprimido sin que suponga un problema.
La función nearest proporciona otro ejemplo de una función con múltiples referencias en
sus argumentos que requiere una anotación explícita.
Prueba a ajustar la firma para ”mentir” sobre los tiempos de vida devueltos:
fn nearest<'a, 'q>(points: &'a [Point], query: &'q Point) -> Option<&'q Point> {
No se hará la compilación, lo que demuestra que el compilador comprueba la validez de
las anotaciones. Debes tener en cuenta que este no es el caso de los punteros sin formato
(inseguros), y es uno de los motivos por los que se cometen errores con Rust inseguro.
Puede que los participantes pregunten cuándo se deben usar los tiempos de vida. Los
préstamos de Rust siempre tienen tiempos de vida. En la mayoría de las ocasiones, la omisión
y la inferencia de tipos hacen que no sea necesario escribirlos. En casos más complicados, las
anotaciones de tiempos de vida pueden ayudar a resolver la ambigüedad. A menudo, sobre
todo cuando se llevan a cabo prototipos, resulta más fácil trabajar únicamente con datos
propios, clonando valores siempre que sea necesario.
fn erase(text: String) {
println!("¡Adiós, {text}!");
}
fn main() {
let text = String::from("El veloz murciélago hindú comía feliz cardillo y kiwi. La c
let fox = Highlight(&text[4..19]);
let dog = Highlight(&text[35..43]);
// erase(text);
146
println!("{fox:?}");
println!("{dog:?}");
}
This slide should take about 5 minutes.
• En el ejemplo anterior, la anotación en Highlight hace que los datos subyacentes
a la &str contenida tengan al menos la misma duración que cualquier instancia de
Highlight que utilice esos datos.
• Si text se consume antes de que acabe el tiempo de vida de fox (o dog), el borrow
checker (verificador de préstamos) muestra un error.
• Los tipos con datos prestados (borrowed) obligan a los usuarios a conservar los datos
originales. Esto puede ser útil para crear vistas ligeras aunque, por lo general, hace que
sean un poco más difíciles de usar.
• Siempre que sea posible, haz que las estructuras de datos sean propietarias directas de
sus datos.
• Algunas estructuras con varias referencias dentro pueden tener más de una anotación
de tiempo de vida. Esto puede ser necesario si hay que describir las relaciones de tiempo
de vida entre las propias referencias, además del tiempo de vida de la propia estructura.
Estos son casos prácticos muy avanzados.
message Person {
optional string name = 1;
optional int32 id = 2;
repeated PhoneNumber phones = 3;
}
Un mensaje proto se codifica como una serie de campos, uno detrás del otro. Cada uno se
implementa como una ”etiqueta” seguida del valor. La etiqueta contiene un número de campo
(por ejemplo, 2 para el campo id de un mensaje de Person) y un tipo de wire que define
cómo se debe definir la carga útil a partir del flujo de bytes.
Los números enteros, incluida la etiqueta, se representan con una codificación de
longitud variable denominada VARINT. A continuación puedes consultar la definición de
147
parse_varint. El código dado también define retrollamadas para gestionar los campos
Person y PhoneNumber, así como analizar un mensaje en una serie de llamadas a dichas
retrollamadas.
Ahora solo tienes que implementar la función parse_field y el trait ProtoMessage para
Person y PhoneNumber.
/// Tipo de wire como se observa en el wire.
enum WireType {
/// Varint WireType indica que el valor es un único VARINT.
Varint,
//I64, -- no es necesario para este ejercicio
/// El Len WireType indica que el valor es una longitud representada como
/// VARINT seguida exactamente de ese número de bytes.
Len,
/// El WireType I32 indica que el valor es de 4 bytes en
/// el orden little endian que contiene un número entero con signo de 32 bits.
I32,
}
#[derive(Debug)]
/// Valor de un campo, escrito en función del tipo de wire.
enum FieldValue<'a> {
Varint(u64),
//I64(i64), -- no es necesario para este ejercicio
Len(&'a [u8]),
I32(i32),
}
#[derive(Debug)]
/// Campo que contiene el número de campo y su valor.
struct Field<'a> {
field_num: u64,
value: FieldValue<'a>,
}
148
impl<'a> FieldValue<'a> {
fn as_string(&self) -> &'a str {
let FieldValue::Len(data) = self else {
panic!("Cadena era esperado ser un campo `Len`");
};
std::str::from_utf8(data).expect("Cadena no válida")
}
#[allow(dead_code)]
fn as_i32(&self) -> i32 {
let FieldValue::I32(value) = self else {
panic!("`i32` era esperado ser un campo `I32`");
};
*value
}
}
/// Analiza un VARINT, que devuelve el valor analizado y los bytes restantes.
fn parse_varint(data: &[u8]) -> (u64, &[u8]) {
for i in 0..7 {
let Some(b) = [Link](i) else {
panic!("No hay suficientes bytes para un varint");
};
if b & 0x80 == 0 {
// Este es el último byte de VARINT, así que conviértelo en
// u64 y haz que lo devuelva.
let mut value = 0u64;
for b in data[..=i].iter().rev() {
value = (value << 7) | (b & 0x7f) as u64;
}
return (value, &data[i + 1..]);
}
}
149
/// Convierte una etiqueta en un número de campo y un WireType.
fn unpack_tag(tag: u64) -> (u64, WireType) {
let field_num = tag >> 3;
let wire_type = WireType::from(tag & 0x7);
(field_num, wire_type)
}
/// Analiza un mensaje de los datos proporcionados, llamando a `T::add_field` para cada
/// del mensaje.
///
/// Se utilizan todos los datos introducidos.
fn parse_message<'a, T: ProtoMessage<'a>>(mut data: &'a [u8]) -> T {
let mut result = T::default();
while !data.is_empty() {
let parsed = parse_field(data);
result.add_field(parsed.0);
data = parsed.1;
}
result
}
#[derive(Debug, Default)]
struct PhoneNumber<'a> {
number: &'a str,
type_: &'a str,
}
#[derive(Debug, Default)]
struct Person<'a> {
name: &'a str,
id: u64,
phone: Vec<PhoneNumber<'a>>,
}
fn main() {
let person: Person = parse_message(&[
0x0a, 0x07, 0x6d, 0x61, 0x78, 0x77, 0x65, 0x6c, 0x6c, 0x10, 0x2a, 0x1a,
150
0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x30, 0x32, 0x2d, 0x35, 0x35, 0x35,
0x2d, 0x31, 0x32, 0x31, 0x32, 0x12, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x1a,
0x18, 0x0a, 0x0e, 0x2b, 0x31, 0x38, 0x30, 0x30, 0x2d, 0x38, 0x36, 0x37,
0x2d, 0x35, 0x33, 0x30, 0x38, 0x12, 0x06, 0x6d, 0x6f, 0x62, 0x69, 0x6c,
0x65,
]);
println!("{:#?}", person);
}
This slide and its sub-slides should take about 30 minutes.
• En este ejercicio hay varios casos en los cuales la lección del protobuf puede fallar, e.g.
si intentas leer un i32 cuando hay menos de 4 bytes restantes en el buffer de datos.
Normalmente usaríamos el enum Result, pero para simplificar el ejercicio inducimos
pánico si ocurre un error. En el día 4 cubriremos el manejo de errores en Rust en mas
detall
23.4.1 Solución
/// Tipo de wire como se observa en el wire.
enum WireType {
/// Varint WireType indica que el valor es un único VARINT.
Varint,
//I64, -- no es necesario para este ejercicio
/// El Len WireType indica que el valor es una longitud representada como
/// VARINT seguida exactamente de ese número de bytes.
Len,
/// El WireType I32 indica que el valor es de 4 bytes en
/// el orden little endian que contiene un número entero con signo de 32 bits.
I32,
}
#[derive(Debug)]
/// Valor de un campo, escrito en función del tipo de wire.
enum FieldValue<'a> {
Varint(u64),
//I64(i64), -- no es necesario para este ejercicio
Len(&'a [u8]),
I32(i32),
}
#[derive(Debug)]
/// Campo que contiene el número de campo y su valor.
struct Field<'a> {
field_num: u64,
value: FieldValue<'a>,
}
151
impl From<u64> for WireType {
fn from(value: u64) -> Self {
match value {
0 => WireType::Varint,
//1 => WireType::I64, no es necesario para este ejercicio
2 => WireType::Len,
5 => WireType::I32,
_ => panic!("Tipo de wire no válido: {value}"),
}
}
}
impl<'a> FieldValue<'a> {
fn as_string(&self) -> &'a str {
let FieldValue::Len(data) = self else {
panic!("Cadena era esperado ser un campo `Len`");
};
std::str::from_utf8(data).expect("Cadena no válida")
}
#[allow(dead_code)]
fn as_i32(&self) -> i32 {
let FieldValue::I32(value) = self else {
panic!("`i32` era esperado ser un campo `I32`");
};
*value
}
}
/// Analiza un VARINT, que devuelve el valor analizado y los bytes restantes.
fn parse_varint(data: &[u8]) -> (u64, &[u8]) {
for i in 0..7 {
let Some(b) = [Link](i) else {
panic!("No hay suficientes bytes para un varint");
};
if b & 0x80 == 0 {
// Este es el último byte de VARINT, así que conviértelo en
152
// u64 y haz que lo devuelva.
let mut value = 0u64;
for b in data[..=i].iter().rev() {
value = (value << 7) | (b & 0x7f) as u64;
}
return (value, &data[i + 1..]);
}
}
153
/// Analiza un mensaje de los datos proporcionados, llamando a `T::add_field` para cada
/// del mensaje.
///
/// Se utilizan todos los datos introducidos.
fn parse_message<'a, T: ProtoMessage<'a>>(mut data: &'a [u8]) -> T {
let mut result = T::default();
while !data.is_empty() {
let parsed = parse_field(data);
result.add_field(parsed.0);
data = parsed.1;
}
result
}
#[derive(Debug, Default)]
struct PhoneNumber<'a> {
number: &'a str,
type_: &'a str,
}
#[derive(Debug, Default)]
struct Person<'a> {
name: &'a str,
id: u64,
phone: Vec<PhoneNumber<'a>>,
}
fn main() {
let person: Person = parse_message(&[
0x0a, 0x07, 0x6d, 0x61, 0x78, 0x77, 0x65, 0x6c, 0x6c, 0x10, 0x2a, 0x1a,
154
0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x30, 0x32, 0x2d, 0x35, 0x35, 0x35,
0x2d, 0x31, 0x32, 0x31, 0x32, 0x12, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x1a,
0x18, 0x0a, 0x0e, 0x2b, 0x31, 0x38, 0x30, 0x30, 0x2d, 0x38, 0x36, 0x37,
0x2d, 0x35, 0x33, 0x30, 0x38, 0x12, 0x06, 0x6d, 0x6f, 0x62, 0x69, 0x6c,
0x65,
]);
println!("{:#?}", person);
}
155
Parte VII
Día 4: Mañana
156
Capítulo 24
Bienvenido al Día 4
Hoy vamos a tratar algunos temas relacionados con la construcción de aplicaciones de grande
escala en Rust:
• Iteradores: información detallada sobre el trait Iterator.
• Módulos y visibilidad.
• Probando.
• Gestión de errores: panics (pánicos), Result y el operador try ?.
• Rust inseguro: una vía de escape en las situaciones en las que no puedes expresarte en
Rust seguro.
Horario
Contando con los descansos de 10 minutos, la duración prevista de la sesión es de unas 2
horas y 40 minutos. Contiene:
Sección Duración
Te damos la bienvenida 3 minutos
Iteradores 45 minutos
Módulos 40 minutos
Probando 45 minutos
157
Capítulo 25
Iteradores
Diapositiva Duración
Iterator 5 minutos
IntoIterator 5 minutos
FromIterator 5 minutos
Ejercicio: Encadenamiento de métodos del iterador 30 minutos
25.1 Iterator
El trait Iterator permite iterar valores en una colección. Requiere un método next y
proporciona muchos otros métodos. Muchos tipos de bibliotecas estándar implementan
Iterator y también está a nuestro alcance:
struct Fibonacci {
curr: u32,
next: u32,
}
fn main() {
let fib = Fibonacci { curr: 0, next: 1 };
for (i, n) in [Link]().take(5) {
158
println!("fib({i}): {n}");
}
}
This slide should take about 5 minutes.
• El trait Iterator implementa muchas operaciones comunes de programación funcional
en colecciones (por ejemplo, map, filter, reduce, etc.). Este es el trait que te permite
encontrar toda la documentación sobre ellas. En Rust, estas funciones deberían generar
un código tan eficiente como las implementaciones imperativas equivalentes.
• IntoIterator es el trait que hace que los bucles funcionen. Se implementa a través de
tipos de colecciones, como Vec<T>, y de referencias a ellas, como &Vec<T> y &[T]. Los
rangos también lo implementan. Esta es la razón por la que se puede iterar sobre un
vector con for i in some_vec { .. }, pero some_vec.next() no existe.
25.2 IntoIterator
El trait Iterator te indica cómo iterar una vez que has creado un iterador. El trait relacionado
IntoIterator indica cómo crear un iterador para un tipo. Es usado automáticamente por
los bucles for.
struct Grid {
x_coords: Vec<u32>,
y_coords: Vec<u32>,
}
struct GridIter {
grid: Grid,
i: usize,
j: usize,
}
159
let res = Some(([Link].x_coords[self.i], [Link].y_coords[self.j]));
self.i += 1;
res
}
}
fn main() {
let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] };
for (x, y) in grid {
println!("punto = {x}, {y}");
}
}
This slide should take about 5 minutes.
Haz clic para leer la documentación para IntoIterator. Cada implementación de
IntoIterator debe declarar dos tipos:
• Item: el tipo sobre el que iteramos, como i8,
• IntoIter: el tipo Iterator devuelto por el método into_iter.
Ten en cuenta que IntoIter y Item están vinculados: el iterador debe tener el mismo tipo
de Item, lo que significa que devuelve Option<Item>.
En el ejemplo se itera sobre todas las combinaciones de las coordenadas x e y.
Prueba a iterar sobre la cuadrícula dos veces en main. ¿Por qué no funciona? Ten en cuenta
que IntoIterator::into_iter tiene la propiedad de self.
Soluciona este problema implementando IntoIterator para &Grid y almacenando una
referencia a Grid en GridIter.
Lo mismo puede ocurrir con los tipos de biblioteca estándar: for e in some_vector
adquirirá la propiedad de some_vector e iterará sobre los elementos propios de ese vector.
En su lugar, puedes utilizar for e in &some_vector para iterar sobre referencias a
elementos de some_vector.
25.3 FromIterator
FromIterator permite construir una colección a partir de un Iterator.
fn main() {
let primes = vec![2, 3, 5, 7];
let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>();
println!("prime_squares: {prime_squares:?}");
}
This slide should take about 5 minutes.
Iterator implementa
fn collect<B>(self) -> B
where
B: FromIterator<Self::Item>,
Self: Sized
160
Hay dos formas de especificar B en este método:
• Con ”turbofish”: some_iterator.collect::<COLLECTION_TYPE>(), tal como se
muestra. La forma abreviada de _ que se utiliza aquí permite que Rust infiera el tipo de
los elementos Vec.
• Con inferencia de tipos: let prime_squares: Vec<_> = some_iterator.collect().
Reescribe el ejemplo para usar esta opción.
Existen implementaciones básicas de FromIterator para Vec, HashMap, etc. También existen
implementaciones mas especializadas que te dejan hacer cosas padres como convertir un
Iterator<Item = Result<V, E>> a un Result<Vec<V>, E>.
#[test]
fn test_offset_one() {
assert_eq!(offset_differences(1, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
assert_eq!(offset_differences(1, vec![1, 3, 5]), vec![2, 2, -4]);
assert_eq!(offset_differences(1, vec![1, 3]), vec![2, -2]);
}
#[test]
fn test_larger_offsets() {
assert_eq!(offset_differences(2, vec![1, 3, 5, 7]), vec![4, 4, -4, -4]);
assert_eq!(offset_differences(3, vec![1, 3, 5, 7]), vec![6, -2, -2, -2]);
assert_eq!(offset_differences(4, vec![1, 3, 5, 7]), vec![0, 0, 0, 0]);
assert_eq!(offset_differences(5, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
}
#[test]
fn test_custom_type() {
assert_eq!(
offset_differences(1, vec![1.0, 11.0, 5.0, 0.0]),
vec![10.0, -6.0, -5.0, 1.0]
161
);
}
#[test]
fn test_degenerate_cases() {
assert_eq!(offset_differences(1, vec![0]), vec![0]);
assert_eq!(offset_differences(1, vec![1]), vec![0]);
let empty: Vec<i32> = vec![];
assert_eq!(offset_differences(1, empty), vec![]);
}
25.4.1 Solución
/// Calcula las diferencias entre los elementos de `values` offset por offset,
/// envolviendo de esta forma los elementos desde el final de `values` hasta el principi
///
/// El elemento `n` del resultado es `values[(n+offset)%len] - values[n]`.
fn offset_differences<N>(offset: usize, values: Vec<N>) -> Vec<N>
where
N: Copy + std::ops::Sub<Output = N>,
{
let a = (&values).into_iter();
let b = (&values).into_iter().cycle().skip(offset);
[Link](b).map(|(a, b)| *b - *a).collect()
}
#[test]
fn test_offset_one() {
assert_eq!(offset_differences(1, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
assert_eq!(offset_differences(1, vec![1, 3, 5]), vec![2, 2, -4]);
assert_eq!(offset_differences(1, vec![1, 3]), vec![2, -2]);
}
#[test]
fn test_larger_offsets() {
assert_eq!(offset_differences(2, vec![1, 3, 5, 7]), vec![4, 4, -4, -4]);
assert_eq!(offset_differences(3, vec![1, 3, 5, 7]), vec![6, -2, -2, -2]);
assert_eq!(offset_differences(4, vec![1, 3, 5, 7]), vec![0, 0, 0, 0]);
assert_eq!(offset_differences(5, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
}
#[test]
fn test_custom_type() {
assert_eq!(
offset_differences(1, vec![1.0, 11.0, 5.0, 0.0]),
vec![10.0, -6.0, -5.0, 1.0]
);
}
#[test]
fn test_degenerate_cases() {
162
assert_eq!(offset_differences(1, vec![0]), vec![0]);
assert_eq!(offset_differences(1, vec![1]), vec![0]);
let empty: Vec<i32> = vec![];
assert_eq!(offset_differences(1, empty), vec![]);
}
fn main() {}
163
Capítulo 26
Módulos
Diapositiva Duración
Módulos 3 minutos
Jerarquía del sistema de archivos 5 minutos
Visibilidad 5 minutos
use, super, self 10 minutos
Ejercicio: Módulos para una biblioteca GUI 15 minutos
26.1 Módulos
Hemos visto cómo los bloques impl nos permiten asignar espacios de nombres de funciones
a un tipo.
Del mismo modo, mod nos permite asignar espacios de nombres a funciones y tipos:
mod foo {
pub fn do_something() {
println!("En el módulo foo");
}
}
mod bar {
pub fn do_something() {
println!("En el módulo bar");
}
}
fn main() {
foo::do_something();
bar::do_something();
}
This slide should take about 3 minutes.
164
• Los paquetes ofrecen funciones e incluyen un archivo [Link] que describe cómo
compilar un paquete de más de un crate.
• Los crates son un árbol de módulos, donde un crate binario crea un ejecutable y un
crate de biblioteca compila una biblioteca.
• Los módulos definen la organización y el ámbito, y son el centro de esta sección.
165
└── top_module/
└── sub_module.rs
• El lugar donde Rust buscará los módulos se puede cambiar con una directiva del
compilador:
#[path = "some/[Link]"]
mod some_module;
Esto resulta útil, por ejemplo, si deseas colocar pruebas de un módulo en un archivo
denominado some_module_test.rs, similar a la convención en Go.
26.3 Visibilidad
Los módulos marcan el límite de la privacidad:
• Los elementos del módulo son privados de forma predeterminada (se ocultan los detalles
de implementación).
• Los elementos superiores y los del mismo nivel siempre están visibles.
• Es decir, si un elemento está visible en el módulo foo, se verá en todos los elementos
descendientes de foo.
mod outer {
fn private() {
println!("outer::private");
}
pub fn public() {
println!("outer::public");
}
mod inner {
fn private() {
println!("outer::inner::private");
}
pub fn public() {
println!("outer::inner::public");
super::private();
}
}
}
fn main() {
outer::public();
}
This slide should take about 5 minutes.
• Haz que los módulos sean públicos con la palabra clave pub.
Además, hay especificadores pub(...) avanzados para restringir el ámbito de la visibilidad
pública.
166
• Consulta el libro Rust Reference.
• Configurar la visibilidad de pub(crate) es un patrón común.
• Aunque es menos frecuente, se puede dar visibilidad a una ruta específica.
• En cualquier caso, se debe dar visibilidad a un módulo antecedente (y a todos sus
descendientes).
Rutas
Las rutas se resuelven de la siguiente manera:
1. Como ruta relativa:
• foo o self::foo hacen referencia a foo en el módulo corriente,
• super::foo hace referencia a foo en el módulo superior.
2. Como ruta absoluta:
• crate::foo hace referencia a foo en la raíz del crate corriente,
• bar::foo hace referencia a foo en el crate bar.
This slide should take about 8 minutes.
• Es habitual ”volver a exportar” los símbolos en una ruta más corta. Por ejemplo, el
archivo [Link] de nivel superior de un crate puede hacer que
mod storage;
167
26.5 Ejercicio: Módulos para una biblioteca GUI
En este ejercicio, vas a reorganizar una pequeña implementación de una biblioteca GUI. Esta
biblioteca define un trait Widget y algunas implementaciones de dicho trait, así como una
función main.
Es habitual colocar cada tipo o conjunto de tipos que estén estrechamente relacionados en su
propio módulo, por lo que cada tipo de widget debe tener su propio módulo.
Configuración de Cargo
El playground de Rust solo admite un archivo, por lo que tendrás que crear un proyecto de
Cargo en tu sistema de archivos local:
cargo init gui-modules
cd gui-modules
cargo run
Edita el archivo src/[Link] resultante para añadir instrucciones mod y añade archivos
adicionales en el directorio src.
Fuente
A continuación, se muestra la implementación de la biblioteca GUI en un solo módulo:
pub trait Widget {
/// Ancho natural de `self`.
fn width(&self) -> usize;
impl Label {
fn new(label: &str) -> Label {
Label { label: label.to_owned() }
}
}
168
}
impl Button {
fn new(label: &str) -> Button {
Button { label: Label::new(label) }
}
}
impl Window {
fn new(title: &str) -> Window {
Window { title: title.to_owned(), widgets: Vec::new() }
}
169
}
writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
}
}
fn main() {
let mut window = Window::new("Demo de la GUI de Rust 1.23");
window.add_widget(Box::new(Label::new("Esta es una demo de la GUI con poco texto."))
window.add_widget(Box::new(Button::new("Haz clic aquí")));
[Link]();
}
This slide and its sub-slides should take about 15 minutes.
Anima a los participantes a dividir el código de un modo que les parezca natural para que se
familiaricen con las declaraciones mod, use y pub. Después, comenta qué tipo de organización
es más idiomática.
26.5.1 Solución
src
├── [Link]
├── widgets
170
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
└── [Link]
// ---- src/[Link] ----
mod button;
mod label;
mod window;
impl Label {
pub fn new(label: &str) -> Label {
Label { label: label.to_owned() }
}
}
// ANCHOR: Label-draw_into
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
// ANCHOR_END: Label-draw_into
writeln!(buffer, "{}", &[Link]).unwrap();
171
}
}
// ---- src/widgets/[Link] ----
use super::{Label, Widget};
impl Button {
pub fn new(label: &str) -> Button {
Button { label: Label::new(label) }
}
}
// ANCHOR: Button-draw_into
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
// ANCHOR_END: Button-draw_into
let width = [Link]();
let mut label = String::new();
[Link].draw_into(&mut label);
impl Window {
pub fn new(title: &str) -> Window {
Window { title: title.to_owned(), widgets: Vec::new() }
}
172
}
// ANCHOR: Window-draw_into
fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
// ANCHOR_END: Window-draw_into
let mut inner = String::new();
for widget in &[Link] {
widget.draw_into(&mut inner);
}
use widgets::Widget;
fn main() {
let mut window = widgets::Window::new("Demo de la GUI de Rust 1.23");
window
.add_widget(Box::new(widgets::Label::new("Esta es una demo de la GUI con poco te
window.add_widget(Box::new(widgets::Button::new("Haz clic aquí")));
[Link]();
173
}
174
Capítulo 27
Probando
Diapositiva Duración
Módulos de Pruebas 5 minutos
Otros tipos de pruebas 5 minutos
Lints de compiladores y Clippy 3 minutos
Ejercicio: Algoritmo de Luhn 30 minutos
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
assert_eq!(first_word(""), "");
175
}
#[test]
fn test_single_word() {
assert_eq!(first_word("Hola"), "Hola");
}
#[test]
fn test_multiple_words() {
assert_eq!(first_word("Hola, mundo"), "Hola");
}
}
• Esto permite realizar pruebas unitarias de los ayudantes privados.
• El atributo #[cfg(test)] solo está activo cuando se ejecuta cargo test.
This slide should take about 5 minutes.
Haz las pruebas en el playground para ver los resultados.
#[test]
fn test_init() {
assert!(init().is_ok());
}
Estas pruebas solo tienen acceso a la API pública de tu crate.
Pruebas de Documentación
Rust cuenta con asistencia integrada para pruebas de documentación:
/// Acorta una cadena según la longitud proporcionada.
///
/// ```
/// # use playground::shorten_string;
/// assert_eq!(shorten_string("Hola, mundo", 5), "Hola");
/// assert_eq!(shorten_string("Hola, mundo", 20), "Hola, mundo");
/// ```
pub fn shorten_string(s: &str, length: usize) -> &str {
&s[..std::cmp::min(length, [Link]())]
}
176
• Los bloques de código en los comentarios /// se ven automáticamente como código de
Rust.
• El código se compilará y ejecutará como parte de cargo test.
• Si añades # al código, se ocultará de los documentos, pero se seguirá compilando o
ejecutando.
• Prueba el código anterior en el playground de Rust.
Algoritmo de Luhn
El algoritmo de Luhn se usa para validar números de tarjetas de crédito. El algoritmo toma
una cadena como entrada y hace lo siguiente para validar el número de la tarjeta de crédito:
• Ignora todos los espacios. Rechaza los números con menos de dos dígitos.
• De derecha a izquierda, duplica cada dos cifras: en el caso del número 1234, se duplica
el 3 y el 1. En el caso del número 98765, se duplica el 6 y el 8.
• Después de duplicar un dígito, se suman los dígitos si el resultado es mayor a 9. Por
tanto, si duplicas 7, pasará a ser 14, lo cual pasará a ser 1 +4 = 5.
• Suma todos los dígitos, no duplicados y duplicados.
• El número de la tarjeta de crédito es válido si la suma termina en 0.
177
El código proporcionado ofrece una implementación errónea del algoritmo de Luhn, junto
con dos pruebas unitarias básicas que confirman que la mayor parte del algoritmo se ha
implementado correctamente.
Copia el fragmento de código que aparece más abajo en la página a [Link] y
escribe pruebas adicionales para descubrir y arreglar errores en la implementación proveída.
pub fn luhn(cc_number: &str) -> bool {
let mut sum = 0;
let mut double = false;
for c in cc_number.chars().rev() {
if let Some(digit) = c.to_digit(10) {
if double {
let double_digit = digit * 2;
sum +=
if double_digit > 9 { double_digit - 9 } else { double_digit };
} else {
sum += digit;
}
double = !double;
} else {
continue;
}
}
sum % 10 == 0
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_valid_cc_number() {
assert!(luhn("4263 9826 4026 9299"));
assert!(luhn("4539 3195 0343 6467"));
assert!(luhn("7992 7398 713"));
}
#[test]
fn test_invalid_cc_number() {
assert!(!luhn("4223 9826 4026 9299"));
assert!(!luhn("4539 3195 0343 6476"));
assert!(!luhn("8273 1232 7352 0569"));
}
}
27.4.1 Solución
// Esta es la versión con errores que aparece en el problema.
#[cfg(never)]
178
pub fn luhn(cc_number: &str) -> bool {
let mut sum = 0;
let mut double = false;
for c in cc_number.chars().rev() {
if let Some(digit) = c.to_digit(10) {
if double {
let double_digit = digit * 2;
sum +=
if double_digit > 9 { double_digit - 9 } else { double_digit };
} else {
sum += digit;
}
double = !double;
} else {
continue;
}
}
sum % 10 == 0
}
for c in cc_number.chars().rev() {
if let Some(digit) = c.to_digit(10) {
digits += 1;
if double {
let double_digit = digit * 2;
sum +=
if double_digit > 9 { double_digit - 9 } else { double_digit };
} else {
sum += digit;
}
double = !double;
} else if c.is_whitespace() {
continue;
} else {
return false;
}
}
fn main() {
let cc_number = "1234 5678 1234 5670";
179
println!(
"¿Es {cc_number} un número de tarjeta de crédito válido? {}",
if luhn(cc_number) { "sí" } else { "no" }
);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_valid_cc_number() {
assert!(luhn("4263 9826 4026 9299"));
assert!(luhn("4539 3195 0343 6467"));
assert!(luhn("7992 7398 713"));
}
#[test]
fn test_invalid_cc_number() {
assert!(!luhn("4223 9826 4026 9299"));
assert!(!luhn("4539 3195 0343 6476"));
assert!(!luhn("8273 1232 7352 0569"));
}
#[test]
fn test_non_digit_cc_number() {
assert!(!luhn("foo"));
assert!(!luhn("foo 0 0"));
}
#[test]
fn test_empty_cc_number() {
assert!(!luhn(""));
assert!(!luhn(" "));
assert!(!luhn(" "));
assert!(!luhn(" "));
}
#[test]
fn test_single_digit_cc_number() {
assert!(!luhn("0"));
}
#[test]
fn test_two_digit_cc_number() {
assert!(luhn(" 0 0 "));
}
}
180
Parte VIII
Día 4: Tarde
181
Capítulo 28
Te damos la bienvenida
Sección Duración
Manejo de Errores 1 hora
Unsafe Rust 1 hora y 5 minutos
182
Capítulo 29
Manejo de Errores
Diapositiva Duración
Pánicos 3 minutos
Result 5 minutos
Operador Try (Intentar) 5 minutos
Conversiones Try (Intentar) 5 minutos
Trait Error 5 minutos
thiserror y anyhow 5 minutos
Ejercicio: Reescribir con Result 30 minutos
29.1 Pánicos
Rust gestiona los errores críticos con un ”pánico”.
Rust activará un panic si se produce un error grave en runtime:
fn main() {
let v = vec![10, 20, 30];
println!("v[100]: {}", v[100]);
}
• Los panics se usan para errores irrecuperables e inesperados.
– Los panics son un síntoma de que hay fallos en el programa.
– Los fallos del tiempo de ejecución, como las comprobaciones de límites fallidas,
pueden causar un pánico
– Las aserciones (como assert!) causan un pánico cuando fallan
– Los pánicos con fines específicos pueden usar la macro panic!.
• Cuando se produce un pánico, se ”desenrolla” la pila y se eliminan los valores como si
las funciones hubieran devuelto un resultado.
• Utiliza API que no activen panics (como Vec::get) si no se admiten fallos.
This slide should take about 3 minutes.
183
De forma predeterminada, el panic hará que la stack se desenrolle. El proceso de desenrrollado
se puede detectar:
use std::panic;
fn main() {
let result = panic::catch_unwind(|| "No hay ningún problema.");
println!("{result:?}");
29.2 Result
El mecanismo primario para el manejo de errores en Rust es el enum Result, que vimos
brevemente al discutir los tipos de la biblioteca estándar.
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("[Link]");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Querido diario: {contents} ({bytes} bytes)");
} else {
println!("No se ha podido leer el contenido del archivo");
}
}
Err(err) => {
println!("No se ha podido abrir el diario: {err}");
}
}
}
This slide should take about 5 minutes.
• Result tiene dos variantes: Ok, que contiene el valor de éxito; y Err, que contiene un
valor de error de algún tipo.
• La signatura de tipo de una función indica si puede producir un error, en este caso
devolverá un valor Result.
184
• Como con Option, no hay manera de olvidarse de manejar un error: no puedes acceder
el valor de éxito o el valor de error sin hacer coincidencia de patrones sobre el Result
para ver que variante tienes. Métodos como unwrap hacen que sea mas fácil escribir
código rápido-y-sucio que no maneja errores de una forma robusta, pero esto significa
que siempre puedes ver en tu código donde no estas manejando errores de la manera
propia.
Más información
Podria ayudar comparar el manejo de errores en Rust con las convenciones de manejo de
errores de otros lenguajes que conocen los estudiantes.
Excepciones
• Muchos lenguajes usan excepciones, e.g. C++, Java, Python.
• En la mayoria
• Exceptions generally unwind the call stack, propagating upward until a try block is
reached. An error originating deep in the call stack may impact an unrelated function
further up.
Error Numbers
• Some languages have functions return an error number (or some other error value)
separately from the successful return value of the function. Examples include C and Go.
• Depending on the language it may be possible to forget to check the error value, in
which case you may be accessing an uninitialized or otherwise invalid success value.
185
let username_file_result = fs::File::open(path);
let mut username_file = match username_file_result {
Ok(file) => file,
Err(err) => return Err(err),
};
fn main() {
//fs::write("[Link]", "alice").unwrap();
let username = read_username("[Link]");
println!("nombre de usuario o error: {username:?}");
}
This slide should take about 5 minutes.
Simplifica la función read_username para usar ?.
Puntos clave:
• La variable username puede ser Ok(string) o Err(error).
• Utiliza la llamada a fs::write para probar las distintas situaciones: sin archivo, archivo
vacío o archivo con nombre de usuario.
• Note that main can return a Result<(), E> as long as it implements std::process::Termination.
In practice, this means that E implements Debug. The executable will print the Err
variant and return a nonzero exit status on error.
Ejemplo
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::fs::File;
use std::io::{self, Read};
186
#[derive(Debug)]
enum ReadUsernameError {
IoError(io::Error),
EmptyUsername(String),
}
fn main() {
//std::fs::write("[Link]", "").unwrap();
let username = read_username("[Link]");
println!("nombre de usuario o error: {username:?}");
}
This slide should take about 5 minutes.
El operador ? debe devolver un valor compatible con el tipo de resultado devuelto de la
función. En Result, significa que los tipos de error deben ser compatibles. Una función
que devuelve Result<T, ErrorOuter> solo puede usar ? en un valor del tipo Result<U,
ErrorInner> si ErrorOuter y ErrorInner son del mismo tipo o si ErrorOuter implementa
. From<ErrorInner>.
Una alternativa habitual a la implementación From es Result::map_err, sobre todo si la
conversión solo se produce en un lugar.
No hay ningún requisito de compatibilidad para Option. Una función que devuelve
Option<T> puede usar el operador ? en Option<U> para tipos arbitrarios de T y U.
187
Una función que devuelve Result no puede usar ? en Option y viceversa. Sin embargo,
Option::ok_or convierte Option en Result, mientras que Result::ok convierte Result
en Option.
fn main() {
fs::write("[Link]", "1i3").unwrap();
match read_count("[Link]") {
Ok(count) => println!("Recuento: {count}"),
Err(err) => println!("Error: {err}"),
}
}
This slide should take about 5 minutes.
La función read_count puede devolver std::io::Error (de las operaciones de archivos) o
std::num::ParseIntError (de String::parse).
Boxing errors saves on code, but gives up the ability to cleanly handle different error cases
differently in the program. As such it's generally not a good idea to use Box<dyn Error> in
the public API of a library, but it can be a good option in a program where you just want to
display the error message somewhere.
Asegúrate de implementar el trait std::error::Error al definir un tipo de error
personalizado para que pueda tener una estructura box. Sin embargo, si necesitas el atributo
no_std, ten en cuenta que el trait std::error::Error de momento solo es compatible con
no_std en nightly.
188
use anyhow::{bail, Context, Result};
use std::fs;
use std::io::Read;
use thiserror::Error;
fn main() {
//fs::write("[Link]", "").unwrap();
match read_username("[Link]") {
Ok(username) => println!("Nombre de usuario: {username}"),
Err(err) => println!("Error: {err:?}"),
}
}
This slide should take about 5 minutes.
thiserror
• La macro de derivación Error la proporciona thiserror y ofrece muchos atributos
útiles para definir los tipos de error de forma compacta.
• El trait std::error::Error se deriva automáticamente.
• El mensaje de #[error] se usa para derivar el trait Display.
anyhow
• anyhow::Error es básicamente un envoltorio alrededor de Box<dyn Error>. Como
tal, no suele ser una buena elección para la API pública de una biblioteca, pero se usa
con frecuencia en aplicaciones.
• anyhow::Result<V> es un alias de tipo para Result<V, anyhow::Error>.
• El tipo de error real que contiene se puede extraer para analizarlo si es necesario.
• La funcionalidad proporcionada por anyhow::Result<T> puede resultar familiar a
los desarrolladores de Go, ya que ofrece patrones de uso y ergonomía similares a (T,
error) de Go.
• anyhow::Context es un trait implementado para los tipos estándar Result y Option.
Se necesita use anyhow::Context para habilitar .context() y .with_context()
en esos tipos.
189
29.7 Ejercicio: Reescribir con Result
A continuación, se implementa un analizador muy sencillo para un lenguaje de expresiones.
Sin embargo, para gestionar los errores, utiliza pánicos. Reescribe este texto para utilizar la
gestión de errores idiomática y propagar los errores a un instrucción de retorno desde main.
No dudes en usar thiserror y anyhow.
CONSEJO: empieza por corregir la gestión de errores en la función parse. Cuando funcione
correctamente, actualiza Tokenizer para implementar Iterator<Item=Result<Token,
TokenizerError>> y gestiónalo en el analizador.
use std::iter::Peekable;
use std::str::Chars;
struct Tokenizer<'a>(Peekable<Chars<'a>>);
impl<'a> Tokenizer<'a> {
fn collect_number(&mut self, first_char: char) -> Token {
let mut num = String::from(first_char);
while let Some(&c @ '0'..='9') = [Link]() {
[Link](c);
[Link]();
190
}
Token::Number(num)
}
191
),
Some(tok) => panic!("Token inesperado: {tok:?}"),
}
}
parse_expr(&mut tokens)
}
fn main() {
let expr = parse("10+foo+20-30");
println!("{expr:?}");
}
29.7.1 Solución
use thiserror::Error;
use std::iter::Peekable;
use std::str::Chars;
#[derive(Debug, Error)]
enum TokenizerError {
192
#[error("Carácter inesperado '{0}' en la entrada")]
UnexpectedCharacter(char),
}
struct Tokenizer<'a>(Peekable<Chars<'a>>);
impl<'a> Tokenizer<'a> {
fn collect_number(&mut self, first_char: char) -> Token {
let mut num = String::from(first_char);
while let Some(&c @ '0'..='9') = [Link]() {
[Link](c);
[Link]();
}
Token::Number(num)
}
#[derive(Debug, Error)]
enum ParserError {
#[error("Error del tokenizador: {0}")]
TokenizerError(#[from] TokenizerError),
#[error("Fin de entrada inesperado")]
UnexpectedEOF,
#[error("Token inesperado: {0:?}")]
UnexpectedToken(Token),
#[error("Número no válido")]
InvalidNumber(#[from] std::num::ParseIntError),
193
}
fn parse_expr<'a>(
tokens: &mut Tokenizer<'a>,
) -> Result<Expression, ParserError> {
let tok = [Link]().ok_or(ParserError::UnexpectedEOF)??;
let expr = match tok {
Token::Number(num) => {
let v = [Link]()?;
Expression::Number(v)
}
Token::Identifier(ident) => Expression::Var(ident),
Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)),
};
// Analiza la operación binaria, si procede.
Ok(match [Link]() {
None => expr,
Some(Ok(Token::Operator(op))) => Expression::Operation(
Box::new(expr),
op,
Box::new(parse_expr(tokens)?),
),
Some(Err(e)) => return Err([Link]()),
Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)),
})
}
parse_expr(&mut tokens)
}
194
Capítulo 30
Unsafe Rust
Diapositiva Duración
Unsafe 5 minutos
Dereferenciación de Punteros Sin Formato 10 minutos
Variables Estáticas Mutables 5 minutos
Uniones 5 minutos
Funciones Inseguras (Unsafe) 5 minutos
Implementación de Traits Unsafe (Inseguras) 5 minutos
Ejercicio: Envoltorio de FFI 30 minutos
195
Unsafe Rust does not mean the code is incorrect. It means that developers have turned off
some compiler safety features and have to write correct code by themselves. It means the
compiler no longer enforces Rust's memory-safety rules.
196
indefinido: *r1 tiene el tiempo de vida 'static, por lo que r3 tiene el tipo &'static String
y, por lo tanto, su duración es mayor que la de s. Para crear una referencia a partir de un
puntero hay que tener mucho cuidado.
fn main() {
println!("HELLO_WORLD: {HELLO_WORLD}");
}
Sin embargo, dado que pueden producirse carreras de datos, no es seguro leer y escribir
variables estáticas mutables:
static mut COUNTER: u32 = 0;
fn add_to_counter(inc: u32) {
// SAFETY: There are no other threads which could be accessing `COUNTER`.
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_counter(42);
30.4 Uniones
Las uniones son como enums (enumeraciones), pero eres tú quien debe hacer el seguimiento
del campo activo:
197
#[repr(C)]
union MyUnion {
i: u8,
b: bool,
}
fn main() {
let u = MyUnion { i: 42 };
println!("int: {}", unsafe { u.i });
println!("bool: {}", unsafe { u.b }); // ¡Comportamiento indefinido!
}
This slide should take about 5 minutes.
Las uniones raramente son necesarias en Rust, ya que se suele utilizar una enum. A veces se
necesitan para interactuar con APIs de biblioteca C.
Si solo quieres reinterpretar los bytes como otro tipo, probablemente te interese
std::mem::transmute o una envoltura segura, como el crate zerocopy.
fn main() {
let emojis = " ∈ ";
// SAFETY: The indices are in the correct order, within the bounds of the
// string slice, and lie on UTF-8 sequence boundaries.
unsafe {
println!("emoji: {}", emojis.get_unchecked(0..4));
println!("emoji: {}", emojis.get_unchecked(4..7));
println!("emoji: {}", emojis.get_unchecked(7..11));
}
// SAFETY: `abs` doesn't deal with pointers and doesn't have any safety
// requirements.
unsafe {
println!("Valor absoluto de -3 según C: {}", abs(-3));
}
198
// println!("recuento de caracteres: {}", count_chars(no seguro {
// emojis.get_unchecked(0..3) }));
}
fn main() {
let mut a = 42;
let mut b = 66;
// SAFETY: ...
unsafe {
swap(&mut a, &mut b);
}
199
Note that unsafe code is allowed within an unsafe function without an unsafe block. We
can prohibit this with #[deny(unsafe_op_in_unsafe_fn)]. Try adding it and see what
happens. This will likely change in a future Rust edition.
/// ...
/// # Seguridad
/// El tipo debe tener una representación definida y no tener espacio de relleno.
pub unsafe trait AsBytes {
fn as_bytes(&self) -> &[u8] {
unsafe {
slice::from_raw_parts(
self as *const Self as *const u8,
size_of_val(self),
)
}
}
}
200
También te recomendamos que consultes el módulo std::ffi. Ahí encontrarás una serie de
tipos de cadena que necesitas para el ejercicio:
mod ffi {
use std::os::raw::{c_char, c_int};
#[cfg(not(target_os = "macos"))]
use std::os::raw::{c_long, c_uchar, c_ulong, c_ushort};
// Diseño según la página del manual de Linux para readdir(3), donde ino_t y
// off_t se resuelven de acuerdo con las definiciones de
// /usr/include/x86_64-linux-gnu/{sys/types.h, bits/typesizes.h}. .
#[cfg(not(target_os = "macos"))]
#[repr(C)]
pub struct dirent {
pub d_ino: c_ulong,
pub d_off: c_long,
201
pub d_reclen: c_ushort,
pub d_type: c_uchar,
pub d_name: [c_char; 256],
}
extern "C" {
pub fn opendir(s: *const c_char) -> *mut DIR;
#[derive(Debug)]
struct DirectoryIterator {
path: CString,
dir: *mut ffi::DIR,
}
impl DirectoryIterator {
fn new(path: &str) -> Result<DirectoryIterator, String> {
// Llama a opendir y devuelve un valor Ok si ha funcionado,
// de lo contrario, devuelve Err con un mensaje.
unimplemented!()
}
202
}
30.7.1 Solución
mod ffi {
use std::os::raw::{c_char, c_int};
#[cfg(not(target_os = "macos"))]
use std::os::raw::{c_long, c_uchar, c_ulong, c_ushort};
// Diseño según la página del manual de Linux para readdir(3), donde ino_t y
// off_t se resuelven de acuerdo con las definiciones de
// /usr/include/x86_64-linux-gnu/{sys/types.h, bits/typesizes.h}. .
#[cfg(not(target_os = "macos"))]
#[repr(C)]
pub struct dirent {
pub d_ino: c_ulong,
pub d_off: c_long,
pub d_reclen: c_ushort,
pub d_type: c_uchar,
pub d_name: [c_char; 256],
}
203
#[cfg(all(target_os = "macos"))]
#[repr(C)]
pub struct dirent {
pub d_fileno: u64,
pub d_seekoff: u64,
pub d_reclen: u16,
pub d_namlen: u16,
pub d_type: u8,
pub d_name: [c_char; 1024],
}
extern "C" {
pub fn opendir(s: *const c_char) -> *mut DIR;
#[derive(Debug)]
struct DirectoryIterator {
path: CString,
dir: *mut ffi::DIR,
}
impl DirectoryIterator {
fn new(path: &str) -> Result<DirectoryIterator, String> {
// Llama a opendir y devuelve un valor Ok si ha funcionado,
// de lo contrario, devuelve Err con un mensaje.
let path =
CString::new(path).map_err(|err| format!("Ruta no válida: {err}"))?;
// SEGURIDAD: path.as_ptr() no puede ser NULL.
let dir = unsafe { ffi::opendir(path.as_ptr()) };
if dir.is_null() {
Err(format!("No se ha podido abrir {:?}", path))
} else {
Ok(DirectoryIterator { path, dir })
204
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn test_nonexisting_directory() {
let iter = DirectoryIterator::new("no-such-directory");
assert!(iter.is_err());
}
205
#[test]
fn test_empty_directory() -> Result<(), Box<dyn Error>> {
let tmp = tempfile::TempDir::new()?;
let iter = DirectoryIterator::new(
[Link]().to_str().ok_or("Hay un carácter no codificado en UTF-8 en la ruta
)?;
let mut entries = [Link]::<Vec<_>>();
[Link]();
assert_eq!(entries, &[".", ".."]);
Ok(())
}
#[test]
fn test_nonempty_directory() -> Result<(), Box<dyn Error>> {
let tmp = tempfile::TempDir::new()?;
std::fs::write([Link]().join("[Link]"), "The Foo Diaries\n")?;
std::fs::write([Link]().join("[Link]"), "<PNG>\n")?;
std::fs::write([Link]().join("[Link]"), "//! Crab\n")?;
let iter = DirectoryIterator::new(
[Link]().to_str().ok_or("Hay un carácter no codificado en UTF-8 en la ruta
)?;
let mut entries = [Link]::<Vec<_>>();
[Link]();
assert_eq!(entries, &[".", "..", "[Link]", "[Link]", "[Link]"]);
Ok(())
}
}
206
Parte IX
Android
207
Capítulo 31
Rust is supported for system software on Android. This means that you can write new services,
libraries, drivers or even firmware in Rust (or improve existing code as needed).
Hoy intentaremos llamar a Rust desde un proyecto personal. Intenta encontrar
una pequeña esquina de tu código base donde podamos mover algunas líneas de
código a Rust. Cuantas menos dependencias y tipos ”exóticos” tenga, mejor. Lo
ideal sería algo que analizara bytes sin procesar.
El orador puede mencionar cualquiera de los siguientes aspectos, debido al aumento del uso
de Rust en Android:
• Ejemplo de servicio: DNS over HTTP
• Bibliotecas: Rutabaga Virtual Graphics Interface
• Controladores de kernel: Binder
• Firmware: firmware de pKVM
208
Capítulo 32
Configurar
We will be using a Cuttlefish Android Virtual Device to test our code. Make sure you have
access to one or create a new one with:
source build/[Link]
lunch aosp_cf_x86_64_phone-trunk_staging-userdebug
acloud create
Consulta el Codelab para desarrolladores de Android para obtener más información.
Puntos clave:
• Cuttlefish es un dispositivo Android de referencia diseñado para funcionar en
ordenadores genéricos Linux. También tenemos previsto ofrecer compatibilidad con
MacOS.
• La imagen del sistema de Cuttlefish mantiene una alta fidelidad a los dispositivos reales
y es el emulador ideal para ejecutar muchos casos prácticos de Rust.
209
Capítulo 33
El sistema de compilación de Android (Soong) es compatible con Rust a través de una serie
de módulos:
210
• Por razones de cumplimiento y rendimiento, Android debe tener crates en estructura de
árbol. También debe existir interoperabilidad con el código C, C++ y Java. Soong cumple
estos requisitos.
• Soong tiene muchas similitudes con Bazel, que es la variante de código abierto de Blaze
(se utiliza en google3).
• Está previsto hacer la transición de Android, ChromeOS y Fuchsia a Bazel.
• Aprender reglas de compilación similares a Bazel es útil para todos los desarrolladores
del SO de Rust.
• Dato curioso: los datos de Star Trek son un Android de tipo Soong.
211
rust_binary {
name: "hello_rust_with_dep",
crate_name: "hello_rust_with_dep",
srcs: ["src/[Link]"],
rustlibs: [
"libgreetings",
"libtextwrap",
],
prefer_rlib: true, // Es necesario para evitar errores de enlace dinámico.
}
rust_library {
name: "libgreetings",
crate_name: "greetings",
srcs: ["src/[Link]"],
}
hello_rust/src/[Link]:
//! Demo de Rust.
use greetings::greeting;
use textwrap::fill;
212
Capítulo 34
AIDL
213
• Note that the directory structure under the aidl/ directory needs to match the package
name used in the AIDL file, i.e. the package is [Link] and
the file is at aidl/com/example/[Link].
214
rust_library {
name: "libbirthdayservice",
srcs: ["src/[Link]"],
crate_name: "birthdayservice",
rustlibs: [
"[Link]-rust",
"libbinder_rs",
],
}
• Point out the path to the generated IBirthdayService trait, and explain why each of
the segments is necessary.
• TODO: What does the binder::Interface trait do? Are there methods to override?
Where source?
215
The process for taking a user-defined service implementation (in this case the BirthdayService
type, which implements the IBirthdayService) and starting it as a Binder service has
multiple steps, and may appear more complicated than students are used to if they've used
Binder from C++ or another language. Explain to students why each step is necessary.
1. Create an instance of your service type (BirthdayService).
2. Wrap the service object in corresponding Bn* type (BnBirthdayService in this case).
This type is generated by Binder and provides the common Binder functionality that
would be provided by the BnBinder base class in C++. We don't have inheritance in Rust,
so instead we use composition, putting our BirthdayService within the generated
BnBinderService.
3. Call add_service, giving it a service identifier and your service object (the
BnBirthdayService object in the example).
4. Call join_thread_pool to add the current thread to Binder's thread pool and start
listening for connections.
34.1.5 Despliegue
Ahora podemos crear, insertar e iniciar el servicio:
m birthday_server
adb push "$ANDROID_PRODUCT_OUT/system/bin/birthday_server" /data/local/tmp
adb root
adb shell /data/local/tmp/birthday_server
Comprueba que el servicio funciona en otra terminal:
adb shell service check birthdayservice
Service birthdayservice: found
También puedes llamar al servicio con service call:
adb shell service call birthdayservice 1 s16 Bob i32 24
Result: Parcel(
0x00000000: 00000000 00000036 00610048 00700070 '....6...H.a.p.p.'
0x00000010: 00200079 00690042 00740072 00640068 'y. .B.i.r.t.h.d.'
0x00000020: 00790061 00420020 0062006f 0020002c 'a.y. .B.o.b.,. .'
0x00000030: 006f0063 0067006e 00610072 00750074 'c.o.n.g.r.a.t.u.'
0x00000040: 0061006c 00690074 006e006f 00200073 'l.a.t.i.o.n.s. .'
0x00000050: 00690077 00680074 00740020 00650068 'w.i.t.h. .t.h.e.'
0x00000060: 00320020 00200034 00650079 00720061 ' .2.4. .y.e.a.r.'
0x00000070: 00210073 00000000 's.!..... ')
216
/// Llama al servicio de felicitación cumpleaños.
fn main() -> Result<(), Box<dyn Error>> {
let name = std::env::args().nth(1).unwrap_or_else(|| String::from("Bob"));
let years = std::env::args()
.nth(2)
.and_then(|arg| [Link]::<i32>().ok())
.unwrap_or(42);
binder::ProcessState::start_thread_pool();
let service = binder::get_interface::<dyn IBirthdayService>(SERVICE_IDENTIFIER)
.map_err(|_| "No se ha podido conectar con el servicio de felicitación de cumple
217
34.1.7 Cambio de API
Ampliemos la API con más funciones. Queremos que los clientes puedan indicar una lista de
líneas para la tarjeta de cumpleaños:
package [Link];
Ok(msg)
218
}
}
birthday_service/src/[Link]:
let msg = [Link](
&name,
years,
&[
String::from("Habby birfday to yuuuuu"),
String::from("And also: many more"),
],
)?;
• TODO: Move code snippets into project files where they'll actually be built?
219
• In Android 13 or higher, fixed-size arrays are supported, i.e. T[N] becomes [T; N].
Fixed-size arrays can have multiple dimensions (e.g. int[3][4]). In the Java backend,
fixed-size arrays are represented as array types.
• Arrays in parcelable fields always get translated to Vec<T>.
interface IBirthdayInfoProvider {
String name();
int years();
}
birthday_service/aidl/com/example/birthdayservice/[Link]:
import [Link];
interface IBirthdayService {
/** The same thing, but using a binder object. */
String wishWithProvider(IBirthdayInfoProvider provider);
fn main() {
binder::ProcessState::start_thread_pool();
let service = connect().expect("No se ha podido conectar con el servicio de felicita
220
// Create a binder object for the `IBirthdayInfoProvider` interface.
let provider = BnBirthdayInfoProvider::new_binder(
InfoProvider { name: [Link](), age: years as u8 },
BinderFeatures::default(),
);
34.2.4 Variables
Binder for Rust supports sending parcelables directly:
birthday_service/aidl/com/example/birthdayservice/[Link]:
package [Link];
parcelable BirthdayInfo {
String name;
int years;
}
birthday_service/aidl/com/example/birthdayservice/[Link]:
import [Link];
interface IBirthdayService {
/** The same thing, but with a parcelable. */
String wishWithInfo(in BirthdayInfo info);
}
birthday_service/src/[Link]:
fn main() {
binder::ProcessState::start_thread_pool();
let service = connect().expect("No se ha podido conectar con el servicio de felicita
221
interface IBirthdayService {
/** The same thing, but loads info from a file. */
String wishFromFile(in ParcelFileDescriptor infoFile);
}
birthday_service/src/[Link]:
fn main() {
binder::ProcessState::start_thread_pool();
let service = connect().expect("No se ha podido conectar con el servicio de felicita
222
Capítulo 35
Testing in Android
Building on Testing, we will now look at how unit tests work in AOSP. Use the rust_test
module for your unit tests:
testing/[Link]:
rust_library {
name: "libleftpad",
crate_name: "leftpad",
srcs: ["src/[Link]"],
}
rust_test {
name: "libleftpad_test",
crate_name: "leftpad_test",
srcs: ["src/[Link]"],
host_supported: true,
test_suites: ["general-tests"],
}
testing/src/[Link]:
//! Left-padding library.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_string() {
assert_eq!(leftpad("foo", 5), " foo");
}
223
#[test]
fn long_string() {
assert_eq!(leftpad("foobar", 6), "foobar");
}
}
You can now run the test with
atest --host libleftpad_test
The output looks like this:
INFO: Elapsed time: 2.666s, Critical Path: 2.40s
INFO: 3 processes: 2 internal, 1 linux-sandbox.
INFO: Build completed successfully, 3 total actions
//comprehensive-rust-android/testing:libleftpad_test_host PASSED in 2.3s
PASSED libleftpad_test.tests::long_string (0.0s)
PASSED libleftpad_test.tests::short_string (0.0s)
Test cases: finished with 2 passing and 0 failing out of 2 test cases
Notice how you only mention the root of the library crate. Tests are found recursively in
nested modules.
35.1 GoogleTest
The GoogleTest crate allows for flexible test assertions using matchers:
use googletest::prelude::*;
#[googletest::test]
fn test_elements_are() {
let value = vec!["foo", "bar", "baz"];
expect_that!(value, elements_are!(eq("foo"), lt("xyz"), starts_with("b")));
}
Si cambiamos el último elemento a "!", la prueba dará error y aparecerá un mensaje de
error estructurado que señala cuál es el fallo:
---- test_elements_are stdout ----
Value of: value
Expected: has elements:
0. is equal to "foo"
1. is less than "xyz"
2. starts with prefix "!"
Actual: ["foo", "bar", "baz"],
where element #2 is "baz", which does not start with "!"
at src/testing/[Link]:5
Error: See failure output above
This slide should take about 5 minutes.
• GoogleTest no forma parte de Rust Playground, por lo que debes llevar a cabo este
ejemplo en un entorno local. Usa cargo add googletest para añadirlo rápidamente
a un proyecto de Cargo que ya tengas.
224
• La línea use googletest::prelude::*; importa una serie de macros y tipos
habituales.
• This just scratches the surface, there are many builtin matchers. Consider going through
the first chapter of ”Advanced testing for Rust applications”, a self-guided Rust course: it
provides a guided introduction to the library, with exercises to help you get comfortable
with googletest macros, its matchers and its overall philosophy.
• A particularly nice feature is that mismatches in multi-line strings are shown as a diff:
#[test]
fn test_multiline_string_diff() {
let haiku = "Se ha encontrado la seguridad de la memoria,\n\
la potente escritura de Rust guía el camino,\n\
protege el código que vayas a escribir.";
assert_that!(
haiku,
eq("Se ha encontrado seguridad en la memoria,\n\
el divertido sentido del humor de Rust guía el camino,\n\
protege el código que vayas a escribir.")
);
}
muestra un diff con colores (colores que no se muestran aquí):
Value of: haiku
Expected: is equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure
Actual: "Memory safety found,\nRust's strong typing guides the way,\nSecure code you'll
which isn't equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure
Difference(-actual / +expected):
Memory safety found,
-Rust's strong typing guides the way,
+Rust's silly humor guides the way,
Secure code you'll write.
at src/testing/[Link]:5
• El crate es un puerto de Rust de GoogleTest para C++.
35.2 Simulaciones
Mockall es una biblioteca que se usa para hacer simulaciones. Debes refactorizar tu código
para usar traits, con los que podrás hacer simulaciones:
use std::time::Duration;
#[mockall::automock]
pub trait Pet {
fn is_hungry(&self, since_last_meal: Duration) -> bool;
}
#[test]
fn test_robot_dog() {
let mut mock_dog = MockPet::new();
225
mock_dog.expect_is_hungry().return_const(true);
assert_eq!(mock_dog.is_hungry(Duration::from_secs(10)), true);
}
This slide should take about 5 minutes.
• Mockall is the recommended mocking library in Android (AOSP). There are other
mocking libraries available on [Link], in particular in the area of mocking HTTP
services. The other mocking libraries work in a similar fashion as Mockall, meaning
that they make it easy to get a mock implementation of a given trait.
• Ten en cuenta que las simulaciones son algo polémicas, ya que te permiten aislar por
completo una prueba de sus dependencias. El resultado inmediato es una ejecución
de pruebas más rápida y estable. Por otro lado, las simulaciones se pueden configurar
de forma incorrecta y devuelven un resultado diferente del que se obtendría con las
dependencias reales.
Si es posible, te recomendamos que uses las dependencias reales. Por ejemplo, muchas
bases de datos te permiten configurar un backend en la memoria. Es decir, en tus pruebas
obtendrás el comportamiento correcto y, además, son rápidas y se limpiarán de forma
automática tras las pruebas.
Del mismo modo, muchos frameworks web te permiten iniciar un servidor en proceso
que se vincula a un puerto aleatorio en localhost. Siempre es mejor utilizar esta
opción en lugar de simular el framework, ya que te ayuda a hacer pruebas con el código
en el entorno real.
• Mockall no forma parte de Rust Playground, por lo que debes ejecutar este ejemplo en
un entorno local. Usa cargo add modelall para añadir de forma rápida Mockall a un
proyecto de Cargo.
• Mockall tiene muchas más funciones. En concreto, puedes configurar expectativas en
función de los argumentos. Aquí utilizamos el ejemplo para simular un gato que tiene
hambre 3 horas después de que le hayan dado de comer:
#[test]
fn test_robot_cat() {
let mut mock_cat = MockPet::new();
mock_cat
.expect_is_hungry()
.with(mockall::predicate::gt(Duration::from_secs(3 * 3600)))
.return_const(true);
mock_cat.expect_is_hungry().return_const(false);
assert_eq!(mock_cat.is_hungry(Duration::from_secs(1 * 3600)), false);
assert_eq!(mock_cat.is_hungry(Duration::from_secs(5 * 3600)), true);
}
• Puedes utilizar .times(n) para limitar el número de veces que se puede llamar
a un método de simulación a n. Si no se cumple, la simulación activará un pánico
automáticamente cuando se elimine.
226
Capítulo 36
Almacenamiento de registros
Utiliza el crate log para que se registre automáticamente en logcat (en el dispositivo) o
stdout (en el host):
hello_rust_logs/[Link]:
rust_binary {
name: "hello_rust_logs",
crate_name: "hello_rust_logs",
srcs: ["src/[Link]"],
rustlibs: [
"liblog_rust",
"liblogger",
],
host_supported: true,
}
hello_rust_logs/src/[Link]:
//! Demo de registros de Rust.
227
adb push "$ANDROID_PRODUCT_OUT/system/bin/hello_rust_logs" /data/local/tmp
adb shell /data/local/tmp/hello_rust_logs
Los registros se muestran en adb logcat:
adb logcat -s rust
09-08 08:38:32.454 2420 2420 D rust: hello_rust_logs: Starting program.
09-08 08:38:32.454 2420 2420 I rust: hello_rust_logs: Things are going fine.
09-08 08:38:32.454 2420 2420 E rust: hello_rust_logs: Something went wrong!
228
Capítulo 37
Interoperabilidad
Rust admite sin problemas la interoperabilidad con otros lenguajes. Esto significa que puedes
hacer lo siguiente:
• Llamar a funciones de Rust desde otros lenguajes.
• Llamar a funciones escritas en otros lenguajes desde Rust.
Cuando llamas a funciones en otro lenguaje, se dice que estás usando una interfaz de función
externa, también denominada FFI.
fn main() {
let x = -42;
// SAFETY: `abs` doesn't have any safety requirements.
let abs_x = unsafe { abs(x) };
println!("{x}, {abs_x}");
}
Ya lo hemos visto en el ejercicio Envoltorio de FFI seguro.
Esto supone un conocimiento completo de la plataforma objetivo. No se recomienda
para producción.
A continuación, estudiaremos otras opciones mejores.
229
37.1.1 Uso de Bindgen
La herramienta bindgen puede generar automáticamente enlaces desde un archivo de
encabezado de C.
En primer lugar, crea una biblioteca de C pequeña:
interoperability/bindgen/libbirthday.h:
typedef struct card {
const char* name;
int years;
} card;
230
rust_binary {
name: "print_birthday_card",
srcs: ["[Link]"],
rustlibs: ["libbirthday_bindgen"],
}
interoperability/bindgen/[Link]:
//! Demo de Bindgen.
fn main() {
let name = std::ffi::CString::new("Peter").unwrap();
let card = card { name: name.as_ptr(), years: 42 };
// SAFETY: The pointer we pass is valid because it came from a Rust
// reference, and the `name` it contains refers to `name` above which also
// remains valid. `print_card` doesn't store either pointer to use later
// after it returns.
unsafe {
print_card(&card as *const card);
}
}
Compila, inserta y ejecuta el binario en tu dispositivo:
m print_birthday_card
adb push "$ANDROID_PRODUCT_OUT/system/bin/print_birthday_card" /data/local/tmp
adb shell /data/local/tmp/print_birthday_card
Por último, podemos ejecutar pruebas generadas automáticamente para comprobar que los
enlaces funcionan:
interoperability/bindgen/[Link]:
rust_test {
name: "libbirthday_bindgen_test",
srcs: [":libbirthday_bindgen"],
crate_name: "libbirthday_bindgen_test",
test_suites: ["general-tests"],
auto_gen_config: true,
clippy_lints: "ninguno", // Archivo generado, se omite la ejecución de lint
lints: "ninguno",
}
atest libbirthday_bindgen_test
231
use std::os::raw::c_int;
extern "C" {
void analyze_numbers(int x, int y);
}
#endif
interoperability/rust/libanalyze/[Link]
rust_ffi {
name: "libanalyze_ffi",
crate_name: "analyze_ffi",
srcs: ["[Link]"],
include_dirs: ["."],
}
Ahora podemos llamarlo desde un binario de C:
interoperability/rust/analyze/main.c
#include "analyze.h"
int main() {
analyze_numbers(10, 20);
analyze_numbers(123, 123);
return 0;
}
interoperability/rust/analyze/[Link]
cc_binary {
name: "analyze_numbers",
srcs: ["main.c"],
static_libs: ["libanalyze_ffi"],
}
Compila, inserta y ejecuta el binario en tu dispositivo:
m analyze_numbers
adb push "$ANDROID_PRODUCT_OUT/system/bin/analyze_numbers" /data/local/tmp
232
adb shell /data/local/tmp/analyze_numbers
”#[no_mangle]” inhabilita la modificación de nombres habitual de Rust, por lo que el
símbolo exportado será el nombre de la función. También puedes utilizar #[export_name
= "some_name"] para especificar el nombre que quieras.
type BlobstoreClient;
233
fn put(self: Pin<&mut BlobstoreClient>, parts: &mut MultiBuf) -> u64;
fn tag(self: Pin<&mut BlobstoreClient>, blobid: u64, tag: &str);
fn metadata(&self, blobid: u64) -> BlobMetadata;
}
}
• Bridge se declara generalmente en un módulo ffi dentro del crate.
• A partir de las declaraciones que se han hecho en el módulo bridge, CXX generará
definiciones de funciones o tipos de Rust y C++ que coincidan para exponer esos
elementos a ambos lenguajes.
• Para ver el código de Rust generado, usa cargo-expand para ver la macro de
procedimiento desplegada. En la mayoría de los ejemplos, se utilizaría cargo expand
::ffi para desplegar únicamente el módulo ffi (aunque esta acción no se aplica a los
proyectos de Android).
• Para ver el código C++ generado, consulta target/cxxbridge.
struct MyType(i32);
impl MyType {
fn foo(&self) {
println!("{}", self.0);
}
}
234
extern "Rust" {
type MultiBuf;
private:
friend ::rust::layout;
struct layout {
static ::std::size_t size() noexcept;
static ::std::size_t align() noexcept;
};
};
type BlobstoreClient;
235
impl BlobstoreClient {
pub fn put(&self, parts: &mut MultiBuf) -> u64 {
extern "C" {
#[link_name = "org$blobstore$cxxbridge1$BlobstoreClient$put"]
fn __put(
_: &BlobstoreClient,
parts: *mut ::cxx::core::ffi::c_void,
) -> u64;
}
unsafe {
__put(self, parts as *mut MultiBuf as *mut ::cxx::core::ffi::c_void)
}
}
}
// ...
• El programador no tiene que asegurar que las firmas que ha introducido son precisas.
CXX lleva a cabo aserciones estáticas en las que las firmas se corresponden exactamente
con lo que se declara en C++.
• Los bloques unsafe extern permiten declarar funciones de C++ que se pueden llamar
de forma segura desde Rust.
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
}
• Solo se admiten enums tipo C (unidad).
• Un número limitado de traits es compatible con #[derive()] en los tipos compartidos.
La función correspondiente también se genera para el código C++; por ejemplo, si
derivas Hash, también genera una implementación de std::hash para el tipo de C++
correspondiente.
236
37.2.6 Enums compartidos
#[cxx::bridge]
mod ffi {
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
}
Rust generado:
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct Suit {
pub repr: u8,
}
#[allow(non_upper_case_globals)]
impl Suit {
pub const Clubs: Self = Suit { repr: 0 };
pub const Diamonds: Self = Suit { repr: 1 };
pub const Hearts: Self = Suit { repr: 2 };
pub const Spades: Self = Suit { repr: 3 };
}
C++ generado:
enum class Suit : uint8_t {
Clubs = 0,
Diamonds = 1,
Hearts = 2,
Spades = 3,
};
• En Rust, el código generado para las enums compartidas es en realidad una estructura
que envuelve un valor numérico. Esto se debe a que no es un comportamiento indefinido
en C++ para que una clase de enum contenga un valor distinto de todas las variantes
enumeradas y nuestra representación en Rust debe tener el mismo comportamiento.
237
return Err(anyhow::Error::msg("fallible1 requiere una profundidad > 0"));
}
Ok("Correcto.".into())
}
• Las funciones de Rust que devuelven Result se convierten en excepciones en C++.
• La excepción que se genera siempre será del tipo rust::Error, que muestra
principalmente una forma de obtener la cadena del mensaje de error. El mensaje de
error procede de la implementación Display del tipo de error.
• Si un pánico pasa de Rust a C++, el proceso siempre finalizará inmediatamente.
fn main() {
if let Err(err) = ffi::fallible(99) {
eprintln!("Error: {}", err);
process::exit(1);
}
}
• Las funciones de C++ declaradas para devolver un Result detectarán cualquier
excepción en C++ y la devolverán como un valor Err a la función de llamada de Rust.
• Si se produce una excepción desde una función externa de ”C++” no declarada por el
bridge de CXX para devolver Result, el programa llamará a std::terminate de C++.
El comportamiento equivale a la misma excepción que se genera mediante una función
noexceptde C++.
238
• Ten en cuenta que String de Rust no se cruza directamente con std::string. Puede
haber varios motivos:
– std::string no mantiene la invariante de UTF-8 que requiere String.
– Los dos tipos tienen diseños diferentes en la memoria y, por lo tanto, no se pueden
transferir directamente entre lenguajes.
– std::string requiere constructores de movimiento que no coincidan con la
semántica de movimiento de Rust, por lo que std::string no se puede transferir
a Rust mediante un valor.
239
cmd: "$(location cxxbridge) $(in) > $(out)",
srcs: ["[Link]"],
out: ["[Link]"],
}
• La herramienta cxxbridge es una herramienta independiente que genera el lado C++
del módulo de bridge. Se incluye en Android y está disponible como herramienta de
Soong.
• Por convención, si el archivo de origen de Rust es [Link], el archivo de encabezado se
llamará [Link].h y el archivo de origen, [Link]. Sin embargo, esta convención en
cuanto a la nomenclatura no es obligatoria.
240
rust_ffi_shared {
name: "libhello_jni",
crate_name: "hello_jni",
srcs: ["src/[Link]"],
rustlibs: ["libjni"],
}
We then call this function from Java:
interoperability/java/[Link]:
class HelloWorld {
private static native String hello(String name);
static {
[Link]("hello_jni");
}
241
Capítulo 38
Ejercicios
Este es un ejercicio de grupo: escogeremos uno de los proyectos con los que se esté trabajando
e intentaremos integrar Rust en él. Algunas sugerencias:
• Llama a tu servicio de AIDL con un cliente escrito en Rust.
• Mueve una función desde tu proyecto a Rust y llámala.
Aquí la solución es abierta, ya que depende de que alguno de los asistentes tenga un fragmento
de código que se pueda convertir en Rust sobre la marcha.
242
Parte X
Chromium
243
Capítulo 39
Rust es compatible con bibliotecas de terceros en Chromium, con código pegamento propio
para conectar Rust y el código de C++ de Chromium ya existente.
Hoy vamos a llamar a Rust para que haga algo divertido con las cadenas. Si tienes
una esquina del código donde se muestra una cadena UTF8 al usuario, no dudes en
seguir estas instrucciones en tu parte del código base en lugar de la parte exacta
de la que hablamos.
244
Capítulo 40
Configurar
245
Información sobre los ejercicios
Esta parte del curso consta de una serie de ejercicios que se complementan entre sí. Las
iremos repartiendo a lo largo del curso en lugar de hacerlos todo al final. Si no tienes tiempo
para completar una parte concreta, no te preocupes, podrás ponerte al día en la siguiente
clase.
246
Capítulo 41
The Rust community typically uses cargo and libraries from [Link]. Chromium is built
using gn and ninja and a curated set of dependencies.
A la hora de escribir código en Rust, hay disponibles varias opciones:
• Usar gn y ninja con la ayuda de las plantillas de //build/rust/*.gni (por ejemplo,
rust_static_library, que veremos más adelante). Se usan la cadena de herramientas
y los crates auditados de Chromium.
• Usar cargo, pero restringiendo el uso de la cadena de herramientas y los crates auditados
de Chromium.
• Usa cargo con una cadena de herramientas o crates descargados de Internet.
A partir de ahora, nos centraremos en gn y ninja, ya que así es como se puede compilar el
código de Rust en el navegador Chromium. De igual forma, Cargo es una parte importante
del ecosistema de Rust y deberías conservarlo en tu caja de herramientas.
Ejercicio rápido
Formad grupos pequeños para:
• Hacer una lluvia de ideas sobre situaciones en las que cargo pueda ofrecer ventajas y
evaluar el perfil de riesgo.
• Debatir en qué herramientas, bibliotecas y grupos de personas hay que confiar al usar
gn y ninja, cargo offline, etc.
Pide a los participantes que eviten mirar las notas del orador antes de completar el ejercicio.
Suponiendo que todas las personas que hacen el curso están en la misma sala, pídeles que
hablen en grupos pequeños de 3 a 4 personas.
Notas y sugerencias relacionadas con la primera parte del ejercicio (”situaciones en las que
Cargo puede ofrecer ventajas”):
• Es genial que al escribir una herramienta o crear prototipos de una parte de Chromium
se pueda acceder al extenso ecosistema de bibliotecas [Link]. Hay un crate para
247
casi cualquier cosa y suelen ser muy fáciles de usar (clap para el análisis de la línea
de comandos, serde para la serialización y deserialización a/desde varios formatos,
itertools para trabajar con iteradores, etc.).
– cargo permite probar una biblioteca fácilmente (solo hay que añadir una línea a
[Link] y empezar a escribir el código).
– Merece la pena comparar cómo la CPAN ayudó a que perl fuera una opción popular
o compararlo con python + pip.
• La experiencia de desarrollo no solo es agradable gracias a las herramientas principales
de Rust (por ejemplo, al usar rustup para cambiar a una versión diferente de
rustc cuando se prueba un crate que necesita funcionar en nightly, stable actual
y antiguas versiones de stable), sino también a un ecosistema de herramientas de
terceros (por ejemplo, Mozilla proporciona cargo vet para optimizar y compartir
auditorías de seguridad; el crate criterion ofrece un método optimizado para ejecutar
comparativas).
– cargo permite añadir fácilmente una herramienta mediante cargoinstall --
locked cargo-vet.
– Merece la pena compararlo con las extensiones de Chrome o con las de VScode.
• Ejemplos generales y genéricos de proyectos en los que cargo puede ser la opción más
adecuada:
– Sorprendentemente, Rust se está volviendo cada vez más popular en el sector por
su función de escritura de herramientas de línea de comandos. La amplitud y la
ergonomía de las bibliotecas son similares a las de Python, pero son más sólidas
(gracias al sistema de tipos enriquecido) y funcionan más rápido (como lenguaje
compilado en lugar de interpretado).
– Para participar en el ecosistema de Rust, es necesario usar herramientas estándar
de Rust, como Cargo. Las bibliotecas que quieran recibir contribuciones externas
y actuar fuera de Chromium (por ejemplo, en entornos de desarrollo de Bazel o
Android/Soong) deberían utilizar Cargo.
• Ejemplos de proyectos relacionados con Chromium que se basan en cargo:
– serde_json_lenient (experimentado en otras partes de Google que ha dado lugar
a PRs con mejoras de rendimiento).
– Bibliotecas de fuentes, como font-types.
– La herramienta gnrt (la veremos más adelante en el curso), que depende de
clap para el análisis de la línea de comandos y de toml para los archivos de
configuración.
* Disclaimer: a unique reason for using cargo was unavailability of gn when
building and bootstrapping Rust standard library when building Rust toolchain.
* run_gnrt.py uses Chromium's copy of cargo and rustc. gnrt depends on
third-party libraries downloaded from the internet, but run_gnrt.py asks
cargo that only --locked content is allowed via [Link].)
Los participantes pueden tratar de identificar si los siguientes elementos son de confianza
implícita o explícita:
• rustc (el compilador de Rust), que a su vez depende de las bibliotecas LLVM, el
compilador Clang, las fuentes rustc (obtenidas de GitHub y revisadas por el equipo de
compilación de Rust), y el compilador binario de Rust descargado para el bootstrapping.
248
• rustup (merece la pena destacar que rustup se desarrolla en la misma organización
que rustc, [Link]
• cargo, rustfmt, etc.
• Varias infraestructuras internas (bots que compilan rustc, sistemas para distribuir la
cadena de herramientas precompiladas a los ingenieros de Chromium, etc.)
• Herramientas de Cargo, como cargo audit, cargo vet, etc.
• Bibliotecas de Rust incluidas en //third_party/rust (auditoría de security@[Link]).
• Otras bibliotecas de Rust (algunas de nicho, otras bastante populares y de uso común).
249
Capítulo 42
Chromium aún no permite usar Rust propio, excepto en casos excepcionales, según lo
aprobado por Area Tech Leads.
La política de Chromium sobre bibliotecas de terceros se describe aquí. Se permite el uso de
Rust para bibliotecas de terceros en algunos casos, incluido si son la mejor opción en cuanto
al rendimiento o seguridad.
Muy pocas bibliotecas de Rust exponen directamente una API de C o C++, por lo que casi todas
estas bibliotecas necesitarán una pequeña parte de código pegamento propio.
"C++" Rust
.- - - - - - - - - -. .- - - - - - - - - - - - - - - - - - - - - - -.
: : : :
: Existing Chromium : : Chromium Rust Existing Rust :
: "C++" : : "wrapper" crate :
: +---------------+ : : +----------------+ +-------------+ :
: | | : : | | | | :
: | o-----+-+-----------+-+-> o-+----------+--> | :
: | | : Language : | | Crate | | :
: +---------------+ : boundary : +----------------+ API +-------------+ :
: : : :
`- - - - - - - - - -' `- - - - - - - - - - - - - - - - - - - - - - -'
El código pegamento propio de Rust para un crate de terceros concreto
normalmente debe guardarse en third_party/rust/<crate>/<version>/wrapper.
Por este motivo, el curso de hoy se centrará en los siguientes temas:
• Incorporación de bibliotecas Rust de terceros (”crates”).
• Escribir código pegamento para poder usar esos crates desde Chromium C++.
Si esta política cambia con el tiempo, el curso irá evolucionando para adaptarse al cambio.
250
Capítulo 43
El código de Rust se suele compilar con cargo. Chromium se compila con gn y ninja para
aumentar la eficiencia. Sus reglas estáticas permiten el máximo paralelismo. Rust no es una
excepción.
rust_static_library("my_rust_lib") {
crate_root = "[Link]"
sources = [ "[Link]" ]
}
También puedes añadir deps en otros segmentos de Rust. Más adelante, lo usaremos en
función del código de terceros.
Debes especificar tanto la raíz del crate como una lista completa de recursos. crate_root es
el archivo proporcionado al compilador de Rust que representa el archivo raíz de la unidad
de compilación, que suele ser [Link]. sources es una lista completa de todos los archivos
de origen que necesita ninja para determinar cuándo es necesario compilar de nuevo.
(No existe source_set en Rust porque un crate completo ya es una unidad de compilación.
Una static_library es la unidad más pequeña).
Puede que los participantes se pregunten por qué necesitamos una plantilla gn en vez de
usar la compatibilidad integrada de gn para las bibliotecas estáticas de Rust. La respuesta es
que esta plantilla es compatible con la interoperabilidad de CXX, las funciones de Rust y las
pruebas unitarias, algunas de las cuales usaremos más adelante.
251
= true al elemento de destino de gn. (Más adelante en el curso veremos circunstancias en
las que es necesario hacerlo).
import("//build/rust/rust_static_library.gni")
rust_static_library("my_rust_lib") {
crate_root = "[Link]"
sources = [
"[Link]",
"[Link]"
]
allow_unsafe = true
}
rust_static_library("my_rust_lib") {
crate_root = "[Link]"
sources = [ "[Link]" ]
}
252
Una demo de algunas de las funciones de anotación de código y exploración de rust-analyzer
puede ser útil si los asistentes se muestran escépticos por los IDE.
Los siguientes pasos pueden servir de ayuda con la demo (aunque puedes usar un fragmento
de Rust relacionado con Chromium que te resulte más familiar):
• Abre components/qr_code_generator/qr_code_generator_ffi_glue.rs.
• Coloca el cursor sobre la llamada QrCode::new (aproximadamente en la línea 26) en
‘qr_code_generator_ffi_glue.rs.
• Demo mostrar la documentación (enlaces típicos: vscode = ctrl ki; vim/CoC = K).
• Demo ir a la definición (enlaces típicos: vscode = F12; vim/CoC = gd) (Esta acción te
llevará a //third_party/rust/.../qr_code-.../src/[Link]).
• Demo esquema y desplázate hasta el método QrCode::with_bits (en la línea 164. El
esquema se encuentra en el panel del explorador de archivos de vscode. Enlaces típicos
de vim/CoC = espacio o).
• Demo type annotations (there are quite a few nice examples in the QrCode::with_bits
method)
Es necesario destacar que hay que volver a ejecutar gn gen ... --export-rust-project
después de editar los archivos [Link] (lo haremos varias veces a lo largo de los ejercicios
de esta sesión).
253
Añade este nuevo elemento de destino de Rust como una dependencia de //ui/base:base.
Declara esta función en la parte superior de ui/base/resource/resource_bundle.cc
(más adelante veremos cómo se puede automatizar mediante herramientas de generación de
enlaces):
extern "C" void hello_from_rust();
Llama a esta función desde algún lugar de ui/base/resource/resource_bundle.cc.
Recomendamos hacerlo en la parte superior de ResourceBundle::RSMangleLocalizedString.
Compila y ejecuta Chromium y asegúrate de que se imprima ”¡Rust te manda un saludo!”
muchas veces.
Si usas VSCode, ahora debes configurar Rust para que funcione correctamente en VSCode.
Nos será útil en ejercicios posteriores. Si lo has completado correctamente, podrás hacer clic
con el botón derecho y pulsar ”Ir a la definición” en println!.
254
Capítulo 44
Probando
La comunidad de Rust suele crear las pruebas unitarias en un módulo situado en el mismo
archivo fuente que el código que se está probando. Este tema ya se ha tratado antes en el
curso y tiene este aspecto:
#[cfg(test)]
mod tests {
#[test]
fn my_test() {
todo!()
}
}
En Chromium colocamos las pruebas unitarias en un archivo fuente independiente y
continuamos con esta práctica con Rust. De esta forma, las pruebas se pueden encontrar de
forma coherente y se evita volver a crear archivos .rs (en la configuración test).
Esta acción genera las siguientes opciones para probar el código de Rust en Chromium:
• Pruebas nativas de Rust (es decir, #[test]). No se recomienda hacerlas fuera de
//third_party/rust.
• Pruebas gtest escritas en C++ y ejercicios con Rust mediante llamadas de FFI. Suficiente
cuando el código de Rust es solo una capa fina de FFI y las pruebas unitarias existentes
proporcionan suficiente cobertura para la función.
• Pruebas gtest creadas en Rust y usando el crate en prueba a través de su API pública
(mediante pub mod for_testing { ... } si es necesario). Este es el tema de las
siguientes diapositivas.
Menciona que los bots de Chromium deberían hacer pruebas nativas de Rust de crates de
terceros tarde o temprano. (Estas pruebas rara vez son necesarias, solo después de añadir o
actualizar crates de terceros).
Algunos ejemplos pueden ayudarte a ilustrar cuándo se debe usar gtest de C++ o gtest de
Rust:
• QR cuenta con muy pocas funciones en la capa de Rust propia (solo es un código
pegamento de FFI) y, por lo tanto, utiliza las pruebas unitarias de C++ existentes para
probar la implementación de C++ y la de Rust (parametrizando las pruebas de modo
que habiliten o inhabiliten Rust mediante un ScopedFeatureList).
255
• La integración hipotética/WIP de PNG puede necesitar una implementación segura en
memoria de las transformaciones de píxeles que proporciona libpng pero que faltan
en el crate png, como por ejemplo, RGBA => BGRA o corrección gamma. Dicha función
puede beneficiarse de pruebas independientes creadas en Rust.
#[gtest(MyRustTestSuite, MyAdditionTest)]
fn test_addition() {
expect_eq!(2 + 2, 4);
}
test("ui_base_unittests") {
...
deps += [ ":my_rust_lib_unittests" ]
}
256
44.3 Macro chromium::import!
Después de añadir :my_rust_lib a GN deps, tenemos que aprender a importar y usar
my_rust_lib desde my_rust_lib_unittest.rs. No hemos proporcionado un crate_name
explícito para my_rust_lib, por lo que el nombre del crate se calcula en función de la ruta
y el nombre de destino completos. Por suerte, podemos evitar trabajar con un nombre
tan poco práctico usando la macro chromium::import! del crate chromium importado
automáticamente:
chromium::import! {
"//ui/base:my_rust_lib";
}
use my_rust_lib::my_function_under_test;
En un segundo plano, la macro se expande a algo parecido a lo siguiente:
extern crate ui_sbase_cmy_urust_ulib as my_rust_lib;
use my_rust_lib::my_function_under_test;
Puedes obtener más información en el comentario del documento de la macro
chromium::import.
rust_static_library permite especificar un nombre explícito mediante la propiedad
crate_name, pero no se recomienda hacerlo. El motivo es porque el nombre del crate debe
ser único en todo el mundo. [Link] garantiza la exclusividad de sus nombres de crate, de
modo que los elementos de destino de GN cargo_crate (generados por la herramienta gnrt
que se explican en una sección posterior) usan nombres de crate cortos.
257
Capítulo 45
258
puntero y una longitud. Esto suele acarrear errores, ya que cada lenguaje representa
los slices vacíos de forma ligeramente distinta.
– Los punteros inteligentes como std::unique_ptr<T>, std::shared_ptr<T> o
Box se admiten de forma nativa. Con los enlaces manuales, sería necesario pasar
punteros sin formato compatibles con ABI de C, lo que aumentaría los riesgos de
tiempo de vida y de seguridad en la memoria.
– Los tipos rust::String y CxxString entienden y mantienen las diferencias
en la representación de cadenas en los distintos lenguajes (por ejemplo,
rust::String::lossy puede crear una cadena de Rust a partir de una entrada
que no sea UTF8 y rust::String::: c_str puede terminar una cadena con un
carácter nulo).
45.1 Ejemplos
CXX necesita que se declare todo el límite de C++ o Rust en los módulos cxx::bridge del
código fuente .rs.
#[cxx::bridge]
mod ffi {
extern "Rust" {
type MultiBuf;
type BlobstoreClient;
259
Limitaciones de CXX
By far the most useful page when using CXX is the type reference.
CXX se adapta básicamente a los casos en los que:
• La interfaz de Rust-C++ es lo suficientemente sencilla como para se pueda declarar por
completo.
• Solo estás usando los tipos compatibles de forma nativa con CXX, como std::unique_ptr,
std::string o &[u8], entre otros.
Tiene muchas limitaciones, por ejemplo, la falta de compatibilidad con el tipo Option de
Rust.
Estas restricciones nos limitan a usar Rust en Chromium solo para ”nodos hoja” muy aislados,
en lugar de para la interoperabilidad arbitraria de Rust-C++. Si te planteas un caso práctico
de Rust en Chromium, un buen punto de partida es hacer un borrador de los enlaces de CXX
para el límite del lenguaje para ver si te parece lo suficientemente sencillo.
In addition, right now, Rust code in one component cannot depend on Rust code in another,
due to linking details in our component build. That's another reason to restrict Rust to use in
leaf nodes.
También debes hablar de algunos de los otros aspectos delicados con CXX, como los siguientes:
• Su gestión de errores se basa en las excepciones de C++ (como se muestra en la siguiente
diapositiva).
• Los punteros de función no son muy fáciles de usar.
260
45.2.1 Manejo de Errores en CXX: Ejemplo de QR
El generador de código QR es un ejemplo en el que el valor booleano se utiliza para comunicar
que el resultado es correcto o no, y dónde se puede transmitir el resultado correcto a través
del límite de FFI:
#[cxx::bridge(namespace = "qr_code_generator")]
mod ffi {
extern "Rust" {
fn generate_qr_code_using_rust(
data: &[u8],
min_version: i16,
out_pixels: Pin<&mut CxxVector<u8>>,
out_qr_size: &mut usize,
) -> bool;
}
}
Puede que los participantes sientan curiosidad acerca de la semántica del resultado de salida
out_qr_size. No se trata del tamaño del vector, sino del tamaño del código QR (y admitimos
que es un poco redundante, ya que se trata de la raíz cuadrada del tamaño del vector).
Cabe destacar la importancia de inicializar out_qr_size antes de llamar a la función de
Rust. La creación de una referencia de Rust que apunte a una memoria no inicializada tiene
como resultado un comportamiento indefinido (a diferencia de C++, cuando solo el acto de
desreferenciar la memoria resulta en comportamiento indefinido).
Si los participantes preguntan por Pin, explica por qué CXX lo necesita para referencias
mutables a datos de C++. Los datos de C++ no se pueden mover como los datos de Rust, ya
que pueden contener punteros de autorreferencia.
261
fn width(self: &PngReader) -> u32;
fn read_rgba8(self: &mut PngReader, output: &mut [u8]) -> bool;
}
}
PngReader y ResultOfPngReader son tipos de Rust. Los objetos de estos tipos no pueden
cruzar el límite de FFI sin la indirección de un Box<T>. No se puede tener un out_parameter:
&mut PngReader, ya que CXX no permite que C++ almacene objetos de Rust por valor.
Este ejemplo ilustra que, aunque CXX no es compatible con plantillas ni genéricos arbitrarios,
podemos transmitirlos a través de los límites de FFI si los especializamos de forma manual o
los monomorfizamos en un tipo no genérico. En el ejemplo, ResultOfPngReader es un tipo
no genérico que redirige los métodos adecuados de Result<T, E> (por ejemplo, a is_err,
unwrap o as_mut).
262
45.3 Ejercicio: Interoperabilidad con C++
Primera parte
• En el archivo de Rust que has creado anteriormente, añade un #[cxx::bridge], que
especifica una sola función, denominada hello_from_rust, a la que se llamará desde
C++, sin parámetros y sin devolver ningún valor.
• Modifica la función hello_from_rust anterior para eliminar extern "C" y
#[no_mangle]. Ahora es solo una función estándar de Rust.
• Modifica el elemento de destino de gn para compilar estos enlaces.
• En el código C++, elimina la declaración de hello_from_rust. En su lugar, incluye el
archivo de encabezado que se ha generado.
• Compila y ejecuta.
Segunda parte
Se recomienda jugar un poco con CXX, ya que nos ayuda a pensar en la flexibilidad que tiene
Rust en Chromium.
Algunas cosas que probar:
• Vuelve a llamar a C++ desde Rust. Necesitarás lo siguiente:
– Un archivo de encabezado adicional que puedes include! desde tu cxx::bridge.
Deberás declarar la función de C++ en el nuevo archivo de encabezado.
– Un bloque unsafe para llamar a una función de este tipo, o bien especificar la
palabra clave unsafe en el #[cxx::bridge], como se describe aquí.
– Es posible que también tengas que incluir #include "third_party/rust/cxx/v1/crate/include/
• Transfiere una cadena de C++ desde C++ a Rust.
• Pasa una referencia a un objeto de C++ en Rust.
• Obtén de forma intencional las firmas de la función de Rust que no coincidan con el
#[cxx::bridge] y familiarízate con los errores que veas.
• Obtén de forma intencional las firmas de la función de C++ que no coincidan con el
#[cxx::bridge] y familiarízate con los errores que veas.
• Transfiere un std::unique_ptr de algún tipo de C++ a Rust, para que a Rust le
pertenezca algún objeto de C++.
• Crea un objeto de Rust y transmítelo a C++ para que sea su propietario. (Nota: necesitas
utilizar un Box).
• Declara algunos métodos en un tipo de C++. Llámalos desde Rust.
• Declara algunos métodos en un tipo de Rust. Llámalos desde C++.
Tercera parte
Ahora que conoces los puntos fuertes y las limitaciones de la interoperabilidad de CXX, piensa
en un par de casos prácticos de Rust en Chromium en los que la interfaz sea bastante sencilla.
Haz un boceto sobre cómo definirías esa interfaz.
263
As students explore Part Two, they're bound to have lots of questions about how to achieve
these things, and also how CXX works behind the scenes.
Estas son algunas de las preguntas que pueden surgir:
• Veo un problema al inicializar una variable de tipo X con el tipo Y, donde X e Y son
tipos de funciones. Esto se debe a que la función de C++ no coincide exactamente con la
declaración de cxx::bridge.
• Parece que puedo convertir libremente referencias de C++ en referencias de Rust. ¿Eso
no supone ningún riesgo de comportamiento indefinido? En el caso de los tipos opacos
de CXX, no, porque su tamaño es cero. Sí supondría un problema de comportamiento
indefinido en el caso de los tipos triviales de CXX, aunque el diseño de CXX hace que sea
bastante difícil crear un ejemplo así.
264
Capítulo 46
Las bibliotecas de Rust se llaman ”crates” y se encuentran en [Link]. Es habitual que los
crates de Rust dependen los unos de otros.
265
Al igual que con cualquier otro archivo [Link], puedes especificar más información
sobre las dependencias. Lo más habitual es que se especifiquen las funciones features que
se quieran habilitar en el crate.
Al añadir un crate a Chromium, a menudo será necesario proporcionar información adicional
en un archivo adicional, gnrt_config.toml, que veremos a continuación.
266
46.4 Generar reglas de compilación gn
Una vez que hayas descargado el crate, genera los archivos [Link] como se indica a
continuación:
vpython3 tools/crates/run_gnrt.py -- gen
Ahora, ejecuta git status. Deberías encontrar lo siguiente:
• Al menos un nuevo código fuente de crate en third_party/rust/chromium_crates_io/vendor.
• Al menos un nuevo [Link] en third_party/rust/<crate name>/v<major
semver version>.
• Un archivo [Link] adecuado.
La ”versión semver mayor” es un número de versión ”semver” de Rust.
Analiza la situación con detalle, sobre todo los elementos generados en third_party/rust.
Habla un poco sobre el semver y, concretamente, sobre la forma en que Chromium permite
que existan varias versiones incompatibles de un crate. No es una situación recomendable,
pero a veces es necesaria en el ecosistema de Cargo.
Por suerte, la mayoría de los crates no contienen scripts de compilación y la mayoría de estos
scripts de compilación solo llevan a cabo dos acciones principales.
267
Si es así, modifica gnrt_config.toml para añadir build-script-outputs al crate. Si se
trata de una dependencia transitiva, de la que el código Chromium no debería depender de
forma directa, añade también allow-first-party-usage=false. En ese archivo ya hay
varios ejemplos:
[[Link]-linebreak]
allow-first-party-usage = false
build-script-outputs = ["[Link]"]
A continuación, vuelve a ejecutar [Link] -- gen para generar de nuevo los archivos
[Link] e informar al ninja de que este archivo de salida concreto se usa como entrada en
los pasos de compilación posteriores.
268
46.7 Auditoría de Crates de Terceros
Añadir nuevas bibliotecas está sujeto a las políticas estándar de Chromium, pero también
a la revisión de seguridad. Como puede que no solo incluyas un único crate sino también
dependencias transitivas, es posible que haya mucho código que revisar. Por otro lado, el
código de Rust seguro puede tener efectos secundarios negativos limitados. ¿Cómo se revisa?
Con el tiempo, Chromium intentará adoptar un proceso basado en cargo vet.
Mientras tanto, se debe hacer lo siguiente para cada nuevo crate:
• Entender por qué se usa cada crate. ¿Cuál es la relación entre los crates? Si el sistema de
compilación de cada crate contiene un archivo [Link] o macros de procedimiento,
averigua para qué sirven. ¿Son compatibles con la forma en la que se compila
normalmente Chromium?
• Comprobar que cada crate tenga un mantenimiento razonable.
• Usar cd third-party/rust/chromium_crates_io; cargo audit para comprobar
si existen vulnerabilidades (primero se tiene que usar cargo install cargo-audit,
lo que, irónicamente, implica descargar muchas dependencias de Internet2).
• Asegúrate de que cualquier código unsafe sea adecuado para la Regla de dos.
• Comprobar si se usan las APIs fs o net.
• Leer todo el código con suficiente profundidad para comprobar si hay algo fuera de
lugar que pueda haberse insertado de forma malintencionada. (Es imposible hacerlo
perfecto, ya que, a menudo, hay demasiado código).
Estas son solo algunas directrices, trabaja con revisores de security@[Link] para
determinar la forma adecuada de utilizar los crates.
269
46.9 Mantener los crates actualizados
Como PROPIETARIO de cualquier dependencia de Chromium de terceros, se espera que la
actualices con las correcciones de seguridad. La idea es que pronto automaticemos esto para
los crates de Rust, pero por ahora sigue siendo tu responsabilidad, igual que cualquier otra
dependencia de terceros.
46.10 Ejercicio
Añade uwuify a Chromium para desactivar las [funciones predeterminadas] del crate
([Link] Supongamos
que el crate se usará en el envío de Chromium, pero no para gestionar entradas no fiables.
(En el siguiente ejercicio, usaremos uwuify de Chromium, pero puedes saltarte este paso y
hacerlo ahora si quieres. También puedes crear un nuevo destino rust_executable que
utilice uwuify).
Los participantes tendrán que descargar muchas dependencias transitivas.
Estos son los crates que se necesitan:
• instant,
• lock_api
• parking_lot
• parking_lot_core
• redox_syscall
• scopeguard
• smallvec
• uwuify
Si los alumnos se descargan más datos, seguramente habrán olvidado desactivar las funciones
predeterminadas.
Gracias a Daniel Liu por este crate.
270
Capítulo 47
En este ejercicio, vas a añadir una función de Chromium completamente nueva que pondrá
en práctica todo lo que hemos aprendido.
Pasos
Modifica ResourceBundle::RSMangleLocalizedString para que traduzca todas las
cadenas antes de que se muestren. En esta compilación especial de Chromium, siempre se
debe hacer esto independientemente de la configuración de mangle_localized_strings_.
Si has hecho correctamente los ejercicios, habrás creado Chrome para hadas.
271
Students will likely need some hints here. Hints include:
• UTF16 y UTF8. Los alumnos deben tener en cuenta que las cadenas de Rust siempre
son UTF8. Probablemente decidirán que es mejor hacer la conversión en C++ usando
base::UTF16ToUTF8 y viceversa.
• Si los participantes deciden hacer la conversión en Rust, deberán tener en cuenta
String::from_utf16, la gestión de errores y los tipos compatibles con CXX que pueden
transferir un gran número de u16s.
• Los alumnos pueden diseñar el límite de C++ o Rust de varias formas diferentes, por
ejemplo, tomando y devolviendo cadenas por valor o colocando una referencia mutable
en una cadena. Si se utiliza una referencia mutable, es probable que CXX indique que
se debe usar Pin. Puede que debas explicar qué hace Pin y, a continuación, explicar
por qué CXX lo necesita para referencias mutables a datos de C++. La respuesta es que
los datos de C++ no se pueden mover como los datos de Rust, ya que pueden contener
punteros de autorreferencia.
• El elemento de destino de C++ que contiene ResourceBundle::MaybeMangleLocalizedString
deberá depender de un elemento rust_static_library. Seguramente los alumnos
ya lo hayan hecho.
• rust_static_library deberá depender de //third_party/rust/uwuify/v0_2:lib.
272
Capítulo 48
Soluciones de Ejercicios
Las soluciones a los ejercicios de Chromium están en esta serie de listas de cambios.
273
Parte XI
274
Capítulo 49
Este es un curso independiente de un día sobre Rust bare-metal, dirigido a personas que
están familiarizadas con los conceptos básicos de Rust (tal vez después de completar el curso
Comprehensive Rust). Lo ideal sería que también tuvieran experiencia con la programación
bare-metal en otros lenguajes, como C.
Hoy vamos a hablar de Rust ”bare-metal”: ejecutar código de Rust sin un sistema operativo.
Se dividirá en varias partes:
• ¿Qué es no_std en Rust?
• Escribir firmware para microcontroladores.
• Escribir código bootloader o kernel para procesadores de aplicaciones.
• Algunos crates útiles para el desarrollo de Rust bare-metal.
For the microcontroller part of the course we will use the BBC micro:bit v2 as an example.
It's a development board based on the Nordic nRF52833 microcontroller with some LEDs and
buttons, an I2C-connected accelerometer and compass, and an on-board SWD debugger.
Para empezar, instala algunas de las herramientas que necesitarás más adelante. En gLinux o
Debian:
sudo apt install gcc-aarch64-linux-gnu gdb-multiarch libudev-dev picocom pkg-config qemu
rustup update
rustup target add aarch64-unknown-none thumbv7em-none-eabihf
rustup component add llvm-tools-preview
cargo install cargo-binutils
curl --proto '=https' --tlsv1.2 -LsSf [Link]
Permite a los usuarios del grupo plugdev acceder al programador micro:bit:
echo 'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d28", MODE="0660", GROUP="logindev", TAG+=
sudo tee /etc/udev/rules.d/[Link]
sudo udevadm control --reload-rules
En MacOS:
xcode-select --install
brew install gdb picocom qemu
275
brew install --cask gcc-aarch64-embedded
rustup update
rustup target add aarch64-unknown-none thumbv7em-none-eabihf
rustup component add llvm-tools-preview
cargo install cargo-binutils
curl --proto '=https' --tlsv1.2 -LsSf [Link]
276
Capítulo 50
no_std
core
alloc
std
• Slices, &str, CStr
• NonZeroU8...
• Option, Result
• Display, Debug, write!...
• Iterator
• panic!, assert_eq!...
• NonNull y todas las funciones relacionadas con punteros habituales
• Future and async/await
• fence, AtomicBool, AtomicPtr, AtomicU32...
• Duration
• Box, Cow, Arc, Rc
• Vec, BinaryHeap, BtreeMap, LinkedList, VecDeque
• String, CString, format!
• Error
• HashMap
• Mutex, Condvar, Barrier, Once, RwLock, mpsc
• File y el resto de fs
• println!, Read, Write, Stdin, Stdout y el resto de io
• Path, OsString
• net
• Command, Child, ExitCode
• spawn, sleep y el resto de thread
• SystemTime, Instant
• HashMap depende de RNG.
• std vuelve a exportar el contenido de core y alloc.
277
50.1 Un programa no_std mínimo
#![no_main]
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo) -> ! {
loop {}
}
• Se compilará en un binario vacío.
• std proporciona un controlador de panic; sin no hay, debemos proporcionar uno
nuestro.
• También puede proporcionarlo otro crate, como panic-halt.
• Dependiendo del objetivo, es posible que tengas que compilar con panic = "abort"
para evitar un error sobre eh_personality.
• Ten en cuenta que no hay main ni ningún otro punto de entrada; depende de ti definir un
punto de entrada propio. Esto suele implicar una secuencia de comandos de enlazador
y algún código de ensamblado de forma que todo esté preparado para que se ejecute el
código de Rust.
50.2 alloc
Para utilizar alloc, debes implementar un asignador global (de heap).
#![no_main]
#![no_std]
use alloc::string::ToString;
use alloc::vec::Vec;
use buddy_system_allocator::LockedHeap;
#[global_allocator]
static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new();
pub fn entry() {
// SAFETY: `HEAP` is only used here and `entry` is only called once.
unsafe {
// Proporciona al asignador algo de memoria para asignar.
HEAP_ALLOCATOR.lock().init(HEAP.as_mut_ptr() as usize, [Link]());
}
278
[Link]("A string".to_string());
}
• buddy_system_allocator es un crate de terceros que implementa un buddy system
allocator (una técnica de asignación de memoria) básico. Hay otros crates disponibles,
pero también puedes escribir el tuyo propio o conectarte a tu asignador.
• El parámetro const de LockedHeap es el orden máximo del asignador. Es decir, en este
caso, puede asignar regiones de hasta 2**32 bytes.
• Si algún crate del árbol de dependencias depende de alloc, debes tener exactamente
un asignador global definido en el binario. Esto se suele hacer en el crate binario de
nivel superior.
• extern crate panic_halt as _ es necesario para asegurar que el crate panic_halt
esté vinculado y así podamos obtener su controlador de panic.
• Este ejemplo se compilará pero no se ejecutará, ya que no cuenta con un punto de
entrada.
279
Capítulo 51
Microcontroladores
mod interrupts;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {}
}
A continuación, veremos cómo se accede a los periféricos con niveles de abstracción cada
vez mayores.
• La macro cortex_m_rt::entry requiere que la función tenga el tipo fn() -> !, ya
que no tiene sentido devolver resultados al controlador de reinicio.
• Ejecuta el ejemplo con cargo embed --bin minimal.
mod interrupts;
280
use core::mem::size_of;
use cortex_m_rt::entry;
// Campos PIN_CNF
const DIR_OUTPUT: u32 = 0x1;
const INPUT_DISCONNECT: u32 = 0x1 << 1;
const PULL_DISABLED: u32 = 0x0 << 2;
const DRIVE_S0S1: u32 = 0x0 << 8;
const SENSE_DISABLED: u32 = 0x0 << 16;
#[entry]
fn main() -> ! {
// Configura los pines 21 y 28 de GPIO 0 como salidas push-pull.
let pin_cnf_21 = (GPIO_P0 + PIN_CNF + 21 * size_of::<u32>()) as *mut u32;
let pin_cnf_28 = (GPIO_P0 + PIN_CNF + 28 * size_of::<u32>()) as *mut u32;
// SAFETY: The pointers are to valid peripheral control registers, and no
// aliases exist.
unsafe {
pin_cnf_21.write_volatile(
DIR_OUTPUT
| INPUT_DISCONNECT
| PULL_DISABLED
| DRIVE_S0S1
| SENSE_DISABLED,
);
pin_cnf_28.write_volatile(
DIR_OUTPUT
| INPUT_DISCONNECT
| PULL_DISABLED
| DRIVE_S0S1
| SENSE_DISABLED,
);
}
281
loop {}
}
• El pin 21 de GPIO 0 está conectado a la primera columna de la matriz de LED y el pin 28
a la primera fila.
Ejecuta el ejemplo con:
cargo embed --bin mmio
use cortex_m_rt::entry;
use nrf52833_pac::Peripherals;
#[entry]
fn main() -> ! {
let p = Peripherals::take().unwrap();
let gpio0 = p.P0;
282
loop {}
}
• Los archivos SVD (System View Description) son archivos XML que suelen proporcionar
los proveedores de silicio y que describen el mapa de memoria del dispositivo.
– Se organizan por periférico, registro, campo y valor, con nombres, descripciones y
direcciones, etc.
– Los archivos SVD suelen tener errores y estar incompletos, por lo que existen varios
proyectos que aplican parches a los errores, añaden detalles que faltan y publican
los crates generados.
• cortex-m-rt proporciona la tabla de vectores, entre otras cosas.
• Si instalas cargo install cargo-binutils puedes ejecutar cargo objdump --bin
pac -- -d --no-show-raw-insn para ver el binario resultante.
Ejecuta el ejemplo con:
cargo embed --bin pac
use cortex_m_rt::entry;
use embedded_hal::digital::OutputPin;
use nrf52833_hal::gpio::{p0, Level};
use nrf52833_hal::pac::Peripherals;
#[entry]
fn main() -> ! {
let p = Peripherals::take().unwrap();
loop {}
}
• set_low y set_high son métodos del trait OutputPin de embedded_hal.
283
• Hay crates HAL para muchos dispositivos Cortex-M y RISC-V, incluidos varios
microcontroladores STM32, GD32, nRF, NXP, MSP430, AVR y PIC.
Ejecuta el ejemplo con:
cargo embed --bin hal
use cortex_m_rt::entry;
use embedded_hal::digital::OutputPin;
use microbit::Board;
#[entry]
fn main() -> ! {
let mut board = Board::take().unwrap();
board.display_pins.col1.set_low().unwrap();
board.display_pins.row1.set_high().unwrap();
loop {}
}
• En este caso, el crate de compatibilidad de placa proporciona solo nombres más útiles y
un poco de inicialización.
• El crate también puede incluir controladores para algunos dispositivos integrados fuera
del propio microcontrolador .
– microbit-v2 incluye un controlador sencillo para la matriz de LED.
Ejecuta el ejemplo con:
cargo embed --bin board_support
284
if pin_input.is_high().unwrap() {
// ...
}
let mut pin_output: P0_01<Output<OpenDrain>> = pin_input
.into_open_drain_output(OpenDrainConfig::Disconnect0Standard1, Level::Low);
pin_output.set_high().unwrap();
// pin_input.is_high(); // Error, se ha movido.
loop {}
}
• Los pines no implementan Copy ni Clone, por lo que solo puede haber una instancia de
cada uno. Cuando se quita un pin de la estructura del puerto, nadie más puede usarlo.
• Si cambias la configuración de un pin, se consumirá la instancia del pin anterior y no
podrás seguir usando la instancia previa.
• El tipo de un valor indica el estado en el que se encuentra: por ejemplo, en este caso,
el estado de configuración de un pin de GPIO. De esta manera, se codifica la máquina
de estados en el sistema de tipos, asegurando así que no se use un pin de cierta forma
sin antes configurarlo correctamente. Las transiciones de estado ilegales se detectan
durante el tiempo de compilación.
• Puedes llamar a is_high en un pin de entrada y a set_high en un pin de salida, pero
no al revés.
• Muchos crates HAL siguen este patrón.
51.6 embedded-hal
The embedded-hal crate provides a number of traits covering common microcontroller
peripherals:
• GPIO
• PWM
• Delay timers
• I2C and SPI buses and devices
Similar traits for byte streams (e.g. UARTs), CAN buses and RNGs and broken out into
embedded-io, embedded-can and rand_core respectively.
Other crates then implement drivers in terms of these traits, e.g. an accelerometer driver
might need an I2C or SPI device instance.
• The traits cover using the peripherals but not initialising or configuring them, as
initialisation and configuration is usually highly platform-specific.
• Hay implementaciones para muchos microcontroladores, así como otras plataformas
como Linux en Raspberry Pi.
• embedded-hal-async provides async versions of the traits.
285
• embedded-hal-nb provides another approach to non-blocking I/O, based on the nb
crate.
51.7.1 Depuración
[Link]:
[[Link]]
chip = "nrf52833_xxAA"
[[Link]]
enabled = true
En un terminal en src/bare-metal/microcontrollers/examples/:
cargo embed --bin board_support debug
En otro terminal del mismo directorio:
En gLinux o Debian:
gdb-multiarch target/thumbv7em-none-eabihf/debug/board_support --eval-command="target re
En MacOS:
arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/board_support --eval-command="targe
En GDB, prueba a ejecutar:
286
b src/bin/board_support.rs:29
b src/bin/board_support.rs:30
b src/bin/board_support.rs:32
c
c
c
287
Capítulo 52
Ejercicios
Leeremos la dirección desde una brújula I2C, y registraremos las lecturas en un puerto serie.
Después de realizar los ejercicios, puedes consultar las soluciones correspondientes.
52.1 Brújula
Leeremos la dirección desde una brújula I2C, y registraremos las lecturas en un puerto serie.
Si tienes tiempo, prueba a mostrarlo también en los LED o usa los botones de alguna forma.
Sugerencias:
• Consulta la documentación sobre los crates lsm303agr y microbit-v2, así como el
hardware de micro:bit.
• La unidad de medición inercial LSM303AGR está conectada al bus I2C interno.
• TWI es otro nombre para I2C, por lo que el periférico I2C maestro se llama TWIM.
• The LSM303AGR driver needs something implementing the embedded_hal::i2c::I2c
trait. The microbit::hal::Twim struct implements this.
• Tienes una estructura microbit::Board con campos para los distintos pines y
periféricos.
• También puedes consultar la [hoja de datos nRF52833]nRF52833 datasheet si quieres,
pero no debería ser necesario para este ejercicio.
Descarga la plantilla de ejercicio y busca los siguientes archivos en el directorio compass.
src/[Link]:
#![no_main]
#![no_std]
use core::fmt::Write;
use cortex_m_rt::entry;
use microbit::{hal::{Delay, uarte::{Baudrate, Parity, Uarte}}, Board};
#[entry]
288
fn main() -> ! {
let mut board = Board::take().unwrap();
writeln!(serial, "Ready.").unwrap();
loop {
// Read compass data and log it to the serial port.
// TODO
}
}
[Link] (you shouldn't need to change this):
[workspace]
[package]
name = "compass"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
cortex-m-rt = "0.7.3"
embedded-hal = "1.0.0"
lsm303agr = "1.1.0"
microbit-v2 = "0.15.0"
panic-halt = "0.2.0"
[Link] (you shouldn't need to change this):
[[Link]]
chip = "nrf52833_xxAA"
[[Link]]
enabled = true
[[Link]]
halt_afterwards = true
289
.cargo/[Link] (you shouldn't need to change this):
[build]
target = "thumbv7em-none-eabihf" # Cortex-M4F
use core::fmt::Write;
use cortex_m_rt::entry;
use core::cmp::{max, min};
use embedded_hal::digital::InputPin;
use lsm303agr::{
AccelMode, AccelOutputDataRate, Lsm303agr, MagMode, MagOutputDataRate,
};
use microbit::display::blocking::Display;
use microbit::hal::twim::Twim;
use microbit::hal::uarte::{Baudrate, Parity, Uarte};
use microbit::hal::{Delay, Timer};
use microbit::pac::twim0::frequency::FREQUENCY_A;
use microbit::Board;
#[entry]
fn main() -> ! {
let mut board = Board::take().unwrap();
290
board.UARTE0,
[Link](),
Parity::EXCLUDED,
Baudrate::BAUD115200,
);
writeln!(serial, "Listo.").unwrap();
loop {
// Lee los datos de la brújula y regístralos en el puerto serie.
while !(imu.mag_status().unwrap().xyz_new_data()
&& imu.accel_status().unwrap().xyz_new_data())
{}
let compass_reading = imu.magnetic_field().unwrap();
let accelerometer_reading = [Link]().unwrap();
writeln!(
serial,
"{},{},{}\t{},{},{}",
compass_reading.x_nt(),
compass_reading.y_nt(),
compass_reading.z_nt(),
accelerometer_reading.x_mg(),
291
accelerometer_reading.y_mg(),
accelerometer_reading.z_mg(),
)
.unwrap();
292
}
impl Mode {
fn next(self) -> Self {
match self {
Self::Compass => Self::Accelerometer,
Self::Accelerometer => Self::Compass,
}
}
}
fn scale(value: i32, min_in: i32, max_in: i32, min_out: i32, max_out: i32) -> i32 {
let range_in = max_in - min_in;
let range_out = max_out - min_out;
cap(min_out + range_out * (value - min_in) / range_in, min_out, max_out)
}
293
Parte XII
294
Capítulo 53
Procesadores de aplicaciones
Hasta ahora hemos hablado de microcontroladores, como la serie Cortex‐M de Arm. Ahora
vamos a probar a escribir algo para Cortex-A. Para simplificar, solo trabajaremos con la placa
'virt' aarch64 de QEMU.
• En términos generales, los microcontroladores no tienen un MMU ni varios niveles de
privilegio (niveles de excepción en las CPU de Arm, anillos en x86), mientras que los
procesadores de aplicaciones sí los tienen.
• QEMU permite emular varias máquinas o modelos de placa diferentes para cada
arquitectura. La placa ”virt” no se corresponde con ningún hardware real concreto,
pero está diseñada exclusivamente para máquinas virtuales.
295
msr tcr_el1, x30
/*
* Ensure everything before this point has completed, then invalidate any
* potentially stale local TLB entries before they start being used.
*/
isb
tlbi vmalle1
ic iallu
dsb nsh
isb
/*
* Configure sctlr_el1 to enable MMU and cache and don't proceed until this
* has completed.
*/
msr sctlr_el1, x30
isb
296
el puntero de la stack.
– El BSS (símbolo de inicio del bloque, por motivos históricos) es la parte del objeto
que contiene variables asignadas de forma estática que se inicializan a cero. Se
omiten en la imagen para evitar malgastar espacio con ceros. El compilador asume
que el cargador se encargará de ponerlos a cero.
• Es posible que el BSS ya esté a cero, dependiendo de cómo se inicialice la memoria y
cómo se cargue la imagen, aunque se pone igualmente a cero para estar seguros.
• Necesitamos habilitar la MMU y la caché antes de leer o escribir memoria. Si no lo
hacemos, sucederá lo siguiente:
– Los accesos no alineados fallarán. Compilamos el código Rust para el objetivo
aarch64-unknown-none, que define +strict-align para evitar que el
compilador genere accesos no alineados. En este caso debería estar bien,
pero no tiene por qué ser así en general.
– Si se estuviera ejecutando en una máquina virtual, podría provocar problemas de
coherencia en la caché. El problema es que la máquina virtual accede a la memoria
directamente con la caché inhabilitada, mientras que el host cuenta con alias que
se pueden almacenar en caché en la misma memoria. Incluso si el host no accede
explícitamente a la memoria, los accesos especulativos pueden provocar que se
llene la caché, haciendo que los cambios de uno u otro se pierdan cuando se borre
la caché o cuando la máquina virtual la habilite. (La caché está codificada por
dirección física, no por VA ni IPA).
• Para simplificar, solo se utiliza una tabla de páginas codificada (consulta idmap.S)
que mapea la identidad del primer GiB de espacio de direcciones para dispositivos, el
siguiente GiB para DRAM y otro GiB más para más dispositivos. Esto coincide con la
disposición de memoria que utiliza QEMU.
• También configuramos el vector de excepción (vbar_el1), del que veremos más
contenido en próximas dipositivas.
• Todos los ejemplos de esta tarde se ejecutarán en el nivel de excepción 1 (EL1). Si
necesitas ejecutar en un nivel de excepción diferente, deberás modificar entry.S según
corresponda.
use core::arch::asm;
use core::panic::PanicInfo;
mod exceptions;
#[no_mangle]
extern "C" fn main(_x0: u64, _x1: u64, _x2: u64, _x3: u64) {
// SAFETY: this only uses the declared registers and doesn't do anything
// with memory.
297
unsafe {
asm!("hvc #0",
inout("w0") PSCI_SYSTEM_OFF => _,
inout("w1") 0 => _,
inout("w2") 0 => _,
inout("w3") 0 => _,
inout("w4") 0 => _,
inout("w5") 0 => _,
inout("w6") 0 => _,
inout("w7") 0 => _,
options(nomem, nostack)
);
}
loop {}
}
(Si realmente quieres hacer esto, utiliza el crate smccc que tiene envoltorios para todas estas
funciones).
• PSCI es la interfaz de coordinación de estado de alimentación de Arm, un conjunto
estándar de funciones para gestionar los estados de alimentación del sistema y de la
CPU, entre otras cosas. Lo implementan el firmware EL3 y los hipervisores en muchos
sistemas.
• La sintaxis 0 => _ significa inicializar el registro a 0 antes de ejecutar el código de
ensamblaje integrado e ignorar su contenido después. Necesitamos utilizar inout en
lugar de in porque la llamada podría alterar el contenido de los registros.
• Esta función main debe ser #[no_mangle] y extern "C", ya que se llama desde nuestro
punto de entrada en entry.S.
• _x0–_x3 son los valores de los registros x0–x3, que el bootloader utiliza habitualmente
para pasar elementos al árbol de dispositivos, como un puntero. De acuerdo con la
convención de llamadas estándar de aarch64 (que es lo que extern "C" usa), los
registros x0–x7 se utilizan para los primeros ocho argumentos que se pasan a una
función, de modo que entry.S no tiene que hacer nada especial, salvo asegurarse de
que no cambia estos registros.
• Ejecuta el ejemplo en QEMU con make qemu_psci en src/bare-metal/aps/examples.
298
• Utiliza la macro addr_of! para obtener punteros de campos de estructuras a partir de
un puntero en la estructura.
impl Uart {
/// Construye una instancia nueva del controlador de UART para un dispositivo PL011
/// dirección base proporcionada.
///
/// # Seguridad
///
/// La dirección base debe apuntar a los 8 registros de control MMIO de un
/// dispositivo PL011, que debe asignarse al espacio de direcciones del proceso
/// como memoria del dispositivo y no tener ningún otro alias.
pub unsafe fn new(base_address: *mut u8) -> Self {
Self { base_address }
}
fn read_flag_register(&self) -> u8 {
// SAFETY: We know that the base address points to the control
// registers of a PL011 device which is appropriately mapped.
299
unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() }
}
}
• Ten en cuenta que Uart::new no es seguro, mientras que los otros métodos sí lo son.
Esto se debe a que mientras que el llamador de Uart::new asegure que se cumplan sus
requisitos de seguridad (es decir, que solo haya una instancia del controlador para una
UART determinada y que nada más asigne alias a su espacio de direcciones), siempre es
más seguro llamar a write_byte más adelante, ya que podemos asumir\ las condiciones
previas necesarias.
• Podríamos haberlo hecho al revés (haciendo que new fuese seguro y write_byte no
seguro), pero\sería mucho menos cómodo de usar, ya que cada lugar que llamase a
write_byte tendría que pensar en la seguridad
• Este es un patrón común para escribir envoltorios seguros de código inseguro: mover la
carga de la prueba de seguridad de un gran número de lugares a otro más pequeño.
300
Desplazamiento Nombre de registro Ancho
0x24 IBRD 16
0x28 FBRD 6
0x2c LCR_H 8
0x30 CR 16
0x34 IFLS 6
0x38 IMSC 11
0x3c RIS 11
0x40 MIS 11
0x44 ICR 11
0x48 DMACR 3
53.5.1 Bitflags
El crate bitflags resulta útil para trabajar con bitflags.
use bitflags::bitflags;
bitflags! {
/// Marcas del registro de marcas de UART.
#[repr(transparent)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Flags: u16 {
/// Borra para enviar.
const CTS = 1 << 0;
/// Conjunto de datos listo.
const DSR = 1 << 1;
/// Detección del portador de datos.
const DCD = 1 << 2;
/// UART está transmitiendo datos.
const BUSY = 1 << 3;
/// El FIFO de recepción está vacío
const RXFE = 1 << 4;
/// El FIFO de transmisión está completo.
const TXFF = 1 << 5;
/// El FIFO de recepción está completo.
const RXFF = 1 << 6;
/// El FIFO de transmisión está vacío.
const TXFE = 1 << 7;
/// Indicador de anillo.
const RI = 1 << 8;
}
}
• La macro bitflags! crea un newtype, como Flags(u16), junto con un montón de
implementaciones de métodos para obtener y definir flags (banderas).
301
53.5.2 Varios registros
Podemos utilizar una estructura para representar la disposición de la memoria de los registros
de UART.
#[repr(C, align(4))]
struct Registers {
dr: u16,
_reserved0: [u8; 2],
rsr: ReceiveStatus,
_reserved1: [u8; 19],
fr: Flags,
_reserved2: [u8; 6],
ilpr: u8,
_reserved3: [u8; 3],
ibrd: u16,
_reserved4: [u8; 2],
fbrd: u8,
_reserved5: [u8; 3],
lcr_h: u8,
_reserved6: [u8; 3],
cr: u16,
_reserved7: [u8; 3],
ifls: u8,
_reserved8: [u8; 3],
imsc: u16,
_reserved9: [u8; 2],
ris: u16,
_reserved10: [u8; 2],
mis: u16,
_reserved11: [u8; 2],
icr: u16,
_reserved12: [u8; 2],
dmacr: u8,
_reserved13: [u8; 3],
}
• #[repr(C)] indica al compilador que ordene los campos de la estructura siguiendo las
mismas reglas que en C. Esto es necesario para que nuestra estructura tenga un diseño
predecible, ya que la representación predeterminada de Rust permite que el compilador
(entre otras cosas) reordene los campos como crea conveniente.
53.5.3 Conductor
Ahora vamos a utilizar la nueva estructura de Registers en nuestro controlador.
/// Controlador para un UART PL011.
#[derive(Debug)]
pub struct Uart {
registers: *mut Registers,
}
302
impl Uart {
/// Construye una instancia nueva del controlador de UART para un dispositivo PL011
/// dirección base proporcionada.
///
/// # Seguridad
///
/// La dirección base debe apuntar a los 8 registros de control MMIO de un
/// dispositivo PL011, que debe asignarse al espacio de direcciones del proceso
/// como memoria del dispositivo y no tener ningún otro alias.
pub unsafe fn new(base_address: *mut u32) -> Self {
Self { registers: base_address as *mut Registers }
}
303
individuales sin crear una referencia intermedia. Sería una acción insegura.
53.5.4 Uso
Vamos a crear un pequeño programa con nuestro controlador para escribir en la consola
serie y compartir los bytes entrantes.
#![no_main]
#![no_std]
mod exceptions;
mod pl011;
use crate::pl011::Uart;
use core::fmt::Write;
use core::panic::PanicInfo;
use log::error;
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
extern "C" fn main(x0: u64, x1: u64, x2: u64, x3: u64) {
// SAFETY: `PL011_BASE_ADDRESS` is the base address of a PL011 device, and
// nothing else accesses that address range.
let mut uart = unsafe { Uart::new(PL011_BASE_ADDRESS) };
loop {
if let Some(byte) = uart.read_byte() {
uart.write_byte(byte);
match byte {
b'\r' => {
uart.write_byte(b'\n');
}
b'q' => break,
_ => {}
}
}
}
writeln!(uart, "¡Adiós!").unwrap();
system_off::<Hvc>().unwrap();
}
• Al igual que en el ejemplo de ensamblaje integrado, esta función main se llama desde
nuestro código de punto de entrada en entry.S. Consulta las notas del orador para
obtener más información.
• Ejecuta el ejemplo en QEMU con make qemu en src/bare-metal/aps/examples.
304
53.6 Almacenamiento de registros
Estaría bien poder utilizar las macros de registro del crate log. Podemos hacerlo
implementando el trait Log.
use crate::pl011::Uart;
use core::fmt::Write;
use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
use spin::mutex::SpinMutex;
struct Logger {
uart: SpinMutex<Option<Uart>>,
}
fn flush(&self) {}
}
log::set_logger(&LOGGER)?;
log::set_max_level(max_level);
Ok(())
}
• La desenvoltura en log es segura porque inicializamos LOGGER antes de llamar a
set_logger.
53.6.1 Uso
Debemos inicializar el registrador antes de utilizarlo.
#![no_main]
#![no_std]
305
mod exceptions;
mod logger;
mod pl011;
use crate::pl011::Uart;
use core::panic::PanicInfo;
use log::{error, info, LevelFilter};
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
extern "C" fn main(x0: u64, x1: u64, x2: u64, x3: u64) {
// SAFETY: `PL011_BASE_ADDRESS` is the base address of a PL011 device, and
// nothing else accesses that address range.
let uart = unsafe { Uart::new(PL011_BASE_ADDRESS) };
logger::init(uart, LevelFilter::Trace).unwrap();
assert_eq!(x1, 42);
system_off::<Hvc>().unwrap();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
error!("{info}");
system_off::<Hvc>().unwrap();
loop {}
}
• Ten en cuenta que nuestro controlador de panic ahora ya puede registrar la información
de los pánicos.
• Ejecuta el ejemplo en QEMU con make qemu_logger en src/bare-metal/aps/examples.
53.7 Excepciones
AArch64 define una tabla de vectores de excepción con 16 entradas, para 4 tipos de
excepciones (synchronous, IRQ, FIQ, SError) desde 4 estados (EL actual con SP0, EL actual
con SPx, EL inferior con AArch64 y EL inferior con AArch32). Implementamos esto en el
ensamblaje para guardar los registros volátiles en la stack antes de llamar al código de Rust:
use log::error;
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
306
extern "C" fn sync_exception_current(_elr: u64, _spsr: u64) {
error!("sync_exception_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn irq_current(_elr: u64, _spsr: u64) {
error!("irq_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn fiq_current(_elr: u64, _spsr: u64) {
error!("fiq_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn serr_current(_elr: u64, _spsr: u64) {
error!("serr_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn sync_lower(_elr: u64, _spsr: u64) {
error!("sync_lower");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn irq_lower(_elr: u64, _spsr: u64) {
error!("irq_lower");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn fiq_lower(_elr: u64, _spsr: u64) {
error!("fiq_lower");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn serr_lower(_elr: u64, _spsr: u64) {
error!("serr_lower");
system_off::<Hvc>().unwrap();
}
• EL significa nivel de excepción (por sus siglas en inglés); todos nuestros ejemplos de
esta tarde se ejecutan en EL1.
• Para simplificar, no distinguimos entre SP0 y SPx para las excepciones del EL actual, ni
entre AArch32 y AArch64 para las excepciones de EL inferiores.
307
• En este ejemplo, nos limitaremos a registrar la excepción y a apagarla, ya que no
esperamos que se produzca ninguna.
• Podríamos pensar en los controladores de excepciones y en nuestro contexto de
ejecución principal como si fueran hilos diferentes. Send y Sync controlarán lo que
podemos compartir entre ellos, igual que con los hilos. Por ejemplo, si queremos
compartir algún valor entre los controladores de excepciones y el resto del programa, y
es Senden vez de Sync, necesitaremos envolverlo en un Mutex, por ejemplo, y ponerlo
en un estático.
308
Capítulo 54
Crates Útiles
54.1 zerocopy
El crate zerocopy (de Fuchsia) proporciona traits y macros para realizar conversiones seguras
entre secuencias de bytes y otros tipos.
use zerocopy::AsBytes;
#[repr(u32)]
#[derive(AsBytes, Debug, Default)]
enum RequestType {
#[default]
In = 0,
Out = 1,
Flush = 4,
}
#[repr(C)]
#[derive(AsBytes, Debug, Default)]
struct VirtioBlockRequest {
request_type: RequestType,
reserved: u32,
sector: u64,
}
fn main() {
let request = VirtioBlockRequest {
request_type: RequestType::Flush,
sector: 42,
..Default::default()
};
309
assert_eq!(
request.as_bytes(),
&[4, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0]
);
}
No es adecuado para MMIO (ya que no utiliza lecturas y escrituras volátiles), pero puede ser
útil para trabajar con estructuras compartidas con hardware (por ejemplo, mediante DMA) o
enviadas a través de alguna interfaz externa.
• FromBytes se puede implementar en tipos en los que cualquier patrón de bytes es
válido, por lo que se puede convertir de forma segura a partir de una secuencia de bytes
que no es fiable.
• Si se intenta derivar FromBytes para estos tipos, se produciría un error, pues
RequestType no utiliza todos los valores u32 posibles como discriminantes y, por tanto,
todos los patrones de bytes son válidos.
• zerocopy::byteorder tiene tipos para primitivos numéricos conscientes del orden
de bytes.
• Ejecuta el ejemplo con cargo run en src/bare-metal/useful-crates/zerocopy-
example/. (No se ejecutará en el playground debido a la dependencia del crate).
54.2 aarch64-paging
El crate aarch64-paging permite crear tablas de páginas de acuerdo con la arquitectura del
sistema de memoria virtual AArch64.
use aarch64_paging::{
idmap::IdMap,
paging::{Attributes, MemoryRegion},
};
310
54.3 buddy_system_allocator
buddy_system_allocator es un crate de terceros que implementa un asignador básico del
sistema buddy. Se puede utilizar tanto para LockedHeap implementando GlobalAlloc, de
forma que puedas usar el crate alloc estándar (tal y como vimos antes), o para asignar
otro espacio de direcciones. Por ejemplo, podríamos querer asignar espacio MMIO para los
registros de dirección base (BAR) de PCI:
use buddy_system_allocator::FrameAllocator;
use core::alloc::Layout;
fn main() {
let mut allocator = FrameAllocator::<32>::new();
allocator.add_frame(0x200_0000, 0x400_0000);
54.4 tinyvec
A veces, se necesita algo que se pueda cambiar de tamaño, como Vec, pero sin asignación
de heap. tinyvec ofrece un vector respaldado por un array o slice, que se podría asignar
estáticamente o en la stack, y que hace un seguimiento de cuántos elementos se usan, entrando
en panic si intentas utilizar más elementos de los asignados.
use tinyvec::{array_vec, ArrayVec};
fn main() {
let mut numbers: ArrayVec<[u32; 5]> = array_vec!(42, 66);
println!("{numbers:?}");
[Link](7);
println!("{numbers:?}");
[Link](1);
println!("{numbers:?}");
}
• tinyvec requiere que el tipo de elemento implemente Default para la inicialización.
• El playground de Rust incluye tinyvec, por lo que este ejemplo se ejecutará bien aunque
esté insertado.
311
54.5 spin
std::sync::Mutex y el resto de los primitivos de sincronización de std::sync no están
disponibles en core o alloc. ¿Cómo podemos gestionar la sincronización o la mutabilidad
interior para, por ejemplo, compartir el estado entre diferentes CPUs?
El crate spin proporciona equivalentes basados en spinlocks de muchos de estos primitivos.
use spin::mutex::SpinMutex;
fn main() {
println!("count: {}", [Link]());
*[Link]() += 2;
println!("count: {}", [Link]());
}
• Intenta evitar interbloqueos si usas bloqueos en los controladores de las interrupciones.
• spin also has a ticket lock mutex implementation; equivalents of RwLock, Barrier and
Once from std::sync; and Lazy for lazy initialisation.
• El crate once_cell también tiene algunos tipos útiles de inicialización tardía con un
enfoque ligeramente distinto al de spin::once::Once.
• El playground de Rust incluye spin, por lo que este ejemplo se ejecutará bien aunque
está insertado.
312
Capítulo 55
Android
Para compilar un binario de Rust bare-metal en AOSP, tienes que usar una regla
rust_ffi_static de Soong para crear tu código Rust y, seguidamente, un cc_binary con
una secuencia de comandos de enlazador para producir el binario en sí. Por último, un
raw_binary para convertir el ELF en un binario sin formato que pueda ejecutarse.
rust_ffi_static {
name: "libvmbase_example",
defaults: ["vmbase_ffi_defaults"],
crate_name: "vmbase_example",
srcs: ["src/[Link]"],
rustlibs: [
"libvmbase",
],
}
cc_binary {
name: "vmbase_example",
defaults: ["vmbase_elf_defaults"],
srcs: [
"idmap.S",
],
static_libs: [
"libvmbase_example",
],
linker_scripts: [
"[Link]",
":vmbase_sections",
],
}
raw_binary {
name: "vmbase_example_bin",
stem: "vmbase_example.bin",
src: ":vmbase_example",
enabled: false,
313
target: {
android_arm64: {
enabled: true,
},
},
}
55.1 vmbase
En el caso de las máquinas virtuales que se ejecutan con crosvm en aarch64, la biblioteca
vmbase proporciona una secuencia de comandos de enlazador y valores predeterminados
útiles para las reglas de compilación, además de un punto de entrada, registro de la consola
UART y mucho más.
#![no_main]
#![no_std]
main!(main);
314
Capítulo 56
Ejercicios
mod exceptions;
mod logger;
mod pl011;
use crate::pl011::Uart;
use arm_gic::gicv3::GicV3;
use core::panic::PanicInfo;
315
use log::{error, info, trace, LevelFilter};
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
extern "C" fn main(x0: u64, x1: u64, x2: u64, x3: u64) {
// SAFETY: `PL011_BASE_ADDRESS` is the base address of a PL011 device, and
// nothing else accesses that address range.
let uart = unsafe { Uart::new(PL011_BASE_ADDRESS) };
logger::init(uart, LevelFilter::Trace).unwrap();
system_off::<Hvc>().unwrap();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
error!("{info}");
system_off::<Hvc>().unwrap();
loop {}
}
src/[Link] (you should only need to change this for the 3rd part of the exercise):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// [Link]
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
316
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use arm_gic::gicv3::GicV3;
use log::{error, info, trace};
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
extern "C" fn sync_exception_current(_elr: u64, _spsr: u64) {
error!("sync_exception_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn irq_current(_elr: u64, _spsr: u64) {
trace!("irq_current");
let intid =
GicV3::get_and_acknowledge_interrupt().expect("No pending interrupt");
info!("IRQ {intid:?}");
}
#[no_mangle]
extern "C" fn fiq_current(_elr: u64, _spsr: u64) {
error!("fiq_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn serr_current(_elr: u64, _spsr: u64) {
error!("serr_current");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn sync_lower(_elr: u64, _spsr: u64) {
error!("sync_lower");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn irq_lower(_elr: u64, _spsr: u64) {
error!("irq_lower");
system_off::<Hvc>().unwrap();
}
#[no_mangle]
extern "C" fn fiq_lower(_elr: u64, _spsr: u64) {
error!("fiq_lower");
system_off::<Hvc>().unwrap();
317
}
#[no_mangle]
extern "C" fn serr_lower(_elr: u64, _spsr: u64) {
error!("serr_lower");
system_off::<Hvc>().unwrap();
}
src/[Link] (no debería ser necesario cambiarlo):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// [Link]
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: main
use crate::pl011::Uart;
use core::fmt::Write;
use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
use spin::mutex::SpinMutex;
struct Logger {
uart: SpinMutex<Option<Uart>>,
}
fn flush(&self) {}
318
}
log::set_logger(&LOGGER)?;
log::set_max_level(max_level);
Ok(())
}
src/[Link] (no debería ser necesario cambiarlo):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// [Link]
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(unused)]
// ANCHOR: Flags
use bitflags::bitflags;
bitflags! {
/// Flags from the UART flag register.
#[repr(transparent)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Flags: u16 {
/// Clear to send.
const CTS = 1 << 0;
/// Data set ready.
const DSR = 1 << 1;
/// Data carrier detect.
const DCD = 1 << 2;
/// UART busy transmitting data.
const BUSY = 1 << 3;
/// Receive FIFO is empty.
const RXFE = 1 << 4;
/// Transmit FIFO is full.
const TXFF = 1 << 5;
319
/// Receive FIFO is full.
const RXFF = 1 << 6;
/// Transmit FIFO is empty.
const TXFE = 1 << 7;
/// Ring indicator.
const RI = 1 << 8;
}
}
// ANCHOR_END: Flags
bitflags! {
/// Flags from the UART Receive Status Register / Error Clear Register.
#[repr(transparent)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct ReceiveStatus: u16 {
/// Framing error.
const FE = 1 << 0;
/// Parity error.
const PE = 1 << 1;
/// Break error.
const BE = 1 << 2;
/// Overrun error.
const OE = 1 << 3;
}
}
// ANCHOR: Registers
#[repr(C, align(4))]
struct Registers {
dr: u16,
_reserved0: [u8; 2],
rsr: ReceiveStatus,
_reserved1: [u8; 19],
fr: Flags,
_reserved2: [u8; 6],
ilpr: u8,
_reserved3: [u8; 3],
ibrd: u16,
_reserved4: [u8; 2],
fbrd: u8,
_reserved5: [u8; 3],
lcr_h: u8,
_reserved6: [u8; 3],
cr: u16,
_reserved7: [u8; 3],
ifls: u8,
_reserved8: [u8; 3],
imsc: u16,
_reserved9: [u8; 2],
ris: u16,
_reserved10: [u8; 2],
320
mis: u16,
_reserved11: [u8; 2],
icr: u16,
_reserved12: [u8; 2],
dmacr: u8,
_reserved13: [u8; 3],
}
// ANCHOR_END: Registers
// ANCHOR: Uart
/// Driver for a PL011 UART.
#[derive(Debug)]
pub struct Uart {
registers: *mut Registers,
}
impl Uart {
/// Constructs a new instance of the UART driver for a PL011 device at the
/// given base address.
///
/// # Safety
///
/// The given base address must point to the MMIO control registers of a
/// PL011 device, which must be mapped into the address space of the process
/// as device memory and not have any other aliases.
pub unsafe fn new(base_address: *mut u32) -> Self {
Self { registers: base_address as *mut Registers }
}
/// Reads and returns a pending byte, or `None` if nothing has been
/// received.
pub fn read_byte(&self) -> Option<u8> {
if self.read_flag_register().contains(Flags::RXFE) {
None
} else {
321
// SAFETY: We know that [Link] points to the control
// registers of a PL011 device which is appropriately mapped.
let data = unsafe { addr_of!((*[Link]).dr).read_volatile() };
// TODO: Check for error conditions in bits 8-11.
Some(data as u8)
}
}
[package]
name = "rtc"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
arm-gic = "0.1.0"
bitflags = "2.6.0"
chrono = { version = "0.4.38", default-features = false }
log = "0.4.22"
smccc = "0.1.1"
spin = "0.9.8"
[build-dependencies]
cc = "1.1.4"
[Link] (no debería ser necesario cambiarlo):
// Copyright 2023 Google LLC
322
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// [Link]
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use cc::Build;
use std::env;
fn main() {
#[cfg(target_os = "linux")]
env::set_var("CROSS_COMPILE", "aarch64-linux-gnu");
#[cfg(not(target_os = "linux"))]
env::set_var("CROSS_COMPILE", "aarch64-none-elf");
Build::new()
.file("entry.S")
.file("exceptions.S")
.file("idmap.S")
.compile("empty")
}
entry.S (no debería ser necesario cambiarlo):
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* [Link]
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
323
.macro mov_i, reg:req, imm:req
movz \reg, :abs_g3:\imm
movk \reg, :abs_g2_nc:\imm
movk \reg, :abs_g1_nc:\imm
movk \reg, :abs_g0_nc:\imm
.endm
324
/**
* This is a generic entry point for an image. It carries out the operations required to
* loaded image to be run. Specifically, it zeroes the bss section using registers x25 a
* prepares the stack, enables floating point, and sets up the exception vector. It pres
* for the Rust entry point, as these may contain boot parameters.
*/
.section .[Link], "ax"
.global entry
entry:
/* Load and apply the memory management configuration, ready to enable MMU and cache
adrp x30, idmap
msr ttbr0_el1, x30
/*
* Ensure everything before this point has completed, then invalidate any potentiall
* local TLB entries before they start being used.
*/
isb
tlbi vmalle1
ic iallu
dsb nsh
isb
/*
* Configure sctlr_el1 to enable MMU and cache and don't proceed until this has comp
*/
msr sctlr_el1, x30
isb
325
0: cmp x29, x30
[Link] 1f
stp xzr, xzr, [x29], #16
b 0b
/**
* Saves the volatile registers onto the stack. This currently takes 14
* instructions, so it can be used in exception handlers with 18 instructions
* left.
*
* On return, x0 and x1 are initialised to elr_el2 and spsr_el2 respectively,
* which can be used as the first and second arguments of a subsequent call.
*/
.macro save_volatile_to_stack
/* Reserve stack space and save registers x0-x18, x29 & x30. */
stp x0, x1, [sp, #-(8 * 24)]!
stp x2, x3, [sp, #8 * 2]
stp x4, x5, [sp, #8 * 4]
stp x6, x7, [sp, #8 * 6]
326
stp x8, x9, [sp, #8 * 8]
stp x10, x11, [sp, #8 * 10]
stp x12, x13, [sp, #8 * 12]
stp x14, x15, [sp, #8 * 14]
stp x16, x17, [sp, #8 * 16]
str x18, [sp, #8 * 18]
stp x29, x30, [sp, #8 * 20]
/*
* Save elr_el1 & spsr_el1. This such that we can take nested exception
* and still be able to unwind.
*/
mrs x0, elr_el1
mrs x1, spsr_el1
stp x0, x1, [sp, #8 * 22]
.endm
/**
* Restores the volatile registers from the stack. This currently takes 14
* instructions, so it can be used in exception handlers while still leaving 18
* instructions left; if paired with save_volatile_to_stack, there are 4
* instructions to spare.
*/
.macro restore_volatile_from_stack
/* Restore registers x2-x18, x29 & x30. */
ldp x2, x3, [sp, #8 * 2]
ldp x4, x5, [sp, #8 * 4]
ldp x6, x7, [sp, #8 * 6]
ldp x8, x9, [sp, #8 * 8]
ldp x10, x11, [sp, #8 * 10]
ldp x12, x13, [sp, #8 * 12]
ldp x14, x15, [sp, #8 * 14]
ldp x16, x17, [sp, #8 * 16]
ldr x18, [sp, #8 * 18]
ldp x29, x30, [sp, #8 * 20]
/**
* This is a generic handler for exceptions taken at the current EL while using
* SP0. It behaves similarly to the SPx case by first switching to SPx, doing
* the work, then switching back to SP0 before returning.
*
* Switching to SPx and calling the Rust handler takes 16 instructions. To
327
* restore and return we need an additional 16 instructions, so we can implement
* the whole handler within the allotted 32 instructions.
*/
.macro current_exception_sp0 handler:req
msr spsel, #1
save_volatile_to_stack
bl \handler
restore_volatile_from_stack
msr spsel, #0
eret
.endm
/**
* This is a generic handler for exceptions taken at the current EL while using
* SPx. It saves volatile registers, calls the Rust handler, restores volatile
* registers, then returns.
*
* This also works for exceptions taken from EL0, if we don't care about
* non-volatile registers.
*
* Saving state and jumping to the Rust handler takes 15 instructions, and
* restoring and returning also takes 15 instructions, so we can fit the whole
* handler in 30 instructions, under the limit of 32.
*/
.macro current_exception_spx handler:req
save_volatile_to_stack
bl \handler
restore_volatile_from_stack
eret
.endm
.balign 0x80
irq_cur_sp0:
current_exception_sp0 irq_current
.balign 0x80
fiq_cur_sp0:
current_exception_sp0 fiq_current
.balign 0x80
serr_cur_sp0:
current_exception_sp0 serr_current
.balign 0x80
328
sync_cur_spx:
current_exception_spx sync_exception_current
.balign 0x80
irq_cur_spx:
current_exception_spx irq_current
.balign 0x80
fiq_cur_spx:
current_exception_spx fiq_current
.balign 0x80
serr_cur_spx:
current_exception_spx serr_current
.balign 0x80
sync_lower_64:
current_exception_spx sync_lower
.balign 0x80
irq_lower_64:
current_exception_spx irq_lower
.balign 0x80
fiq_lower_64:
current_exception_spx fiq_lower
.balign 0x80
serr_lower_64:
current_exception_spx serr_lower
.balign 0x80
sync_lower_32:
current_exception_spx sync_lower
.balign 0x80
irq_lower_32:
current_exception_spx irq_lower
.balign 0x80
fiq_lower_32:
current_exception_spx fiq_lower
.balign 0x80
serr_lower_32:
current_exception_spx serr_lower
idmap.S (no debería ser necesario cambiarlo):
/*
* Copyright 2023 Google LLC
*
329
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* [Link]
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Access flag. */
.set .L_TT_AF, 0x1 << 10
/* Not global. */
.set .L_TT_NG, 0x1 << 11
.set .L_TT_XN, 0x3 << 53
330
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Code will start running at this symbol which is placed at the start of the
* image.
*/
ENTRY(entry)
MEMORY
{
image : ORIGIN = 0x40080000, LENGTH = 2M
}
SECTIONS
{
/*
* Collect together the code.
*/
.init : ALIGN(4096) {
text_begin = .;
*(.[Link])
*(.init.*)
} >image
.text : {
*(.text.*)
} >image
text_end = .;
/*
* Collect together read-only data.
*/
.rodata : ALIGN(4096) {
rodata_begin = .;
*(.rodata.*)
} >image
.got : {
*(.got)
} >image
rodata_end = .;
/*
* Collect together the read-write data including .bss at the end which
* will be zero'd by the entry code.
*/
.data : ALIGN(4096) {
data_begin = .;
*(.data.*)
331
/*
* The entry point code assumes that .data is a multiple of 32
* bytes long.
*/
. = ALIGN(32);
data_end = .;
} >image
. = ALIGN(4K);
PROVIDE(dma_region = .);
/*
* Remove unused sections from the image.
*/
/DISCARD/ : {
/* The image loads itself so doesn't need these sections. */
*(.[Link])
*(.hash)
*(.interp)
*(.eh_frame_hdr)
*(.eh_frame)
*(.[Link]-id)
}
}
Makefile (no debería ser necesario cambiarlo):
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
332
# [Link]
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
all: [Link]
build:
cargo build
[Link]: build
$(OBJCOPY) -O binary target/aarch64-unknown-none/debug/rtc $@
qemu: [Link]
qemu-system-aarch64 -machine virt,gic-version=3 -cpu max -serial mon:stdio -display
clean:
cargo clean
rm -f *.bin
.cargo/[Link] (you shouldn't need to change this):
[build]
target = "aarch64-unknown-none"
rustflags = ["-C", "link-arg=-[Link]"]
Ejecuta el código en QEMU con make qemu.
333
mod exceptions;
mod logger;
mod pl011;
mod pl031;
use crate::pl031::Rtc;
use arm_gic::gicv3::{IntId, Trigger};
use arm_gic::{irq_enable, wfi};
use chrono::{TimeZone, Utc};
use core::hint::spin_loop;
use crate::pl011::Uart;
use arm_gic::gicv3::GicV3;
use core::panic::PanicInfo;
use log::{error, info, trace, LevelFilter};
use smccc::psci::system_off;
use smccc::Hvc;
#[no_mangle]
extern "C" fn main(x0: u64, x1: u64, x2: u64, x3: u64) {
// SAFETY: `PL011_BASE_ADDRESS` is the base address of a PL011 device, and
// nothing else accesses that address range.
let uart = unsafe { Uart::new(PL011_BASE_ADDRESS) };
logger::init(uart, LevelFilter::Trace).unwrap();
334
GicV3::set_priority_mask(0xff);
gic.set_interrupt_priority(PL031_IRQ, 0x80);
gic.set_trigger(PL031_IRQ, Trigger::Level);
irq_enable();
gic.enable_interrupt(PL031_IRQ, true);
system_off::<Hvc>().unwrap();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
335
error!("{info}");
system_off::<Hvc>().unwrap();
loop {}
}
[Link]:
use core::ptr::{addr_of, addr_of_mut};
#[repr(C, align(4))]
struct Registers {
/// Registro de datos
dr: u32,
/// Registro de coincidencias
mr: u32,
/// Registro de cargas
lr: u32,
/// Registro de control
cr: u8,
_reserved0: [u8; 3],
/// Interrumpe Mask Set o Clear register
imsc: u8,
_reserved1: [u8; 3],
/// Estado de interrupción sin procesar
ris: u8,
_reserved2: [u8; 3],
/// Estado de interrupción enmascarada
mis: u8,
_reserved3: [u8; 3],
/// Interrumpir registro de limpieza
icr: u8,
_reserved4: [u8; 3],
}
impl Rtc {
/// Crea una instancia nueva del controlador RTC para un dispositivo PL031 en la
/// dirección base proporcionada.
///
/// # Seguridad
///
/// El objeto la dirección base debe apuntar a los registros de control MMIO de un d
/// PL031, que debe asignarse al espacio de direcciones del proceso
/// como memoria del dispositivo y no tener ningún otro alias.
pub unsafe fn new(base_address: *mut u32) -> Self {
Self { registers: base_address as *mut Registers }
}
336
/// Lee el valor de RTC actual.
pub fn read(&self) -> u32 {
// SAFETY: We know that [Link] points to the control registers
// of a PL031 device which is appropriately mapped.
unsafe { addr_of!((*[Link]).dr).read_volatile() }
}
337
unsafe { addr_of_mut!((*[Link]).icr).write_volatile(0x01) }
}
}
338
Parte XIII
Concurrencia: mañana
339
Capítulo 57
Te Damos la Bienvenida a
Concurrencia en Rust
Rust es totalmente compatible con la concurrencia mediante hilos del SO con exclusiones
mutuas y canales.
El sistema de tipos de Rust desempeña un papel importante al hacer que muchos errores
de concurrencia sean errores en tiempo de compilación. A menudo, esto se conoce como
concurrencia sin miedo, ya que puedes confiar en el compilador para asegurar la corrección
en el tiempo de ejecución.
Horario
Including 10 minute breaks, this session should take about 3 hours and 20 minutes. It contains:
Sección Duración
Hilos 30 minutos
Canales 20 minutos
Send y Sync 15 minutos
Estado compartido 30 minutos
Ejercicios 1 hora y 10 minutos
340
Capítulo 58
Hilos
Diapositiva Duración
Hilos Simples 15 minutos
Hilos con ámbito 15 minutos
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("Recuento en el hilo: {i}!");
thread::sleep(Duration::from_millis(5));
}
});
for i in 1..5 {
println!("Hilo principal: {i}");
thread::sleep(Duration::from_millis(5));
}
}
• Los hilos son todos hilos daemon, y el hilo principal no espera por ellos.
• Los pánicos de los hilos son independientes entre sí.
– Los pánicos pueden transportar una carga útil, que se puede desempaquetar con
downcast_ref.
This slide should take about 15 minutes.
341
• Rust thread APIs look not too different from e.g. C++ ones.
• Run the example.
– 5ms timing is loose enough that main and spawned threads stay mostly in lockstep.
– Notice that the program ends before the spawned thread reaches 10!
– This is because main ends the program and spawned threads do not make it persist.
* Compare to pthreads/C++ std::thread/boost::thread if desired.
• How do we wait around for the spawned thread to complete?
• thread::spawn returns a JoinHandle. Look at the docs.
– JoinHandle has a .join() method that blocks.
• Use let handle = thread::spawn(...) and later [Link]() to wait for the
thread to finish and have the program count all the way to 10.
• Now what if we want to return a value?
• Look at docs again:
– thread::spawn's closure returns T
– JoinHandle .join() returns thread::Result<T>
• Use the Result return value from [Link]() to get access to the returned value.
• Ok, what about the other case?
– Trigger a panic in the thread. Note that this doesn't panic main.
– Access the panic payload. This is a good time to talk about Any.
• Now we can return values from threads! What about taking inputs?
– Capture something by reference in the thread closure.
– An error message indicates we must move it.
– Move it in, see we can compute and then return a derived value.
• If we want to borrow?
– Main kills child threads when it returns, but another function would just return
and leave them running.
– That would be stack use-after-return, which violates memory safety!
– How do we avoid this? see next slide.
fn foo() {
let s = String::from("Hola");
thread::spawn(|| {
println!("Longitud: {}", [Link]());
});
}
342
fn main() {
foo();
}
Sin embargo, puedes usar un hilo con ámbito para lo siguiente:
use std::thread;
fn main() {
let s = String::from("Hola");
thread::scope(|scope| {
[Link](|| {
println!("Longitud: {}", [Link]());
});
});
}
This slide should take about 13 minutes.
• La razón es que, cuando se completa la función thread::scope, se asegura que todos
los hilos están unidos, por lo que pueden devolver datos prestados.
• Se aplican las reglas normales de préstamo de Rust: un hilo puede tomar datos prestados
de manera mutable o cualquier número de hilos puede tomar datos prestados de manera
inmutable.
343
Capítulo 59
Canales
Diapositiva Duración
Transmisores y Receptores 10 minutos
Canales sin límites 2 minutos
Canales delimitados 10 minutos
fn main() {
let (tx, rx) = mpsc::channel();
[Link](10).unwrap();
[Link](20).unwrap();
344
• send() y recv() devuelven Result. Si devuelven Err, significa que el homólogo
Sender o Receiver se ha eliminado y el canal se ha cerrado.
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let thread_id = thread::current().id();
for i in 1..10 {
[Link](format!("Mensaje {i}")).unwrap();
println!("{thread_id:?}: mensaje enviado {i}");
}
println!("{thread_id:?}: completado");
});
thread::sleep(Duration::from_millis(100));
fn main() {
let (tx, rx) = mpsc::sync_channel(3);
thread::spawn(move || {
let thread_id = thread::current().id();
for i in 1..10 {
[Link](format!("Mensaje {i}")).unwrap();
println!("{thread_id:?}: mensaje enviado {i}");
}
println!("{thread_id:?}: completado");
});
thread::sleep(Duration::from_millis(100));
345
for msg in [Link]() {
println!("Principal: ha recibido {msg}");
}
}
This slide should take about 8 minutes.
• Al llamar a send, se bloqueará el hilo hasta que haya espacio suficiente en el canal para
el mensaje nuevo. El hilo se puede bloquear de forma indefinida si no hay nadie que lea
el canal.
• Si se cierra el canal, se anulará la llamada a send y se producirá un error (por eso
devuelve Result). Un canal se cierra cuando se elimina el receptor.
• A bounded channel with a size of zero is called a ”rendezvous channel”. Every send will
block the current thread until another thread calls recv.
346
Capítulo 60
Send y Sync
Diapositiva Duración
Traits de Marcador 2 minutos
Send 2 minutos
Sync 2 minutos
Ejemplos 10 minutos
60.2 Send
Un tipo T es Send si es seguro mover un valor T a otro hilo.
El efecto de mover la propiedad a otro hilo es que los destructores se ejecutarán en ese hilo.
Por tanto, la cuestion es cuándo se puede asignar un valor a un hilo y desasignarlo en otro.
This slide should take about 2 minutes.
Por ejemplo, solo se puede acceder a una conexión a la biblioteca SQLite desde un único hilo.
347
60.3 Sync
Un tipo T es Sync si es seguro acceder a un valor T desde varios hilos al mismo
tiempo.
En concreto, la definición es la siguiente:
T es Sync únicamente si &T es Send.
This slide should take about 2 minutes.
Esta instrucción es, básicamente, una forma resumida de indicar que, si un tipo es seguro
para los hilos en uso compartido, también lo es para pasar referencias de él a través de los
hilos.
Esto se debe a que, si el tipo es Sync, significa que se puede compartir entre múltiples hilos
sin el riesgo de que haya carreras de datos u otros problemas de sincronización, por lo que es
seguro moverlo a otro hilo. También es seguro mover una referencia al tipo a otro hilo, ya
que se puede acceder de forma segura a los datos a los que hace referencia desde cualquier
hilo.
60.4 Ejemplos
Send + Sync
La mayoría de los tipos que encuentras son Send + Sync:
• i8, f32, bool, char, &str, etc.
• (T1, T2), [T; N], &[T], struct { x: T }, etc.
• String, Option<T>, Vec<T>, Box<T>, etc.
• Arc<T>: explícitamente seguro para los hilos mediante el recuento atómico de
referencias.
• Mutex<T>: explícitamente seguro para los hilos mediante bloqueo interno.
• mpsc::Sender<T>: As of 1.72.0.
• AtomicBool, AtomicU8, etc.: utiliza instrucciones atómicas especiales.
Los tipos genéricos suelen ser Send + Sync cuando los parámetros del tipo son Send +
Sync.
Send + !Sync
Estos tipos se pueden mover a otros hilos, pero no son seguros para los hilos. Normalmente,
esto se debe a la mutabilidad interior:
• mpsc::Receiver<T>
• Cell<T>
• RefCell<T>
!Send + Sync
Estos tipos son seguros para los hilos (thread safe), pero no se pueden mover a otro hilo:
• MutexGuard<T: Sync>: Uses OS level primitives which must be deallocated on the
thread which created them.
348
!Send + !Sync
Estos tipos no son seguros para los hilos y no se pueden mover a otros hilos:
• Rc<T>: cada Rc<T> tiene una referencia a un RcBox<T>, que contiene un recuento de
referencias no atómico.
• *const T, *mut T: Rust asume que los punteros sin procesar pueden tener
consideraciones especiales de concurrencia.
349
Capítulo 61
Estado compartido
Diapositiva Duración
Arc 5 minutos
Mutex 15 minutos
Ejemplo 10 minutos
61.1 Arc
Arc<T> permite el acceso compartido de solo lectura a través de Arc::clone:
use std::sync::Arc;
use std::thread;
fn main() {
let v = Arc::new(vec![10, 20, 30]);
let mut handles = Vec::new();
for _ in 1..5 {
let v = Arc::clone(&v);
[Link](thread::spawn(move || {
let thread_id = thread::current().id();
println!("{thread_id:?}: {v:?}");
}));
}
handles.into_iter().for_each(|h| [Link]().unwrap());
println!("v: {v:?}");
}
This slide should take about 5 minutes.
• Arc son las siglas de ”Atomic Reference Counted” (recuento atómico de referencias),
una versión de Rc segura para los hilos que utiliza operaciones atómicas.
350
• Arc<T> implementa Clone, independientemente de si T lo hace o no. Implementa Send
y Sync si T implementa ambos.
• Arc::clone() tiene el coste de las operaciones atómicas que se ejecutan; después el
uso de T es libre.
• Hay que prestar atención a los ciclos de referencia, ya que Arc no usa un recolector de
memoria residual para detectarlos.
– std::sync::Weak puede resultar útil.
61.2 Mutex
Mutex<T> ensures mutual exclusion and allows mutable access to T behind a read-only
interface (another form of interior mutability):
use std::sync::Mutex;
fn main() {
let v = Mutex::new(vec![10, 20, 30]);
println!("v: {:?}", [Link]().unwrap());
{
let mut guard = [Link]().unwrap();
[Link](40);
}
61.3 Ejemplo
Veamos cómo funcionan Arc y Mutex:
use std::thread;
// usar std::sync::{Arc, Mutex};
351
fn main() {
let v = vec![10, 20, 30];
let handle = thread::spawn(|| {
[Link](10);
});
[Link](1000);
[Link]().unwrap();
println!("v: {v:?}");
}
This slide should take about 8 minutes.
Solución posible:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let v = Arc::new(Mutex::new(vec![10, 20, 30]));
let v2 = Arc::clone(&v);
let handle = thread::spawn(move || {
let mut v2 = [Link]().unwrap();
[Link](10);
});
{
let mut v = [Link]().unwrap();
[Link](1000);
}
[Link]().unwrap();
println!("v: {v:?}");
}
Puntos a destacar:
• v se envuelve tanto en Arc como en Mutex, porque sus preocupaciones son ortogonales.
– Envolver un Mutex en un Arc es un patrón habitual para compartir el estado
mutable entre hilos.
• v: Arc<_> se debe clonar como v2 antes de poder moverlo a otro hilo. Ten en cuenta
que move se ha añadido a la firma lambda.
• Se introducen bloqueos para limitar al máximo el ámbito de LockGuard.
352
Capítulo 62
Ejercicios
Diapositiva Duración
La cena de los filósofos 20 minutos
Comprobador de enlaces multihilo 20 minutos
Soluciones 30 minutos
struct Fork;
struct Philosopher {
name: String,
// left_fork: ...
353
// right_fork: ...
// thoughts: ...
}
impl Philosopher {
fn think(&self) {
[Link]
.send(format!("¡Eureka! ¡{} tiene una nueva idea!", &[Link]))
.unwrap();
}
fn eat(&self) {
// Recoge los tenedores...
println!("{} está comiendo...", &[Link]);
thread::sleep(Duration::from_millis(10));
}
}
fn main() {
// Crea tenedores
// Crea filósofos
354
cargo add --features blocking,rustls-tls reqwest
cargo add scraper
cargo add thiserror
Si cargo add da error: no such subcommand, edita el archivo [Link] de
forma manual. Añade las dependencias que se indican más abajo.
Las llamadas a cargo add actualizarán el archivo [Link] para que tenga este aspecto:
[package]
name = "link-checker"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
reqwest = { version = "0.11.12", features = ["blocking", "rustls-tls"] }
scraper = "0.13.0"
thiserror = "1.0.37"
Ya puedes descargar la página de inicio. Prueba con un sitio pequeño, como [Link]
El archivo src/[Link] debería tener un aspecto similar a este:
use reqwest::blocking::Client;
use reqwest::Url;
use scraper::{Html, Selector};
use thiserror::Error;
#[derive(Error, Debug)]
enum Error {
#[error("Error de solicitud: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("respuesta HTTP incorrecta: {0}")]
BadResponse(String),
}
#[derive(Debug)]
struct CrawlCommand {
url: Url,
extract_links: bool,
}
355
let base_url = [Link]().to_owned();
let body_text = [Link]()?;
let document = Html::parse_document(&body_text);
fn main() {
let client = Client::new();
let start_url = Url::parse("[Link]
let crawl_command = CrawlCommand{ url: start_url, extract_links: true };
match visit_page(&client, &crawl_command) {
Ok(links) => println!("Enlaces: {links:#?}"),
Err(err) => println!("No se han podido extraer los enlaces: {err:#}"),
}
}
Ejecuta el código en src/[Link] con
cargo run
Tasks
• Comprueba los enlaces en paralelo con los hilos: envía las URLs que se van a comprobar
a un canal y deja que varios hilos comprueben las URLs en paralelo.
• Amplía esta opción para extraer enlaces de todas las páginas del dominio
[Link]. Define un límite máximo de 100 páginas para que el sitio no
te bloquee.
62.3 Soluciones
La cena de los filósofos
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::Duration;
356
struct Fork;
struct Philosopher {
name: String,
left_fork: Arc<Mutex<Fork>>,
right_fork: Arc<Mutex<Fork>>,
thoughts: mpsc::SyncSender<String>,
}
impl Philosopher {
fn think(&self) {
[Link]
.send(format!("¡Eureka! ¡{} tiene una nueva idea!", &[Link]))
.unwrap();
}
fn eat(&self) {
println!("{} está intentando comer", &[Link]);
let _left = self.left_fork.lock().unwrap();
let _right = self.right_fork.lock().unwrap();
fn main() {
let (tx, rx) = mpsc::sync_channel(10);
for i in 0..[Link]() {
let tx = [Link]();
let mut left_fork = Arc::clone(&forks[i]);
let mut right_fork = Arc::clone(&forks[(i + 1) % [Link]()]);
357
thoughts: tx,
left_fork,
right_fork,
};
thread::spawn(move || {
for _ in 0..100 {
[Link]();
[Link]();
}
});
}
drop(tx);
for thought in rx {
println!("{thought}");
}
}
Comprobador de Enlaces
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use reqwest::blocking::Client;
use reqwest::Url;
use scraper::{Html, Selector};
use thiserror::Error;
#[derive(Error, Debug)]
enum Error {
#[error("Error de solicitud: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("respuesta HTTP incorrecta: {0}")]
BadResponse(String),
}
#[derive(Debug)]
struct CrawlCommand {
url: Url,
extract_links: bool,
}
358
if !command.extract_links {
return Ok(link_urls);
}
struct CrawlState {
domain: String,
visited_pages: std::collections::HashSet<String>,
}
impl CrawlState {
fn new(start_url: &Url) -> CrawlState {
let mut visited_pages = std::collections::HashSet::new();
visited_pages.insert(start_url.as_str().to_string());
CrawlState { domain: start_url.domain().unwrap().to_string(), visited_pages }
}
359
type CrawlResult = Result<Vec<Url>, (Url, Error)>;
fn spawn_crawler_threads(
command_receiver: mpsc::Receiver<CrawlCommand>,
result_sender: mpsc::Sender<CrawlResult>,
thread_count: u32,
) {
let command_receiver = Arc::new(Mutex::new(command_receiver));
for _ in 0..thread_count {
let result_sender = result_sender.clone();
let command_receiver = command_receiver.clone();
thread::spawn(move || {
let client = Client::new();
loop {
let command_result = {
let receiver_guard = command_receiver.lock().unwrap();
receiver_guard.recv()
};
let Ok(crawl_command) = command_result else {
// Se ha descartado el remitente. No se enviarán más comandos.
break;
};
let crawl_result = match visit_page(&client, &crawl_command) {
Ok(link_urls) => Ok(link_urls),
Err(error) => Err((crawl_command.url, error)),
};
result_sender.send(crawl_result).unwrap();
}
});
}
}
fn control_crawl(
start_url: Url,
command_sender: mpsc::Sender<CrawlCommand>,
result_receiver: mpsc::Receiver<CrawlResult>,
) -> Vec<Url> {
let mut crawl_state = CrawlState::new(&start_url);
let start_command = CrawlCommand { url: start_url, extract_links: true };
command_sender.send(start_command).unwrap();
let mut pending_urls = 1;
match crawl_result {
Ok(link_urls) => {
for url in link_urls {
360
if crawl_state.mark_visited(&url) {
let extract_links = crawl_state.should_extract_links(&url);
let crawl_command = CrawlCommand { url, extract_links };
command_sender.send(crawl_command).unwrap();
pending_urls += 1;
}
}
}
Err((url, error)) => {
bad_urls.push(url);
println!("Se ha producido un error de rastreo: {:#}", error);
continue;
}
}
}
bad_urls
}
fn main() {
let start_url = reqwest::Url::parse("[Link]
let bad_urls = check_links(start_url);
println!("URLs incorrectas: {:#?}", bad_urls);
}
361
Parte XIV
Concurrencia: tarde
362
Capítulo 63
Te damos la bienvenida
Comparaciones
• Python tiene un modelo similar en su asyncio. Sin embargo, su tipo Future está basado
en retrollamadas y no se sondea. Los programas asíncronos de Python requieren un
”bucle”, similar a un tiempo de ejecución en Rust.
• Promise de JavaScript es parecido, pero también se basa en retrollamadas. El tiempo
de ejecución del lenguaje implementa el bucle de eventos, por lo que muchos de los
detalles de la resolución de Promise están ocultos.
Horario
Including 10 minute breaks, this session should take about 3 hours and 20 minutes. It contains:
Sección Duración
Conceptos básicos de Async 30 minutos
Canales y Control de Flujo 20 minutos
Inconvenientes 55 minutos
Ejercicios 1 hora y 10 minutos
363
Capítulo 64
Diapositiva Duración
async/await 10 minutos
Future 4 minutos
Runtimes (Tiempos de Ejecución) 10 minutos
Tasks 10 minutos
64.1 async/await
En general, el código asíncrono de Rust se parece mucho al código secuencial ”normal”:
use futures::executor::block_on;
fn main() {
block_on(async_main(10));
}
This slide should take about 6 minutes.
Puntos clave:
• Ten en cuenta que este es un ejemplo simplificado para mostrar la sintaxis. No hay
ninguna operación de larga duración ni concurrencia real.
364
• ¿Cuál es el tipo de resultado devuelto de una llamada asíncrona?
– Consulta el tipo con let future: () = async_main(10); en main .
• The ”async” keyword is syntactic sugar. The compiler replaces the return type with a
future.
• No se puede hacer que main sea asíncrono sin dar instrucciones adicionales al
compilador sobre cómo usar el futuro devuelto.
• You need an executor to run async code. block_on blocks the current thread until the
provided future has run to completion.
• .await espera de forma asíncrona la finalización de otra operación. A diferencia de
block_on, .await no bloquea el hilo.
• .await can only be used inside an async function (or block; these are introduced later).
64.2 Future
Future es un trait implementado por objetos que representan una operación que puede que
aún no se haya completado. Se puede sondear un futuro y poll devuelve un Poll.
use std::pin::Pin;
use std::task::Context;
365
64.3 Runtimes (Tiempos de Ejecución)
Un runtime ofrece asistencia para realizar operaciones de forma asíncrona (un reactor) y
es responsable de ejecutar futuros (un ejecutor). Rust no cuenta con un tiempo de ejecución
”integrado”, pero hay varias opciones disponibles:
• Tokio: eficaz, con un ecosistema bien desarrollado de funciones, como Hyper para HTTP
o Tonic para usar gRPC.
• async-std: se trata de un ”std para async” e incluye un tiempo de ejecución básico en
async::task.
• smol: sencillo y ligero.
Varias aplicaciones de mayor tamaño tienen sus propios tiempos de ejecución. Por ejemplo,
Fuchsia ya tiene uno.
This slide and its sub-slides should take about 10 minutes.
• Ten en cuenta que, de los tiempos de ejecución enumerados, el playground de Rust solo
admite Tokio. El playground tampoco permite ningún tipo de E/S, por lo que la mayoría
de elementos asíncronos interesantes no se pueden ejecutar. en él.
• Los futuros son ”inertes”, ya que no realizan ninguna acción (ni siquiera iniciar una
operación de E/S) a menos que haya un ejecutor que los sondee. Muy diferente de las
promesas de JavaScript, por ejemplo, que se ejecutan hasta su finalización, aunque
nunca se utilicen.
64.3.1 Tokio
Tokio provides:
• Un tiempo de ejecución multihilo para ejecutar código asíncrono.
• Una versión asíncrona de la biblioteca estándar.
• Un amplio ecosistema de bibliotecas.
use tokio::time;
#[tokio::main]
async fn main() {
tokio::spawn(count_to(10));
for i in 1..5 {
println!("Tarea principal: {i}");
time::sleep(time::Duration::from_millis(5)).await;
}
}
• Con la macro tokio::main, podemos hacer que main sea asíncrono.
366
• La función spawn crea una ”tarea” simultánea.
• Nota: spawn utiliza un Future, no se llama a .await en count_to.
Más información:
• ¿Por qué count_to no suele llegar a 10? Se trata de un ejemplo de cancelación asíncrona.
tokio::spawn devuelve un controlador que puede esperarse hasta que termine.
• Prueba count_to(10).await en lugar de usar spawn.
• Intenta esperar a la correción de la tarea de tokio::spawn.
64.4 Tasks
Rust tiene un sistema de tareas, que es una forma de hilo ligero.
Una tarea tiene un solo futuro de nivel superior que el ejecutor sondea para hacer que
progrese. El futuro puede tener uno o varios futuros anidados que su método poll sondea, lo
que se corresponde con una pila de llamadas. La concurrencia dentro de una tarea es posible
mediante el sondeo de varios futuros secundarios , como una carrera de un temporizador y
una operación de E/S.
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> io::Result<()> {
let listener = TcpListener::bind("[Link]:0").await?;
println!("escuchando en el puerto {}", listener.local_addr()?.port());
loop {
let (mut socket, addr) = [Link]().await?;
println!("conexión de {addr:?}");
tokio::spawn(async move {
socket.write_all(b"¿Quién eres?\n").[Link]("error de socket");
367
• Pide a los alumnos que vean cuál sería el estado del servidor de ejemplo con algunos
clientes conectados. ¿Qué tareas hay? ¿Cuáles son sus futuros?
• This is the first time we've seen an async block. This is similar to a closure, but does not
take any arguments. Its return value is a Future, similar to an async fn.
• Refactoriza el bloque asíncrono en una función y mejora la gestión de errores con ?.
368
Capítulo 65
Diapositiva Duración
Canales asíncronos 10 minutos
Unir 4 minutos
Seleccionar 5 minutos
println!("ping_handler completo");
}
#[tokio::main]
async fn main() {
let (sender, receiver) = mpsc::channel(32);
let ping_handler_task = tokio::spawn(ping_handler(receiver));
for i in 0..10 {
[Link](()).[Link]("No se ha podido enviar el ping.");
println!("Se han enviado {} pings hasta ahora.", i + 1);
}
369
drop(sender);
ping_handler_task.[Link]("Se ha producido un error en la tarea del controlador
}
This slide should take about 8 minutes.
• Cambia el tamaño del canal a 3 y comprueba cómo afecta a la ejecución.
• Overall, the interface is similar to the sync channels as seen in the morning class.
• Prueba a quitar la llamada a std::mem::drop. ¿Qué sucede? ¿Por qué?
• El crate Flume tiene canales que implementan sync y async,send y recv. Esto puede
resultar práctico para aplicaciones complejas con tareas de E/S y tareas pesadas de
procesamiento de CPU.
• Es preferible trabajar con canales async por la capacidad de combinarlos con otros
future para poder crear un flujo de control complejo.
65.2 Unir
Una operación join espera hasta que todos los futuros estén listos y devuelve una colección
de sus resultados. Es similar a [Link] en JavaScript o [Link] en Python.
use anyhow::Result;
use futures::future;
use reqwest;
use std::collections::HashMap;
#[tokio::main]
async fn main() {
let urls: [&str; 4] = [
"[Link]
"[Link]
"[Link]
"BAD_URL",
];
let futures_iter = urls.into_iter().map(size_of_page);
let results = future::join_all(futures_iter).await;
let page_sizes_dict: HashMap<&str, Result<usize>> =
urls.into_iter().zip(results.into_iter()).collect();
println!("{:?}", page_sizes_dict);
}
This slide should take about 4 minutes.
Copia este ejemplo en el archivo src/[Link] que has preparado y ejecútalo desde ahí.
• En el caso de varios futuros de tipos distintos, puedes utilizar std::future::join!,
pero debes saber cuántos futuros tendrás en el tiempo de compilación. Esto se encuentra
370
actualmente en el crate futures, que pronto se estabilizará en std::future.
• The risk of join is that one of the futures may never resolve, this would cause your
program to stall.
• También puedes combinar join_all con join!, por ejemplo, para unir todas las
solicitudes a un servicio HTTP, así como una consulta a la base de datos. Prueba a
añadir un tokio::time::sleepal futuro mediantefutures::join!. No se trata de
un tiempo de espera (para eso se requiere select!, que se explica en
el siguiente capítulo), sino que muestra join¡.
65.3 Seleccionar
Una operación select espera hasta que un conjunto de futuros esté listo y responde al resultado
de ese futuro. En JavaScript, esto es similar a [Link]. En Python, se compara con
[Link](task_set, return_when=asyncio.FIRST_COMPLETED).
Similar to a match statement, the body of select! has a number of arms, each of the
form pattern = future => statement. When a future is ready, its return value is
destructured by the pattern. The statement is then run with the resulting variables. The
statement result becomes the result of the select! macro.
use tokio::sync::mpsc::{self, Receiver};
use tokio::time::{sleep, Duration};
#[derive(Debug, PartialEq)]
enum Animal {
Cat { name: String },
Dog { name: String },
}
async fn first_animal_to_finish_race(
mut cat_rcv: Receiver<String>,
mut dog_rcv: Receiver<String>,
) -> Option<Animal> {
tokio::select! {
cat_name = cat_rcv.recv() => Some(Animal::Cat { name: cat_name? }),
dog_name = dog_rcv.recv() => Some(Animal::Dog { name: dog_name? })
}
}
#[tokio::main]
async fn main() {
let (cat_sender, cat_receiver) = mpsc::channel(32);
let (dog_sender, dog_receiver) = mpsc::channel(32);
tokio::spawn(async move {
sleep(Duration::from_millis(500)).await;
cat_sender.send(String::from("Felix")).[Link]("No se ha podido enviar el g
});
tokio::spawn(async move {
sleep(Duration::from_millis(50)).await;
dog_sender.send(String::from("Rex")).[Link]("No se ha podido enviar el per
371
});
372
Capítulo 66
Inconvenientes
Async / await provides convenient and efficient abstraction for concurrent asynchronous
programming. However, the async/await model in Rust also comes with its share of pitfalls
and footguns. We illustrate some of them in this chapter.
Esta sección tiene una duración aproximada de 55 minutos. Contiene:
Diapositiva Duración
Bloqueo del ejecutor 10 minutos
Pin 20 minutos
Traits asíncronos 5 minutos
Cancelación 20 minutos
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
373
let sleep_futures = (1..=10).map(|t| sleep_ms(&start, t, t * 10));
join_all(sleep_futures).await;
}
This slide should take about 10 minutes.
• Ejecuta el código y comprueba que las suspensiones se producen de forma consecutiva
y no simultánea.
• La versión "current_thread" reúne todas las tareas en un solo hilo. Esto consigue que
el efecto sea más obvio, pero el error sigue estando presente en la versión multihilo.
• Cambia std::thread::sleep a tokio::time::sleep. y espera su resultado.
• Otra solución sería tokio::task::spawn_blocking, que genera un hilo real y
transforma su controlador en un futuro sin bloquear el ejecutor.
• No debes pensar en las tareas como hilos del sistema operativo. No se asignan 1
a 1 y la mayoría de los ejecutores permitirán que se ejecuten muchas tareas en
un solo hilo del sistema operativo. Esta situación es especialmente problemática
cuando se interactúa con otras bibliotecas a través de FFI, donde dicha biblioteca
puede depender del almacenamiento local de hilos o puede asignarse a hilos
específicos del sistema operativo (por ejemplo, CUDA). En estos casos es preferible usar
tokio::task::spawn_blocking.
• Utiliza las exclusión mutuas de sincronización con cuidado. Si mantienes una exclusión
mutua sobre un .await, puede que se bloquee otra tarea y que esta se esté ejecutando
en el mismo hilo.
66.2 Pin
Los bloques y las funciones asíncronos devuelven tipos que implementan el trait Future.
El tipo devuelto es el resultado de una transformación del compilador que convierte las
variables locales en datos almacenados en el futuro.
Algunas de estas variables pueden dirigir punteros a otras variables locales. Por este motivo, el
futuro nunca debería trasladarse a otra ubicación de memoria, ya que esta acción invalidaría
esos punteros.
Para evitar que el tipo futuro se mueva en la memoria, solo se puede sondear mediante un
puntero fijado. Pin es un envoltorio que rodea a una referencia y que no permite todas las
operaciones que moverían la instancia a la que apunta a otra ubicación de memoria.
use tokio::sync::{mpsc, oneshot};
use tokio::task::spawn;
use tokio::time::{sleep, Duration};
374
// Un trabajador que espera trabajo en una cola y lo ejecuta.
async fn worker(mut work_queue: mpsc::Receiver<Work>) {
let mut iterations = 0;
loop {
tokio::select! {
Some(work) = work_queue.recv() => {
sleep(Duration::from_millis(10)).await; // Simula que trabaja.
work.respond_on
.send([Link] * 1000)
.expect("no se ha podido enviar la respuesta");
iterations += 1;
}
// TODO: informar del número de iteraciones cada 100 ms
}
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(10);
spawn(worker(rx));
for i in 0..100 {
let resp = do_work(&tx, i).await;
println!("resultado del trabajo de la iteración {i}: {resp}");
}
}
This slide should take about 20 minutes.
• Puede que reconozcas esto como un ejemplo del patrón actor. Los actores suelen llamar
a select! en un bucle.
• Esta sección es un resumen de algunas de las lecciones anteriores, así que tómate tu
tiempo .
– Si añade un _ = sleep(Duration::from_millis(100)) => { println!(..)
} a select!, nunca se ejecutará. ¿Por qué?
– En su lugar, añade un timeout_fut que contenga ese futuro fuera de loop:
let timeout_fut = sleep(Duration::from_millis(100));
loop {
select! {
..,
375
_ = timeout_fut => { println!(..); },
}
}
– Continuará sin funcionar. Sigue los errores del compilador y añade &mut a
timeout_fut en select! para ir despejando el problema. A continuación, usa
Box::pin:
let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
loop {
select! {
..,
_ = &mut timeout_fut => { println!(..); },
}
}
– This compiles, but once the timeout expires it is Poll::Ready on every iteration
(a fused future would help with this). Update to reset timeout_fut every time it
expires:
let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
loop {
select! {
_ = &mut timeout_fut => {
println!(..);
timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
},
}
}
• Box se asigna en el montículo. En algunos casos, std::pin::pin! (solo si se ha
estabilizado recientemente, con código antiguo que suele utilizar tokio::pin!)
también es una opción, pero difícil de utilizar en un futuro que se reasigna.
• Otra alternativa es no utilizar pin, sino generar otra tarea que se enviará a un canal de
oneshot cada 100 ms.
• Los datos que contienen punteros a sí mismos se denominan autoreferenciales.
Normalmente, el verificador de préstamos de Rust evitaría que se movieran los datos
de autorreferencia, ya que las referencias no pueden tener una duración mayor que la
de los datos a los que apuntan. Sin embargo, el verificador de préstamos no verifica la
transformación del código de las funciones y los bloques asíncronos.
• Pin es un envoltorio que rodea a una referencia. No se puede mover un objeto desde su
lugar mediante un puntero fijado. Sin embargo, sí se puede mover mediante un puntero
no fijado.
• El método poll del trait Future utiliza Pin<&mut Self> en lugar de &mut Self para
hacer referencia a la instancia. Por eso solo se puede llamar desde un puntero fijado.
376
fn includes -> impl Future<Output = ...>.
However, even with the native support today there are some pitfalls around async fn and
RPIT in traits:
• Return-position impl Trait captures all in-scope lifetimes (so some patterns of borrowing
cannot be expressed)
• Traits whose methods use return-position impl trait or async are not dyn compatible.
If we do need dyn support, the crate async_trait provides a workaround through a macro,
with some caveats:
use async_trait::async_trait;
use std::time::Instant;
use tokio::time::{sleep, Duration};
#[async_trait]
trait Sleeper {
async fn sleep(&self);
}
struct FixedSleeper {
sleep_ms: u64,
}
#[async_trait]
impl Sleeper for FixedSleeper {
async fn sleep(&self) {
sleep(Duration::from_millis(self.sleep_ms)).await;
}
}
async fn run_all_sleepers_multiple_times(
sleepers: Vec<Box<dyn Sleeper>>,
n_times: usize,
) {
for _ in 0..n_times {
println!("ejecutando todos los sleepers…".);
for sleeper in &sleepers {
let start = Instant::now();
[Link]().await;
println!("ha dormido {} ms", [Link]().as_millis());
}
}
}
#[tokio::main]
async fn main() {
let sleepers: Vec<Box<dyn Sleeper>> = vec![
Box::new(FixedSleeper { sleep_ms: 50 }),
Box::new(FixedSleeper { sleep_ms: 100 }),
];
377
run_all_sleepers_multiple_times(sleepers, 5).await;
}
This slide should take about 5 minutes.
• async_trait es fácil de usar, pero ten en cuenta que utiliza asignaciones de montículos
para conseguirlo. Esta asignación de montículo tiene una sobrecarga de rendimiento.
• Los problemas de compatibilidad del lenguaje con async trait son muy complejos y
no vale la pena describirlos en profundidad. Niko Matsakis lo explica muy bien en esta
publicación, por si te interesa investigar más a fondo.
• Prueba a crear una estructura que entre en suspensión durante un periodo aleatorio y
añádela a Vec.
66.4 Cancelación
Si eliminas un futuro, no se podrá volver a sondear. Este fenómeno se denomina cancelación
y puede producirse en cualquier momento de await. Hay que tener cuidado para asegurar
que el sistema funcione correctamente, incluso cuando se cancelen los futuros. Por ejemplo,
no debería sufrir interbloqueos o perder datos.
use std::io::{self, ErrorKind};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
struct LinesReader {
stream: DuplexStream,
}
impl LinesReader {
fn new(stream: DuplexStream) -> Self {
Self { stream }
}
378
async fn slow_copy(source: String, mut dest: DuplexStream) -> std::io::Result<()> {
for b in [Link]() {
dest.write_u8(b).await?;
tokio::time::sleep(Duration::from_millis(10)).await
}
Ok(())
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (client, server) = tokio::io::duplex(5);
let handle = tokio::spawn(slow_copy("hi\nthere\n".to_owned(), client));
impl LinesReader {
fn new(stream: DuplexStream) -> Self {
Self { stream, bytes: Vec::new(), buf: [0] }
379
}
async fn next(&mut self) -> io::Result<Option<String>> {
// prefijo buf y bytes con self.
// ...
let raw = std::mem::take(&mut [Link]);
let s = String::from_utf8(raw)
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "not UTF-8"))?;
// ...
}
}
• Interval::tick es a prueba de cancelaciones, ya que registra si una marca se ha
'entregado'.
• AsyncReadExt::read es a prueba de cancelaciones porque o devuelve los datos o no
los lee.
• AsyncBufReadExt::read_line es similar al ejemplo y no está configurado a prueba
de cancelaciones. Consulta su documentación para obtener información detallada y
alternativas.
380
Capítulo 67
Ejercicios
Diapositiva Duración
La cena de los filósofos 20 minutos
Aplicación de chat de difusión 30 minutos
Soluciones 20 minutos
struct Fork;
struct Philosopher {
name: String,
// left_fork: ...
// right_fork: ...
// thoughts: ...
}
impl Philosopher {
async fn think(&self) {
[Link]
.send(format!("¡Eureka! ¡{} tiene una nueva idea!", &[Link]))
381
.await
.unwrap();
}
async fn eat(&self) {
// Keep trying until we have both forks
println!("{} está comiendo...", &[Link]);
time::sleep(time::Duration::from_millis(5)).await;
}
}
#[tokio::main]
async fn main() {
// Crea tenedores
// Crea filósofos
[dependencies]
tokio = { version = "1.26.0", features = ["sync", "time", "macros", "rt-multi-thread"] }
Además, ten en cuenta que esta vez tienes que utilizar Mutex y el módulo mpsc del crate
tokio.
This slide should take about 20 minutes.
• ¿Puedes conseguir que tu implementación tenga un solo hilo?
382
Crea un proyecto de Cargo y añade las siguientes dependencias:
[Link]:
[package]
name = "chat-async"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = { version = "0.3.30", features = ["sink"] }
http = "1.1.0"
tokio = { version = "1.38.0", features = ["full"] }
tokio-websockets = { version = "0.8.3", features = ["client", "fastrand", "server", "sha
Dos binarios
Normalmente, en un proyecto de Cargo, solo puedes tener un archivo binario y un archivo
src/[Link]. En este proyecto, se necesitan dos binarios, uno para el cliente y otro para el
servidor. Puedes convertirlos en dos proyectos de Cargo independientes, pero los incluiremos
en un solo proyecto de Cargo con dos binarios. Para que funcione, el código del cliente y del
servidor deben aparecer en src/bin (consulta la documentación).
Copia el fragmento de código del servidor y del cliente que aparecen más abajo en
src/bin/[Link] y src/bin/[Link], respectivamente. Tu tarea es completar estos
archivos como se describe a continuación.
src/bin/[Link]:
use futures_util::sink::SinkExt;
use futures_util::stream::StreamExt;
use std::error::Error;
use std::net::SocketAddr;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast::{channel, Sender};
use tokio_websockets::{Message, ServerBuilder, WebSocketStream};
async fn handle_connection(
addr: SocketAddr,
mut ws_stream: WebSocketStream<TcpStream>,
383
bcast_tx: Sender<String>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let (bcast_tx, _) = channel(16);
loop {
let (socket, addr) = [Link]().await?;
println!("Nueva conexión de {addr:?}");
let bcast_tx = bcast_tx.clone();
tokio::spawn(async move {
// Envuelve el flujo TCP sin procesar en un websocket.
let ws_stream = ServerBuilder::new().accept(socket).await?;
#[tokio::main]
async fn main() -> Result<(), tokio_websockets::Error> {
let (mut ws_stream, _) =
ClientBuilder::from_uri(Uri::from_static("[Link]
.connect()
.await?;
384
Ejecutar los binarios
Ejecuta el servidor con:
cargo run --bin server
y el cliente con:
cargo run --bin client
Tasks
• Implementa la función handle_connection en src/bin/[Link].
– Sugerencia: usa tokio::select! para realizar dos tareas simultáneamente en un
bucle continuo. Una tarea recibe mensajes del cliente y los transmite. La otra envía
los mensajes que recibe el servidor al cliente.
• Completa la función principal en src/bin/[Link].
– Sugerencia: al igual que antes, usa tokio::select! en un bucle continuo para
realizar dos tareas simultáneamente: (1) leer los mensajes del usuario desde la
entrada estándar y enviarlos al servidor, y (2) recibir mensajes del servidor y
mostrárselos al usuario.
• Opcional: cuando termines, cambia el código para difundir mensajes a todos los clientes,
excepto al remitente.
67.3 Soluciones
La Cena de Filósofos --- Async
use std::sync::Arc;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::Mutex;
use tokio::time;
struct Fork;
struct Philosopher {
name: String,
left_fork: Arc<Mutex<Fork>>,
right_fork: Arc<Mutex<Fork>>,
thoughts: Sender<String>,
}
impl Philosopher {
async fn think(&self) {
[Link]
.send(format!("¡Eureka! ¡{} tiene una nueva idea!", &[Link]))
.await
.unwrap();
}
async fn eat(&self) {
// Keep trying until we have both forks
385
let (_left_fork, _right_fork) = loop {
// Recoge los tenedores...
let left_fork = self.left_fork.try_lock();
let right_fork = self.right_fork.try_lock();
let Ok(left_fork) = left_fork else {
// If we didn't get the left fork, drop the right fork if we
// have it and let other tasks make progress.
drop(right_fork);
time::sleep(time::Duration::from_millis(1)).await;
continue;
};
let Ok(right_fork) = right_fork else {
// If we didn't get the right fork, drop the left fork and let
// other tasks make progress.
drop(left_fork);
time::sleep(time::Duration::from_millis(1)).await;
continue;
};
break (left_fork, right_fork);
};
#[tokio::main]
async fn main() {
// Crea tenedores
let mut forks = vec![];
(0..[Link]()).for_each(|_| [Link](Arc::new(Mutex::new(Fork))));
// Crea filósofos
let (philosophers, mut rx) = {
let mut philosophers = vec![];
let (tx, rx) = mpsc::channel(10);
for (i, name) in [Link]().enumerate() {
let left_fork = Arc::clone(&forks[i]);
let right_fork = Arc::clone(&forks[(i + 1) % [Link]()]);
[Link](Philosopher {
name: name.to_string(),
left_fork,
right_fork,
thoughts: [Link](),
});
}
386
(philosophers, rx)
// tx se elimina aquí, por lo que no tenemos que eliminarlo explícitamente más t
};
async fn handle_connection(
addr: SocketAddr,
mut ws_stream: WebSocketStream<TcpStream>,
bcast_tx: Sender<String>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
ws_stream
.send(Message::text("Te damos la bienvenida al chat. Escribe un mensaje".to_stri
.await?;
let mut bcast_rx = bcast_tx.subscribe();
387
if let Some(text) = msg.as_text() {
println!("Del cliente {addr:?} {text:?}");
bcast_tx.send([Link]())?;
}
}
Some(Err(err)) => return Err([Link]()),
None => return Ok(()),
}
}
msg = bcast_rx.recv() => {
ws_stream.send(Message::text(msg?)).await?;
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let (bcast_tx, _) = channel(16);
loop {
let (socket, addr) = [Link]().await?;
println!("Nueva conexión de {addr:?}");
let bcast_tx = bcast_tx.clone();
tokio::spawn(async move {
// Envuelve el flujo TCP sin procesar en un websocket.
let ws_stream = ServerBuilder::new().accept(socket).await?;
#[tokio::main]
async fn main() -> Result<(), tokio_websockets::Error> {
let (mut ws_stream, _) =
ClientBuilder::from_uri(Uri::from_static("[Link]
.connect()
.await?;
388
let mut stdin = BufReader::new(stdin).lines();
}
}
}
389
Parte XV
Conclusiones
390
Capítulo 68
¡Gracias!
Gracias por realizar el curso Comprehensive Rust . Esperamos que te haya gustado y que te
haya resultado útil.
Nos lo hemos pasado muy bien preparando el curso. Sabemos que no es perfecto, así que si
has detectado algún error o tienes ideas para mejorarlo, ponte en contacto con nosotros en
GitHub. Nos encantaría saber tu opinión.
391
Capítulo 69
Glosario
392
• Simultaneidad:
ejecución de varias tareas o procesos al mismo tiempo.
• Simultaneidad en Rust:
consulta Simultaneidad en Rust.
• Constante:
valor que no cambia durante la ejecución de un programa.
• Flujo de control:
el orden en el que se ejecutan las instrucciones individuales en un programa.
• Fallo:
fallo o finalización de un programa de forma inesperada y sin gestionar.
• Enumeración:
tipo de dato que contiene una de varias constantes con nombre, posiblemente con una
tupla o estructura asociada.
• Error:
condición o resultado inesperado que se desvía del comportamiento esperado.
• Gestión de errores:
el proceso de gestionar y responder a los errores que se producen durante la ejecución
del programa.
• Ejercicio:
una tarea o problema diseñado para practicar y poner a prueba las habilidades de
programación.
• Función:
bloque de código reutilizable que lleva a cabo una tarea específica.
• Recolector de elementos no utilizados:
mecanismo que libera automáticamente la memoria que ocupan objetos que ya no se
utilizan.
• Genéricos:
una función que permite escribir código con marcadores de posición para los tipos, lo
que permite reutilizar código con distintos tipos de datos.
• Inmutable:
que no se puede cambiar después de crearse.
• Prueba de integración:
tipo de prueba que verifica las interacciones entre diferentes partes o componentes de
un sistema.
• Palabra clave:
palabra reservada en un lenguaje de programación que tiene un significado específico
y que no se puede utilizar como identificador.
• Biblioteca:
una colección de rutinas o código precompilados que pueden utilizar los programas.
• Macro:
las macros de Rust se pueden reconocer por llevar ! en el nombre. Las macros se utilizan
cuando las funciones normales no son suficientes. Un ejemplo típico es format!, que
utiliza un número variable de argumentos que no es compatible con las funciones de
Rust.
• Función main:
los programas de Rust empiezan a ejecutarse con la función main.
• Coincidencia:
construcción de flujo de control en Rust que permite la coincidencia de patrones con el
valor de una expresión.
• Pérdida de memoria:
situación en la que un programa no libera memoria que ya no se necesita, lo que provoca
393
un aumento gradual en el uso de memoria.
• Método:
una función asociada a un objeto o a un tipo en Rust.
• Módulo:
espacio de nombres que contiene definiciones, como funciones, tipos o traits, para
organizar el código en Rust.
• Mover:
la transferencia de la propiedad de un valor de una variable a otra en Rust.
• Mutable:
una propiedad en Rust que permite que se modifiquen las variables después de que se
hayan declarado.
• Propiedad:
el concepto de Rust que define qué parte del código es responsable de gestionar la
memoria asociada a un valor.
• Pánico:
condición de error irrecuperable en Rust que provoca la finalización del programa.
• Parámetro:
valor que se transfiere a una función o método cuando se llama.
• Patrón:
una combinación de valores, literales o estructuras que se pueden comparar con una
expresión de Rust.
• Carga útil:
los datos o la información que transporta un mensaje, evento o estructura de datos.
• Programa:
conjunto de instrucciones que un ordenador puede ejecutar para llevar a cabo una
tarea específica o resolver un problema concreto.
• Lenguaje de programación:
un sistema formal que se utiliza para comunicar instrucciones a un ordenador, como
Rust.
• Receptor:
el primer parámetro de un método de Rust que representa la instancia en la que se
llama al método.
• Recuento de referencias:
técnica de gestión de la memoria en la que se hace un seguimiento del número de
referencias a un objeto y se desasigna cuando el recuento llega a cero.
• Retorno:
una palabra clave de Rust que se utiliza para indicar el valor que se devuelve de una
función.
• Rust:
lenguaje de programación de sistemas que se centra en la seguridad, el rendimiento y
la simultaneidad.
• Rust Fundamentals:
Days 1 to 4 of this course.
• Rust en Android:
consulta Rust en Android.
• Rust en Chromium:
consulta Rust en Chromium.
• Seguro:
se refiere al código que cumple las reglas de propiedad y préstamos de Rust, lo que evita
errores relacionados con la memoria.
• Ámbito:
394
la región de un programa en la que una variable es válida y se puede utilizar.
• Biblioteca estándar:
una colección de módulos que proporcionan funciones esenciales en Rust.
• Static:
una palabra clave de Rust que se utiliza para definir variables o elementos estáticos con
un tiempo de vida 'static.
• string:
A data type storing textual data. See Strings for more.
• Struct:
tipo de datos compuestos de Rust que agrupa variables de diferentes tipos bajo un
mismo nombre.
• Prueba:
módulo de Rust que contiene funciones que comprueban que otras funciones sean
correctas.
• Hilo:
una secuencia de ejecución independiente en un programa que permite la ejecución
simultánea.
• Seguridad en hilos:
la propiedad de un programa que asegura un comportamiento correcto en un entorno
multihilo.
• Trait:
conjunto de métodos definidos para un tipo desconocido que proporciona una forma de
lograr el polimorfismo en Rust.
• Límite del trait:
una abstracción en la que puedes requerir que los tipos implementen algunos traits de
tu interés.
• Tupla:
tipo de datos compuestos que contiene variables de diferentes tipos. Los campos de
tuplas no tienen nombre y se accede a ellos por sus números ordinales.
• Tipo:
una clasificación que especifica qué operaciones se pueden llevar a cabo en valores de
una clase concreta en Rust.
• Inferencia de tipos:
capacidad del compilador de Rust para deducir el tipo de una variable o expresión.
• Comportamiento indefinido:
acciones o condiciones en Rust que no tienen ningún resultado especificado, lo que a
menudo provoca un comportamiento impredecible del programa.
• Unión:
tipo de datos que puede contener valores de distintos tipos, pero solo de uno en uno.
• Prueba unitaria:
Rust incluye asistencia integrada para llevar a cabo pruebas unitarias de pequeño
tamaño y pruebas de integración de mayor tamaño. Consulta la página Pruebas unitarias.
• Tipo de unidad:
tipo que no contiene datos, escrito como una tupla sin miembros.
• Inseguro:
el subconjunto de Rust que te permite activar un comportamiento indefinido. Consulta
Rust inseguro.
• Variable:
una ubicación de la memoria que almacena datos. Las variables son válidas en un
ámbito
395
Capítulo 70
La comunidad de Rust ha creado una gran cantidad de recursos online sin coste y de gran
calidad.
Documentación oficial
El proyecto Rust cuenta con muchos recursos. Estos tratan sobre Rust en general:
• The Rust Programming Language: el libro canónico sobre Rust sin coste alguno. Trata
el lenguaje de forma detallada e incluye algunos proyectos que los usuarios pueden
compilar.
• Rust by Example: trata la sintaxis de Rust a través de una serie de ejemplos que muestran
distintas construcciones. A veces incluye pequeños ejercicios en los que se te pide que
amplíes el código de los ejemplos.
• La biblioteca estándar de Rust: documentación completa de la biblioteca estándar de
Rust.
• The Rust Reference: un libro incompleto que describe la gramática y el modelo de
memoria de Rust.
Consulta guías más especializadas en el sitio oficial de Rust:
• The Rustonomicon: trata de Rust inseguro, incluido cómo trabajar con punteros sin
formato e interactuar con otros lenguajes (FFI).
• Asynchronous Programming in Rust: incluye el nuevo modelo de programación
asíncrona que se introdujo después de que se escribiera el libro de Rust.
• The Embedded Rust Book: una introducción sobre el uso de Rust en dispositivos
integrados sin sistema operativo.
396
• Rust for professonals: trata la sintaxis de Rust comparándola con otros lenguajes, como
C, C++, Java, JavaScript y Python.
• Rust on Exercism: más de 100 ejercicios para aprender Rust.
• Ferrous Teaching Material: una serie de pequeñas presentaciones que cubren tanto la
parte básica como la parte más avanzada del lenguaje Rust. También se tratan otros
temas como WebAssembly y async/await.
• Advanced testing for Rust applications: a self-paced workshop that goes beyond Rust's
built-in testing framework. It covers googletest, snapshot testing, mocking as well as
how to write your own custom test harness.
• Beginner's Series to Rust y Take your first steps with Rust: dos guías de Rust dirigidas
a nuevos desarrolladores. La primera es un conjunto de 35 vídeos y la segunda es un
conjunto de 11 módulos que cubren la sintaxis y las construcciones básicas de Rust.
• Learn Rust With Entirely Too Many Linked Lists: exploración detallada de las reglas de
gestión de la memoria de Rust a través de la implementación de algunos tipos diferentes
de estructuras de listas.
Consulta The Little Book of Rust Books para ver más libros de Rust.
397
Capítulo 71
Créditos
Este material se basa en las numerosas fuentes de documentación sobre Rust. Consulta la
página de otros recursos para ver una lista completa de recursos útiles.
El material de Comprehensive Rust está sujeto a los términos de la licencia Apache 2.0. Para
obtener más información, consulta LICENSE.
Rust by Example
Algunos ejemplos y ejercicios se han copiado y adaptado del libro Rust by Example. Consulta
el directorio third_party/rust-by-example/ para obtener más información, incluidos
los términos de la licencia.
Rust on Exercism
Se han copiado y adaptado algunos ejercicios del recurso Rust on Exercism. Consulta el
directorio third_party/rust-on-exercism/ para obtener más información, incluidos los
términos de la licencia.
CXX
En la sección Interoperabilidad con C++ se usa una imagen de CXX. Consulta el directorio
third_party/cxx/ para obtener más información, incluidos los términos de la licencia.
398