gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-wasm-wasi/src/lib.rs Rust · 593 siq'ikuna · 21.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! llimphi-wasm-wasi — corre apps **WASI de consola** sobre wasmi.
2//!
3//! El catálogo no es trampa: además de las apps Tier 3 (UI, ABI propio
4//! `wasm_view → WireNode`), el grueso del WASM público son **programas de línea
5//! de comandos** compilados a `wasm32-wasi`: importan `wasi_snapshot_preview1`,
6//! tienen un `_start` y escriben a stdout. Este crate los ejecuta: un shim
7//! mínimo de WASI preview1 instancia el módulo, captura su stdout/stderr y
8//! devuelve la salida + el código de salida. No es una GUI — es una consola,
9//! que es exactamente lo que esos módulos son.
10//!
11//! Alcance honesto: se cubre el subconjunto que usa una herramienta CLI típica
12//! (escribir, leer stdin, args, env, reloj, random, salir). Lo que toca
13//! **filesystem o sockets reales** no está (esos imports no se enlazan: el
14//! módulo que los use trap-ea al instanciar — la misma frontera física que el
15//! resto del runtime). El component-model y las apps web (wasm-bindgen/DOM) son
16//! otra historia: necesitan otro runtime / un navegador (eso es `puriy`).
17
18use wasmi::{Caller, Engine, Linker, Module, Store};
19
20/// Qué clase de módulo WASM es, mirando sus exports.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum WasmKind {
23    /// App Tier 3 de Llimphi: exporta `wasm_view` (UI sobre el SDK).
24    Tier3,
25    /// Programa WASI de consola: exporta `_start`.
26    WasiConsole,
27    /// Ni una ni otra (no sabemos correrlo).
28    Unknown,
29}
30
31/// Clasifica un `.wasm` por sus exports. Tier 3 manda (si exporta `wasm_view`
32/// es una app de UI aunque también tuviera `_start`).
33pub fn detect_kind(wasm: &[u8]) -> WasmKind {
34    let engine = Engine::default();
35    let module = match Module::new(&engine, wasm) {
36        Ok(m) => m,
37        Err(_) => return WasmKind::Unknown,
38    };
39    let mut tiene_view = false;
40    let mut tiene_start = false;
41    for exp in module.exports() {
42        match exp.name() {
43            "wasm_view" => tiene_view = true,
44            "_start" => tiene_start = true,
45            _ => {}
46        }
47    }
48    if tiene_view {
49        WasmKind::Tier3
50    } else if tiene_start {
51        WasmKind::WasiConsole
52    } else {
53        WasmKind::Unknown
54    }
55}
56
57/// La salida de correr un programa de consola.
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct ConsoleOutput {
60    pub stdout: Vec<u8>,
61    pub stderr: Vec<u8>,
62    /// Código de salida (de `proc_exit`); `0` si el `_start` retornó normalmente.
63    pub exit_code: i32,
64}
65
66impl ConsoleOutput {
67    /// stdout como texto (lossy — no asumimos UTF-8 perfecto).
68    pub fn stdout_text(&self) -> std::borrow::Cow<'_, str> {
69        String::from_utf8_lossy(&self.stdout)
70    }
71    /// stderr como texto (lossy).
72    pub fn stderr_text(&self) -> std::borrow::Cow<'_, str> {
73        String::from_utf8_lossy(&self.stderr)
74    }
75}
76
77/// Estado del proceso WASI que vive en el `Store` de wasmi.
78struct WasiState {
79    stdout: Vec<u8>,
80    stderr: Vec<u8>,
81    /// argv como bytes terminados en NUL (incluye argv[0]).
82    args: Vec<Vec<u8>>,
83    /// environ como `CLAVE=valor` en bytes.
84    env: Vec<Vec<u8>>,
85    /// stdin disponible para `fd_read`.
86    stdin: Vec<u8>,
87    stdin_pos: usize,
88    /// Fijado por `proc_exit`; corta la ejecución vía trap.
89    exit_code: Option<i32>,
90    /// Semilla determinista para `random_get` (tests reproducibles).
91    rng: u64,
92}
93
94// errno de WASI preview1 (los pocos que usamos).
95const ERRNO_SUCCESS: i32 = 0;
96const ERRNO_BADF: i32 = 8;
97const ERRNO_INVAL: i32 = 28;
98
99/// Mensaje del trap que usamos para señalar `proc_exit` (no es un error real).
100const EXIT_TRAP: &str = "wasi:proc_exit";
101
102/// Corre un módulo WASI de consola: instancia, llama `_start`, captura
103/// stdout/stderr. `args` (sin contar argv[0], que se sintetiza) y `env`
104/// (`("CLAVE","valor")`) y `stdin` alimentan al programa.
105pub fn run_console(
106    wasm: &[u8],
107    args: &[String],
108    env: &[(String, String)],
109    stdin: &[u8],
110) -> Result<ConsoleOutput, String> {
111    let engine = Engine::default();
112    let module = Module::new(&engine, wasm).map_err(|e| format!("compilar wasm: {e}"))?;
113
114    let mut argv: Vec<Vec<u8>> = vec![b"app\0".to_vec()];
115    for a in args {
116        let mut v = a.clone().into_bytes();
117        v.push(0);
118        argv.push(v);
119    }
120    let environ: Vec<Vec<u8>> = env
121        .iter()
122        .map(|(k, val)| {
123            let mut v = format!("{k}={val}").into_bytes();
124            v.push(0);
125            v
126        })
127        .collect();
128
129    let state = WasiState {
130        stdout: Vec::new(),
131        stderr: Vec::new(),
132        args: argv,
133        env: environ,
134        stdin: stdin.to_vec(),
135        stdin_pos: 0,
136        exit_code: None,
137        rng: 0x9E37_79B9_7F4A_7C15,
138    };
139    let mut store = Store::new(&engine, state);
140    let mut linker = Linker::<WasiState>::new(&engine);
141    link_wasi(&mut linker)?;
142
143    let instance = linker
144        .instantiate_and_start(&mut store, &module)
145        .map_err(|e| format!("instanciar: {e}"))?;
146    let start = instance
147        .get_typed_func::<(), ()>(&store, "_start")
148        .map_err(|e| format!("export `_start`: {e}"))?;
149
150    // `_start` puede terminar por retorno normal o por el trap de `proc_exit`.
151    let exit_code = match start.call(&mut store, ()) {
152        Ok(()) => store.data().exit_code.unwrap_or(0),
153        Err(e) => {
154            // Si fue nuestro proc_exit, no es un error: el código está guardado.
155            if let Some(code) = store.data().exit_code {
156                code
157            } else {
158                let st = store.data();
159                return Err(format!(
160                    "trap en _start: {e} (stdout capturado: {})",
161                    String::from_utf8_lossy(&st.stdout)
162                ));
163            }
164        }
165    };
166
167    let st = store.data();
168    Ok(ConsoleOutput {
169        stdout: st.stdout.clone(),
170        stderr: st.stderr.clone(),
171        exit_code,
172    })
173}
174
175/// Lee `len` bytes de la memoria del guest en un `Vec`.
176fn mem_read(caller: &Caller<'_, WasiState>, ptr: i32, len: i32) -> Option<Vec<u8>> {
177    let mem = caller.get_export("memory")?.into_memory()?;
178    let mut buf = vec![0u8; len.max(0) as usize];
179    mem.read(caller, ptr.max(0) as usize, &mut buf).ok()?;
180    Some(buf)
181}
182
183/// Escribe `bytes` en la memoria del guest en `ptr`.
184fn mem_write(caller: &mut Caller<'_, WasiState>, ptr: i32, bytes: &[u8]) -> bool {
185    match caller.get_export("memory").and_then(|e| e.into_memory()) {
186        Some(mem) => mem.write(caller, ptr.max(0) as usize, bytes).is_ok(),
187        None => false,
188    }
189}
190
191fn write_u32(caller: &mut Caller<'_, WasiState>, ptr: i32, v: u32) -> bool {
192    mem_write(caller, ptr, &v.to_le_bytes())
193}
194
195/// Registra el shim de `wasi_snapshot_preview1`. Sólo el subconjunto de
196/// consola: lo que no se enlaza, un módulo que lo importe lo descubre al
197/// instanciar (no instancia) — frontera física.
198fn link_wasi(linker: &mut Linker<WasiState>) -> Result<(), String> {
199    const NS: &str = "wasi_snapshot_preview1";
200    let err = |what: &str, e: wasmi::errors::LinkerError| format!("enlazar {what}: {e}");
201
202    // fd_write(fd, iovs, iovs_len, nwritten) -> errno
203    linker
204        .func_wrap(
205            NS,
206            "fd_write",
207            |mut caller: Caller<'_, WasiState>, fd: i32, iovs: i32, iovs_len: i32, nwritten: i32| -> i32 {
208                let mut collected = Vec::new();
209                for i in 0..iovs_len.max(0) {
210                    let base = iovs + i * 8;
211                    let hdr = match mem_read(&caller, base, 8) {
212                        Some(h) => h,
213                        None => return ERRNO_INVAL,
214                    };
215                    let ptr = u32::from_le_bytes(hdr[0..4].try_into().unwrap()) as i32;
216                    let len = u32::from_le_bytes(hdr[4..8].try_into().unwrap()) as i32;
217                    match mem_read(&caller, ptr, len) {
218                        Some(d) => collected.extend_from_slice(&d),
219                        None => return ERRNO_INVAL,
220                    }
221                }
222                let total = collected.len() as u32;
223                match fd {
224                    1 => caller.data_mut().stdout.extend_from_slice(&collected),
225                    2 => caller.data_mut().stderr.extend_from_slice(&collected),
226                    _ => return ERRNO_BADF,
227                }
228                if !write_u32(&mut caller, nwritten, total) {
229                    return ERRNO_INVAL;
230                }
231                ERRNO_SUCCESS
232            },
233        )
234        .map_err(|e| err("fd_write", e))?;
235
236    // fd_read(fd, iovs, iovs_len, nread) -> errno  (sólo fd 0 = stdin)
237    linker
238        .func_wrap(
239            NS,
240            "fd_read",
241            |mut caller: Caller<'_, WasiState>, fd: i32, iovs: i32, iovs_len: i32, nread: i32| -> i32 {
242                if fd != 0 {
243                    return ERRNO_BADF;
244                }
245                let mut total = 0u32;
246                for i in 0..iovs_len.max(0) {
247                    let base = iovs + i * 8;
248                    let hdr = match mem_read(&caller, base, 8) {
249                        Some(h) => h,
250                        None => return ERRNO_INVAL,
251                    };
252                    let ptr = u32::from_le_bytes(hdr[0..4].try_into().unwrap()) as i32;
253                    let len = u32::from_le_bytes(hdr[4..8].try_into().unwrap()) as usize;
254                    let (chunk, nuevo_pos) = {
255                        let st = caller.data();
256                        let restante = &st.stdin[st.stdin_pos.min(st.stdin.len())..];
257                        let n = len.min(restante.len());
258                        (restante[..n].to_vec(), st.stdin_pos + n)
259                    };
260                    if !chunk.is_empty() && !mem_write(&mut caller, ptr, &chunk) {
261                        return ERRNO_INVAL;
262                    }
263                    caller.data_mut().stdin_pos = nuevo_pos;
264                    total += chunk.len() as u32;
265                    if chunk.is_empty() {
266                        break;
267                    }
268                }
269                if !write_u32(&mut caller, nread, total) {
270                    return ERRNO_INVAL;
271                }
272                ERRNO_SUCCESS
273            },
274        )
275        .map_err(|e| err("fd_read", e))?;
276
277    // args_sizes_get(argc, buf_size) -> errno
278    linker
279        .func_wrap(
280            NS,
281            "args_sizes_get",
282            |mut caller: Caller<'_, WasiState>, argc: i32, buf_size: i32| -> i32 {
283                let (n, bytes) = {
284                    let st = caller.data();
285                    (st.args.len() as u32, st.args.iter().map(|a| a.len() as u32).sum::<u32>())
286                };
287                if !write_u32(&mut caller, argc, n) || !write_u32(&mut caller, buf_size, bytes) {
288                    return ERRNO_INVAL;
289                }
290                ERRNO_SUCCESS
291            },
292        )
293        .map_err(|e| err("args_sizes_get", e))?;
294
295    // args_get(argv, buf) -> errno
296    linker
297        .func_wrap(
298            NS,
299            "args_get",
300            |mut caller: Caller<'_, WasiState>, argv: i32, buf: i32| -> i32 {
301                escribir_vector(&mut caller, argv, buf, |st| &st.args)
302            },
303        )
304        .map_err(|e| err("args_get", e))?;
305
306    // environ_sizes_get(count, buf_size) -> errno
307    linker
308        .func_wrap(
309            NS,
310            "environ_sizes_get",
311            |mut caller: Caller<'_, WasiState>, count: i32, buf_size: i32| -> i32 {
312                let (n, bytes) = {
313                    let st = caller.data();
314                    (st.env.len() as u32, st.env.iter().map(|a| a.len() as u32).sum::<u32>())
315                };
316                if !write_u32(&mut caller, count, n) || !write_u32(&mut caller, buf_size, bytes) {
317                    return ERRNO_INVAL;
318                }
319                ERRNO_SUCCESS
320            },
321        )
322        .map_err(|e| err("environ_sizes_get", e))?;
323
324    // environ_get(environ, buf) -> errno
325    linker
326        .func_wrap(
327            NS,
328            "environ_get",
329            |mut caller: Caller<'_, WasiState>, environ: i32, buf: i32| -> i32 {
330                escribir_vector(&mut caller, environ, buf, |st| &st.env)
331            },
332        )
333        .map_err(|e| err("environ_get", e))?;
334
335    // clock_time_get(id, precision, time) -> errno  (reloj fijo: determinista)
336    linker
337        .func_wrap(
338            NS,
339            "clock_time_get",
340            |mut caller: Caller<'_, WasiState>, _id: i32, _precision: i64, time: i32| -> i32 {
341                if !mem_write(&mut caller, time, &0u64.to_le_bytes()) {
342                    return ERRNO_INVAL;
343                }
344                ERRNO_SUCCESS
345            },
346        )
347        .map_err(|e| err("clock_time_get", e))?;
348
349    // random_get(buf, len) -> errno  (xorshift determinista)
350    linker
351        .func_wrap(
352            NS,
353            "random_get",
354            |mut caller: Caller<'_, WasiState>, buf: i32, len: i32| -> i32 {
355                let mut bytes = Vec::with_capacity(len.max(0) as usize);
356                let mut x = caller.data().rng;
357                for _ in 0..len.max(0) {
358                    x ^= x << 13;
359                    x ^= x >> 7;
360                    x ^= x << 17;
361                    bytes.push((x & 0xff) as u8);
362                }
363                caller.data_mut().rng = x;
364                if !mem_write(&mut caller, buf, &bytes) {
365                    return ERRNO_INVAL;
366                }
367                ERRNO_SUCCESS
368            },
369        )
370        .map_err(|e| err("random_get", e))?;
371
372    // proc_exit(code) -> ! : guarda el código y corta vía trap.
373    linker
374        .func_wrap(
375            NS,
376            "proc_exit",
377            |mut caller: Caller<'_, WasiState>, code: i32| -> Result<(), wasmi::Error> {
378                caller.data_mut().exit_code = Some(code);
379                Err(wasmi::Error::host(ProcExit))
380            },
381        )
382        .map_err(|e| err("proc_exit", e))?;
383
384    // fd_close(fd) -> errno  (no-op para stdio)
385    linker
386        .func_wrap(NS, "fd_close", |_c: Caller<'_, WasiState>, _fd: i32| -> i32 { ERRNO_SUCCESS })
387        .map_err(|e| err("fd_close", e))?;
388
389    // fd_seek(fd, offset, whence, newoffset) -> errno  (stdio no busca: 0)
390    linker
391        .func_wrap(
392            NS,
393            "fd_seek",
394            |mut caller: Caller<'_, WasiState>, _fd: i32, _off: i64, _whence: i32, newoffset: i32| -> i32 {
395                let _ = mem_write(&mut caller, newoffset, &0u64.to_le_bytes());
396                ERRNO_SUCCESS
397            },
398        )
399        .map_err(|e| err("fd_seek", e))?;
400
401    // fd_fdstat_get(fd, stat) -> errno  (stdio = char device; 24 bytes a cero+tipo)
402    linker
403        .func_wrap(
404            NS,
405            "fd_fdstat_get",
406            |mut caller: Caller<'_, WasiState>, _fd: i32, stat: i32| -> i32 {
407                // fs_filetype=2 (character_device); resto a cero.
408                let mut buf = [0u8; 24];
409                buf[0] = 2;
410                if !mem_write(&mut caller, stat, &buf) {
411                    return ERRNO_INVAL;
412                }
413                ERRNO_SUCCESS
414            },
415        )
416        .map_err(|e| err("fd_fdstat_get", e))?;
417
418    // fd_prestat_get(fd, prestat) -> errno  : no hay dirs preabiertos ⇒ BADF.
419    // (wasi-libc itera hasta recibir BADF; devolverlo es lo correcto.)
420    linker
421        .func_wrap(NS, "fd_prestat_get", |_c: Caller<'_, WasiState>, _fd: i32, _p: i32| -> i32 {
422            ERRNO_BADF
423        })
424        .map_err(|e| err("fd_prestat_get", e))?;
425
426    // fd_prestat_dir_name(fd, path, path_len) -> errno : idem, BADF.
427    linker
428        .func_wrap(
429            NS,
430            "fd_prestat_dir_name",
431            |_c: Caller<'_, WasiState>, _fd: i32, _p: i32, _l: i32| -> i32 { ERRNO_BADF },
432        )
433        .map_err(|e| err("fd_prestat_dir_name", e))?;
434
435    Ok(())
436}
437
438/// Marcador de host-error que usamos para `proc_exit` (no es una falla real).
439#[derive(Debug)]
440struct ProcExit;
441impl std::fmt::Display for ProcExit {
442    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443        write!(f, "{EXIT_TRAP}")
444    }
445}
446impl std::error::Error for ProcExit {}
447impl wasmi::errors::HostError for ProcExit {}
448
449/// Escribe un vector de cadenas estilo argv/environ: un array de punteros en
450/// `ptr_array` y los bytes contiguos en `buf`. Patrón común a `args_get` y
451/// `environ_get`.
452fn escribir_vector(
453    caller: &mut Caller<'_, WasiState>,
454    ptr_array: i32,
455    buf: i32,
456    sel: impl Fn(&WasiState) -> &Vec<Vec<u8>>,
457) -> i32 {
458    let items: Vec<Vec<u8>> = sel(caller.data()).clone();
459    let mut cursor = buf;
460    for (i, item) in items.iter().enumerate() {
461        // puntero i → posición actual del buffer
462        if !write_u32(caller, ptr_array + (i as i32) * 4, cursor as u32) {
463            return ERRNO_INVAL;
464        }
465        if !mem_write(caller, cursor, item) {
466            return ERRNO_INVAL;
467        }
468        cursor += item.len() as i32;
469    }
470    ERRNO_SUCCESS
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    /// Compila WAT a wasm en el test (sin toolchain wasm32-wasi).
478    fn wat(src: &str) -> Vec<u8> {
479        wat::parse_str(src).expect("WAT válido")
480    }
481
482    /// Un módulo WASI mínimo: escribe `msg` a stdout vía fd_write y sale con
483    /// `code` vía proc_exit. Arma el iovec en memoria en runtime.
484    fn modulo_hola(msg: &str, code: i32) -> Vec<u8> {
485        // Layout de memoria: [0..]=iovec(ptr=8,len), [8..]=msg.
486        let bytes: String = msg.bytes().map(|b| format!("\\{b:02x}")).collect();
487        let len = msg.len();
488        wat(&format!(
489            r#"(module
490              (import "wasi_snapshot_preview1" "fd_write"
491                (func $fd_write (param i32 i32 i32 i32) (result i32)))
492              (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
493              (memory (export "memory") 1)
494              (data (i32.const 8) "{bytes}")
495              (func (export "_start")
496                ;; iovec en 0: ptr=8, len={len}
497                (i32.store (i32.const 0) (i32.const 8))
498                (i32.store (i32.const 4) (i32.const {len}))
499                ;; fd_write(1, iovs=0, iovs_len=1, nwritten=20)
500                (drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 20)))
501                (call $proc_exit (i32.const {code}))
502              )
503            )"#
504        ))
505    }
506
507    #[test]
508    fn detect_kind_distingue_wasi_de_tier3() {
509        let wasi = modulo_hola("x", 0);
510        assert_eq!(detect_kind(&wasi), WasmKind::WasiConsole);
511        // Un módulo que exporta wasm_view es Tier3.
512        let tier3 = wat(r#"(module (func (export "wasm_view") (result i64) (i64.const 0)))"#);
513        assert_eq!(detect_kind(&tier3), WasmKind::Tier3);
514        // Sin ninguno de los dos: Unknown.
515        let nada = wat(r#"(module (func (export "otra")))"#);
516        assert_eq!(detect_kind(&nada), WasmKind::Unknown);
517    }
518
519    #[test]
520    fn corre_y_captura_stdout_y_codigo() {
521        let out = run_console(&modulo_hola("hola mundo\n", 0), &[], &[], &[]).expect("corre");
522        assert_eq!(out.stdout_text(), "hola mundo\n");
523        assert_eq!(out.exit_code, 0);
524        assert!(out.stderr.is_empty());
525    }
526
527    #[test]
528    fn proc_exit_propaga_codigo() {
529        let out = run_console(&modulo_hola("x", 7), &[], &[], &[]).expect("corre");
530        assert_eq!(out.exit_code, 7);
531        assert_eq!(out.stdout_text(), "x");
532    }
533
534    #[test]
535    fn args_y_environ_se_exponen() {
536        // Un módulo que pide los tamaños de args y environ y los escribe a
537        // memoria; comprobamos que el shim reporta los conteos correctos.
538        let src = r#"(module
539          (import "wasi_snapshot_preview1" "args_sizes_get"
540            (func $args_sizes (param i32 i32) (result i32)))
541          (import "wasi_snapshot_preview1" "environ_sizes_get"
542            (func $env_sizes (param i32 i32) (result i32)))
543          (import "wasi_snapshot_preview1" "fd_write"
544            (func $fd_write (param i32 i32 i32 i32) (result i32)))
545          (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
546          (memory (export "memory") 1)
547          (func (export "_start")
548            ;; args count → @0, buf → @4 ; environ count → @8, buf → @12
549            (drop (call $args_sizes (i32.const 0) (i32.const 4)))
550            (drop (call $env_sizes (i32.const 8) (i32.const 12)))
551            ;; iovec @16 apuntando a @0, longitud 16 (los 4 u32) → stdout
552            (i32.store (i32.const 16) (i32.const 0))
553            (i32.store (i32.const 20) (i32.const 16))
554            (drop (call $fd_write (i32.const 1) (i32.const 16) (i32.const 1) (i32.const 40)))
555            (call $proc_exit (i32.const 0))
556          ))"#;
557        let out = run_console(
558            &wat(src),
559            &["uno".into(), "dos".into()],
560            &[("K".into(), "v".into())],
561            &[],
562        )
563        .expect("corre");
564        // stdout = 4 u32 LE: argc, args_buf_size, env_count, env_buf_size.
565        let w = |i: usize| u32::from_le_bytes(out.stdout[i * 4..i * 4 + 4].try_into().unwrap());
566        // argc = argv0 ("app") + 2 = 3.
567        assert_eq!(w(0), 3, "argc");
568        assert_eq!(w(2), 1, "environ count = 1 (K=v)");
569    }
570
571    #[test]
572    fn stdin_se_lee() {
573        // Lee 5 bytes de stdin a memoria @100 y los reescribe a stdout.
574        let src = r#"(module
575          (import "wasi_snapshot_preview1" "fd_read"
576            (func $fd_read (param i32 i32 i32 i32) (result i32)))
577          (import "wasi_snapshot_preview1" "fd_write"
578            (func $fd_write (param i32 i32 i32 i32) (result i32)))
579          (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
580          (memory (export "memory") 1)
581          (func (export "_start")
582            ;; iovec @0: ptr=100, len=5
583            (i32.store (i32.const 0) (i32.const 100))
584            (i32.store (i32.const 4) (i32.const 5))
585            (drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))
586            ;; reescribir lo leído (mismo iovec) a stdout
587            (drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 12)))
588            (call $proc_exit (i32.const 0))
589          ))"#;
590        let out = run_console(&wat(src), &[], &[], b"hello world").expect("corre");
591        assert_eq!(&out.stdout, b"hello");
592    }
593}