WebAssembly 是 Deno 的好搭档

2020-08-28 10:48:22 浏览数 (1)

作者 | Michael Yuan

译者 | 王强

本文要点

  • Deno 和 Node.js 都在基于 C/C 的运行时上执行 JavaScript 代码,以实现较高的性能。
  • Deno 是单一的二进制应用,不兼容 NPM 模块,并且很难将原生模块加入应用中。
  • WebAssembly 提供了一种在 Deno 应用中运行高性能代码的途径。
  • 对于服务端应用程序来说,WebAssembly 是安全、可移植和轻量级的容器。
  • Rust 编译器工具链为 WebAssembly 提供了强大的支持。

备受期待的 Deno 项目 终于发布了 1.0 版本!Deno 是由 Node.js 的创始人 Ryan Dahl 创建的,旨在解决他所说的“我为 Node.js 感到遗憾的十件事”。

Deno 抛弃了 NPM 和臭名昭著的 node_modules。它是单个二进制可执行文件,可运行以 TypeScript 和 JavaScript 编写的应用程序。

但是,尽管 TypeScript 和 JavaScript 适合大多数 Web 应用程序,但它们可能难以满足计算密集型任务的需求,如神经网络训练和推理、机器学习和加密应用等。实际上,Node.js 就经常需要使用原生库来执行这些任务(例如使用 openssl 执行加密任务)。

既然没有类似 NPM 的系统来加入原生模块的话,我们该怎样在 Deno 上编写需要原生性能的服务端应用程序呢?这就要轮到 WebAssembly 上场了!在这篇文章中,我们将在 Rust 中编写一些高性能函数,并将它们编译为 WebAssembly,然后在你的 Deno 应用程序中运行它们。

太长不看版

从 GitHub 克隆或 fork这个 Deno 入门项目模板。按照说明操作,只需 5 分钟你就能在 Deno 中运行第一个 WebAssembly 函数(由 Rust 编写)。

一点背景

Node.js 之所以非常成功,是因为它为开发人员做到了鱼与熊掌兼得:JavaScript 的易用性(尤其是编写基于事件的异步应用程序时)以及 C/C 的高性能。Node.js 应用程序是用 JavaScript 编写的,但会在基于 C/C 的原生运行时上执行,这些运行时包括谷歌 V8 JavaScript 引擎和许多原生库模块。Deno 希望能复制这种成功路径,但在这个过程中它使用了 TypeScript 和 Rust 支持的现代技术栈。

Deno 是用于 JavaScript 和 TypeScript 的简单、现代化且安全的运行时,它使用了 V8 引擎,并在 Rust 内构建。——deno.land 网站。

在他的著名演讲“我为 Node.js 感到遗憾的十件事”中,Node.js 的创建者 Ryan Dahl 解释了从头开始创建 Deno 这个 Node.js 的竞争对手(甚至替代者)的理由。Dahl 的遗憾主要集中在 Node.js 管理第三方代码和模块的机制上。

  • 用于将 C 模块链接到 Node.js 的复杂构建系统。
  • package.json、node_modules、index.js 和其他 NPM 工件引入了不必要的复杂性。

于是,Deno 在管理依赖项时有意选择了一些方式来避免上述问题。

  • Deno 是单个二进制可执行文件。
  • 应用程序是使用 TypeScript 或 JavaScript 编写的,在代码中将依赖项明确声明为 import 语句,并带有完整的 URL,链接到依赖项的源代码。
  • Deno 与 Node.js 模块不兼容。

这些都没问题,但那些需要更高性能的应用程序该怎么办呢?例如需要在毫秒级别执行复杂神经网络模型运算的 AI 即服务应用程序?在 Deno 和 Node.js 中,许多函数都是通过 TypeScript 或 JavaScript API 调用,但以 Rust 或 C 语言编写的原生代码执行。在 Node.js 中,开发人员总是可以选择从 JavaScript API 调用第三方原生库。但我们目前无法在 Deno 中这样做吗?

Deno 中的 WebAssembly 支持

WebAssembly 是一种轻量级虚拟机,旨在以接近原生的速度执行可移植字节码。你可以将 Rust 或 C/C 函数编译为 WebAssembly 字节码,然后从 TypeScript 访问这些函数。对于某些任务,它可能比用 TypeScript 编写的等效函数要快得多。例如,这份 IBM 研究 发现,对于某些数据处理算法,Rust 和 WebAssembly 可以将 Node.js 的执行速度提高 1200%至 1500%。

Deno 内部使用谷歌 V8 引擎。V8 不仅是一个 JavaScript 运行时,还是一个 WebAssembly 虚拟机。Deno 对 WebAssembly 提供了开箱即用的支持。Deno 为你的 TypeScript 应用程序提供了一个 API,以调用 WebAssembly 中的函数。

实际上,WebAssembly 中已经实现了一些流行的 Deno 组件。例如,Deno 中的 sqlite module 是使用 Emscripten 将 sqlite 的 C 源代码编译到 WebAssembly 中的成果。Deno WASI 组件 使 WebAssembly 应用程序可以访问操作系统的底层资源,例如文件系统。在本文中,我将教你如何用 Rust 和 WebAssembly 编写高性能的 Deno 应用程序。

设 置

当然,第一步是 安装 Deno!在大多数系统上,这一步只需一条命令足矣。

代码语言:javascript复制
 $ curl -fsSL https://deno.land/x/install/install.sh | sh

由于我们正在用 Rust 编写函数,因此你还需要 安装 Rust 语言编译器和工具。

代码语言:javascript复制
 $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

最后,ssvmup 工具可以自动执行构建过程并生成所有工件,以使你的 Deno 应用程序轻松调用 Rust 函数。同样,用一条命令就能安装 ssvmup 依赖项。

代码语言:javascript复制
$ curl https://raw.githubusercontent.com/second-state/ssvmup/master/installer/init.sh -sSf | sh

注意:ssvmup 使用 wasm-bindgen 在 JavaScript 和 Rust 源代码之间自动生成“胶水”代码,以便它们可以使用自己的原生数据类型来通信。没有它,函数参数和返回值只能限制在 WebAssembly 原生支持的一些非常简单的类型上(如 32 位整数)。例如,如果没有 ssvmup 和 wasm-bindgen,你就无法使用字符串或数组。

Hello world

首先,我们来研究一下 Deno hello world 示例中使用的 hello world 示例。你可以从 GitHub 获取 hello world 源代码和应用程序模板。

Rust 函数位于 src/lib.rs 文件中,只需在输入字符串前加上“hello”即可。注意,say() 函数使用 #[wasm_bindgen] 注解,使 ssvmup 可以生成必要的“管道”,以从 TypeScript 来调用它。

代码语言:javascript复制
#[wasm_bindgen]
pub fn say(s: &str) -> String {
  let r = String::from("hello ");
  return r   s;
}

Deno 应用程序位于 deno/server.ts 文件中。这个应用程序从 pkg/functions_lib.js 文件(由 ssvmup 工具生成)中导入 Rustsay() 函数。functions_lib.js 这个文件名是由 Cargo.toml 文件中定义的 Rust 项目名称确定的。

代码语言:javascript复制
import { serve } from "https://deno.land/std@0.54.0/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 (" Worldn");
  req.respond(r);
}

现在我们运行 ssvmup,将 Rust 函数构建为一个 Deno WebAssembly 函数。

代码语言:javascript复制
$ ssvmup build --target deno

ssvmup 成功完成任务后,你可以检查 pkg/functions_lib.js 文件,看看 Deno WebAssembly API 是怎样执行已编译的 WebAssembly 文件 pkg/functions_lib.wasm 的。接下来运行 Deno 应用程序。Deno 需要读取文件系统的权限(因为它需要加载 WebAssembly 文件),并需要访问网络(因为它需要接收和响应 HTTP 请求)。

代码语言:javascript复制
$ deno run --allow-read --allow-net deno/server.ts

注意:如果你之前已经安装了 Deno,并在这里遇到了一个错误,这很可能是由于缓存过的库的版本冲突导致的。按照 这里的指导 来重建 Deno 缓存。在另一个终端窗口中,你现在可以访问 Deno Web 应用程序,让它通过 HTTP 连接说 hello 了!

代码语言:javascript复制
$ curl http://localhost:8000/
hello World

一个更复杂的例子

入门模板项目包括了几个更详细的示例,以展示如何在 Deno TypeScript 和 Rust 函数之间传递复杂的数据。下面是 src/lib.rs 中的其他一些 Rust 函数。请注意,它们每个都用 #[wasm_bindgen] 注解过了。

代码语言:javascript复制
#[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();
}

也许最有趣的是 create_line() 函数。它接收两个 JSON 字符串(每个字符串代表一个 Point 结构),并返回一个代表 Line 结构的 JSON 字符串。注意,Point 和 Line 结构都使用 Serialize 和 Deserialize 注解,这样 Rust 编译器就能自动生成必要的代码,以支持它们与 JSON 字符串之间的转换。

代码语言:javascript复制
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;
}

接下来我们检查一下 JavaScript 程序 deno/test.ts,它显示了如何调用 Rust 函数。如你所见,String 和 &str 是 JavaScript 的简单字符串,i32 是数字,而 Vec或 &[8] 是 JavaScript Uint8Array。JavaScript 对象需要先通过 JSON.stringify() 或 JSON.parse() 才能传入 Rust 函数或从 Rust 函数返回。

代码语言:javascript复制
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 );

运行 ssvmup 构建 Rust 库之后,在 Deno 运行时中运行 deno/test.ts 会生成以下输出:

代码语言:javascript复制
$ ssvmup build --target deno
... Building the wasm file and JS shim file in pkg/ ...
$ deno run --allow-read 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"
}

下一步计划

现在我们就可以创建 Rust 函数,并从 Deno TypeScript 应用程序访问它们。你可以在 Rust 函数中放置大量计算密集型任务,并通过 Deno 提供高性能和安全的 Web 服务。这类服务的例子包括机器学习和图像识别等。

将来,你还可以通过 WebAssembly 系统接口(WASI),在你的 Deno 应用程序中访问随机数、环境变量和文件系统等系统资源。

作者介绍

Michael Yuan 博士 是 五本软件工程书籍 的作者。他的最新著作《构建区块链应用》由 Addison-Wesley 在 2019 年 12 月出版。Yuan 博士是 Second State 的联合创始人,这是一家将 WebAssembly 和 Rust 技术引入云、区块链和 AI 应用程序的初创公司。Second State 使开发人员能够在 Node.js 上部署快速、安全、可移植和无服务器的 Rust 函数。感兴趣的读者可以订阅 WebAssembly.Today 通讯来获取最新信息。

参考阅读:

https://www.infoq.com/articles/deno-loves-webassembly/

0 人点赞