tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
llimphi-wasm-wasi/src/lib.rs
Historia
Quién tocó qué
crudo
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}