Compilando Rust para WebAssembly e importando via Javascript

Olá Pessoal 🖖 Este artigo tem intuíto de apresentar conceitos iniciais sobre WebAssembly e como utilizar Rust para compilar para binário do WebAssembly. Após compilado o WebAssembly, iremos importá-lo em Javascript.

Iremos escrever a estrutura de dados fila e importá-la em Javascript, no entanto, iremos adotar uma implementação simples, já que o intuito é entender o processo de compilação/importação de WebAssembly. Caso o leitor procure um exemplo mais complexo, tenho essa implementação, epqueue, que inspirou esse post.

O que é e para quê serve WebAssembly?

WebAssembly(Wasm) é um conjunto de especificações que definem um formato de código binário(machine code), semelhante ao Assembly, e também define uma maquina virtual para executar esse código binário. Ambos, maquina virtual e código binário, tem intuíto de serem rápidos e exigirem pouco espaço de armazenamento.

Atualmente, Wasm é suportado pela maiorias do navegadores e muitas linguagem tem ferramentas para compilar código fonte para Wasm. Dessa forma, é possível rodar no browser código escrito em qualquer linguagem, basta compila-la para Wasm e importa-la no navegador.

Nesse artigo, iremos utilizar a linguagem Rust, que é uma linguagem que suporta muito bem Wasm, e compila para um código muito eficiente. No entanto, não será aprofundado em features/sintaxe do Rust, para não confundir o leitor.

Ferramentas necessárias

Será utilizado a ferramenta Cargo do Rust, para criar a estrutura inicial do nosso projeto e adicionar as dependências necessárias. Para realizar o build do código utilizaremos wasm-pack e wasm_bindgen. Além de serem úteis para fazer o build da nossa aplicação, essas ferramentas também são úteis para contornar algumas limitações do WebAssembly.

Setup e estrutura inicial

Para instalar o Cargo e o compilador Rust recomendo seguir o tutorial da pagina oficial do Rust, Install Rust. De forma simplificada, essa instalação pode ser feita executando o seguinte comando:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Para instalar wasm-pack, também há um tutorial na pagina oficial, Install wasm-pack. Para usuários de linux, atualmente, recomenda-se executar o comando:

 curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Após isso, iremos criar a estrutura inicial do nosso exemplo:

# Cria o diretório wasm-demo com estrutura inicial
cargo init --lib wasm-demo

Com estrutura inicial criada, iremos colar a seguinte configuração no arquivo Cargo.toml, que indica as dependências utilizadas.

[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2.63"
js-sys = "0.3.61"

Com isso, obtemos a seguinte estrutura:

wasm-demo
├── Cargo.toml
└── src
    └── lib.rs

Para verificar se tudo foi instalado corretamente, pode se executar o seguinte comando:

wasm-pack build --target nodejs

Ele irá gerar o build da aplicação em Wasm, no diretório pkg.

Implementação em Rust

Agora iremos implementar o código da nossa fila. O primeiro passo é definirmos em src/lib.rs uma estrutura que irá representar a fila:

use wasm_bindgen::prelude::JsValue;

pub struct Queue {
    vec: Vec<JsValue>
}

A estrutura possui um campo chamado vec, que armazena um vetor, seus elementos são do tipo JsValue, que representa um objeto Javascript. Para indicar ao wasm_bindgen que queremos importar essa estrutura como uma classe JS, iremos adicionar a seguinte macro:

use wasm_bindgen::prelude::{JsValue, wasm_bindgen};

#[wasm_bindgen]
pub struct Queue {
    vec: Vec<JsValue>
}

Agora iremos implementar o construtor da nossa estrutura:


// trecho anterior omitido

impl Queue {
    pub fn new() -> Queue {
        Queue {
            vec: Vec::new()
        }
    }
}   

Isto indica ao Rust como criar uma instância da estrutura, para indicar que essa estrutura pode ser construída via Javascript, fazemos a seguinte alteração:

#[wasm_bindgen]
impl Queue {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Queue {
        Queue {
            vec: Vec::new()
        }
    }
}

Com a notação wasm_bindgen(constructor), indicamos que Queue pode ser criada em Javascript utilizando a keyword new. Por fim, iremos implementar os métodos push e pop da fila:

#[wasm_bindgen]
impl Queue {

    // trecho anterior omitido

    pub fn push(&mut self, element: JsValue) {
        self.vec.push(element);
    }

    pub fn pop(&mut self) -> JsValue {
        self.vec.remove(0)
    }
}

Finalizamos nossa implementação, após essas adições o código do exemplo se encontra neste gist.

Compilando o código para WebAssembly

Finalizado a implementação, iremos compilar nosso código para WebAssembly e gerar um pacote Javascript. Para isso, execute o comando:

wasm-pack build --target nodejs

Com isso, é criado o diretório pkg, que contém a resultado do processo de build.

pkg
├── package.json
├── wasm_demo_bg.wasm
├── wasm_demo_bg.wasm.d.ts
├── wasm_demo.d.ts
└── wasm_demo.js

O arquivo wasm_demo_bg.wasm, é o código binário Wasm. Enquanto o arquivo wasm_demo.js é responsável por importar o binário e expor a estrutura que definimos em Rust, Queue.

Agora, podemos importar em Javascript e utilizar nosso pacote Wasm. Para importar e usar o pacote, basta no diretório wasm-demo, abrir o REPL do node, digitando node, e executar os comandos do exemplo a seguir:

// Importando pacote e criando nova fila
const {Queue} = require('./pkg');
let queue = new Queue();

// Inserindo elementos
queue.push('primeiro elemento');
queue.push(['segundo elemento', 'do tipo array']);
queue.push({'msg': 'terceiro elemento do tipo JSON'});

// Imprimindo elementos
console.log(queue.pop()); // imprime: primeiro elemento
console.log(queue.pop()); // imprime: [ 'segundo elemento', 'do tipo array' ]
console.log(queue.pop()); // imprime: { msg: 'terceiro elemento do tipo JSON' }

Como o diretório pkg trata-se de um pacote JS, ele pode ser utilizado não somente como citado anteriormente, mas também pode ser publicado no npm ou instalado em outro projeto local, usando npm install ./pkg.

Referências

Espero que o artigo tenha causado interesse no leitor sobre o tema. Caso sim, deixo aqui uma lista de referências para aprofundar o assunto:

Dúvidas e sugestões são bem-vindas. Obrigado, aos leitores 🙏

muito bom, explicou muito bem, eu pretendo depois aprender mais sobre o webassemby quando ele ficar mais "estável" na web, e tiver mais ferramentas para a geração do binário, gostei muito do seu artigo 👍