Ejecutar Rust en Wippy

Compila un componente WebAssembly en Rust, regístralo en Wippy y exponlo mediante entradas de función, CLI y HTTP.

Clasificación: Tutorial ejecutable con un toolchain externo de componentes Rust. La página incluye WIT, la implementación Rust, el registro Wippy, el flujo del hash de integridad, los comandos, los resultados esperados y las comprobaciones de fallos.

Qué Vamos a Construir

Un componente Rust con cuatro funciones exportadas:

  • greet — Recibe un nombre y devuelve un saludo
  • add — Suma dos enteros
  • fibonacci — Calcula el enésimo número de Fibonacci
  • list-files — Enumera archivos de un directorio montado

Expondremos estas como funciones invocables, un comando CLI y un endpoint HTTP.

Prerrequisitos

  • El runtime Wippy v0.3.32a.
  • Rust toolchain con el target wasm32-wasip1.
  • Un toolchain de C funcional. En Linux, cargo-component también requiere las bibliotecas de desarrollo de OpenSSL.
  • cargo-component 0.21.1, la versión utilizada en este tutorial.
rustup target add wasm32-wasip1
cargo install cargo-component --version 0.21.1 --locked

Crea el scaffold generado del componente y los directorios de Wippy:

mkdir rust-wasm-demo
cd rust-wasm-demo
cargo component new --lib demo
mkdir -p app/src/demo/wasm

En PowerShell:

New-Item -ItemType Directory -Path rust-wasm-demo
Set-Location rust-wasm-demo
cargo component new --lib demo
New-Item -ItemType Directory -Path app\src\demo\wasm -Force

cargo component new escribe un Cargo.toml, src/lib.rs y un archivo WIT compatibles, y más adelante regenera src/bindings.rs. Mantén la versión generada de wit-bindgen-rt emparejada con la de cargo-component: la herramienta describe esta interfaz como experimental y no garantiza la compatibilidad del código generado entre versiones.

Estructura del Proyecto

rust-wasm-demo/
├── demo/                    # Rust component
│   ├── Cargo.toml
│   ├── wit/
│   │   └── world.wit       # WIT interface
│   └── src/
│       ├── bindings.rs      # generated by cargo-component
│       └── lib.rs           # implementation
└── app/                     # Wippy application
    ├── wippy.lock
    └── src/
        ├── _index.yaml      # Infrastructure
        └── demo/
            ├── _index.yaml  # CLI processes
            └── wasm/
                ├── _index.yaml          # WASM entries
                └── demo_component.wasm  # Compiled binary

Paso 1: Crear la Interfaz WIT

WIT (WebAssembly Interface Types) define el contrato entre el host y el guest:

Crea demo/wit/world.wit:

package component:demo;

world demo {
    export greet: func(name: string) -> string;
    export add: func(a: s32, b: s32) -> s32;
    export fibonacci: func(n: u32) -> u64;
    export list-files: func(path: string) -> string;
}

Cada export se convierte en una función que Wippy puede invocar.

Paso 2: Implementar en Rust

Conserva el demo/Cargo.toml generado. Los metadatos de su paquete deben apuntar a component:demo, igual que el paquete WIT, y el tipo de crate de la biblioteca debe seguir siendo cdylib.

Crea demo/src/lib.rs:

#[allow(warnings)]
mod bindings;

use bindings::Guest;

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello, {}!", name)
    }

    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn fibonacci(n: u32) -> u64 {
        if n <= 1 {
            return n as u64;
        }
        let (mut a, mut b) = (0u64, 1u64);
        for _ in 2..=n {
            let next = a + b;
            a = b;
            b = next;
        }
        b
    }

    fn list_files(path: String) -> String {
        let mut result = String::new();
        match std::fs::read_dir(&path) {
            Ok(entries) => {
                for entry in entries {
                    match entry {
                        Ok(e) => {
                            let name = e.file_name().to_string_lossy().to_string();
                            let meta = e.metadata();
                            let (kind, size) = match meta {
                                Ok(m) => {
                                    let kind = if m.is_dir() { "dir" } else { "file" };
                                    (kind, m.len())
                                }
                                Err(_) => ("?", 0),
                            };
                            let line = format!("{:<6} {:>8}  {}", kind, size, name);
                            println!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                        Err(e) => {
                            let line = format!("error: {}", e);
                            eprintln!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                    }
                }
            }
            Err(e) => {
                let line = format!("cannot read {}: {}", path, e);
                eprintln!("{}", line);
                result.push_str(&line);
                result.push('\n');
            }
        }
        result
    }
}

bindings::export!(Component with_types_in bindings);

El módulo bindings es generado por cargo-component a partir de la definición WIT.

Paso 3: Compilar el Componente

cd demo
cargo component build --release

Esto produce target/wasm32-wasip1/release/demo.wasm. Cópialo a tu aplicación Wippy:

mkdir -p ../app/src/demo/wasm
cp target/wasm32-wasip1/release/demo.wasm ../app/src/demo/wasm/demo_component.wasm

En PowerShell:

New-Item -ItemType Directory -Path ..\app\src\demo\wasm -Force
Copy-Item -LiteralPath target\wasm32-wasip1\release\demo.wasm `
  -Destination ..\app\src\demo\wasm\demo_component.wasm

Obtiene el hash SHA-256 para verificación de integridad:

sha256sum ../app/src/demo/wasm/demo_component.wasm

En PowerShell, usa:

(Get-FileHash ..\app\src\demo\wasm\demo_component.wasm -Algorithm SHA256).Hash.ToLowerInvariant()

Copia los 64 caracteres hexadecimales en minúsculas en cada YOUR_HASH_HERE de abajo. El campo final debe tener la forma sha256:<64-hex-characters>; es el hash del binario copiado, no del código Rust ni de la ruta de compilación original.

Paso 4: Aplicación Wippy

Infraestructura

Crea app/src/_index.yaml:

version: "1.0"
namespace: demo

entries:
  - name: gateway
    kind: http.service
    meta:
      comment: HTTP server
    addr: ":8090"
    lifecycle:
      auto_start: true

  - name: api
    kind: http.router
    meta:
      comment: Public API router
      server: demo:gateway
    prefix: /

  - name: processes
    kind: process.host
    lifecycle:
      auto_start: true

  - name: terminal
    kind: terminal.host
    lifecycle:
      auto_start: true

  - name: policy
    kind: security.policy
    meta:
      comment: Grants access to mounted filesystems and WASM functions
    policy:
      actions:
        - fs.get
        - funcs.call
      resources: "*"
      effect: allow

Montar un sistema de archivos en un módulo WASM y llamar a una función WASM son ambas acciones controladas. La política las concede; las entradas que las necesitan la referencian.

Funciones WASM

Crea app/src/demo/wasm/_index.yaml:

version: "1.0"
namespace: demo.wasm

entries:
  - name: assets
    kind: fs.directory
    meta:
      comment: Filesystem with WASM binaries
    directory: ./src/demo/wasm

  - name: greet_function
    kind: function.wasm
    meta:
      comment: Greet function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    pool:
      type: inline

  - name: add_function
    kind: function.wasm
    meta:
      comment: Add function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: add
    pool:
      type: inline

  - name: fibonacci_function
    kind: function.wasm
    meta:
      comment: Fibonacci function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: fibonacci
    pool:
      type: inline

Puntos clave:

  • Una sola entrada fs.directory proporciona el binario WASM
  • Múltiples funciones referencian el mismo binario con diferentes valores de method
  • El campo hash verifica la integridad del binario al momento de carga
  • El pool inline serializa las llamadas mediante una instancia caliente. Restablece el estado de ejecución de cada llamada entre llamadas síncronas; usa otro tipo de pool si necesitas workers concurrentes.

Funciones con WASI

La función list-files accede al sistema de archivos, por lo que necesita imports WASI:

  - name: list_files_function
    kind: function.wasm
    meta:
      comment: Filesystem listing with WASI mounts
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data
    pool:
      type: inline

La sección wasi.mounts mapea una entrada del sistema de archivos de Wippy a una ruta del guest. Dentro del módulo WASM, /data apunta al directorio demo.wasm:assets.

Comandos CLI

Crea app/src/demo/_index.yaml:

version: "1.0"
namespace: demo.cli

entries:
  - name: wasm_cli_policy
    kind: security.policy
    policy:
      actions:
        - fs.get
      resources:
        - demo.wasm:assets
      effect: allow

  - name: ls
    kind: process.wasm
    meta:
      comment: List files from mounted WASI filesystem
      command:
        name: ls
        short: List files from mounted directory
        security:
          actor: {id: demo.cli:ls}
          policies: [demo:policy]
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data

El bloque meta.command registra el proceso como un comando CLI con nombre. El comando greet no necesita imports WASI ya que sólo usa operaciones de strings. El comando ls necesita acceso al sistema de archivos, por lo que además lleva el contexto de seguridad que concede el montaje.

Endpoint HTTP

Agrega a app/src/demo/wasm/_index.yaml:

  - name: http_greet
    kind: function.wasm
    meta:
      comment: Greet exposed via wasi-http transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    transport: wasi-http
    pool:
      type: inline

  - name: http_greet_endpoint
    kind: http.endpoint
    meta:
      comment: HTTP POST endpoint for WASM greet
      router: demo:api
    method: POST
    path: /greet
    func: http_greet

El transporte wasi-http mapea el contexto de solicitud/respuesta HTTP a los argumentos y resultados WASM.

Paso 5: Inicializar y Ejecutar

cd app
wippy init

Ejecutar Comandos CLI

# List available commands
wippy run list
Available commands:

  greet  Greet someone via WASM  (demo.cli:greet)
  ls  List files from mounted directory  (demo.cli:ls)

Run with: wippy run <command>

Los argumentos que siguen al nombre del comando se pasan a la función exportada como parámetros de tipo string, así que cada comando toma exactamente los argumentos que declara su firma WIT:

# Run greet
wippy run greet World
Hello, World!
# Run ls to list mounted directory
wippy run ls /data

El comando debe mostrar al menos demo_component.wasm con su tamaño de archivo y terminar con el estado 0. Wippy no imprime los payloads de retorno arbitrarios de process.wasm; por eso el ejemplo CLI usa la función Rust que escribe en stdout de WASI.

Ejecutar como Servicio

wippy run

Esto inicia el servidor HTTP en el puerto 8090. El transporte wasi-http pasa el cuerpo de la petición como el único argumento string de la función:

curl -X POST http://localhost:8090/greet -d 'World'
Hello, World!

Llamar desde Lua

Las funciones WASM se invocan de la misma manera que las funciones Lua. El proceso llamante necesita funcs.call sobre el objetivo, que demo:policy concede:

local funcs = require("funcs")

local greeting, err = funcs.call("demo.wasm:greet_function", "World")
-- greeting: "Hello, World!"

local sum, err = funcs.call("demo.wasm:add_function", 6, 7)
-- sum: 13

local fib, err = funcs.call("demo.wasm:fibonacci_function", 10)
-- fib: 55

Solución de problemas y limpieza

  • Si se desconoce cargo component, instálalo y vuelve a ejecutar cargo component build; cargo build por sí solo no genera los mismos bindings ni la salida de componente para esta configuración.
  • Es normal que falte src/bindings.rs antes de la primera compilación. Si falta después de cargo component build, no se pudo resolver el paquete WIT o los metadatos del componente; corrige ese error antes de copiar un binario.
  • WASM hash mismatch significa que el binario cambió después de calcular el digest documentado o que queda un placeholder. Vuelve a copiar el binario release, recalcula el digest y actualiza todas las entradas que lo referencian.
  • Un error al instanciar imports significa que el componente importa un perfil de host que la entrada no declara. Conserva wasi:cli, wasi:io, wasi:clocks y wasi:filesystem en los ejemplos del sistema de archivos.
  • cannot read /data significa que la ruta guest de wasi.mounts o su entrada del sistema de archivos no coincide con el registro.
  • Detén el runtime HTTP con Ctrl+C. La salida de Rust permanece en demo/target/; elimina ese directorio y el archivo .wasm copiado para limpiar los artefactos generados.

Siguientes pasos