feat(renaser): Fase 7a — el userspace nace del Grafo de Objetos

El kernel deja de empotrar las apps. Las cinco aplicaciones ya no
llegan por include_bytes! en main.rs: nacen del grafo, gobernadas por
un Manifiesto de Génesis que también vive en el grafo.

- almacen: el SuperBloque gana el ancla `manifiesto: Option<Hash>`
  (gemela de `raiz`, del lado del kernel) + accesores. VERSION 1→2 —
  un disco v1 se reformatea.
- manifiesto.rs: implementados `cargar` (lee el manifiesto del grafo)
  y `sembrar_genesis` (puebla un disco virgen con las 5 apps de
  génesis). El bytecode viaja empotrado AÚN, sólo como semilla
  transitoria (la Fase 7b lo mueve al constructor de imagen `boot`).
- kernel_main: `cargar_userspace` reemplaza las 5 `encender_app`
  escritas a mano; `encender_app` recupera el bytecode del grafo —
  `recuperar` verifica el hash, un módulo corrupto se niega y el
  arranque sigue.
- wasm: el techo de memoria pasa a ser por-app (del manifiesto).

Compila limpio. Verificación en QEMU pendiente (la corre el operador):
la pantalla debe verse idéntica a la Fase 6.2 + la línea «manifiesto».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 15:11:47 +00:00
parent 4f31146533
commit bce4abd8cc
6 changed files with 302 additions and 100 deletions
+32 -7
View File
@@ -33,7 +33,9 @@ use crate::drivers::disco::{self, TAM_SECTOR};
const MAGIA: [u8; 8] = *b"RENASGRF";
/// Version del formato en disco. Un disco con otra version se reformatea.
const VERSION: u32 = 1;
/// v2 (Fase 7) — el superbloque gana el ancla `manifiesto`; un disco v1 se
/// reformatea al arrancar, como cualquier disco ajeno.
const VERSION: u32 = 2;
/// Techo del tamaño de un objeto serializado: 1 MiB. Acota los buferes de E/S
/// y permite descartar un registro corrupto sin intentar leer un disparate.
@@ -56,7 +58,7 @@ pub struct Objeto {
}
/// El superbloque: el sector 0 del disco. Ancla el grafo entero — dice por
/// donde continua el log y cual es el objeto raiz.
/// donde continua el log, cual es el objeto raiz y cual el manifiesto.
#[derive(Serialize, Deserialize)]
struct SuperBloque {
/// Firma magica: debe ser [`MAGIA`].
@@ -67,15 +69,20 @@ struct SuperBloque {
cursor: u64,
/// El objeto raiz del DAG: el punto de entrada que el userspace fija y lee.
raiz: Option<Hash>,
/// El Manifiesto de Genesis (Fase 7): el objeto que dicta que apps nacen
/// del grafo al arrancar. Ancla del kernel, gemela de `raiz` (del userspace).
manifiesto: Option<Hash>,
}
/// El estado vivo del almacen: el cursor del log, la raiz y el indice en
/// memoria que traduce cada hash al sector donde habita su registro.
/// El estado vivo del almacen: el cursor del log, la raiz, el manifiesto y el
/// indice en memoria que traduce cada hash al sector donde habita su registro.
struct Almacen {
/// Proximo sector libre del log.
cursor: u64,
/// El objeto raiz del DAG.
raiz: Option<Hash>,
/// El objeto del Manifiesto de Genesis (Fase 7).
manifiesto: Option<Hash>,
/// Indice hash -> sector del registro. Se reconstruye al arrancar.
indice: BTreeMap<Hash, u64>,
/// Capacidad del disco, en sectores.
@@ -116,16 +123,16 @@ pub fn init() -> Result<Resumen, &'static str> {
let mut sector0 = [0u8; TAM_SECTOR];
disco::leer_sectores(0, &mut sector0)?;
let (cursor, raiz, indice, formateado) =
let (cursor, raiz, manifiesto, indice, formateado) =
match postcard::take_from_bytes::<SuperBloque>(&sector0) {
// Disco de renaser, con la version corriente: adoptar su grafo.
Ok((sb, _)) if sb.magia == MAGIA && sb.version == VERSION => {
let indice = reconstruir_indice(sb.cursor)?;
(sb.cursor, sb.raiz, indice, false)
(sb.cursor, sb.raiz, sb.manifiesto, indice, false)
}
// Disco virgen, ajeno o de otra version: empezar de cero. El log
// arranca en el sector 1, justo despues del superbloque.
_ => (1, None, BTreeMap::new(), true),
_ => (1, None, None, BTreeMap::new(), true),
};
let objetos = indice.len();
@@ -133,6 +140,7 @@ pub fn init() -> Result<Resumen, &'static str> {
let almacen = Almacen {
cursor,
raiz,
manifiesto,
indice,
capacidad,
};
@@ -204,6 +212,7 @@ fn persistir(almacen: &Almacen) -> Result<(), &'static str> {
version: VERSION,
cursor: almacen.cursor,
raiz: almacen.raiz,
manifiesto: almacen.manifiesto,
};
let bytes = postcard::to_allocvec(&sb).map_err(|_| "no se pudo serializar el superbloque")?;
if bytes.len() > TAM_SECTOR {
@@ -290,3 +299,19 @@ pub fn fijar_raiz(hash: Hash) -> Result<(), &'static str> {
almacen.raiz = Some(hash);
persistir(&almacen)
}
/// El hash del objeto del Manifiesto de Genesis, si el disco tiene uno
/// anclado. Gemelo de [`raiz`], pero del lado del kernel: lo lee la Fase 7
/// para descubrir que apps poblar al arrancar.
pub fn manifiesto() -> Option<Hash> {
ALMACEN.get().and_then(|mutex| mutex.lock().manifiesto)
}
/// Ancla un objeto como el Manifiesto de Genesis y graba el cambio en el
/// superbloque. Gemelo de [`fijar_raiz`].
pub fn fijar_manifiesto(hash: Hash) -> Result<(), &'static str> {
let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
let mut almacen = mutex.lock();
almacen.manifiesto = Some(hash);
persistir(&almacen)
}
+78 -69
View File
@@ -66,33 +66,9 @@ use async_system::executor::Executor;
use baliza::BALIZA_PANICO;
use consola::{Consola, CONSOLA};
use grafico::{
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX,
ANCHO_MAX,
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, ALTO_MAX, ANCHO_MAX,
};
/// El modulo WASM del userspace, empotrado en el binario del kernel para esta
/// fase de pruebas. Es un `.wasm` puro, compilado aparte para `wasm32`. La
/// Fase 5 lo instancia DOS veces —el mismo bytecode, dos regiones distintas—
/// para demostrar la multitarea cooperativa sobre el espacio unico.
static APP_WASM: &[u8] = include_bytes!("../assets/app.wasm");
/// La aplicacion DISCOLA: un modulo WASM cuyo `tick` cae en un bucle cerrado y
/// jamas retorna. Existe para una sola cosa — demostrar que el guardarrail de
/// combustible la fulmina sin despeinar al kernel ni a sus vecinas.
static DISCOLA_WASM: &[u8] = include_bytes!("../assets/discola.wasm");
/// La aplicacion GLOTONA: un modulo WASM que reclama memoria lineal sin freno.
/// Demuestra el guardarrail ESPACIAL — el techo de memoria la desaloja con la
/// baliza amarilla, gemela de la purpura del desalojo por combustible.
static GLOTONA_WASM: &[u8] = include_bytes!("../assets/glotona.wasm");
/// La aplicacion CRONISTA: la primera ciudadana del userspace que escribe en el
/// almacenamiento PERSISTENTE. En cada arranque graba un objeto en el grafo
/// —enlazado al del arranque anterior—, lo corona como raiz y pinta una celda
/// por cada arranque registrado. El disco recuerda; la cuenta sobrevive a los
/// reinicios. Demuestra las capacidades `sys_object_*` de la Fase 6.1c.
static CRONISTA_WASM: &[u8] = include_bytes!("../assets/cronista.wasm");
/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU.
static CONFIG_ARRANQUE: BootloaderConfig = {
let mut config = BootloaderConfig::new_default();
@@ -152,16 +128,80 @@ async fn tarea_sonda_disco() {
consola.presentar();
}
/// Da vida a una aplicacion del userspace: la carga en su region y, si lo
/// logra, la despacha como tarea cooperativa del reactor. Una carga fallida se
/// salda pintando su region con la baliza de desalojo — el kernel no se inmuta.
fn encender_app(ejecutor: &mut Executor, bytecode: &'static [u8], region: RegionPantalla) {
match wasm::AplicacionWasm::cargar(bytecode, region) {
/// Da vida a una aplicacion del userspace a partir de su `EntradaApp` del
/// manifiesto: recupera su bytecode del grafo, la carga en su region y la
/// despacha como tarea cooperativa del reactor. Si el bytecode falta, esta
/// corrupto, o la carga fracasa, se salda pintando la region de la app con
/// la baliza de desalojo — el kernel no se inmuta y sigue con las demas.
fn encender_app(ejecutor: &mut Executor, entrada: &manifiesto::EntradaApp) {
let region = entrada.region();
// Recuperar el bytecode del grafo. `recuperar` recomputa el hash del
// objeto y verifica su integridad: un bytecode corrupto se delata aqui
// —y la app se niega, no se instancia un modulo en el que no se confia.
let bytecode = match almacen::recuperar(&entrada.bytecode) {
Ok(Some(objeto)) => objeto.datos,
_ => {
consola::pintar_desalojo(region, Color::DESALOJO);
return;
}
};
match wasm::AplicacionWasm::cargar(&bytecode, region, entrada.techo_memoria as usize) {
Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
Err(_) => consola::pintar_desalojo(region, Color::DESALOJO),
}
}
/// Escribe una linea en la consola global y la presenta. Atajo para los
/// informes de arranque; no hace nada si la consola aun no existe.
fn reportar(linea: &str) {
if let Some(consola) = CONSOLA.get() {
let mut consola = consola.lock();
consola.escribir(linea);
consola.escribir("\n");
consola.presentar();
}
}
/// FASE 7 :: puebla el userspace DESDE EL GRAFO. Carga el Manifiesto de
/// Genesis; si el disco no tiene uno —disco virgen—, lo siembra y lo vuelve a
/// cargar. Por cada `EntradaApp`, enciende su aplicacion. Toda falla se
/// reporta a la consola y NO detiene el arranque: el kernel se levanta con
/// las apps que pueda — o con ninguna, si el grafo no tiene userspace.
fn cargar_userspace(ejecutor: &mut Executor) {
let manifiesto = match manifiesto::cargar() {
Ok(Some(m)) => Some(m),
// Disco sin manifiesto: sembrar la genesis y volver a cargarlo.
Ok(None) => match manifiesto::sembrar_genesis() {
Ok(_) => {
reportar("manifiesto :: genesis sembrada en disco virgen");
manifiesto::cargar().ok().flatten()
}
Err(motivo) => {
reportar(&format!("manifiesto :: siembra fallida -- {motivo}"));
None
}
},
Err(motivo) => {
reportar(&format!("manifiesto :: carga fallida -- {motivo}"));
None
}
};
match &manifiesto {
Some(m) => reportar(&format!(
"manifiesto :: {} apps nacidas del grafo",
m.apps.len(),
)),
None => reportar("manifiesto :: sin userspace -- el kernel se levanta solo"),
}
if let Some(m) = manifiesto {
for entrada in &m.apps {
encender_app(ejecutor, entrada);
}
}
}
/// Localiza la mayor region de RAM libre que el cargador reporto — la cantera
/// de la que el DMA del disco tomara sus marcos fisicos.
fn mayor_region_usable(regiones: &MemoryRegions) -> Option<(u64, u64)> {
@@ -291,50 +331,19 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
}
}
// --- 7. FASE 5/6 :: levantar el reactor y poblar el userspace con CINCO
// aplicaciones WASM concurrentes, cada una en su propia region:
//
// * App 1 — instancia de hello_wasm, a la izquierda, gobernada
// por el teclado.
// * App 2 — segunda instancia del MISMO bytecode, a la derecha:
// un unico modulo en la RAM unificada, dos vidas aisladas.
// * App discola — su `tick` es un bucle cerrado: el escudo de
// COMBUSTIBLE la desaloja (baliza purpura) en su 1er fotograma.
// * App glotona — reclama memoria sin freno: el escudo ESPACIAL
// la desaloja (baliza amarilla) en su 1er fotograma.
// * App cronista — escribe en el GRAFO DE OBJETOS persistente:
// cada arranque deja un objeto enlazado al anterior y pinta una
// celda por arranque. El disco recuerda entre reinicios.
// --- 7. FASE 7 :: levantar el reactor y poblar el userspace DESDE EL
// GRAFO. El kernel ya no empotra los modulos WASM: lee el
// Manifiesto de Genesis —si el disco esta virgen, lo siembra— e
// instancia cada `EntradaApp` recuperando su bytecode del grafo de
// objetos. Las cinco apps de genesis (dos instancias de hello, la
// discola, la glotona y la cronista) nacen ahora del disco, no del
// binario del kernel.
//
// Las interrupciones se habilitan AHORA: el temporizador marcara el
// compas de los fotogramas y la IRQ del teclado difundira cada
// scancode a los canales que las apps consultan. ---
let mut ejecutor = Executor::nuevo();
encender_app(
&mut ejecutor,
APP_WASM,
RegionPantalla { x: 100, y: 120, ancho: 480, alto: 560 },
);
encender_app(
&mut ejecutor,
APP_WASM,
RegionPantalla { x: 700, y: 120, ancho: 480, alto: 560 },
);
encender_app(
&mut ejecutor,
DISCOLA_WASM,
RegionPantalla { x: 60, y: 700, ancho: 360, alto: 80 },
);
encender_app(
&mut ejecutor,
GLOTONA_WASM,
RegionPantalla { x: 460, y: 700, ancho: 360, alto: 80 },
);
encender_app(
&mut ejecutor,
CRONISTA_WASM,
RegionPantalla { x: 860, y: 700, ancho: 360, alto: 80 },
);
cargar_userspace(&mut ejecutor);
// FASE 6.2 :: una tarea mas del reactor — no una app WASM— que sondea el
// disco de forma ASINCRONA: la demostracion de que la IRQ del disco
// conduce la E/S sin detener a las aplicaciones visuales.
+115 -21
View File
@@ -11,16 +11,13 @@
// del manifiesto, lo deserializa, y por cada `EntradaApp` recupera el objeto
// de bytecode —verificado por su hash— y lo inyecta en `wasmi`.
//
// ESTADO: andamiaje de la Fase 7a. Los tipos y la (de)serializacion estan
// completos; `cargar` y `sembrar_genesis` son esbozos — se implementan al
// abordar la 7a, cuando el superbloque gane su campo `manifiesto`. Ver
// `FASE7.md` para el plan completo.
// ESTADO: Fase 7a. Tipos, (de)serializacion, carga desde el grafo y siembra
// de la genesis, implementados. La siembra es TRANSITORIA — el bytecode aun
// viaja empotrado (`include_bytes!`, abajo); la Fase 7b lo movera al
// constructor de imagen `boot` y el kernel dejara de empotrar una sola app.
// Ver `FASE7.md` para el plan completo.
// =============================================================================
// Fase 7a en construccion: el modulo aun no se cablea a `kernel_main`. El
// `allow` cae en cuanto `cargar`/`sembrar_genesis` tengan llamador real.
#![allow(dead_code)]
use alloc::string::String;
use alloc::vec::Vec;
@@ -104,21 +101,118 @@ impl Manifiesto {
/// Lee el manifiesto del grafo: toma su hash del ancla del superbloque,
/// recupera el objeto y lo deserializa. `Ok(None)` si el disco aun no tiene
/// manifiesto anclado — el caller debe entonces sembrar la genesis.
///
/// ANDAMIAJE (Fase 7a-4): depende de `almacen::manifiesto()` —el nuevo ancla
/// del superbloque— todavia por implementar (tarea 7a-2).
pub fn cargar() -> Result<Option<Manifiesto>, &'static str> {
todo!("Fase 7a-4: leer almacen::manifiesto(), recuperar el objeto y deserializar")
let hash = match crate::almacen::manifiesto() {
Some(hash) => hash,
None => return Ok(None),
};
// `recuperar` recomputa el hash del objeto y verifica su integridad: un
// manifiesto corrupto se delata aqui.
let objeto = crate::almacen::recuperar(&hash)?
.ok_or("manifiesto :: el objeto anclado no existe en el grafo")?;
let manifiesto = Manifiesto::deserializar(&objeto.datos)?;
Ok(Some(manifiesto))
}
/// Siembra el grafo en un disco sin manifiesto: graba el bytecode de las
/// aplicaciones de genesis, compone un `Manifiesto` por defecto con sus
/// regiones y cuotas, lo graba y lo ancla en el superbloque. Devuelve el
/// hash del manifiesto recien anclado.
///
/// ANDAMIAJE (Fase 7a-3): la semilla TRANSITORIA — en la 7a el bytecode aun
/// llega vacia `include_bytes!`; la 7b mueve la siembra al constructor de
/// imagen `boot` y elimina el empotrado del kernel.
// =============================================================================
// La genesis — la semilla transitoria de la Fase 7a
// -----------------------------------------------------------------------------
// El bytecode de las apps de genesis viaja, POR AHORA, empotrado en el kernel.
// Es el unico `include_bytes!` que sobrevive a la Fase 7a — y solo como
// semilla: en un disco virgen, `sembrar_genesis` lo graba en el grafo una vez.
// La Fase 7b lo movera al constructor de imagen `boot` y este bloque morira.
// =============================================================================
static APP_WASM: &[u8] = include_bytes!("../assets/app.wasm");
static DISCOLA_WASM: &[u8] = include_bytes!("../assets/discola.wasm");
static GLOTONA_WASM: &[u8] = include_bytes!("../assets/glotona.wasm");
static CRONISTA_WASM: &[u8] = include_bytes!("../assets/cronista.wasm");
/// Descriptor de una app de genesis: lo que el kernel sabe de ella ANTES de
/// que exista en el grafo. `region` es `(x, y, ancho, alto)` en pixeles.
struct AppGenesis {
nombre: &'static str,
bytecode: &'static [u8],
region: (u32, u32, u32, u32),
techo_memoria: u32,
}
/// El userspace de genesis: las cinco aplicaciones que pueblan un disco
/// virgen, con las regiones de la Fase 6.2. `app.wasm` aparece dos veces
/// —dos instancias del mismo bytecode—; el grafo, direccionado por contenido,
/// lo guarda una sola vez.
fn genesis() -> [AppGenesis; 5] {
let techo = crate::wasm::TECHO_MEMORIA as u32;
[
AppGenesis {
nombre: "hola-izq",
bytecode: APP_WASM,
region: (100, 120, 480, 560),
techo_memoria: techo,
},
AppGenesis {
nombre: "hola-der",
bytecode: APP_WASM,
region: (700, 120, 480, 560),
techo_memoria: techo,
},
AppGenesis {
nombre: "discola",
bytecode: DISCOLA_WASM,
region: (60, 700, 360, 80),
techo_memoria: techo,
},
AppGenesis {
nombre: "glotona",
bytecode: GLOTONA_WASM,
region: (460, 700, 360, 80),
techo_memoria: techo,
},
AppGenesis {
nombre: "cronista",
bytecode: CRONISTA_WASM,
region: (860, 700, 360, 80),
techo_memoria: techo,
},
]
}
/// Siembra el grafo en un disco sin manifiesto: graba el bytecode de cada app
/// de genesis como un objeto, compone un `Manifiesto` con sus regiones y
/// cuotas, lo graba —con las aristas hacia los objetos de bytecode— y lo
/// ancla en el superbloque. Devuelve el hash del manifiesto recien anclado.
pub fn sembrar_genesis() -> Result<Hash, &'static str> {
todo!("Fase 7a-3: grabar los bytecodes de genesis + el manifiesto por defecto, y anclarlo")
let mut apps: Vec<EntradaApp> = Vec::new();
let mut hijos: Vec<Hash> = Vec::new();
for app in genesis() {
// Grabar el bytecode como objeto del grafo. Idempotente: dos
// instancias de la misma app comparten un unico objeto.
let bytecode = crate::almacen::almacenar(app.bytecode.to_vec(), Vec::new())?;
if !hijos.contains(&bytecode) {
hijos.push(bytecode);
}
let (x, y, ancho, alto) = app.region;
apps.push(EntradaApp {
nombre: String::from(app.nombre),
bytecode,
region_x: x,
region_y: y,
region_ancho: ancho,
region_alto: alto,
techo_memoria: app.techo_memoria,
estado: None,
});
}
// El objeto del manifiesto: sus `hijos` son los objetos de bytecode, de
// modo que el grafo lo lea como el nodo padre del userspace.
let manifiesto = Manifiesto {
version: VERSION_MANIFIESTO,
apps,
};
let bytes = manifiesto.serializar()?;
let hash = crate::almacen::almacenar(bytes, hijos)?;
crate::almacen::fijar_manifiesto(hash)?;
Ok(hash)
}
+14 -3
View File
@@ -35,7 +35,11 @@ const FUEL_FOTOGRAMA: u64 = 2_000_000;
/// Techo de memoria lineal por aplicacion: 4 MiB. Un modulo que intente crecer
/// su memoria mas alla es desalojado — el aislamiento ESPACIAL del userspace,
/// gemelo del techo TEMPORAL que impone el combustible.
const TECHO_MEMORIA: usize = 4 * 1024 * 1024;
///
/// Desde la Fase 7 el techo es POR-APP: cada `EntradaApp` del manifiesto
/// lleva el suyo. Esta constante es el valor por DEFECTO — el que usan las
/// apps de genesis (ver `manifiesto::genesis`).
pub(crate) const TECHO_MEMORIA: usize = 4 * 1024 * 1024;
/// Por que el kernel da por terminada —desaloja— una aplicacion WASM.
#[derive(Clone, Copy)]
@@ -88,7 +92,14 @@ impl AplicacionWasm {
/// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
/// el reactor en cada pulso del reloj.
pub fn cargar(bytecode: &[u8], region: RegionPantalla) -> Result<AplicacionWasm, FallaApp> {
///
/// `techo_memoria` es la cuota de memoria lineal de ESTA app, en bytes —
/// desde la Fase 7 la dicta su `EntradaApp` del manifiesto.
pub fn cargar(
bytecode: &[u8],
region: RegionPantalla,
techo_memoria: usize,
) -> Result<AplicacionWasm, FallaApp> {
// 1. El motor, con metricas de combustible y compilacion ANTICIPADA: la
// traduccion del modulo ocurre ahora, de modo que el `fuel` mida
// despues solo EJECUCION, jamas compilacion diferida.
@@ -106,7 +117,7 @@ impl AplicacionWasm {
// ya con la app cargada: una carga fallida no deja canales huerfanos.
let canal = crate::async_system::teclado::crear_canal();
let limites = StoreLimitsBuilder::new()
.memory_size(TECHO_MEMORIA)
.memory_size(techo_memoria)
// Una expansion denegada se convierte en TRAMPA, no en un -1 que la
// app pudiera ignorar: asi el kernel la captura y la desaloja.
.trap_on_grow_failure(true)