2 puntos por GN⁺ 2025-02-05 | 1 comentarios | Compartir por WhatsApp
  • Un desarrollador dedicó noches y fines de semana durante 6 meses a crear Semblance, una VM de WebAssembly basada en C, convirtiéndola en un proyecto de aprendizaje de largo plazo enfocado en profundizar en la especificación, en lugar de otro proyecto paralelo de corta duración
  • WebAssembly es un bytecode estándar para ejecutar código no confiable en el navegador a velocidad cercana a la nativa, y su uso fuera del navegador también está creciendo en casos como Fastly, Shopify y Zed
  • El objetivo de Semblance era aprender la WebAssembly Core Specification y prepararse para contribuir a runtimes industriales como Wasmtime, por lo que implementar todos los opcodes o pasar la core test suite quedó fuera del alcance
  • El intérprete funciona verificando primero los magic bytes y la versión 1 de un archivo .wasm, luego decodifica secciones, resuelve imports, instancia el módulo, busca la función exportada, inicializa el stack y el activation frame, y ejecuta un opcode switch loop
  • Aunque no es un runtime completo, es lento, tiene fugas de memoria y podría seguir siendo vulnerable a módulos maliciosos; aun así, al ejecutar Hello, World! cumplió su propósito de aprendizaje de la especificación

Por qué eligió WebAssembly

  • WebAssembly es un conjunto de instrucciones estándar y un formato de bytecode para una máquina virtual de pila
  • El punto de partida fue el compilador de C/C++ a JavaScript de Emscripten
    • Emscripten convertía LLVM IR a JavaScript para permitir ejecutar código C y C++ en la web
    • Los desarrolladores de navegadores y el proyecto Emscripten limitaron el código generado a un subconjunto simple de JavaScript para optimizar el rendimiento, y ese subconjunto se estandarizó como asm.js
    • Más adelante, para evitar el overhead de parseo de JavaScript, se diseñó Wasm como un formato de bytecode independiente
  • Recientemente, el uso de WebAssembly también está aumentando fuera del navegador
    • Edge Compute de Fastly y Functions de Shopify están construidos sobre motores WebAssembly
    • El sistema de extensiones de Zed también está basado en Wasm
  • Bytecode Alliance lidera un esfuerzo de toda la industria para apoyar el uso de WebAssembly fuera del navegador, y trabaja en el runtime Wasmtime y la estandarización de WASI
    • WASI permite que los módulos WebAssembly se comuniquen con el sistema operativo anfitrión mediante host calls similares a POSIX
    • Ofrece una forma estándar de interactuar con interfaces externas como entrada/salida estándar, sistema de archivos y red

De proyecto paralelo a proyecto de aprendizaje

  • Existía un patrón repetido de iniciar nuevos proyectos paralelos, perder el interés en pocas semanas y pasar a otro proyecto
  • Hacía falta un proyecto grande que permitiera enfocarse durante más tiempo y que tratara un nivel más bajo del stack de computación que el del trabajo habitual
  • Como muchos proyectos paralelos habían quedado abandonados por falta de dirección, buscaba un proyecto con un objetivo claro y un resultado visible
  • Después de sentir que un perfil de ingeniería generalista influía al buscar un trabajo estable, buscaba un área de especialización con la que pudiera construir un conjunto de habilidades en forma de T
  • La posibilidad de diseñar entornos de cómputo personalizados con syscalls a medida en WebAssembly parecía atractiva como herramienta de ingeniería de plataforma

Objetivos y alcance de Semblance

  • El proyecto se llama Semblance y es un intérprete de WebAssembly escrito en C
  • El objetivo principal era familiarizarse con la WebAssembly Core Specification
  • A largo plazo, también buscaba acumular suficiente conocimiento sobre WebAssembly como para poder contribuir a runtimes industriales como Wasmtime
  • Al ser un proyecto con fines de aprendizaje, no había intención de implementar todos los opcodes ni de pasar la core test suite
  • Se tomó como criterio de éxito suficiente poder ejecutar “Hello, World!”
  • Reconoce que no tiene experiencia profesional desarrollando en C y que la calidad del código C no es ideal, por lo que agradece revisiones de código

Flujo de ejecución de Hello, World!

  • El código C de ejemplo declara la función externa puts e imprime "Hello, World!\n" desde hello()
  • WebAssembly se genera con Clang
    • --target=wasm32
    • -nostdlib
    • -Wl,--no-entry
    • -Wl,--export-all
    • -Wl,--allow-undefined
    • -O3
  • El hello.wasm generado se ejecuta con semblance hello.wasm --invoke hello
  • Al iniciar el programa, se inicializa memoria en el stack para los argumentos de línea de comandos, el módulo WebAssembly decodificado y el store que contiene el estado del runtime
  • Se parsean los argumentos de línea de comandos y, si son inválidos, el programa termina

Decodificación binaria y estructura del módulo

  • El intérprete primero decodifica el formato binario de WebAssembly
  • Al inicio del archivo verifica los magic bytes "\0asm"
  • Comprueba que la versión del formato binario sea 1
  • Luego decodifica las secciones incluidas
  • Una vez terminada la decodificación, la estructura WasmModule queda poblada con las funciones, tipos, imports, data y otros elementos de hello.wasm
    • types
    • funcs
    • tables
    • mems
    • globals
    • elems
    • datas
    • start
    • imports
    • exports
    • customs
    • meta

Resolución de imports y env::puts

  • La única función host que Semblance ofrece actualmente es env::puts
  • Los imports de funciones host se manejan asignando un native function pointer en el store e insertando el funcaddr generado en el array imports del módulo
  • hostcall_puts usa el offset i32 recibido como argumento
    • Obtiene el puntero de data desde la primera instancia de memoria del store
    • Imprime en la salida estándar la cadena ubicada en el offset usando printf("%s", ...)
  • Al recorrer la lista de imports, si encuentra un elemento cuyo module name es "env" e item name es "puts", registra la función host con register_hostcall_puts

Instanciación y ejecución de opcodes

  • Después de resolver los imports, el módulo se instancia
  • La instanciación incluye una etapa de validación
    • Realiza pruebas de verificación de tipos y de que el módulo tenga una forma correcta
    • Inicializa memory, global y table
    • Si existe una función start, la llama
  • El resultado de la instanciación es WasmModuleInst, que contiene la estructura en tiempo de ejecución y los exports listos para usar
  • Busca la función exportada tomando como base el nombre indicado por el argumento de línea de comandos --invoke
    • Recorre los exports y devuelve el WasmExportInst cuyo nombre coincide
    • Verifica que el export devuelto sea una function
  • La llamada a la función requiere inicializar el stack y el activation frame
  • La parte central de la ejecución es el opcode switch loop
    • Lee en orden las Wasm instructions del cuerpo de la función
    • Según el opcode, realiza operaciones sobre el stack y el store
    • Por ejemplo, maneja i32.const, i64.const, f32.const, f64.const, i32.ge_s, call, nop, unreachable, end, entre otros
    • Si un opcode no está manejado, imprime "unhandled opcode [...]" y devuelve un trap

La instrucción call y llamadas a funciones host

  • El manejo de WasmOpCall usa funcidx, el valor inmediato del opcode call, para buscar la instancia de función en el store
  • Con base en la información de tipo de la función, hace pop del stack de los argumentos necesarios
  • Comprueba el tipo de la instancia de función para distinguir si es una función Wasm o una función host nativa
  • Si es una función host, ejecuta el function pointer hostfunc almacenado como una llamada nativa
  • Si hay un valor de retorno, lo vuelve a hacer push al stack
  • Cuando la función hello llega al opcode end, el opcode switch loop termina y vuelve a main

Resultado y límites pendientes

  • El resultado de la ejecución se muestra en la salida estándar de la siguiente manera
    • Hello, World!
    • Ok []
  • El proyecto puede ejecutar un programa simple “Hello, World!”
  • La cobertura de opcodes no es completa
  • El código es desordenado, lento y tiene fugas de memoria
  • Podría ser vulnerable a módulos maliciosos
  • Aun así, aprendió mucho sobre la WebAssembly Core Specification y le ayudó a salir de su zona de familiaridad como ingeniero
  • Después de crear su propio intérprete, siente que obtuvo suficiente conocimiento sobre WebAssembly como para poder contribuir a runtimes industriales como Wasmtime
  • Es posible que siga agregando algunos opcodes en el futuro, pero pretende cerrar la etapa de tomar este proyecto como proyecto principal

1 comentarios

 
GN⁺ 2025-02-05
Opiniones en Hacker News
  • Hace tiempo hice un intérprete de Wasm en Scheme, así que me alegra ver que más gente lo implementa por su cuenta.
    No es tan difícil como parece, así que vale la pena echarle un vistazo a la especificación e intentarlo; no hace falta implementar todas las instrucciones, basta con hacer lo suficiente para divertirse.

  • Como alguien que también lo implementó, un consejo: en spec-test hay muchos formatos raros de Wasm en texto cuya forma de compilarlos no es intuitiva, pero si usas el conversor wast2json puedes generar una descripción JSON más simple junto con archivos Wasm binarios normales.

    • En Chicory hicimos lo mismo: https://github.com/dylibso/chicory
      Mientras más rápido puedas ejecutar esta suite de pruebas, más te ayudará con la velocidad de iteración y la precisión.
      Me tomó un tiempo hacer que todo funcionara, pero una vez logrado llegué muy rápido al punto de poder ejecutar cualquier cosa, y aunque la suite de pruebas no es completa, cubre alrededor del 95%: https://github.com/WebAssembly/testsuite
    • Yo también estoy haciendo un compilador y esto me sirve.
      Ya había visto estas pruebas antes, pero no sabía cómo usarlas y me resultaban frustrantes y confusas.
  • Sobre interpretar Wasm directamente, este paper me pareció bastante interesante: https://arxiv.org/abs/2205.01183
    Con base en eso hice https://github.com/peterseymour/winter, y aprendí que Wasm no es tan simple como uno cree.

  • Pregunta de principiante: me da curiosidad cómo se hace la depuración de un intérprete cuando no estás escribiendo código directamente para el target.
    También me pregunto hasta qué punto ayuda hacer fuzzing de cadenas de opcodes, qué tan grandes son en la práctica las diferencias entre motores Wasm del lado del servidor y motores basados en navegador, y cuánto trabajo implicaría cambiar uno por el otro.

  • Es un enfoque interesante y un gran trabajo.
    Quien quiera ver lo esencial lo encontrará, en general, en este archivo: https://github.com/irrio/semblance/blob/main/src/wrun.c
    Pensándolo bien, habría estado bueno que el proyecto siguiera Wasm-C-API(https://github.com/WebAssembly/wasm-c-api) como interfaz estándar.
    Ya es una API en C y la adoptan la mayoría de los runtimes Wasm, como Wasmer, V8, wasmi, etc., así que los desarrolladores familiarizados con esa API podrían probarlo más fácilmente.
    Si el autor está suficientemente familiarizado con Wasm y quiere contribuir a Wasmer, los parches o mejoras también son bienvenidos.

    • El autor claramente parece estar suficientemente familiarizado con Wasm, y probablemente también sabe lo bastante como para evitar a la empresa que intentó registrar la marca WebAssembly.
      Dijo: “preocupaciones comprensibles sobre el hecho de que Wasmer, con inversión de VC, intentara registrar como marca WebAssembly, el nombre de una organización sin fines de lucro”, lo cual reconoce el error.
    • Installed-Size: 266 MB de Wasmer, ¿qué demonios es esto?
    • Hasta donde sé, decir que Wasmer adoptó Wasm-C-API queda bastante lejos de la realidad.
      https://github.com/wasmerio/wasmer/issues/2615 quedó abandonado y luego se cerró automáticamente.
  • Una pregunta algo más polémica: me pregunto si tienen pensado agregar instrucciones preliminares de tail call.
    Del lado de la especificación de Wasm las rechazaron por ser demasiado “de alto nivel”, pero el comité de C también rechazó una propuesta de Dennis Ritchie.
    Aun así, yo seguiría apostando mi dinero por Ritchie, y creo que Rob Pike también apostó en esa dirección.
    Si no, ¿por qué habría creado Golang? Las tail calls solo son de alto nivel cuando las llamadas también lo son.

  • Estaría bueno que le eches un vistazo a Orca. Creo que podrías contribuir bien ahí.
    [0] https://orca-app.dev

  • Yo también decidí concentrarme en un solo proyecto en vez de andar saltando entre cosas nuevas y brillantes, con la excepción de poner a los asistentes de IA a hacer tareas rutinarias.
    Como mínimo, es bastante frustrante.
    Y también iba a sugerir usar una biblioteca como libffi para manejar múltiples argumentos y demás al llamar desde C.

  • Cuando WebAssembly se usa como API de plugins en lugares como zed, me pregunto cómo depuran su código los desarrolladores de plugins.
    Por ejemplo, si se puede depurar con breakpoints y si, cuando el código crashea, se puede obtener un stack trace.