Rust y WebAssembly
WebAssembly (que se suele abreviar como Wasm) te permite ejecutar código escrito en lenguajes como Rust dentro de un navegador web, junto a JavaScript. Piénsalo como un segundo motor en el navegador: JavaScript sigue manejando la página, pero cuando hay trabajo pesado (filtros de imagen, física, procesar archivos grandes) puedes pasárselo a código Rust compilado y rápido.
En esta página vas a escribir una pequeña biblioteca en Rust, compilarla a Wasm y llamarla desde una página web. Nada de esto funciona en el Playground de Rust (necesita un navegador y herramientas extra), así que la mayoría de los ejemplos son bloques de código para tu propia máquina. Eso sí, las partes que son Rust puro las puedes probar ahora mismo.
Qué es WebAssembly y por qué encaja con Rust
Sección titulada «Qué es WebAssembly y por qué encaja con Rust»WebAssembly es un formato binario compacto para programas. Todos los navegadores importantes lo pueden ejecutar, a una velocidad cercana a la nativa, dentro del mismo entorno aislado (sandbox) de seguridad que JavaScript. No es solo para navegadores: los servidores y las plataformas edge también ejecutan Wasm.
Rust es uno de los lenguajes más populares para Wasm, por varias razones:
- Sin recolector de basura ni un runtime grande. Rust no necesita enviar maquinaria extra junto a tu código, así que los archivos
.wasmpueden ser pequeños. - Muy buenas herramientas.
wasm-bindgenescribe por ti el código de unión entre Rust y JavaScript, ywasm-packlo compila todo con un solo comando. - Seguridad y velocidad. Tienes la misma seguridad de memoria y el mismo rendimiento que en un ordenador de escritorio.
Instalar las herramientas
Sección titulada «Instalar las herramientas»Primero, dile a Rust que puede compilar para el target de Wasm. Un target es “el tipo de máquina donde se va a ejecutar el código”; wasm32-unknown-unknown significa “Wasm de 32 bits, sin ningún sistema operativo en concreto”:
rustup target add wasm32-unknown-unknownDespués, instala wasm-pack. Compila tu crate, ejecuta wasm-bindgen y pone todo lo que necesitas en una carpeta. Usa el instalador oficial o compílalo con Cargo:
# Instalador para Linux / macOS de la web de wasm-packcurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# o, en cualquier sistema con Rust instaladocargo install wasm-pack --lockedEscribir una biblioteca Rust para Wasm
Sección titulada «Escribir una biblioteca Rust para Wasm»Un módulo Wasm es una biblioteca, no un programa con main. Crea una:
cargo new --lib hello-wasmcd hello-wasmLuego edita Cargo.toml. La línea crate-type le dice a Cargo que genere una biblioteca apta para .wasm (cdylib), y wasm-bindgen es el crate que conecta Rust y JavaScript:
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"Ahora escribe dos funciones en src/lib.rs. El atributo #[wasm_bindgen] marca cada una como “se puede llamar desde JavaScript”:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}
#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("¡Hola, {name}! Este texto se construyó en Rust.")}Eso es todo el Rust que necesitas. Fíjate en que son funciones normales: wasm-bindgen se encarga de convertir un string de JavaScript en un &str, y el String que devuelves otra vez en un string de JavaScript.
Prueba primero la lógica en tu máquina
Sección titulada «Prueba primero la lógica en tu máquina»Esta costumbre ahorra mucho tiempo: mantén el trabajo de verdad en funciones normales de Rust y pruébalas en tu ordenador con cargo test o con un main rápido. Depurar en un navegador es más lento que depurar en una terminal. La misma lógica, sin el atributo, funciona en cualquier sitio:
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("¡Hola, {name}! Este texto se construyó en Rust.")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}Salida:
¡Hola, Ferris! Este texto se construyó en Rust.2 + 3 = 5Cuando la lógica es correcta, el envoltorio #[wasm_bindgen] es solo una puerta fina hacia el navegador. (Los tests marcados con #[test] también se ejecutan en tu máquina con cargo test; consulta Testing.)
Compilar con wasm-pack
Sección titulada «Compilar con wasm-pack»Desde la carpeta del crate, ejecuta:
wasm-pack build --target web--target web significa “genera archivos que un navegador pueda cargar directamente con una etiqueta <script type="module">”, sin necesitar un bundler como webpack. Cuando lo ejecutamos, la nueva carpeta pkg/ contenía:
pkg/├── hello_wasm_bg.wasm el código Rust compilado├── hello_wasm.js código de unión en JavaScript que lo carga y envuelve cada función├── hello_wasm.d.ts tipos de TypeScript para tus funciones├── hello_wasm_bg.wasm.d.ts└── package.jsonEl guion de hello-wasm se convirtió en un guion bajo en los nombres de archivo. Por defecto, wasm-pack build hace un build de release (optimizado); añade --dev para uno más rápido y sin optimizar mientras experimentas.
Llamarlo desde JavaScript
Sección titulada «Llamarlo desde JavaScript»Crea index.html junto a Cargo.toml (para que pkg/ quede al lado):
<!doctype html><html lang="es"><head> <meta charset="utf-8"> <title>Hola desde Rust</title></head><body> <p id="message">Cargando…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // descarga e inicia el archivo .wasm
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>La exportación por defecto, init(), descarga el archivo .wasm y lo inicia. Después, add y greet son funciones normales de JavaScript.
Los navegadores no cargan módulos desde una dirección file://, así que necesitas un pequeño servidor web local. Si tienes Python instalado, esto es lo más rápido:
python3 -m http.server 8080Abre http://localhost:8080 en tu navegador. Cuando lo hicimos, la página mostró:
¡Hola, Ferris! Este texto se construyó en Rust. 2 + 3 = 5Esa frase la construyó Rust, ejecutándose dentro del navegador. 🦀
Qué cruza la frontera barato (y qué no)
Sección titulada «Qué cruza la frontera barato (y qué no)»Rust y JavaScript no comparten la memoria de la misma forma. El módulo Wasm tiene su propio bloque de memoria, y los valores tienen que viajar entre los dos lados. Algunos viajes son gratis; otros cuestan una copia:
| Tipo en Rust | En JavaScript | Coste |
|---|---|---|
i32, u32, f64, bool |
number, boolean |
Barato: se pasa directamente |
&str, String |
string |
Una copia, más convertir de UTF-16 a UTF-8 (o al revés) |
&[u8], Vec<u8> |
Uint8Array |
Una copia hacia la memoria de Wasm (o desde ella) |
&mut [u8] |
Uint8Array |
Se copia al entrar y se vuelve a copiar después de la llamada |
Así que la regla de oro es: haz pocas llamadas, y que cada una haga mucho trabajo. Llamar a Rust una vez para procesar una imagen entera es rápido. Llamar a Rust un millón de veces, una por píxel, gasta casi todo el tiempo en cruzar la frontera.
Así se ve en la práctica. Esta función aclara un búfer entero de valores de píxeles con una sola llamada:
use wasm_bindgen::prelude::*;
/// Hace más brillante cada píxel. JS pasa un Uint8Array.#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}Desde JavaScript, después de await init(), llamarías a brighten(pixels, 20) con un Uint8Array, y el array se actualiza en el sitio. El bucle es Rust puro, así que lo puedes probar en tu máquina:
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add se detiene en 255 en vez de desbordarse *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}Salida:
[30, 120, 255]Fíjate en que 250 + 20 dio 255, no un fallo ni una vuelta a cero: saturating_add se detiene en el máximo. Es justo lo que quieres para los colores.
Mantén pequeño el archivo .wasm
Sección titulada «Mantén pequeño el archivo .wasm»Cada byte de tu archivo .wasm se tiene que descargar antes de que la página lo pueda usar. Algunos consejos:
- Publica siempre el build de release. Para nuestro pequeño ejemplo (con el texto original en inglés),
wasm-pack build --devgeneró un archivo.wasmde 58.591 bytes; el build de release por defecto ocupaba 21.530 bytes. (Son datos de nuestra máquina conwasm-optdesactivado; tus números serán distintos.) - Deja que wasm-pack ejecute
wasm-opt. En modo release, wasm-pack pasa por defecto el optimizadorwasm-optde Binaryen sobre el resultado, lo que normalmente lo reduce todavía más. - Activa la compresión en tu servidor. Nuestro archivo de 21.530 bytes se quedó en 8.464 bytes con gzip.
- Prueba ajustes pensados para el tamaño, y luego mide:
[profile.release]opt-level = "z" # optimiza para tamaño ("s" es una versión más suave)lto = truecodegen-units = 1Sé honesto contigo y mide. En nuestro ejemplo de dos funciones, estos ajustes no ayudaron: el archivo salió de 22.817 bytes, un poco más grande. Suelen compensar en crates más grandes. Cosas como format! y los mensajes de panic también arrastran código, así que un crate que los evita puede ser mucho más pequeño.
Dónde se usa Wasm en el mundo real
Sección titulada «Dónde se usa Wasm en el mundo real»Algunos ejemplos, cada uno con el artículo de la propia empresa (en inglés):
- Figma compila su editor de diseño en C++ a WebAssembly y contó que así redujo el tiempo de carga a un tercio. (No es Rust, pero muestra lo que Wasm puede hacer en una aplicación grande.)
- Cloudflare Workers admite Rust mediante el crate
workers-rs: tu código Rust se compila a Wasm y se ejecuta en los servidores de Cloudflare por todo el mundo. - Shopify Functions son módulos WebAssembly que personalizan tiendas, y Shopify recomienda Rust como el lenguaje con mejor rendimiento para ellas.
- La extensión de navegador de 1Password pasó el rellenado de páginas a WebAssembly y contó que funcionaba al menos el doble de rápido.
Errores comunes
Sección titulada «Errores comunes»Olvidar crate-type = ["cdylib"]. Sin esa línea, wasm-pack se detiene enseguida. Por suerte, el error te dice exactamente qué añadir:
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]El mensaje dice que el crate-type tiene que ser cdylib para compilar a WebAssembly, y te muestra las líneas que debes añadir a Cargo.toml.
Abrir index.html con doble clic. La página se queda en “Cargando…”, y la consola del navegador (en Chrome) dice:
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.Es decir, la política CORS bloqueó el script porque las peticiones entre orígenes solo funcionan con protocolos como http o https, no con file. La solución: sirve la carpeta con un servidor web local, como python3 -m http.server, y abre la dirección http://localhost.
Llamar a una función antes de await init(). El módulo Wasm todavía no está cargado, así que el código de unión no tiene nada a lo que llamar. Cuando quitamos la línea de init(), Chrome mostró Cannot read properties of undefined (reading '__wbindgen_free') (no se pueden leer propiedades de undefined). Haz siempre await init() primero.
Siguientes pasos: aplicaciones web completas en Rust
Sección titulada «Siguientes pasos: aplicaciones web completas en Rust»Llamar a unas cuantas funciones Rust desde JavaScript es un gran comienzo. Si quieres construir toda la aplicación web en Rust, echa un vistazo a estos frameworks. Los dos compilan a Wasm y te dejan escribir componentes en Rust, un poco como en React:
- Leptos (en inglés): un framework rápido y moderno que también puede generar las páginas en el servidor.
- Yew (en inglés): un framework maduro basado en componentes.
Resumen
Sección titulada «Resumen»- WebAssembly es un formato binario rápido y seguro que los navegadores (y los servidores) pueden ejecutar; Rust compila muy bien a él porque no tiene recolector de basura.
- Añade el target con
rustup target add wasm32-unknown-unknowne instalawasm-pack. - Marca las funciones con
#[wasm_bindgen], poncrate-type = ["cdylib", "rlib"]y compila conwasm-pack build --target web. - En JavaScript, primero
await init()y después llama a tus funciones; sirve la página porhttp://, no porfile://. - Los números cruzan la frontera baratos; los strings y los slices se copian, así que haz pocas llamadas y que cada una haga mucho.
- Publica builds de release con compresión, y mide antes de fiarte de los ajustes de tamaño.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué comando añade el target de Rust para WebAssembly en el navegador?
2. ¿Qué hace el atributo #[wasm_bindgen]?
3. Tu página se queda en "Cargando…" y la consola muestra un error de CORS para una dirección file:///. ¿Cómo lo arreglas?
4. ¿Cuál de estos valores es el más barato de pasar de JavaScript a Rust?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Compiling from Rust to WebAssembly — MDN (en inglés): un tutorial paso a paso que crea y ejecuta un proyecto con wasm-pack.
- The wasm-bindgen Guide (en inglés): todos los tipos admitidos, cómo llamar a las APIs del navegador y muchos ejemplos.
- Documentación de wasm-pack (en inglés): todas las opciones de
build, los targets y los perfiles. - wasm32-unknown-unknown — The rustc book (en inglés): qué admite este target y cuáles son sus límites.
- WebAssembly en MDN: qué es Wasm y cómo lo ejecutan los navegadores, sin depender de Rust.