Introducción al trabajo avanzado con WebAssembly y JavaScript en Node.js
\n\nWebAssembly (Wasm) ya no es una tecnología exótica solo para navegadores. En el ecosistema de Node.js, abre oportunidades únicas para ejecutar cálculos de alto rendimiento escritos en C/C++, Rust o Go, con una sobrecarga mínima. A nivel avanzado, trabajar con Wasm en Node.js requiere un profundo conocimiento de la gestión de memoria, las llamadas asíncronas y la optimización de la transferencia de datos entre entornos de ejecución. En este artículo, analizaremos ejemplos prácticos de integración de WebAssembly con JavaScript utilizando Node.js.
\n\n1. Compilación y carga de módulos Wasm en Node.js
\n\nPreparación del archivo Wasm
\nPara empezar, necesitamos un módulo Wasm compilado. Supongamos que tenemos una función simple en C que suma dos números:
\n\n// sum.c\nint add(int a, int b) {\n return a + b;\n}\n\nCompilamos con Emscripten o wasm-pack:
\nemcc sum.c -O3 -o sum.wasm --no-entry -s EXPORTED_FUNCTIONS="['_add']"\n\nCarga del módulo en Node.js
\nEn Node.js, podemos cargar un módulo Wasm de dos maneras: mediante readFileSync síncrono o WebAssembly.instantiate asíncrono. Veamos la opción asíncrona avanzada con importaciones:
const fs = require('fs');\nconst path = require('path');\n\nasync function loadWasmModule(filePath, imports = {}) {\n const wasmBuffer = fs.readFileSync(path.resolve(filePath));\n const result = await WebAssembly.instantiate(wasmBuffer, imports);\n return result.instance;\n}\n\n(async () => {\n const instance = await loadWasmModule('./sum.wasm');\n const { add } = instance.exports;\n console.log('Resultado:', add(5, 7)); // 12\n})();\n\nAquí, imports es un objeto que puede contener funciones de JavaScript invocables desde Wasm (por ejemplo, para registro o gestión de memoria).
2. Gestión de memoria: trabajo con la memoria lineal de Wasm
\n\nUna de las principales dificultades del trabajo avanzado con Wasm es la gestión de la memoria lineal. Los módulos Wasm tienen su propia memoria aislada (ArrayBuffer), a la que JavaScript puede acceder directamente a través de instance.exports.memory. Veamos un ejemplo donde pasamos un array de números de JS a Wasm, lo procesamos y devolvemos el resultado.
Ejemplo: suma de un array en C
\n// sum_array.c\nint sum_array(int* arr, int len) {\n int sum = 0;\n for(int i = 0; i < len; i++) {\n sum += arr[i];\n }\n return sum;\n}\n\nCompilamos con exportación de memoria:
\nemcc sum_array.c -O3 -o sum_array.wasm --no-entry \\\n -s EXPORTED_FUNCTIONS="['_sum_array']" \\\n -s EXPORTED_RUNTIME_METHODS="['cwrap', 'getValue']"\n\nCódigo JavaScript para transferencia de datos
\nasync function sumArrayWithWasm(numbers) {\n const instance = await loadWasmModule('./sum_array.wasm');\n const memory = instance.exports.memory;\n const sumArray = instance.exports.sum_array;\n\n // Reservamos memoria para el array (4 bytes por int)\n const bytesPerElement = 4;\n const offset = instance.exports.malloc ? \n instance.exports.malloc(numbers.length * bytesPerElement) : \n 0; // Si no hay malloc, usamos una dirección fija\n\n // Creamos un Uint32Array para escribir los datos\n const buffer = new Uint32Array(memory.buffer, offset, numbers.length);\n buffer.set(numbers);\n\n // Llamamos a la función Wasm\n const result = sumArray(offset, numbers.length);\n\n // Liberamos la memoria (si hay free)\n if (instance.exports.free) {\n instance.exports.free(offset);\n }\n\n return result;\n}\n\n(async () => {\n const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\n const sum = await sumArrayWithWasm(arr);\n console.log('Suma del array:', sum); // 55\