Desarrollo de una VM de WebAssembly escrita en C
(irreducible.io)- 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
putse imprime"Hello, World!\n"desdehello() - WebAssembly se genera con Clang
--target=wasm32-nostdlib-Wl,--no-entry-Wl,--export-all-Wl,--allow-undefined-O3
- El
hello.wasmgenerado se ejecuta consemblance 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
WasmModulequeda poblada con las funciones, tipos, imports, data y otros elementos dehello.wasmtypesfuncstablesmemsglobalselemsdatasstartimportsexportscustomsmeta
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
funcaddrgenerado en el arrayimportsdel módulo hostcall_putsusa 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 conregister_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
WasmExportInstcuyo nombre coincide - Verifica que el export devuelto sea una function
- Recorre los exports y devuelve el
- 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
WasmOpCallusafuncidx, el valor inmediato del opcodecall, 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
hostfuncalmacenado como una llamada nativa - Si hay un valor de retorno, lo vuelve a hacer push al stack
- Cuando la función
hellollega al opcodeend, el opcode switch loop termina y vuelve amain
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
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
wast2jsonpuedes generar una descripción JSON más simple junto con archivos Wasm binarios normales.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
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.
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 MBde Wasmer, ¿qué demonios es esto?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
libffipara 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.