gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Definitions

⎇ cb0c5c22a81a940753d632a21ed3d7f101e0f6e3 crates/runtime/arje-wasm/src/lib.rs Rust · 153 lines · 5.9 KB History Who touched what raw

feat(arje-wasm): bump wasmi 0.40 → 1.0 — unifica el runtime WASM con renaser cb0c5c22a · sergio ·

1//! Encarnación de Payload::Wasm vía wasmi.
2//!
3//! Cada Ente Wasm corre en un hilo dedicado (wasmi es síncrono) que se
4//! comunica con el grafo vía un identificador propio. El thread::JoinHandle
5//! se descarta — el ciclo de vida del Wasm se controla por su `entry`
6//! function: cuando retorna, el Ente se considera disuelto.
7//!
8//! ## Host imports expuestos
9//!   - `ente.log(ptr: i32, len: i32)`           imprime una string UTF-8
10//!   - `ente.exit(code: i32)`                   solicita salida del Ente
11//!
12//! Más adelante: `ente.bus_call`, `ente.cap_invoke`, etc.
13
14use arje_card::EntityCard;
15use std::sync::atomic::{AtomicI32, Ordering};
16use std::sync::Arc;
17use tracing::{error, info, warn};
18use wasmi::{Caller, CompilationMode, Config, Engine, Linker, Memory, Module, Store};
19
20/// Estado por instancia Wasm. Se accede tanto desde host imports (vía
21/// `Caller::data()`) como desde el thread runner para estado de salida.
22pub struct WasmEnte {
23    pub id: ulid::Ulid,
24    pub label: String,
25    pub exit_code: Arc<AtomicI32>,
26}
27
28/// Encarna un payload Wasm en un hilo dedicado. Devuelve un identificador
29/// no-PID que el grafo trata como Ente Virtual con cuerpo de cómputo.
30pub fn incarnate_wasm(card: &EntityCard, module_bytes: Vec<u8>, entry: String) -> anyhow::Result<()> {
31    let label = card.label.clone();
32    let id = card.id;
33    let exit_code = Arc::new(AtomicI32::new(0));
34    let exit_code_handle = exit_code.clone();
35
36    std::thread::Builder::new()
37        .name(format!("wasm-{label}"))
38        .spawn(move || {
39            if let Err(e) = run_wasm(WasmEnte { id, label: label.clone(), exit_code: exit_code_handle.clone() }, &module_bytes, &entry) {
40                error!(?e, %label, "Wasm ente terminó con error");
41                exit_code_handle.store(-1, Ordering::Relaxed);
42            }
43        })?;
44    Ok(())
45}
46
47fn run_wasm(ente: WasmEnte, module_bytes: &[u8], entry: &str) -> anyhow::Result<()> {
48    // Compilación ansiosa (Eager): el módulo se traduce entero ahora, no
49    // perezosamente bajo demanda. Da un comportamiento predecible y
50    // paridad con el motor wasmi del kernel de renaser — ambos en
51    // wasmi 1.0, mismo ABI de host en Linux y en bare-metal.
52    let mut config = Config::default();
53    config.compilation_mode(CompilationMode::Eager);
54    let engine = Engine::new(&config);
55    let module = Module::new(&engine, module_bytes)
56        .map_err(|e| anyhow::anyhow!("Wasm module compile: {e}"))?;
57    let mut store = Store::new(&engine, ente);
58    let mut linker = <Linker<WasmEnte>>::new(&engine);
59
60    linker.func_wrap("ente", "log", |caller: Caller<'_, WasmEnte>, ptr: i32, len: i32| {
61        host_log(caller, ptr, len);
62    })?;
63
64    linker.func_wrap("ente", "exit", |mut caller: Caller<'_, WasmEnte>, code: i32| {
65        caller.data_mut().exit_code.store(code, Ordering::Relaxed);
66    })?;
67
68    // wasmi 1.0 fusiona instanciación y arranque: `instantiate_and_start`
69    // instancia el módulo y ejecuta su sección `(start)` si la tuviera
70    // (este módulo no la tiene — su `_start` es un export convencional
71    // que el caller invoca explícitamente más abajo).
72    let instance = linker
73        .instantiate_and_start(&mut store, &module)
74        .map_err(|e| anyhow::anyhow!("Wasm instantiate: {e}"))?;
75
76    let func = instance.get_typed_func::<(), ()>(&store, entry)
77        .map_err(|e| anyhow::anyhow!("Wasm get_func {entry}: {e}"))?;
78
79    info!(label = %store.data().label, %entry, "Wasm ente ejecutando");
80    func.call(&mut store, ()).map_err(|e| anyhow::anyhow!("Wasm call {entry}: {e}"))?;
81    let code = store.data().exit_code.load(Ordering::Relaxed);
82    info!(label = %store.data().label, code, "Wasm ente terminó");
83    Ok(())
84}
85
86fn host_log(caller: Caller<'_, WasmEnte>, ptr: i32, len: i32) {
87    let memory = match caller.get_export("memory").and_then(|e| e.into_memory()) {
88        Some(m) => m,
89        None => {
90            warn!("Wasm ente sin memoria exportada — log ignorado");
91            return;
92        }
93    };
94    let data = read_memory(&caller, memory, ptr, len);
95    match std::str::from_utf8(&data) {
96        Ok(s) => info!(label = %caller.data().label, "[wasm] {s}"),
97        Err(_) => warn!(label = %caller.data().label, "Wasm log con bytes no UTF-8"),
98    }
99}
100
101fn read_memory(caller: &Caller<'_, WasmEnte>, memory: Memory, ptr: i32, len: i32) -> Vec<u8> {
102    let ptr = ptr.max(0) as usize;
103    let len = len.max(0) as usize;
104    let data = memory.data(caller);
105    if ptr.saturating_add(len) > data.len() {
106        return Vec::new();
107    }
108    data[ptr..ptr + len].to_vec()
109}
110
111/// Módulo WAT mínimo de demostración. Llama a `ente.log` con "hola fractal".
112/// Compilado a binario Wasm en runtime con `wat`.
113pub fn demo_module_bytes() -> anyhow::Result<Vec<u8>> {
114    let wat = r#"
115(module
116  (import "ente" "log"  (func $log  (param i32 i32)))
117  (import "ente" "exit" (func $exit (param i32)))
118  (memory (export "memory") 1)
119  (data (i32.const 0) "hola fractal desde wasm")
120  (func (export "_start")
121    (call $log (i32.const 0) (i32.const 23))
122    (call $exit (i32.const 0))
123  )
124)
125"#;
126    Ok(wat::parse_str(wat)?)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    /// Verifica el runtime de punta a punta sobre wasmi 1.0: WAT → wasm,
134    /// motor Eager, `Module::new`, `Linker` + `func_wrap`,
135    /// `instantiate_and_start`, llamada a `_start`, y los host imports
136    /// `ente.log` / `ente.exit`. Que compile no basta — debe ejecutar.
137    #[test]
138    fn demo_corre_en_wasmi_1() {
139        let bytes = demo_module_bytes().expect("el WAT del demo compila a wasm");
140        let exit_code = Arc::new(AtomicI32::new(-99));
141        let ente = WasmEnte {
142            id: ulid::Ulid::new(),
143            label: "test".into(),
144            exit_code: exit_code.clone(),
145        };
146        run_wasm(ente, &bytes, "_start").expect("el módulo demo ejecuta sin error");
147        assert_eq!(
148            exit_code.load(Ordering::Relaxed),
149            0,
150            "el host import `ente.exit(0)` fijó el código de salida"
151        );
152    }
153}