WebAssembly y WASI con Go: Navegadores y Runtimes

WebAssembly (WASM) es un formato de instrucción binaria diseñado para ejecutarse en un entorno de ejecución (runtime) altamente optimizado, con un modelo de memoria lineal y un sandbox estricto. Cuando compilas Go para este formato, tienes dos caminos principales dependiendo de dónde pretendas ejecutar el código. El primero es para el navegador, utilizando el target GOOS=js GOARCH=wasm. En este escenario, el binario no puede hablar directamente con el DOM o la red; requiere un archivo “puente” llamado wasm_exec.js (que reside en tu instalación de Go) para que JavaScript pueda gestionar la memoria de Go y permitir que el scheduler de Go interactúe con el event loop de JS.

El segundo camino es para entornos fuera del navegador, como microservicios, plugins de proxies (como Envoy) o funciones en el edge (como Cloudflare Workers), utilizando WASI [disponible desde Go 1.21] con el target GOOS=wasip1 GOARCH=wasm. WASI es la WebAssembly System Interface, un estándar que define cómo un módulo WASM debe realizar llamadas al sistema (lectura de archivos, tiempo del sistema, etc.) de forma segura.

El motivo por el cual Go es tan potente en WASM es que no estás enviando solo tu lógica de negocio; estás empaquetando el runtime completo de Go, lo que incluye su Garbage Collector (GC) y su scheduler de goroutines. Esto garantiza que tu código se comporte exactamente igual que en un servidor Linux, pero con una ventaja crítica: la ejecución ocurre en un entorno aislado. Sin embargo, este diseño tiene un costo: los binarios resultantes suelen ser pesados (varios MB), lo que puede ser un problema en entornos donde la latencia de red o el tiempo de arranque (cold start) es vital. Para casos donde el tamaño es la prioridad absoluta, como en controladores de IoT o micro-plugins, se suele recurrir a TinyGo, un compilador que optimiza la salida sacrificando parte de la capacidad de la librería estándar para lograr binarios de apenas unos pocos KB.

// package main es necesario para generar un ejecutable/módulo WASM.
package main

import (
	"fmt"
	"os"
	"time"
)

// procesarDatos simula una tarea intensiva que consume CPU y memoria.
func procesarDatos(n int) int {
	res := 0
	for i := 0; i < n; i++ {
		res += i
	}
	return res
}

func main() {
	// El uso de time.Now() funciona en WASI y en el navegador (vía JS bridge).
	start := time.Now()

	// Simulamos la captura de argumentos, común en entornos CLI o WASI.
	input := 1000000
	if len(os.Args) > 1 {
		fmt.Sscanf(os.Args[1], "%d", &input)
	}

	fmt.Printf("Iniciando procesamiento de %d elementos...\n", input)

	resultado := procesarDatos(input)

	// La salida estándar (stdout) se redirige al log de la consola en el browser
	// o al stdout del runtime WASI (como Wasmtime).
	elapsed := time.Since(start)
	fmt.Printf("Resultado: %d | Tiempo transcurrido: %s\n", resultado, elapsed)
}

Análisis del flujo de ejecución

El código anterior es una estructura funcional que se comporta de forma distinta según el comando de compilación:

  1. Escenario Browser: Si ejecutas GOOS=js GOARCH=wasm go build -o main.wasm, el binario resultante incluirá todo el runtime de Go. Para ejecutarlo, necesitarás un archivo HTML que cargue wasm_exec.js y luego instancie main.wasm. Cuando el código llama a fmt.Printf, el runtime de Go no escribe en un terminal real, sino que llama a una función de JavaScript proporcionada por el bridge que finalmente imprime en la consola del navegador.

  2. Escenario WASI: Si ejecutas GOOS=wasip1 GOARCH=wasm go build -o main.wasm, el binario está diseñado para hablar con la interfaz WASI. Al ejecutarlo con un runtime como wasmtime main.wasm, la llamada a os.Args o fmt.Printf no pasa por JavaScript, sino que intercepta llamadas al sistema estandarizadas que el runtime de WebAssembly proporciona para interactuar con tu máquina local de forma segura.

En ambos casos, la gestión de memoria es transparente para ti: el Garbage Collector se ejecuta dentro del sandbox de WASM, limpiando los objetos creados en procesarDatos sin que el host (el navegador o el runtime) tenga que intervenir.

El error frecuente

Un error común al migrar de Go estándar a WASI es asumir que tienes acceso completo al sistema operativo. Si intentas usar el paquete net/http para levantar un servidor web estándar dentro de un módulo compilado con GOOS=wasip1, el programa fallará o lanzará un error de ejecución.

Esto sucede porque WASI, por diseño, es un modelo de “capacidades” limitado; no incluye una pila de red (TCP/UDP) completa por defecto como lo hace un sistema operativo convencional. Si necesitas comunicación de red en WASM, en el navegador debes usar la API de fetch mediante el bridge de JavaScript, y en WASI deberás utilizar implementaciones específicas que el host proporcione a través de extensiones de WASI, ya que el estándar base no garantiza la presencia de sockets.

242

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio