Unsafe y punteros raw
El mundo seguro
En código Nyx normal, no puedes corromper la memoria. El recolector de basura gestiona las asignaciones, los límites de arrays se verifican y los punteros nulos no existen. Esta seguridad tiene un pequeño costo — el GC agrega overhead, las verificaciones de límites agregan instrucciones.
A veces necesitas escapar de estas barandillas: escribir un asignador de memoria, interactuar con hardware, o exprimir la última gota de rendimiento de un bucle caliente. Para eso existe unsafe.
El bloque unsafe
Las operaciones unsafe deben estar envueltas en un bloque unsafe:
fn main() { unsafe { let ptr: *int = alloc(8) // asignar 8 bytes *ptr = 42 // escribir en memoria cruda print(*ptr) // leer de memoria cruda: 42 free(ptr) // liberar } }
La palabra clave unsafe es un contrato: "Yo, el programador, garantizo que este código es correcto. El compilador no debería intentar protegerme aquí."
Raw pointers
Un raw pointer es una dirección de memoria. A diferencia de las referencias en código seguro, los raw pointers pueden ser nulos, colgantes o desalineados. Te dan acceso directo a la memoria.
fn main() { var x: int = 100 unsafe { let ptr: *int = &x // obtener dirección de x print(*ptr) // desreferenciar: 100 *ptr = 200 // modificar a través del puntero print(x) // 200 — ¡x cambió! } }
&x— el operador address-of. Devuelve un puntero ax.*ptr— el operador de desreferencia. Lee o escribe el valor en la dirección.
Gestión manual de memoria
En Nyx seguro, el recolector de basura maneja todas las asignaciones. En código unsafe, gestionas la memoria tú mismo:
fn main() { unsafe { // Asignar espacio para 10 enteros (10 * 8 bytes) let arr: *int = alloc(80) // Escribir valores var i: int = 0 while i < 10 { *(arr + i) = i * i // aritmética de punteros i += 1 } // Leer valores i = 0 while i < 10 { print(*(arr + i)) i += 1 } free(arr) } }
Salida:
0 1 4 9 16 25 36 49 64 81
alloc(bytes) asigna memoria cruda (como malloc de C). free(ptr) la libera. Olvidar llamar free causa una fuga de memoria. Llamar free dos veces causa comportamiento indefinido.
Aritmética de punteros
Puedes sumar a punteros para navegar por la memoria:
unsafe { let base: *int = alloc(32) // 4 enteros * 8 bytes cada uno *(base + 0) = 10 *(base + 1) = 20 *(base + 2) = 30 *(base + 3) = 40 // base + N avanza N * sizeof(int) bytes print(*(base + 2)) // 30 free(base) }
Variables estáticas
Las variables estáticas viven durante toda la vida del programa, fuera del recolector de basura:
static var contador_requests: int = 0 fn registrar_request() { contador_requests = contador_requests + 1 print("Request #" + int_to_string(contador_requests)) } fn main() { registrar_request() // Request #1 registrar_request() // Request #2 registrar_request() // Request #3 }
Las variables estáticas son útiles para estado global que necesita ser rápido y libre de GC.
Lecturas y escrituras volátiles
Normalmente, el compilador puede reordenar o eliminar operaciones de memoria si cree que son redundantes. Las operaciones volátiles previenen esto:
unsafe { let ptr: *int = alloc(8) volatile_write(ptr, 42) // escritura garantizada let v: int = volatile_read(ptr) // lectura garantizada free(ptr) }
Volatile es esencial para:
- I/O mapeado en memoria (hablar con hardware)
- Manejadores de señales (variables modificadas por eventos externos)
- Memoria compartida entre procesos
Operaciones atómicas
Cuando múltiples threads acceden a la misma memoria, necesitas operaciones atómicas para prevenir condiciones de carrera sin un mutex:
unsafe { let contador: *int = alloc(8) atomic_store(contador, 0) // Patrón de incremento thread-safe: let actual: int = atomic_load(contador) atomic_store(contador, actual + 1) print(atomic_load(contador)) // 1 free(contador) }
Las operaciones atómicas usan consistencia secuencial — la garantía de ordenamiento de memoria más fuerte. Cada thread ve las operaciones en el mismo orden.
Cuándo usar unsafe
Usa unsafe solo cuando debas:
- FFI — llamar funciones C que toman raw pointers
- Código crítico en rendimiento — evitar el GC en bucles calientes
- Estructuras de datos de bajo nivel — implementar asignadores personalizados o estructuras lock-free
- Interacción con hardware — I/O mapeado en memoria, drivers de dispositivos
Nunca uses unsafe solo porque "se siente más rápido." Mide primero. El GC está altamente optimizado y agrega menos overhead del que la mayoría piensa.
Ejercicios
- Escribe una función unsafe que asigne un array de 100 enteros, los llene con valores 0-99, los sume, libere la memoria y devuelva la suma.
- Crea una variable estática de contador e increméntala desde dos funciones diferentes. Imprime el valor final.
- Escribe una función que intercambie dos enteros usando raw pointers:
fn swap(a: int, b: int).
- Asigna un bloque de memoria, escribe valores usando aritmética de punteros, luego léelos de vuelta en orden inverso.
- Escribe un programa que use
volatile_writeyvolatile_readpara simular un registro de hardware que un manejador de señales podría modificar.
Resumen
unsafe { }habilita operaciones que evitan las verificaciones de seguridad.&xobtiene la dirección de una variable.*ptrdesreferencia un puntero.alloc(bytes)asigna memoria.free(ptr)la libera.- Aritmética de punteros:
*(ptr + offset)accede memoria en un offset. static varcrea variables que viven toda la vida del programa.volatile_read/volatile_writeprevienen reordenamiento del compilador.atomic_load/atomic_storeproporcionan acceso a memoria thread-safe.- Solo usa unsafe cuando las alternativas seguras sean insuficientes. Mide primero.
Siguiente capítulo: Async y event loop →