Como usar WASM em aplicações server-side com Wasmtime

1. Introdução ao WebAssembly (WASM) no lado servidor

O WebAssembly (WASM) foi originalmente concebido para executar código de alto desempenho em navegadores, mas seu potencial vai muito além do frontend. Hoje, o WASM está revolucionando o desenvolvimento server-side ao oferecer um ambiente de execução portátil, seguro e eficiente para carregar código de terceiros ou módulos especializados sem comprometer a estabilidade do servidor principal.

As vantagens de executar WASM no servidor incluem:
- Isolamento: cada módulo executa em uma sandbox, sem acesso direto ao sistema operacional.
- Portabilidade: binários compilados para WASM rodam em qualquer arquitetura (x86, ARM) sem recompilação.
- Segurança: o runtime controla explicitamente quais recursos o módulo pode acessar.
- Leveza: ao contrário de containers completos, módulos WASM iniciam em milissegundos.

No ecossistema server-side, três runtimes se destacam: Wasmer, WasmEdge e Wasmtime. Optamos pelo Wasmtime por ser o runtime de referência mantido pela Bytecode Alliance, com suporte nativo ao padrão WASI (WebAssembly System Interface) e integração simplificada com linguagens como Rust, C, Python e Go.

2. Configuração do ambiente de desenvolvimento com Wasmtime

Instalação do Wasmtime CLI

Para instalar o Wasmtime via linha de comando, execute:

curl https://wasmtime.dev/install.sh -sSf | bash

Ou, se preferir gerenciadores de pacotes:

# macOS (Homebrew)
brew install wasmtime

# Linux (via pacotes oficiais)
sudo apt install wasmtime

Compilando um módulo WASM simples em Rust

Primeiro, crie um projeto Rust que compile para WASM:

cargo new --lib modulo_saudacao
cd modulo_saudacao

Edite Cargo.toml para adicionar:

[lib]
crate-type = ["cdylib"]

[dependencies]

No arquivo src/lib.rs, escreva:

#[no_mangle]
pub extern "C" fn saudacao(nome: *const u8, tamanho: usize) -> *mut u8 {
    let nome_str = unsafe {
        std::slice::from_raw_parts(nome, tamanho)
    };
    let nome = String::from_utf8_lossy(nome_str);
    let mensagem = format!("Olá, {}! Bem-vindo ao WASM server-side.", nome);

    // Retorna ponteiro para a mensagem (simplificado)
    let bytes = mensagem.into_bytes();
    let ptr = bytes.as_ptr() as *mut u8;
    std::mem::forget(bytes); // Evita que o Rust desaloque a memória
    ptr
}

Compile para WASM:

rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release

O arquivo gerado estará em target/wasm32-wasi/release/modulo_saudacao.wasm.

Executando via linha de comando

wasmtime run modulo_saudacao.wasm --invoke saudacao

3. Integração do Wasmtime em aplicações server-side (exemplo em Rust)

Crie um projeto Rust para o servidor:

cargo new servidor_wasm
cd servidor_wasm

Adicione no Cargo.toml:

[dependencies]
wasmtime = "14.0"
tokio = { version = "1", features = ["full"] }

No src/main.rs, implemente o runtime básico:

use wasmtime::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Configura o engine Wasmtime
    let engine = Engine::default();
    let module = Module::from_file(&engine, "modulo_saudacao.wasm")?;

    // Cria um store com limites de memória
    let mut store = Store::new(&engine, ());
    store.limiter(|state| {
        // Limita a 10 MB de memória
        state.set_memory_limit(10 * 1024 * 1024);
    });

    // Instancia o módulo
    let instance = Instance::new(&mut store, &module, &[])?;

    // Obtém a função exportada
    let saudacao = instance.get_typed_func::<(i32, i32), i32, _>(&mut store, "saudacao")?;

    // Prepara os parâmetros (ponteiro e tamanho da string)
    let nome = "Maria";
    let nome_ptr = 0; // Endereço simplificado na memória linear
    let nome_len = nome.len() as i32;

    // Chama a função
    let resultado_ptr = saudacao.call(&mut store, (nome_ptr, nome_len))?;
    println!("Função WASM executada. Resultado no endereço: {}", resultado_ptr);

    Ok(())
}

4. Comunicação bidirecional: memória compartilhada e chamadas de host

Para ler o resultado da função WASM, precisamos acessar a memória linear do módulo:

// Após chamar a função, leia a memória
let memory = instance.get_memory(&mut store, "memory").unwrap();
let data = memory.data(&store);
let resultado = String::from_utf8_lossy(&data[resultado_ptr as usize..]);
println!("Resultado: {}", resultado);

Implementando funções de host (host functions)

Vamos criar uma função que permite ao módulo WASM fazer uma requisição HTTP:

No servidor Rust:

use wasmtime::*;
use reqwest;

fn criar_host_functions(engine: &Engine) -> Vec<Extern> {
    let http_get = Func::wrap(engine, |url_ptr: i32, url_len: i32| -> i32 {
        // Implementação simplificada
        println!("Host function chamada com url_ptr={}, url_len={}", url_ptr, url_len);
        0
    });

    vec![http_get.into()]
}

No módulo WASM (Rust):

extern "C" {
    fn http_get(url_ptr: *const u8, url_len: usize) -> i32;
}

#[no_mangle]
pub extern "C" fn processar_dados() -> i32 {
    let url = "https://api.exemplo.com/dados";
    unsafe {
        http_get(url.as_ptr(), url.len());
    }
    0
}

5. Isolamento e segurança: sandboxing de código WASM

O Wasmtime oferece controle granular sobre recursos:

use wasmtime::*;
use std::time::Duration;

let mut config = Config::new();
config.wasm_multi_value(true);
config.wasm_reference_types(true);

let engine = Engine::new(&config)?;

let mut store = Store::new(&engine, ());

// Configura limites
store.limiter(|state| {
    state.set_memory_limit(50 * 1024 * 1024); // 50 MB
    state.set_table_limit(1000);
});

// Tempo máximo de execução
store.set_epoch_deadline(1);
// Em outro thread, avance o relógio
std::thread::spawn(|| {
    std::thread::sleep(Duration::from_secs(5));
    store.set_epoch_deadline(0);
});

Para controlar acesso a sistema de arquivos e rede, utilize o WASI:

let wasi_ctx = wasmtime_wasi::WasiCtxBuilder::new()
    .inherit_stdio()
    .preopened_dir("/tmp", "/tmp")?
    .env("ALLOWED_HOSTS", "api.exemplo.com")?
    .build();

let mut store = Store::new(&engine, wasi_ctx);

6. Otimização de desempenho e cache de módulos WASM

Compilação JIT vs AOT

Por padrão, o Wasmtime usa compilação JIT (Just-In-Time). Para compilação AOT (Ahead-Of-Time), serialize o módulo compilado:

// Compila e serializa
let module = Module::new(&engine, &wasm_bytes)?;
let compiled = module.serialize()?;
std::fs::write("modulo.cwasm", compiled)?;

// Carrega sem recompilar
let module = unsafe { Module::deserialize(&engine, &compiled_bytes)? };

Estratégias de cache

Implemente um cache baseado em hash do módulo:

use std::collections::HashMap;
use std::sync::Mutex;

struct WasmCache {
    cache: Mutex<HashMap<String, Vec<u8>>>,
}

impl WasmCache {
    fn get_or_compile(&self, path: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let hash = sha256::digest(std::fs::read(path)?);
        let mut cache = self.cache.lock().unwrap();

        if let Some(compiled) = cache.get(&hash) {
            return Ok(compiled.clone());
        }

        let engine = Engine::default();
        let module = Module::from_file(&engine, path)?;
        let compiled = module.serialize()?;
        cache.insert(hash, compiled.clone());
        Ok(compiled)
    }
}

7. Casos de uso reais e integração com frameworks server-side

Plugins dinâmicos em servidores web (Axum)

use axum::{Router, routing::post, Json};
use serde::{Deserialize, Serialize};
use wasmtime::*;

#[derive(Deserialize)]
struct PluginRequest {
    nome_plugin: String,
    entrada: String,
}

#[derive(Serialize)]
struct PluginResponse {
    resultado: String,
}

async fn executar_plugin(Json(payload): Json<PluginRequest>) -> Json<PluginResponse> {
    let engine = Engine::default();
    let module_path = format!("plugins/{}.wasm", payload.nome_plugin);
    let module = Module::from_file(&engine, &module_path).unwrap();

    let mut store = Store::new(&engine, ());
    let instance = Instance::new(&mut store, &module, &[]).unwrap();

    let func = instance.get_typed_func::<(i32, i32), i32, _>(&mut store, "processar").unwrap();
    let resultado = func.call(&mut store, (0, payload.entrada.len() as i32)).unwrap();

    Json(PluginResponse {
        resultado: format!("Processado: {}", resultado),
    })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/plugin", post(executar_plugin));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

8. Desafios, limitações e melhores práticas

Limitações atuais

  • I/O assíncrono nativo: WASI preview 1 não suporta async I/O diretamente. WASI preview 2 (em desenvolvimento) trará esse suporte.
  • Threading: WASM não possui suporte nativo a threads no servidor, embora existam propostas como o proposal de threads.
  • Debugging: ferramentas como wasmtime debug e integração com GDB/LDB estão amadurecendo.

Melhores práticas

  1. Sempre limite recursos: configure memória máxima, tempo de execução e tabelas.
  2. Valide entradas: sanitize dados passados para funções WASM.
  3. Use cache de módulos: evite recompilar módulos frequentemente usados.
  4. Prefira WASI para I/O: evite passar ponteiros de memória bruta sempre que possível.
  5. Monitore e profile: utilize ferramentas como perf e integração com OpenTelemetry.

Roadmap do Wasmtime

O Wasmtime está evoluindo rapidamente. As principais novidades futuras incluem:
- WASI preview 2: suporte nativo a async I/O, streams e componentes.
- Component Model: permitirá composição de módulos WASM como componentes reutilizáveis.
- Melhorias de desempenho: compilação AOT otimizada e suporte a SIMD.


Referências