Índice

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ó!
    }
}

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:

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:

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

  1. 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.
  1. Crea una variable estática de contador e increméntala desde dos funciones diferentes. Imprime el valor final.
  1. Escribe una función que intercambie dos enteros usando raw pointers: fn swap(a: int, b: int).
  1. Asigna un bloque de memoria, escribe valores usando aritmética de punteros, luego léelos de vuelta en orden inverso.
  1. Escribe un programa que use volatile_write y volatile_read para simular un registro de hardware que un manejador de señales podría modificar.

Resumen

Siguiente capítulo: Async y event loop →

← Anterior: FFI — Llamar código C Siguiente: Async y event loop →