Basado en mi experiencia con range-set-blazeun proyecto de estructura de datos, aquí están las decisiones que recomiendo, descritas una a la vez. Para evitar dudas, las expresaré como reglas.
En 2019, el cocreador de Docker, Solomon Hykes tuiteó:
Si WASM+WASI existiera en 2008, no habríamos necesitado crear Docker. Así de importante es. Webassembly en el servidor es el futuro de la informática. El eslabón perdido era una interfaz de sistema estandarizada. Esperemos que WASI esté a la altura.
Hoy en día, si sigues las noticias sobre tecnología, verás titulares optimistas como estos:
Si WASM WASI estuviera realmente listo y fuera útil, todos ya lo estarían usando. El hecho de que sigamos viendo estos titulares sugiere que aún no está listo. En otras palabras, no necesitarían seguir insistiendo en que WASM WASI está listo si realmente lo estuviera.
A partir de WASI Preview 1, así es como están las cosas: puede acceder a algunas operaciones de archivos, variables de entorno y tener acceso a la generación de tiempo y números aleatorios. Sin embargo, no hay soporte para la creación de redes.
WASM WASI podría ser útil para ciertos servicios web estilo AWS Lambda, pero incluso eso es incierto. ¿Porque no preferirías compilar tu código Rust de forma nativa y ejecutarlo dos veces más rápido a la mitad del costo en comparación con WASM WASI?
Quizás WASM WASI sea útil para complementos y extensiones. En genómica, tengo una extensión Rust para Python, que compilo para 25 combinaciones diferentes (5 versiones de Python en 5 objetivos de sistema operativo). Incluso con eso, no cubro todos los sistemas operativos y familias de chips posibles. ¿Puedo reemplazar esos objetivos del sistema operativo con WASM WASI? No, sería demasiado lento. ¿Podría agregar WASM WASI como sexto objetivo general? Tal vez, pero si realmente necesito portabilidad, ya debo admitir Python y debería usar Python.
Entonces, ¿para qué sirve WASM WASI? En este momento, su principal valor radica en ser un paso hacia la ejecución de código en el navegador o en sistemas integrados.
En la Regla 1, mencioné de pasada los “objetivos del sistema operativo”. Profundicemos en los objetivos de Rust: información esencial no solo para WASM WASI, sino también para el desarrollo general de Rust.
En mi máquina Windows, puedo compilar un proyecto Rust para ejecutarlo en Linux o macOS. De manera similar, desde una máquina Linux, puedo compilar un proyecto Rust para Windows o macOS. Estos son los comandos que uso para agregar y verificar el destino de Linux en una máquina con Windows:
rustup target add x86_64-unknown-linux-gnu
cargo check --target x86_64-unknown-linux-gnu
Aparte: mientras
cargo checkverifica que el código se compila; la creación de un ejecutable completamente funcional requiere herramientas adicionales. Para realizar una compilación cruzada de Windows a Linux (GNU), también necesitará instalar el compilador Linux GNU C/C++ y la cadena de herramientas correspondiente. Eso puede ser complicado. Afortunadamente, para los objetivos WASM que nos interesan, la cadena de herramientas necesaria es fácil de instalar.
Para ver todos los objetivos que admite Rust, use el comando:
rustc --print target-list
Enumerará más de 200 objetivos, incluidos x86_64-unknown-linux-gnu, wasm32-wasip1y wasm32-unknown-unknown.
Los nombres de destino contienen hasta cuatro partes: familia de CPU, proveedor, sistema operativo y entorno (por ejemplo, GNU vs LVMM):
Ahora que entendemos algo sobre los objetivos, sigamos adelante e instalemos el que necesitamos para WASM WASI.
Para ejecutar nuestro código Rust en WASM fuera de un navegador, debemos apuntar wasm32-wasip1 (WebAssembly de 32 bits con WASI Preview 1). También instalaremos WASMTIME, un tiempo de ejecución que nos permite ejecutar módulos WebAssembly fuera del navegador, usando WASI.
rustup target add wasm32-wasip1
cargo install wasmtime-cli
Para probar nuestra configuración, creemos un nuevo mensaje “¡Hola, WebAssembly!” Proyecto Rust usando cargo new. Esto inicializa un nuevo paquete Rust:
cargo new hello_wasi
cd hello_wasi
Editar src/main.rs leer:
fn main() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}
Aparte: profundizaremos en el
#[cfg(...)]atributo, que permite la compilación condicional, en la Regla 4.
Ahora, ejecute el proyecto con cargo runy deberías ver Hello, world! impreso en la consola.
A continuación, cree un .cargo/config.toml archivo, que especifica cómo Rust debe ejecutar y probar el proyecto cuando apunta a WASM WASI.
[target.wasm32-wasip1]
runner = "wasmtime run --dir ."
Aparte: esto
.cargo/config.tomlEl archivo es diferente del principal.Cargo.tomlarchivo, que define las dependencias y los metadatos de su proyecto.
Ahora, si dices:
cargo run --target wasm32-wasip1
deberías ver Hello, WebAssembly!. ¡Felicidades! Acaba de ejecutar con éxito algún código Rust en el entorno WASM WASI similar a un contenedor.
Ahora investiguemos #[cfg(...)]—Una herramienta esencial para compilar código condicionalmente en Rust. En la Regla 3, vimos:
fn main() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}
El #[cfg(...)] Las líneas le indican al compilador de Rust que incluya o excluya ciertos elementos de código según condiciones específicas. Un “elemento de código” se refiere a una unidad de código como una función, declaración o expresión.
Con #[cfg(…)] líneas, puede compilar condicionalmente su código. En otras palabras, puedes crear diferentes versiones de tu código para diferentes situaciones. Por ejemplo, al compilar para el wasm32 objetivo, el compilador ignora el #[cfg(not(target_arch = "wasm32"))] bloque y solo incluye lo siguiente:
fn main() {
println!("Hello, WebAssembly!");
}
Las condiciones se especifican mediante expresiones, por ejemplo, target_arch = "wasm32". Las claves admitidas incluyen target_os y target_arch. Consulte la referencia de óxido para la lista completa de claves admitidas. También puedes crear expresiones con funciones de Cargo, que aprenderemos en la Regla 6.
Puedes combinar expresiones con los operadores lógicos. not, anyy all. La compilación condicional de Rust no utiliza la versión tradicional. if...then...else declaraciones. En su lugar, debes utilizar #[cfg(...)] y su negación para manejar diferentes casos:
#[cfg(not(target_arch = "wasm32"))]
...
#[cfg(target_arch = "wasm32")]
...
Para compilar condicionalmente un archivo completo, coloque #![cfg(...)] en la parte superior del archivo. (Observe el “!”). Esto es útil cuando un archivo solo es relevante para un objetivo o configuración específicos.
También puedes usar cfg expresiones en Cargo.toml para incluir condicionalmente dependencias. Esto le permite adaptar las dependencias a diferentes objetivos. Por ejemplo, esto dice “depende del criterio con Rayón cuando no apuntas wasm32”.
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.5.1", features = ["rayon"] }
Aparte: Para obtener más información sobre el uso
cfgexpresiones enCargo.tomlmira mi artículo: Nueve Rust Cargo.toml Wats y Wat Nots: Domine las reglas de formato de Cargo.toml y evite la frustración | Hacia la ciencia de datos (medium.com).
Es hora de intentar correr su proyecto sobre WASM WASI. Como se describe en la Regla 3, cree un .cargo/config.toml archivo para su proyecto. Le indica a Cargo cómo ejecutar y probar su proyecto en WASM WASI.
[target.wasm32-wasip1]
runner = "wasmtime run --dir ."
Próximo, su proyecto, como todo buen código, ya debería contener pruebas. Mi range-set-blaze El proyecto incluye, por ejemplo, esta prueba:
#[test]
fn insert_255u8() {
let range_set_blaze = RangeSetBlaze::<u8>::from_iter([255]);
assert!(range_set_blaze.to_string() == "255..=255");
}
Intentemos ahora ejecutar las pruebas de su proyecto en WASM WASI. Utilice el siguiente comando:
cargo test --target wasm32-wasip1
Si esto funciona, es posible que haya terminado, pero probablemente no funcione. Cuando me pruebo esto range-set-blazerecibo este mensaje de error que se queja sobre el uso de Rayon en WASM.
error: Rayon cannot be used when targeting wasi32. Try disabling default features.
--> C:\Users\carlk\.cargo\registry\src\index.crates.io-6f17d22bba15001f\criterion-0.5.1\src\lib.rs:31:1
|
31 | compile_error!("Rayon cannot be used when targeting wasi32. Try disabling default features.");
Para corregir este error, primero debemos comprender las características de Cargo.
Para resolver problemas como el error Rayon en la Regla 5, es importante comprender cómo funcionan las funciones de Cargo.
En Cargo.tomlun opcional [features] La sección le permite definir diferentes configuraciones o versiones de su proyecto dependiendo de qué funciones estén habilitadas o deshabilitadas. Por ejemplo, aquí hay una parte simplificada del Cargo.toml archivo de la Proyecto de evaluación comparativa de criterios:
[features]
default = ["rayon", "plotters", "cargo_bench_support"]
rayon = ["dep:rayon"]
plotters = ["dep:plotters"]
html_reports = []
cargo_bench_support = [][dependencies]
#...
# Optional dependencies
rayon = { version = "1.3", optional = true }
plotters = { version = "^0.3.1", optional = true, default-features = false, features = [
"svg_backend",
"area_series",
"line_series",
] }
Esto define cuatro características de Cargo: rayon, plotters, html_reportsy cargo_bench_support. Dado que cada característica puede incluirse o excluirse, estas cuatro características crean 16 configuraciones posibles del proyecto. Tenga en cuenta también la característica especial de carga predeterminada.
Una función Cargo puede incluir otras funciones Cargo. En el ejemplo, el especial default La función Cargo incluye otras tres funciones Cargo: rayon, plottersy cargo_bench_support.
Una característica de Cargo puede incluir una dependencia. El rayon La característica de carga anterior incluye el rayon crate como un paquete dependiente.
Además, los paquetes dependientes pueden tener sus propias características Cargo. Por ejemplo, el plotters La característica de carga anterior incluye el plotters paquete dependiente con las siguientes funciones de carga habilitadas: svg_backend, area_seriesy line_series.
Puede especificar qué funciones de Cargo habilitar o deshabilitar cuando se ejecuta cargo check, cargo build, cargo runo cargo test. Por ejemplo, si está trabajando en el proyecto Criterion y desea verificar solo el html_reports característica sin ningún valor predeterminado, puede ejecutar:
cargo check --no-default-features --features html_reports
Este comando le indica a Cargo que no incluya ninguna característica de Cargo de forma predeterminada, sino que habilite específicamente la html_reports Característica de carga.
Dentro de su código Rust, puede incluir/excluir elementos de código según las funciones de Cargo habilitadas. La sintaxis utiliza #cfg(…)según la Regla 4:
#[cfg(feature = "html_reports")]
SOME_CODE_ITEM
Con esta comprensión de las características de Cargo, ahora podemos intentar solucionar el problema. Rayon error que encontramos al ejecutar pruebas en WASM WASI.
Cuando intentamos correr cargo test --target wasm32-wasip1parte del mensaje de error decía: Criterion ... Rayon cannot be used when targeting wasi32. Try disabling default features. Esto sugiere que deberíamos desactivar Criterion. rayon Función de carga al apuntar a WASM WASI.
Para hacer esto, necesitamos hacer dos cambios en nuestro Cargo.toml. Primero, necesitamos desactivar el rayon característica de Criterion en el [dev-dependencies] sección. Entonces, esta configuración inicial:
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
se convierte en esto, donde desactivamos explícitamente las funciones predeterminadas para Criterion y luego habilitamos todas las funciones de Cargo excepto rayon.
[dev-dependencies]
criterion = { version = "0.5.1", features = [
"html_reports",
"plotters",
"cargo_bench_support"],
default-features = false }
A continuación, para garantizar rayon todavía se usa para objetivos que no son WASM, lo volvemos a agregar con una dependencia condicional en Cargo.toml como sigue:
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.5.1", features = ["rayon"] }
En general, al apuntar a WASM WASI, es posible que deba modificar sus dependencias y sus características de Cargo para garantizar la compatibilidad. A veces este proceso es sencillo, pero otras veces puede resultar desafiante o incluso imposible, como veremos en la Regla 8.
Aparte: en el próximo artículo de esta serie, sobre WASM en el navegador, profundizaremos en las estrategias para solucionar dependencias.
Después de ejecutar las pruebas nuevamente, superamos el error anterior, solo para encontrar uno nuevo, ¡que es progreso!
#[test]
fn test_demo_i32_len() {
assert_eq!(demo_i32_len(i32::MIN..=i32::MAX), u32::MAX as usize + 1);
^^^^^^^^^^^^^^^^^^^^^ attempt to compute
`usize::MAX + 1_usize`, which would overflow
}
El compilador se queja de que u32::MAX as usize + 1 se desborda. En Windows de 64 bits, la expresión no se desborda porque usize es lo mismo que u64 y puede sostener u32::MAX as usize + 1. WASM, sin embargo, es un entorno de 32 bits, por lo que usize es lo mismo que u32 y la expresión es demasiado grande.
La solución aquí es reemplazar usize con u64asegurándose de que la expresión no se desborde. En términos más generales, el compilador no siempre detecta estos problemas, por lo que es importante revisar el uso de usize y isize. Si te refieres al tamaño o índice de una estructura de datos de Rust, usize es correcto. Sin embargo, si trabaja con valores que exceden los límites de 32 bits, debe usar u64 o i64.
Aparte: en un entorno de 32 bits, una matriz Rust,
Vec,BTreeSetetc., solo puede contener hasta 2³²−1=4,294,967,295 elementos.
Entonces, solucionamos el problema de dependencia y abordamos un usize rebosar. ¿Pero podemos arreglarlo todo? Lamentablemente, la respuesta es no.
WASM WASI Preview 1 (la versión actual) admite el acceso a archivos (dentro de un directorio específico), la lectura de variables de entorno y el trabajo con tiempo y números aleatorios. Sin embargo, sus capacidades son limitadas en comparación con lo que cabría esperar de un sistema operativo completo.
Si su proyecto requiere acceso a redes, tareas asincrónicas con Tokio o subprocesos múltiples con Rayon, desafortunadamente, estas funciones no son compatibles con la Vista previa 1.
Afortunadamente, se espera que WASM WASI Preview 2 mejore estas limitaciones y ofrezca más funciones, incluido un mejor soporte para redes y posiblemente tareas asincrónicas.
Entonces, sus pruebas pasan WASM WASI y su proyecto se ejecuta exitosamente. ¿Terminaste? No exactamente. Porque, como me gusta decir:
Si no está en CI, no existe.
La integración continua (CI) es un sistema que puede ejecutar automáticamente sus pruebas cada vez que actualiza su código, asegurando que su código continúe funcionando como se espera. Al agregar WASM WASI a su CI, puede garantizar que los cambios futuros no romperán la compatibilidad de su proyecto con el objetivo WASM WASI.
En mi caso, mi proyecto está alojado en GitHub y uso GitHub Actions como mi sistema de CI. Aquí está la configuración que agregué .github/workflows/ci.yml para probar mi proyecto en WASM WASI:
test_wasip1:
name: Test WASI P1
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: wasm32-wasip1
- name: Install Wasmtime
run: |
curl https://wasmtime.dev/install.sh -sSf | bash
echo "${HOME}/.wasmtime/bin" >> $GITHUB_PATH
- name: Run WASI tests
run: cargo test --verbose --target wasm32-wasip1
Al integrar WASM WASI en CI, puedo agregar código nuevo a mi proyecto con confianza. CI probará automáticamente que todo mi código siga siendo compatible con WASM WASI en el futuro.