El dilema de los lenguajes de sistemas
Durante 50 años programar sistemas fue elegir entre control inseguro con C/C++ o seguridad con recolector de basura en Java/Go. El dilema que Rust vino a disolver.
Durante medio siglo, programar sistemas fue una elección forzada entre dos males. O tomabas el control absoluto de la memoria con C y C++ y aceptabas que un solo descuido corrompiera el proceso, o delegabas ese control a un recolector de basura con Java o Go y renunciabas a decidir cuándo y dónde vive cada byte. No existía una tercera puerta. Rust es esa tercera puerta, y para entender por qué importa hay que sentir primero el peso del dilema.
- Reconocer el eje que enfrenta control contra seguridad de memoria.
- Ver, en código, por qué C y C++ son inseguros por diseño.
- Comprender el coste real y medible de un recolector de basura.
- Situar el hueco exacto que Rust vino a ocupar.
Un solo compromiso, dos bandos
Todo lenguaje de sistemas responde a una pregunta antes que a ninguna otra: ¿quién gestiona la memoria? La respuesta te asigna un bando, y cada bando cobra su precio.
flowchart LR P[Programa de sistemas] --> Q[Quien gestiona la memoria] Q -->|el programador| C[C y C++: control total pero memoria insegura] Q -->|un runtime con GC| G[Java y Go: seguros pero con recolector] Q -->|el compilador| R[Rust: control total y seguro sin GC] style C fill:#f38ba8,color:#11111b style G fill:#fab387,color:#11111b style R fill:#a6e3a1,color:#11111b
Durante décadas la fila de abajo parecía imposible: se daba por hecho que control y seguridad eran magnitudes inversas. Rust demuestra que la disyuntiva era falsa, pero solo se aprecia su logro tras ver qué duele en cada bando.
El precio del control: C y C++
En C y C++ tú decides cada asignación y cada liberación. Ese poder es real, pero el lenguaje no te protege de ti mismo. Dos clases de bug han costado décadas de vulnerabilidades:
#include <string.h>
char *saludo(void) {
char buf[8]; // 8 bytes en la pila
strcpy(buf, "hola, mundo"); // 12 bytes copiados: desbordamiento de buffer
return buf; // devolvemos un puntero a memoria local...
} // ...que deja de existir aqui: puntero colgante
Este fragmento contiene dos de los fallos más explotados de la historia:
- Desbordamiento de buffer:
strcpyescribe 12 bytes en un arreglo de 8, pisando la pila. Así funcionan innumerables exploits de ejecución remota de código. - Puntero colgante (use-after-free):
bufvive en la pila desaludo; al retornar, esa memoria se reutiliza, y quien lea el puntero devuelto obtendrá basura o datos de otro.
Lo aterrador no es que el código falle, sino que puede que no falle: compila sin errores, quizá funciona en tus pruebas, y revienta en producción con datos reales. C te da un cuchillo sin mango. C++ moderno añade unique_ptr, vector y RAII para mitigarlo, pero nada en el lenguaje impide volver a coger el cuchillo por el filo.
Microsoft y Google han reportado, de forma independiente y sostenida en el tiempo, que alrededor del 70% de sus vulnerabilidades de gravedad alta provienen de errores de gestión de memoria en C y C++: use-after-free, desbordamientos, doble liberación. No es incompetencia de sus ingenieros, que están entre los mejores del mundo: es que el lenguaje hace estos errores demasiado fáciles de cometer y demasiado difíciles de detectar.
El precio de la seguridad: Java y Go
La otra familia resolvió el problema quitándote el volante. Java, Go, C# y compañía introducen un recolector de basura (GC): un subsistema que rastrea qué memoria sigue viva y libera el resto automáticamente. El puntero colgante desaparece, porque nada se libera mientras alguien lo use.
// Go: imposible tener un puntero colgante. El GC vela por la memoria.
func procesar(datos []byte) []byte {
salida := make([]byte, len(datos)) // reservado en el heap gestionado
copy(salida, datos)
return salida // el GC liberara esto cuando nadie lo use
}
El precio se paga en otro sitio, y es doble:
- Pausas impredecibles: el GC debe, cada cierto tiempo, recorrer el grafo de objetos vivos. Aunque los recolectores modernos son concurrentes y sus pausas de “parar el mundo” bajan del milisegundo, siguen introduciendo latencia en la cola (tail latency) justo cuando más tráfico hay. Para un juego a 120 fps, un motor de trading o un marcapasos, ese jitter es inaceptable.
- Sin control fino: no decides cuándo se libera la memoria ni cómo se dispone en caché. Pagas un sobrecoste de espacio (el heap gestionado suele necesitar de 2 a 5 veces la memoria del dato vivo) y renuncias a la localidad de caché que exige el alto rendimiento.
Para la inmensa mayoría del software (un backend CRUD, una app de escritorio, un script) el GC es una bendición: elimina toda una clase de bugs a cambio de un coste que ni notas. El problema aparece en el dominio de los sistemas, donde la latencia predecible y el control de la memoria no son un lujo sino el requisito. Ahí, “no puedes tener control fino” es una respuesta inaceptable.
El hueco que nadie llenaba
Pon las dos columnas frente a frente y el dilema queda desnudo:
| Lenguaje | Control fino | Seguro de memoria | Sin pausas de GC |
|---|---|---|---|
| C / C++ | sí | no | sí |
| Java / Go | no | sí | no |
| Rust | sí | sí | sí |
Rust rechaza la premisa de que haya que elegir. El mismo saludo que en C era una bomba, en Rust ni siquiera llega a ejecutarse:
// El equivalente Rust del saludo() de C. NO COMPILA, y esa es la victoria.
fn saludo() -> &str {
let buf = String::from("hola, mundo");
&buf
}
// error[E0106]: missing lifetime specifier
// error[E0515]: cannot return reference to local variable `buf`
// el valor `buf` se libera al final de la funcion
El compilador detecta, antes de generar un solo byte de máquina, que estás devolviendo una referencia a memoria que va a morir. No hay GC vigilando en ejecución: la garantía se demostró en compilación y desaparece del binario final. La versión correcta es igual de directa y no cuesta nada en tiempo de ejecución:
fn saludo() -> String {
String::from("hola, mundo") // se MUEVE al llamador: sin copia, sin dangling, sin GC
}
Durante cincuenta años, “control contra seguridad” se enseñó como una ley de la naturaleza del diseño de lenguajes, tan firme como una compensación termodinámica. C y C++ ocupaban un extremo; Java, Go y los lenguajes con GC el otro; y el centro se creía vacío por necesidad, no por casualidad. La aportación histórica de Rust no es una sintaxis bonita ni un cargo cómodo: es haber demostrado que la disyuntiva era un artefacto de nuestras herramientas, no de la realidad. Se puede tener el control al ras del metal de C y, a la vez, la garantía de que no habrá use-after-free ni carreras de datos, y todo verificado por el compilador sin coste en ejecución. Ese resultado reordenó las prioridades de toda la industria: por eso Rust entró en el kernel de Linux, en Windows, en Android y en la infraestructura de medio internet. No porque esté de moda, sino porque cerró un hueco que se creía imposible de cerrar. Interiorizar esto es el primer paso de tu cambio de mentalidad: cuando el compilador te frene, no te está quitando poder, te está dando el que C nunca pudo garantizarte.
- Si has programado en C o C++: recuerda el peor bug de memoria que hayas sufrido (un segfault intermitente, una corrupción rara). ¿Cuánto tardaste en encontrarlo? Rust lo habría convertido en un error de compilación.
- Si vienes de un lenguaje con GC: averigua en qué percentil de latencia (p99, p999) se nota tu recolector. Pregúntate qué harías si esa cola de milisegundos fuera inaceptable.
- Dibuja la tabla de las tres garantías de memoria y coloca dónde cae tu lenguaje principal. Guarda ese dibujo: en la próxima lección veremos cómo Rust ocupa la fila de abajo.