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.