El compilador Rust se actualizó recientemente para utilizar LLVM 19 y este cambio acompaña algunas actualizaciones del conjunto predeterminado de funciones de destino habilitadas para los destinos WebAssembly del compilador Rust.
La versión beta de Rust actual que se convertirá en Rust 1.82 el 17 de octubre de 2024, refleja todos estos cambios y se puede utilizar para realizar pruebas.
WebAssembly es un estándar en evolución en el que se agregan extensiones con el tiempo a través de un proceso de propuestas.
Las propuestas de WebAssembly alcanzan la madurez, se fusionan con la especificación misma, se implementan en los motores y permanecen así durante bastante tiempo antes de que las cadenas de herramientas de los productores (por ejemplo, LLVM) se actualicen para habilitar estas propuestas lo suficientemente maduras de forma predeterminada.
En LLVM 19, esto ha sucedido con las propuestas de valores múltiples y tipos de referencia para las características de destino de LLVM/Rust multivalue y reference-types.
Ahora están habilitadas de forma predeterminada en LLVM y transitivamente, significa que también están habilitadas de forma predeterminada para Rust.
Los objetivos de WebAssembly para Rust ahora tienen documentación mejorada sobre las propuestas de WebAssembly y sus características de destino correspondientes. En esta publicación, analizaremos estos cambios y analizaremos en profundidad qué está cambiando en LLVM.
Propuestas de WebAssembly y características de destino del compilador
Las propuestas de WebAssembly son los medios formales por los cuales el estándar WebAssembly en sí evoluciona con el tiempo. La mayoría de las propuestas necesitan la integración de la cadena de herramientas de una forma u otra, por ejemplo, nuevas banderas en LLVM o el compilador Rust.
El mecanismo -Ctarget-feature=... se utiliza para implementar esto en la actualidad. Esta es una señal para LLVM y el compilador Rust de qué propuestas de WebAssembly están habilitadas o deshabilitadas.
Existe una relación débil entre el nombre de una propuesta (a menudo, el nombre del repositorio de GitHub de la propuesta) y el nombre de la característica que utiliza LLVM/Rust. Por ejemplo, existe una propuesta de múltiples valores, pero una multivalue característica.
El ciclo de vida de la implementación de una función en Rust/LLVM normalmente se ve así:
- Se crea una nueva propuesta de WebAssembly en un nuevo repositorio, por ejemplo WebAssembly/foo.
- Finalmente, Rust/LLVM implementará la propuesta en
-Ctarget-feature=+foo - Finalmente, la propuesta ascendente se fusiona con la especificación y WebAssembly/foo se convierte en un repositorio archivado.
- Rust/LLVM habilitan la función
-Ctarget-feature=+foode forma predeterminada, pero normalmente también conservan la posibilidad de deshabilitarla.
Las características de destino reference-typesy multivalue en Rust se encuentran en el paso (4) aquí ahora y esta publicación explica las consecuencias de hacerlo.
Habilitar tipos de referencia de forma predeterminada
La propuesta de tipos de referencia para WebAssembly introdujo algunos conceptos nuevos, en particular el externreftipo, que es un recurso GC definido por el host al que WebAssembly no puede acceder, pero que puede pasar. Rust no tiene soporte para el externreftipo WebAssembly y LLVM 19 no cambia eso.
Los módulos WebAssembly producidos a partir de Rust seguirán sin usar el tipo externref ni tendrán un medio para poder hacerlo. Esto puede habilitarse en el futuro (por ejemplo, un tipo hipotético core::arch::wasm32::Externref o similar), pero lo más probable es que solo se haga de forma voluntaria y no afectará al código preexistente de forma predeterminada.
Sin embargo, la propuesta de tipos de referencia también incluía la posibilidad de tener varias tablas WebAssembly en un único módulo. En la versión original de la especificación WebAssembly solo se permitía una única tabla y esta restricción se relajó con la propuesta de tipos de referencia.
LLVM y Rust utilizan las tablas WebAssembly para implementar llamadas de función indirectas. Por ejemplo, los punteros de función en WebAssembly son en realidad índices de tabla y las llamadas de función indirectas son una instrucción call_indirect WebAssembly con este índice de tabla.
Con la propuesta de tipos de referencia call_indirect se actualizó la codificación binaria de instrucciones. Antes de la propuesta de tipos de referencia call_indirect se codificaba con un byte cero fijo en su instrucción (que debía ser exactamente 0x00).
Este byte cero fijo se relajó a un LEB de 32 bits para indicar qué tabla call_indirectestaba usando la instrucción. Para aquellos que no están familiarizados, LEB es una forma de codificar números enteros de varios bytes en una cantidad menor de bytes para números enteros más pequeños.
Por ejemplo, el entero de 32 bits 0 se puede codificar como 0x00con un LEB. Los LEB son flexibles para permitir codificaciones “demasiado largas”, por lo que el entero 0 se puede codificar adicionalmente como 0x80 0x00.
El soporte de LLVM para la compilación separada del código fuente en un binario WebAssembly significa que cuando se emite un archivo de objeto, no se conoce el índice final de la tabla que se va a utilizar en el binario final.
Antes de los tipos de referencia, solo había una opción, la tabla 0, por lo que 0x00 siempre se usaba al codificar call_indirectinstrucciones.
Sin embargo, después de los tipos de referencia, LLVM emitirá un LEB demasiado largo con la forma que es la longitud máxima de un LEB 0x80 0x80 0x80 0x80 0x00 de 32 bits . Luego, el enlazador completa este LEB con una reubicación al índice de tabla real que usa el módulo final.
Al poner todo esto junto, significa que con LLVM 19, que tiene la reference-typescaracterística habilitada de forma predeterminada, cualquier módulo WebAssembly con una llamada de función indirecta (que es casi siempre el caso del código Rust) producirá un binario WebAssembly que no puede ser decodificado por motores y herramientas que no admitan la propuesta de tipos de referencia.
Se espera que este cambio tenga un impacto bajo debido a la antigüedad de la propuesta de tipos de referencia y la amplitud de la implementación en los motores. Sin embargo, dada la multitud de motores WebAssembly, se recomienda que todos los usuarios de WebAssembly prueben Rust 1.82 beta y vean si el módulo producido aún se ejecuta en el motor de su elección.
LLVM, Rust y tablas múltiples
Un punto interesante que vale la pena mencionar es que, a pesar de la propuesta de tipos de referencia que permite múltiples tablas en módulos WebAssembly, esto no es realmente aprovechado en este momento ni por LLVM ni por Rust. Los módulos WebAssembly emitidos seguirán teniendo como máximo una tabla de funciones.
Esto significa que la codificación excesivamente larga de 5 bytes del índice 0 0x80 0x80 0x80 0x80 0x00no es realmente necesaria en este momento. LLD, el enlazador de LLVM para WebAssembly, desea procesar todas las reubicaciones de LEB de una manera similar, lo que actualmente fuerza esta codificación de 5 bytes de cero.
Por ejemplo, cuando una función llama a otra función, la instrucción codifica el índice de la función de destino como un LEB call de 5 bytes que se completa mediante el enlazador. Muy a menudo hay más de una función, por lo que la codificación de 5 bytes permite codificar todos los índices de función posibles.
En el futuro, LLVM también podría comenzar a utilizar varias tablas. Por ejemplo, LLVM podría tener un modo en el futuro en el que haya un tipo de tabla por función en lugar de una única tabla heterogénea. Esto puede permitir que los motores implementen call_indirectde manera más eficiente. Sin embargo, esto no está implementado en este momento.
Para los usuarios que desean un módulo WebAssembly de tamaño mínimo (por ejemplo, si se encuentra en un contexto web y envía bytes por cable), se recomienda utilizar una herramienta de optimización como, por ejemplo, wasm-opt para reducir el tamaño de la salida de LLVM. Incluso antes de este cambio con los tipos de referencia, se recomienda hacer esto, ya que wasm-optnormalmente puede optimizar aún más la salida predeterminada de LLVM. Al optimizar un módulo a través de wasm-optestas codificaciones de 5 bytes del índice 0, todas se reducen a un solo byte.
Habilitar valores múltiples de forma predeterminada
La segunda característica habilitada de forma predeterminada en LLVM 19 es multivalue. La propuesta de múltiples valores para WebAssembly permite que las funciones tengan más de un valor de retorno, por ejemplo. Además, las instrucciones de WebAssembly también pueden tener más de un valor de retorno. Esta propuesta es una de las primeras en fusionarse con la especificación de WebAssembly después del MVP original y se ha implementado en muchos motores durante bastante tiempo.
Sin embargo, las consecuencias de habilitar esta función de forma predeterminada en LLVM son menores para Rust que habilitar la reference-typesfunción de forma predeterminada. La ABI C predeterminada de LLVM para el código WebAssembly no cambia incluso cuando multivalueestá habilitada. Además, extern "C" la ABI de Rust para WebAssembly tampoco cambia y sigue coincidiendo con la de LLVM (o intenta hacerlo, las diferencias con LLVM se consideran errores que se deben corregir). A pesar de esto, el cambio tiene la posibilidad de seguir afectando a los usuarios de Rust.
Rust ha admitido durante algún tiempo una extern "wasm" ABI en Nightly que era un medio experimental para exponer la capacidad de definir una función en Rust que devolviera múltiples valores (por ejemplo, se utilizó la propuesta de múltiples valores). Debido a los cambios de infraestructura y las refactorizaciones en LLVM, esta característica de Rust se ha eliminado y ya no se admite en Nightly. Como resultado, ya no existe ningún método posible para escribir una función en Rust que devuelva múltiples valores en el nivel de tipo de función de WebAssembly.
En resumen, se espera que este cambio no afecte a ningún código de Rust en circulación, a menos que estuvieras usando la función Nightly, extern "wasm"en cuyo caso te verás obligado a dejar de brindar soporte para esa función y usar extern "C"en su lugar.
La compatibilidad con funciones de retorno múltiple de WebAssembly en Rust es un tema más amplio de lo que esta publicación puede cubrir, pero en este momento es un área que está lista para la contribución de colaboradores debidamente motivados.
Aparte: Estabilidad de ABI y WebAssembly
En relación con el tema de las ABI y sus multivaluecaracterísticas, quizás valga la pena repasar un poco qué significan las ABI para WebAssembly. La definición actual de la extern "C"ABI para WebAssembly está documentada en el repositorio de convenciones de herramientas y esto es lo que Clang implementa también para el código C. LLVM también implementa suficiente compatibilidad para la reducción a WebAssembly como para soportar todo esto. La extern "RustABI no es estable en WebAssembly, como es el caso de todos los objetivos de Rust, y está sujeta a cambios con el tiempo. No hay documentación de referencia en este momento sobre lo que extern "Rust" hay en WebAssembly.
La extern "C"ABI, que también utiliza el código C de forma predeterminada, es difícil de cambiar porque a menudo se requiere estabilidad en distintas versiones del compilador. Por ejemplo, se podría esperar que el código WebAssembly compilado con LLVM 18 funcione con el código compilado con LLVM 20. Esto significa que cambiar la ABI es una tarea abrumadora que requiere campos de versión, marcadores explícitos, etc., para ayudar a evitar desajustes.
extern "Rust"Sin embargo, la ABI está sujeta a cambios con el tiempo. Un buen ejemplo de esto podría ser que, cuando multivalue se habilita la función, la extern "Rust" ABI se puede redefinir para utilizar los valores de retorno múltiples que WebAssembly admitiría. Esto permitiría retornos mucho más eficientes de valores mayores a 64 bits. Implementar esto requeriría soporte en LLVM, que actualmente no está presente.
Todo esto significa que el uso de retornos múltiples en funciones, o la característica WebAssembly que multivaluehabilita, aún está en el horizonte y no se ha implementado. Primero, LLVM deberá implementar soporte de reducción completo para generar funciones WebAssembly con retornos múltiples, y luego extern "Rust"podrá cambiar para usar esto cuando sea totalmente compatible. En un futuro aún más lejano, el código C podría cambiar, pero eso llevará bastante tiempo debido a su historia de compatibilidad entre versiones.
Habilitación de futuras propuestas para WebAssembly
Esta no es la primera vez que una propuesta de WebAssembly pasa de estar desactivada de forma predeterminada a estar activada de forma predeterminada en LLVM, ni será la última. Por ejemplo, LLVM ya habilita la propuesta de extensión de signo de forma predeterminada, algo que no tenía MVP WebAssembly.
Se espera que en un futuro no muy lejano la propuesta de no conversión de fp a int probablemente esté habilitada de forma predeterminada. Estos cambios actualmente no se realizan con criterios estrictos en mente (por ejemplo, los motores N deben tener esto implementado durante M años), y es posible que se produzcan fallas.
Si está utilizando un motor WebAssembly que no admite los módulos emitidos por Rust 1.82 beta y LLVM 19, sus opciones son:
- Prueba a comprobar si el motor que estás usando tiene actualizaciones disponibles. Es posible que estés usando una versión anterior que no admitía una función, pero una versión más nueva sí la admite.
- Abra un problema para generar conciencia de que un cambio está causando problemas. Esto se puede hacer en el repositorio de su motor, en el repositorio de Rust o en el repositorio de convenciones de herramientas de WebAssembly . Sin embargo, se recomienda buscar primero para confirmar que no haya un problema abierto.
- Vuelva a compilar su código con las funciones deshabilitadas, más sobre esto en la siguiente sección.
La suposición general detrás de la habilitación de nuevas funciones de forma predeterminada es que es una operación relativamente libre de problemas para los usuarios finales y que, al mismo tiempo, brinda beneficios de rendimiento para todos (por ejemplo, no convertir fp a int hará que las conversiones de float a int sean más óptimas). Si las actualizaciones terminan causando problemas, es mejor advertirlo con anticipación para que los planes de implementación se puedan ajustar si es necesario.
Deshabilitar las propuestas WebAssembly predeterminadas
Por diversas razones, es posible que desees deshabilitar las funciones de WebAssembly activadas por defecto: por ejemplo, tal vez tu motor sea difícil de actualizar o no admita una nueva función. Deshabilitar funciones activadas por defecto, lamentablemente, no es la tarea más sencilla. En particular, no es suficiente usar -Ctarget-features=-sign-extla función para deshabilitar solo la compilación de tu propio proyecto porque la biblioteca estándar de Rust, que se entrega en forma precompilada, aún se compila con la función activada.
Para desactivar la propuesta WebAssembly predeterminada, es necesario utilizar -Zbuild-std la función Cargo. Por ejemplo:
$ export RUSTFLAGS=-Ctarget-cpu=mvp
$ cargo +nightly build -Zbuild-std=panic_abort,std --target wasm32-unknown-unknown
Esto volverá a compilar la biblioteca estándar de Rust además de su propio código con la “CPU MVP”, que es el marcador de posición de LLVM para todas las propuestas de WebAssembly, deshabilitada. Esto deshabilitará sign-ext, reference-types, multi-value, etc.