BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos O Deno ama WebAssembly

O Deno ama WebAssembly

Favoritos

Pontos Principais

  • O Deno e o Node.js executam JavaScript em tempo de execução baseados em C/C++ para ter alto desempenho;

  • O Deno é uma aplicação binária incompatível com módulos NPM, e que não tem uma maneira fácil de incorporar módulos nativos nas aplicações;

  • O WebAssembly fornece uma maneira de executar código de alto desempenho nas aplicações Deno;

  • O WebAssembly é um container seguro, portátil e leve para aplicações do lado servidor;

  • As ferramentas do compilador Rust oferecem excelente suporte para o WebAssembly.

O aguardado projeto Deno finalmente atingiu sua versão 1.0! O Deno foi criado pelo desenvolvedor do Node.js, Ryan Dahl, para abordar o que chamou de "10 coisas que lamento sobre o Node.js".

O Deno acabou com o NPM e os reprováveis node_modules. Ele é um binário único que executa aplicações escritas em TypeScript e JavaScript.

No entanto, embora o TypeScript e o JavaScript sejam adequados para a maioria das aplicações web, eles podem ser inadequados para tarefas mais intensas, como treinamento e inferência de rede neural, aprendizado de máquina e criptografia. Na verdade, o próprio Node.js muitas vezes precisa de bibliotecas nativas para essas tarefas, como o OpenSSL, no caso de criptografia.

Sem um sistema semelhante ao NPM para incorporar módulos nativos, como podemos escrever aplicações do lado do servidor que requerem desempenho nativo no Deno? O WebAssembly está aqui para ajudar! Neste artigo, iremos escrever funções de alto desempenho usando o Rust, depois, vamos compilá-las em WebAssembly, e executá-las dentro da aplicação Deno.

TL;DR

Clone ou faça um fork deste modelo simples de projeto do Deno no GitHub. Siga as instruções e terá a primeira função WebAssembly (criada em Rust) em execução no Deno em apenas 5 minutos.

Um pouco de história

O Node.js é muito bem-sucedido porque deu aos desenvolvedores o melhor dos dois mundos: A facilidade de uso do JavaScript, especialmente para aplicações assíncronas baseadas em eventos, e o alto desempenho do C/C++. As aplicações Node.js são escritas em JavaScript, mas são executadas em um tempo de execução em C/C++ nativo, incluindo o mecanismo Google V8 JavaScript e muitos módulos de biblioteca nativa. O Deno pretende repetir esta fórmula para o sucesso, mas no processo, suporta uma pilha de tecnologia moderna com o TypeScript e o Rust.

O Deno é um runtime simples, moderno e seguro para JavaScript e TypeScript, que usa o V8, e é construído em Rust. O site do Deno é deno.land.

Na famosa apresentação, "10 coisas que lamento sobre o Node.js", o criador do Node.js, Ryan Dahl, explicou a razão para recomeçar e criar o Deno como um concorrente, ou até mesmo, como sendo um substituto do Node.js. Os arrependimentos de Dahl estão centrados em como o Node.js gerencia os códigos e os módulos de terceiros.

  • O complexo sistema de build para vincular módulos C ao Node.js;
  • As complexidades desnecessárias do package.json, node_modules, index.js e outros artefatos NPM.

Como resultado, o Deno faz algumas escolhas muito conscientes e opinativas para gerenciar as dependências.

  • O Deno é um único executável binário;
  • Os aplicativos são criados em TypeScript ou JavaScript com dependências explicitamente declaradas no código como instruções de import, com link da URL completa para o código-fonte da dependência;
  • O Deno não é compatível com módulos Node.js.

Mas e as aplicações que exigem alto desempenho, como aplicações de IA como serviço que precisam executar modelos de rede neural complexos em frações de segundos? No Deno e no Node.js, muitas funções são chamadas por meio de uma API TypeScript ou JavaScript, mas são executadas como código nativo criado em Rust ou C. No Node.js, sempre há a opção de chamar bibliotecas nativas de terceiros a partir do API JavaScript. Mas será que podemos fazer isso com Deno?

Suporte para WebAssembly no Deno

O WebAssembly é uma máquina virtual leve projetada para executar bytecodes portáteis próximo à velocidade nativa. Podemos compilar funções Rust ou C/C++ para os bytecodes WebAssembly e acessar essas funções a partir do TypeScript. Para algumas tarefas, pode ser muito mais rápido do que executar funções equivalentes criadas no próprio TypeScript. Por exemplo, este estudo da IBM descobriu que o Rust e o WebAssembly podem melhorar a velocidade de execução do Node.js em 1200% a 1500% para certos algoritmos de processamento de dados.

O Deno usa o motor V8 interno do Google. O V8 não é apenas um runtime em JavaScript, mas também uma máquina virtual WebAssembly, que é compatível com o Deno pronto para uso. O Deno fornece uma API para sua aplicação TypeScript para chamar funções em WebAssembly.

Na verdade, alguns componentes Deno populares já estão implementados no WebAssembly. O módulo sqlite em Deno é criado compilando o código-fonte C do sqlite em WebAssembly usando Emscripten. O componente WASI do Deno permite que as aplicações WebAssembly acessem os recursos internos do sistema operacional, como o sistema de arquivos. Neste artigo, vamos ensinar a escrever aplicações Deno de alto desempenho em Rust e WebAssembly.

Configuração

O primeiro passo, claro, é instalar o Deno! Na maioria dos sistemas, é apenas um único comando.

$ curl -fsSL https://deno.land/x/install/install.sh | sh

Como estamos escrevendo funções no Rust, precisamos instalar também os compiladores e ferramentas desta linguagem.

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

Finalmente, a ferramenta ssvmup automatiza o processo de construção e gera todos os artefatos tornando mais fácil as chamadas da aplicação Deno as funções do Rust. Novamente, em um único comando instalamos a dependência ssvmup.

$ curl https://raw.githubusercontent.com/second-state/ssvmup/master/installer/init.sh -sSf | sh

Nota: O ssvmup usa wasm-bindgen para gerar automaticamente o código "cola" entre o código-fonte JavaScript e Rust, para que possam se comunicar usando os tipos de dados nativos. Sem ele, os argumentos da função e os valores de retorno seriam limitados a tipos muito simples (ou seja, inteiros de 32 bits) suportados nativamente pelo WebAssembly. Por exemplo, strings ou arrays não seriam possíveis sem o ssvmup e o wasm-bindgen.

Hello world

Para começar, vamos dar uma olhada em um exemplo simples de hello world com base no hello world do Deno. Podemos obter o código-fonte e o modelo da aplicação hello world no GitHub.

A função Rust está no arquivo src/lib.rs e simplesmente acrescenta "hello" a uma string de entrada. Observe que a função say() está anotada com #[wasm_bindgen], permitindo que ssvmup gere o "encanamento" necessário para chamá-la do TypeScript.

#[wasm_bindgen]
pub fn say(s: &str) -> String {
  let r = String::from("hello ");
  return r + s;
}

A aplicação Deno existe no arquivo deno/server.ts. A aplicação importa a função Rust say() do arquivo pkg/functions_lib.js, que é gerado pela ferramenta ssvmup. O nome do arquivo functions_lib.js depende do nome do projeto Rust definido no arquivo Cargo.toml.

import { serve } from "https://deno.land/std/http/server.ts";
import { say } from '../pkg/functions_lib.js';
 
type Resp = {
    body: string;
}
 
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  let r = {} as Resp;
  r.body = say (" World\n");
  req.respond(r);
}

Agora, vamos executar o ssvmup para construir a função Rust em uma função Deno WebAssembly.

$ ssvmup build --target deno

Depois que o ssvmup for concluído com êxito, podemos inspecionar o arquivo pkg/functions_lib.js para ver como a API Deno WebAssembly é usada para executar o arquivo WebAssembly compilado pkg/functions_lib.wasm.

Em seguida, execute o aplicativo Deno. O Deno requer permissões para ler o sistema de arquivos, pois precisa carregar o arquivo WebAssembly e para acessar a rede, pois precisa receber e responder às solicitações HTTP.

$ deno run --allow-read --allow-net --allow-env --unstable deno/server.ts

Nota: Se você instalou o Deno no passado e encontrou um erro neste momento, é provável que tenha sido devido a um conflito de versões da biblioteca em cache. Siga essas instruções para recarregar seu cache Deno.

No terminal, podemos acessar o aplicativo da web Deno para fazê-lo dizer olá por meio de uma conexão HTTP!

$ curl http://localhost:8000/
hello  World

Um exemplo mais complexo

O projeto do modelo inicial inclui vários exemplos elaborados para mostrar como passar dados complexos entre as funções do Deno TypeScript e Rust. Aqui estão algumas funções adicionais do Rust, em src/lib.rs. Observe que cada um é anotado com #[wasm_bindgen].

#[wasm_bindgen]
pub fn obfusticate(s: String) -> String {
  (&s).chars().map(|c| {
    match c {
      'A' ..= 'M' | 'a' ..= 'm' => ((c as u8) + 13) as char,
      'N' ..= 'Z' | 'n' ..= 'z' => ((c as u8) - 13) as char,
      _ => c
    }
  }).collect()
}
 
#[wasm_bindgen]
pub fn lowest_common_denominator(a: i32, b: i32) -> i32 {
  let r = lcm(a, b);
  return r;
}
 
#[wasm_bindgen]
pub fn sha3_digest(v: Vec<u8>) -> Vec<u8> {
  return Sha3_256::digest(&v).as_slice().to_vec();
}
 
#[wasm_bindgen]
pub fn keccak_digest(s: &[u8]) -> Vec<u8> {
  return Keccak256::digest(s).as_slice().to_vec();
}

Talvez o mais interessante seja a função create_line(). Ela leva duas strings JSON, cada uma representando uma estrutura Point, e retorna uma string JSON representando uma estrutura Line. Observe que as estruturas Point e Line são anotadas com Serialize e Deserialize para que o compilador Rust gere automaticamente o código necessário para oferecer suporte à conversão de e para strings JSON.

use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct Point {
  x: f32,
  y: f32
}
 
#[derive(Serialize, Deserialize, Debug)]
struct Line {
  points: Vec<Point>,
  valid: bool,
  length: f32,
  desc: String
}
 
#[wasm_bindgen]
pub fn create_line (p1: &str, p2: &str, desc: &str) -> String {
  let point1: Point = serde_json::from_str(p1).unwrap();
  let point2: Point = serde_json::from_str(p2).unwrap();
  let length = ((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)).sqrt();
 
  let valid = if length == 0.0 { false } else { true };
  let line = Line { points: vec![point1, point2], valid: valid, length: length, desc: desc.to_string() };
  return serde_json::to_string(&line).unwrap();
}
 
#[wasm_bindgen]
pub fn say(s: &str) -> String {
  let r = String::from("hello ");
  return r + s;
}

A seguir, vamos examinar o programa JavaScript deno/test.ts, que mostra como chamar as funções Rust. Como podemos ver, o String e o /str são simplesmente strings em JavaScript, o i32 são números e o Vec<u8> ou o /[8] são JavaScript Uint8Array. Os objetos JavaScript precisam passar por JSON.stringify() ou JSON.parse() antes de serem passados ou retornados das funções Rust.


 
import { say, obfusticate, lowest_common_denominator, sha3_digest, keccak_digest, create_line } from '../pkg/functions_lib.js';
 
const encoder = new TextEncoder();
 
console.log( say("SSVM") );
console.log( obfusticate("A quick brown fox jumps over the lazy dog") );
console.log( lowest_common_denominator(123, 2) );
console.log( sha3_digest(encoder.encode("This is an important message")) );
console.log( keccak_digest(encoder.encode("This is an important message")) );
 
var p1 = {x:1.5, y:3.8};
var p2 = {x:2.5, y:5.8};
var line = JSON.parse(create_line(JSON.stringify(p1), JSON.stringify(p2), "A thin red line"));
console.log( line );

Depois de executar o ssvmup para construir a biblioteca Rust, executar deno/test.ts no runtime Deno produz a seguinte saída:

$ ssvmup build --target deno
... Building the wasm file and JS shim file in pkg/ ...
 
$ deno run --allow-read --allow-env --unstable deno/test.ts
hello SSVM
N dhvpx oebja sbk whzcf bire gur ynml qbt
246
Uint8Array(32) [
  87, 27, 231, 209, 189, 105, 251,  49,
  ... ...
]
Uint8Array(32) [
  126, 194, 241, 200, 151, 116, 227,
  ... ...
]
{
  points: [ { x: 1.5, y: 3.8 }, { x: 2.5, y: 5.8 } ],
  valid: true,
  length: 2.2360682,
  desc: "A thin red line"
}

Qual é o próximo passo

Agora podemos criar funções Rust e acessá-las a partir de uma aplicação Deno TypeScript. Podemos colocar muitas tarefas computacionalmente intensas em funções Rust e oferecer alto desempenho e serviços web seguros por meio do Deno. Exemplos de tais serviços incluem machine learning e reconhecimento de imagem.

Além disso, podemos acessar os recursos do sistema, como números aleatórios, variáveis de ambiente e o sistema de arquivos por meio da WebAssembly Systems Interface (WASI) da nossa aplicação Deno.

Sobre o autor

Dr. Michael Yuan é autor de cinco livros sobre engenharia de software. Seu livro mais recente, Building Blockchain Apps, foi publicado pela Addison-Wesley em dezembro de 2019. O Dr. Yuan é cofundador da Second State, uma startup que traz as tecnologias WebAssembly e Rust para aplicações em nuvem, blockchain e IA. O Second State permite que os desenvolvedores implantem funções Rust rápidas, seguras, portáteis e sem servidor no Node.js. Fique sabendo das novidades assinando o boletim informativo WebAssembly.Today.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

BT