Experimentando con Dart y Wasm
Compilando Dart a Wasm, y llamando módulos Wasm desde Dart
Por Liam Appelbe y Michael Thomsen
WebAssembly (comúnmente abreviado como Wasm) es “un formato de instrucción binaria para una máquina virtual basada en pila”. Aunque Wasm fue diseñado originalmente para ejecutar código nativo en la web, Wasm ha evolucionado hasta convertirse en una tecnología general para ejecutar código compilado a través de múltiples plataformas. Dart ya es un lenguaje altamente portable y multiplataforma, así que estamos muy interesados en cómo Wasm podría permitirnos extender estas cualidades de Dart.
¿Por qué experimentar con Wasm?
#Wasm ha ganado amplio soporte por parte de los proveedores de navegadores, como Chrome, Edge, Firefox y WebKit. Esto hace de Wasm una perspectiva muy interesante para ejecutar código binario en el navegador. Sin embargo, originalmente Wasm no fue diseñado para lenguajes de programación con recolección de basura (GC), como Dart y Java/Kotlin, lo que dificulta compilar eficientemente lenguajes basados en GC a Wasm. Al participar en el proyecto Wasm en su reciente propuesta de GC, esperamos tanto proporcionar retroalimentación técnica sobre la propuesta como aprender más sobre qué beneficios podríamos obtener al ejecutar apps web basadas en Dart mediante código Wasm.
Una segunda cualidad de Wasm es que los módulos binarios Wasm son independientes de la plataforma. Esto potencialmente hace que la interoperabilidad con código existente sea más práctica: si ese código existente pudiera compilarse a Wasm, entonces las apps de Dart en todas las plataformas podrían depender de un único módulo binario Wasm compartido.
En el resto de este post discutiremos nuestros experimentos con Wasm y Dart en dos formas:
-
Compilación de Dart a Wasm: Extender nuestros compiladores AOT con soporte para compilar código fuente de Dart a código binario Wasm (issue 32894).
-
Interop de Dart a Wasm: Soporte para llamar desde código Dart a módulos Wasm compilados (issues 37355 y 37882).
Compilando Dart a Wasm
#Como se mencionó, Wasm se originó como una forma de ejecutar código nativo en la web. La web tradicionalmente funciona con código JavaScript, que se ejecuta en una máquina virtual (VM) que realiza compilación just-in-time (JIT) del código JavaScript a código nativo mientras la app web está en ejecución. En los frameworks actuales de Dart que apuntan a la web, como Flutter web, el código de la app de Dart se compila a JavaScript optimizado para su despliegue, y este JavaScript luego es compilado por JIT por la plataforma web a código nativo cuando la app está en ejecución.
Estamos investigando la compilación de código Dart directamente a código nativo de Wasm para ver si podemos obtener un camino más directo para ejecutar código nativo en la web. El formato de ensamblaje de Wasm es de bajo nivel y está más cerca del código JavaScript al nivel de abstracción del código máquina, lo que conduce a un tiempo de inicio mejorado y generalmente una eficiencia más predecible.
El soporte de Dart para compilar a Wasm es una investigación en etapa inicial y el compilador está incompleto, pero estamos experimentando para aprender. Siempre hemos tenido interés en Wasm como objetivo de compilación para Dart, pero su forma original no funciona bien para lenguajes con recolección de basura. Wasm carece de soporte incorporado para la recolección de basura, así que lenguajes como Dart deben incluir una implementación de recolección de basura en el módulo Wasm compilado. Incluir una implementación de GC sería altamente complejo, inflaría el tamaño del código Wasm compilado y perjudicaría el tiempo de inicio, y no se prestaría bien a la interoperabilidad a nivel de objetos con el resto del sistema del navegador.
Afortunadamente, un esfuerzo en curso en la comunidad de WebAssembly, conocido como Wasm GC, está explorando la posibilidad de expandir Wasm con soporte directo y performante para lenguajes con recolección de basura. Dado nuestro interés de larga data en Wasm, vimos una oportunidad para involucrar a la comunidad y proporcionar experiencia del mundo real escribiendo un compilador que traduce Dart a Wasm GC.
Es demasiado pronto para predecir a dónde nos llevará esto, pero nuestros prototipos iniciales muestran resultados muy positivos, con benchmarks iniciales mostrando tanto un tiempo más rápido hasta el primer frame como un tiempo promedio de frame / throughput más rápido. Si estás interesado en aprender más sobre el proyecto, echa un vistazo al código fuente de wasm_prototype.
Interoperabilidad con código Wasm (package:wasm)
#Además de compilar a Wasm, también estamos interesados en investigar si Wasm puede usarse para integrarse con código existente de una manera más multiplataforma. Varios lenguajes soportan la compilación a módulos que siguen la convención de llamada de C, y con Dart FFI tienes interoperabilidad con estos módulos. Dart FFI puede ser una excelente forma de aprovechar código fuente y bibliotecas existentes, en lugar de tener que reimplementar el código en Dart.
Sin embargo, como los módulos de C son específicos de la plataforma, distribuir paquetes compartidos con módulos nativos de C es complicado: requiere un sistema de build universal, o distribuir múltiples módulos binarios (uno para cada plataforma deseada). La distribución sería mucho más fácil si un solo formato de ensamblaje binario de Wasm pudiera usarse en todas las plataformas. Entonces, en lugar de compilar tu biblioteca a código binario específico de la plataforma para cada plataforma objetivo, podrías compilarla una vez a un módulo binario Wasm y ejecutarlo en todas partes. Esto potencialmente abriría la puerta a la distribución fácil en pub.dev de paquetes que contienen código nativo.
Estamos experimentando con soporte para interop de Wasm en un nuevo paquete, package:wasm. Este prototipo está construido sobre el runtime
Wasmer, y soporta WASI
para la interacción con el SO. Ten en cuenta que nuestro prototipo actual está incompleto y soporta solo plataformas de escritorio (Windows, Linux y macOS).
Ejemplo: Llamando a la biblioteca de compresión Brotli
#Echemos un vistazo a un ejemplo de uso de package:wasm para aprovechar la biblioteca de compresión Brotli, compilada a un módulo Wasm. En el ejemplo, leeremos un archivo de entrada, lo comprimiremos, reportaremos su ratio de compresión, y luego lo descomprimiremos y verificaremos que obtengamos la entrada de vuelta. Consulta el repositorio de GitHub para el
código fuente de ejemplo completo. Como package:wasm
está construido sobre dart:ffi, puedes encontrar los pasos familiares si tienes experiencia con FFI.
Hay varias formas de compilar código C a Wasm, pero en este caso usamos wasienv. Los detalles completos están disponibles en el README.
Para este ejemplo, intentaremos llamar a estas funciones de Brotli para comprimir y descomprimir datos:
int BrotliEncoderCompress(
int quality, int lgwin, int mode, size_t input_size,
const uint8_t* input_buffer, size_t* output_size,
uint8_t* output_buffer);
int BrotliDecoderDecompress(
size_t encoded_size, const uint8_t* encoded_buffer,
size_t* output_size, uint8_t* output_buffer);
Los argumentos quality, lgwin y mode son parámetros de ajuste para el codificador. Los detalles no son relevantes para el ejemplo, así que solo usaremos valores por defecto para estos. Otra cosa a tener en cuenta es que
output_size es un parámetro de entrada-salida. Cuando llamamos a estas funciones, output_size
debe inicializarse con el tamaño del output_buffer que hemos asignado, y después se establecerá a la cantidad del buffer que se usó realmente.
El primer paso es usar nuestro binario Wasm compilado para construir un objeto WasmModule. Los datos binarios deben ser un
Uint8List, que podemos obtener leyéndolos desde un archivo usando file.readAsBytesSync().
var brotliPath = Platform.script.resolve(‘libbrotli.wasm’);
var moduleData = File(brotliPath.path).readAsBytesSync();
var module = WasmModule(moduleData);
Una herramienta de depuración muy útil para asegurar que nuestro módulo Wasm tiene la API que esperamos es module.describe(). Esto devuelve un string que lista todos los imports y exports del módulo.
print(module.describe());
Para nuestra biblioteca Brotli, esta es la salida:
import function: int32 wasi_unstable::fd_close(int32)
import function: int32 wasi_unstable::fd_write(int32, int32, int32, int32)
import function: int32 wasi_unstable::fd_fdstat_get(int32, int32)
import function: int32 wasi_unstable::fd_seek(int32, int64, int32, int32)
import function: void wasi_unstable::proc_exit(int32)
export memory: memory
export function: int32 BrotliDecoderSetParameter(int32, int32, int32)
export function: int32 BrotliDecoderCreateInstance(int32, int32, int32)
export function: void BrotliDecoderDestroyInstance(int32)
export function: int32 BrotliDecoderDecompress(int32, int32, int32, int32)
…
export function: int32 BrotliEncoderSetParameter(int32, int32, int32)
export function: int32 BrotliEncoderCreateInstance(int32, int32, int32)
export function: void BrotliEncoderDestroyInstance(int32)
export function: int32 BrotliEncoderMaxCompressedSize(int32)
export function: int32 BrotliEncoderCompress(int32, int32, int32, int32, int32, int32, int32)
…
Podemos ver que el módulo importa algunas funciones WASI, y exporta su memoria y un conjunto de funciones de Brotli. Las dos funciones que nos interesan están exportadas, pero su firma se ve un poco diferente. Esto se debe a que Wasm solo soporta ints y floats de 32-bit y 64-bit. Los punteros se han convertido en índices
int32 dentro de la memoria exportada.
El siguiente paso es instanciar el módulo. Durante la instanciación debemos llenar cada import que el módulo esté esperando. La instanciación usa el patrón builder
(module.instantiate(). initialization… .build()). Nuestra biblioteca solo importa funciones WASI, así que podemos simplemente llamar
enableWasi():
var instance = module.instantiate().enableWasi().build();
Si tuviéramos imports de funciones adicionales no WASI podríamos usar addFunction() para importar una función de Dart a la biblioteca wasm.
Ahora que tenemos un WasmInstance, podemos buscar cualquiera de sus funciones exportadas, o inspeccionar su memoria:
var memory = instance.memory;
var compress = instance.lookupFunction(“BrotliEncoderCompress”);
var decompress = instance.lookupFunction(“BrotliDecoderDecompress”);
Lo siguiente que queremos hacer es usar las funciones de compress y decompress en nuestro archivo de entrada. Pero no podemos pasar los datos directamente a estas funciones. Las funciones de C toman punteros
uint8_t a los datos, pero en código Wasm estos punteros se convierten en índices
int32 dentro de la memoria de la instancia. Brotli también reporta el tamaño de los datos comprimidos y descomprimidos usando punteros
size_t, que también se convierten en
int32s.
Así que para pasar nuestros datos a las funciones tenemos que copiarlos en la memoria de la instancia y pasar su índice a la función. Necesitamos 5 regiones de memoria: los datos de entrada, los datos comprimidos, el tamaño comprimido, los datos descomprimidos y el tamaño descomprimido. Por simplicidad vamos a simplemente tomar algunas áreas no usadas de memoria, pero también podrías exportar
malloc() y free() en tu biblioteca.
Para asegurar que estamos poniendo los datos en memoria no usada, vamos a expandir la memoria de la instancia y usar la nueva región para nuestros datos:
var inputPtr = memory.lengthInBytes;
memory.grow((3 * inputData.length /
WasmMemory.kPageSizeInBytes).ceil());
var memoryView = memory.view;
var outputPtr = inputPtr + inputData.length;
var outSizePtr = outputPtr + inputData.length;
var decodedPtr = outSizePtr + 4;
var decSizePtr = decodedPtr + inputData.length;
Nuestras regiones de memoria se ven así:
[initial instance memory][input][output][output size][decoded][decoded size]
Luego, cargamos los datos de entrada en la memoria y llamamos a nuestra función de compresión:
memoryView.setRange(
inputPtr, inputPtr + inputData.length, inputData);
var status = compress(kDefaultQuality, kDefaultWindow, kDefaultMode,
inputData.length, inputPtr, outSizePtr, outputPtr);
El resto del ejemplo funciona de manera similar. Este es el resultado:
Loading lipsum.txt
Input size: 3210 bytes
Compressing…
Compression status: 1
Compressed size: 1198 bytes
Space saving: 62.68%
Decompressing…
Decompression status: 1
Decompressed size: 3210 bytes
Verifying decompression…
Decompression succeeded :)
Probando package:wasm
#Si estás interesado en probar el interop de Wasm, consulta el
README de package:wasm
para instrucciones.
Roadmap
#Tanto la compilación a Wasm como el interop de Wasm son experimentos. Si esos experimentos demuestran ser fructíferos, planeamos continuar desarrollándolos y eventualmente convertirlos en versiones estables y soportadas. Sin embargo, si aprendemos que algo no funciona como se pretende, o vemos falta de interés, discontinuaremos los experimentos.
Estamos haciendo estos experimentos para aprender, con dos componentes principales. Primero, queremos aprender sobre la viabilidad de soportar técnicamente Wasm y cuáles podrían ser las características de este soporte. ¿Puede hacer que el código Dart sea más rápido, más pequeño o más predecible? Segundo, estamos interesados en explorar qué nuevas capacidades técnicas podría desbloquear Wasm y qué nuevos casos de uso podrían habilitar para los desarrolladores de Dart. ¿Podemos hacer que el interop con código nativo sea más portable?
¿Cómo crees que Wasm podría aplicar a tus necesidades? ¿Para qué crees que lo usarías? Nos encantaría escuchar tus pensamientos. Por favor haznoslo saber en el grupo de discusión Dart misc.
Más de Dart
Dart en Google Summer of Code 2026
¡Estamos emocionados de anunciar que el proyecto Dart será mentor de contribuidores por séptima vez en Google Summer of Code 2026!
Resultados de Google Summer of Code 2025
El programa Google Summer of Code (GSoC) se enfoca en involucrar a nuevos desarrolladores de todo el mundo con el desarrollo de software de código abierto. Google…