Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly (con Golang) en Vue 3 con Vite y Pinia. Voy a explicar cómo comunicar las funciones de WASM con JavaScript y viceversa, dejando mucho código del lado de Golang con WASM.

Toma en cuenta que es una documentación muy específica a mi modo de trabajo. No esperes un tutorial paso a paso.

¿Deberías usar Comlink?

Comlink es una librería que facilita el trabajo de los workers para que parezcan funciones locales. Normalmente uno se comunica con postMessage y escucha la respuesta de vuelta en el onmessage, pero es muy complejo.

Primero decidí no usar Comlink pero conforme la app fue creciendo decidí usar Comlink. A Golang compilado como WASM no le importa si usas o no Comlink, pero a Vue 3 sí.

Sin usar Comlink

Si tú no quieres usar Comlink entonces veamos la función que convierte una función de Golang normal y la convierte en una función JavaScript. Aquí estoy armando un mensaje de respuesta con la acción y el valor para que el invocador (en onmessage) sepa qué acción se ha realizado y los datos que lleva esa acción.

func factoryParaWorker(laFuncion func(js.Value, []js.Value) js.Value, accion string) js.Func {
	return js.FuncOf(func(this js.Value, args []js.Value) any {
		valor := laFuncion(this, args)
		return armarMensajeDeRespuesta(accion, []any{valor})
	})
}

Y la de armar mensaje de respuesta queda así:

func armarMensajeDeRespuesta(accion string, argumentos []any) js.Value {
	objeto := js.Global().Get("Object").New()
	arreglo := js.Global().Get("Array").New(argumentos...)
	objeto.Set("accion", accion)
	objeto.Set("datos", arreglo)
	return objeto
}

Necesitamos la acción porque en el onmessage necesitamos saber cuál acción acaba de ser ejecutada. Y necesitamos el factoryParaWorker porque las funciones tienen la siguiente firma para que puedan ser invocadas entre ellas:

func miFuncion(this js.Value, parametros []js.Value) js.Value {
	
	return js.ValueOf("Hola")
}

Anteriormente yo invocaba a postmessage al final de cada función pero eso no servía para cuando quería usar las funciones entre ellas, así que las expongo con el factory para worker y ya luego en mi worker hago:

const resultados = await self["parzibyte"][accion](...datos);
self.postMessage(resultados);

Y desde cualquier lugar solo hay que escuchar el onmessage del worker.

Con comlink

Pero ahora con Comlink es distinto. Por alguna extraña razón no puedo exponer la función directamente porque el resultado es undefined del lado del worker. Entonces lo siguiente es incorrecto:

objetoParzibyte.Set("iniciarBaseDeDatos", IniciarBaseDeDatosConPromesa)

Porque aunque IniciarBaseDeDatosConPromesa devuelve un js.Value, recibo undefined al invocarlo. Tuve que crear otra función así:

func crearFuncionDeJSDevolviendoJsValue(laFuncion func(js.Value, []js.Value) js.Value) js.Func {
	return js.FuncOf(func(this js.Value, args []js.Value) any {
		valor := laFuncion(this, args)
		return armarOtroMensajeDeRespuesta(valor)
	})
}

Y la función que arma el mensaje:

func armarOtroMensajeDeRespuesta(argumentos any) js.Value {
	objeto := js.Global().Get("Object").New()
	objeto.Set("resultado", argumentos)
	return objeto
}

Así es, debo crear un objeto “dummy” que guarda los resultados en su propiedad “resultado” y devolverlo. Veamos cómo iniciar la base de datos, ya que la promesa estará en resultado.resultado:

const respuesta = await self.parzibyte.iniciarBaseDeDatos();
await respuesta.resultado;

Para otros casos que devuelven un arreglo, simplemente modifiqué la store:

const resultado = await envolturaWorkerPuenteWasm.ejecutarAccionEnWasm(accion, datos);
return resultado.resultado;

Y listo.

Comlink.expose({
	iniciar: async () => {
		// Código aquí
	},
	ejecutarAccionEnWasm: async (accion, datos) => {
		return await self["parzibyte"][accion](...datos);
	}
});

En iniciar cargo el wasm, lo instancio y le digo a wasm que haga su propio init. Luego expongo la función ejecutarAccionEnWasm que simplemente invocará a la función del objeto parzibyte. Lo he hecho en una store:

import { defineStore } from 'pinia'
import * as Comlink from "comlink"
import ArchivoWorkerPuenteWasm from "@/worker_puente_wasm?worker"
const workerPuenteWasm = new ArchivoWorkerPuenteWasm();
const envolturaWorkerPuenteWasm = Comlink.wrap(workerPuenteWasm);
await envolturaWorkerPuenteWasm.iniciar();

export const useWasmStore = defineStore('wasm', () => {
    async function ejecutarAccionEnWasm(accion:string, datos: Array<any>){
        return envolturaWorkerPuenteWasm.ejecutarAccionEnWasm(accion, datos);
    }

    return {ejecutarAccionEnWasm }
})

Me gusta que puedo hacer un await para el iniciar, así me aseguro de que la BD está lista. Luego expongo ejecutarAccionEnWasm que es solo una envoltura para la misma función del worker. Entonces la comunicación es:

Quien sea que invoque a la store -> worker -> objeto parzibyte de WASM

Y finalmente en Vue dentro de la app (que es la principal) hago el init estando seguro de que nada se va a renderizar hasta que la store termine de esperar a las promesas:

import { useWasmStore } from './stores/wasmStore';
// La cargamos sin usar para que la
// store pueda iniciar la BD esperando
// que la promesa se resuelva
const wasmStore = useWasmStore();

Luego ya puedo usarla en cualquier lugar, por ejemplo en otro componente que no es la App:

const plantillaRecienInsertada = await wasmStore.ejecutarAccionEnWasm("insertarPlantilla", [imagenCodificada, plantilla.value.nombre])

Y en este caso se me va a devolver lo que Golang devuelva con WASM. El js.Value que devuelve la función invocada es lo que yo tendré al resolverse la promesa, todo limpio sin onmessage. Solo promesas.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

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