WebAssembly 是 Deno 的好搭档
本文要点
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!在大多数系统上,这一步只需一条命令足矣。
$ curl -fsSL https://deno.land/x/install/install.sh | sh
由于我们正在用 Rust 编写函数,因此你还需要 安装 Rust 语言编译器和工具。
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
最后,ssvmup 工具可以自动执行构建过程并生成所有工件,以使你的 Deno 应用程序轻松调用 Rust 函数。同样,用一条命令就能安装 ssvmup 依赖项。
$ 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 来调用它。
#[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 项目名称确定的。
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 (" World\n"); req.respond(r);}
现在我们运行 ssvmup,将 Rust 函数构建为一个 Deno WebAssembly 函数。
$ ssvmup build --target deno
ssvmup 成功完成任务后,你可以检查 pkg/functions_lib.js 文件,看看 Deno WebAssembly API 是怎样执行已编译的 WebAssembly 文件 pkg/functions_lib.wasm 的。接下来运行 Deno 应用程序。Deno 需要读取文件系统的权限(因为它需要加载 WebAssembly 文件),并需要访问网络(因为它需要接收和响应 HTTP 请求)。
$ deno run --allow-read --allow-net deno/server.ts
注意:如果你之前已经安装了 Deno,并在这里遇到了一个错误,这很可能是由于缓存过的库的版本冲突导致的。按照 这里的指导 来重建 Deno 缓存。在另一个终端窗口中,你现在可以访问 Deno Web 应用程序,让它通过 HTTP 连接说 hello 了!
$ curl http://localhost:8000/hello World
一个更复杂的例子
入门模板项目包括了几个更详细的示例,以展示如何在 Deno TypeScript 和 Rust 函数之间传递复杂的数据。下面是 src/lib.rs 中的其他一些 Rust 函数。请注意,它们每个都用 #[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();}
也许最有趣的是 create_line() 函数。它接收两个 JSON 字符串(每个字符串代表一个 Point 结构),并返回一个代表 Line 结构的 JSON 字符串。注意,Point 和 Line 结构都使用 Serialize 和 Deserialize 注解,这样 Rust 编译器就能自动生成必要的代码,以支持它们与 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;}
接下来我们检查一下 JavaScript 程序 deno/test.ts,它显示了如何调用 Rust 函数。如你所见,String 和 &str 是 JavaScript 的简单字符串,i32 是数字,而 Vec或 &[8] 是 JavaScript Uint8Array。JavaScript 对象需要先通过 JSON.stringify() 或 JSON.parse() 才能传入 Rust 函数或从 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 );
运行 ssvmup 构建 Rust 库之后,在 Deno 运行时中运行 deno/test.ts 会生成以下输出:
$ ssvmup build --target deno... Building the wasm file and JS shim file in pkg/ ...$ deno run --allow-read deno/test.tshello SSVMN dhvpx oebja sbk whzcf bire gur ynml qbt246Uint8Array(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 应用程序中访问随机数、环境变量和文件系统等系统资源。
©著作权归作者所有:来自51CTO博客作者mb5fd86ddc9c8d5的原创作品,如需转载,请注明出处,否则将追究法律责任