Node.js SDK

在这篇教程中,我会向你展示如何通过 WasmEdge Node.js SDK 将用 Rust 写的 WebAssembly 函数合并进 Node.js 的服务端应用程序 中。这个方法可以将 Rust 的性能,WebAssembly 的安全性可移植性,和 JavaScript 的易用性结合。一个典型的应用程序就像这样。

  • host 应用程序是一个用 JavaScript 写的 Node.js web 应用程序,它可以调用 WebAssembly 函数。
  • WebAssembly 字节码程序是用 Rust 写的,运行在 WasmEdge Runtime,可以被 Node.js web 应用程序调用。

Fork 这个 Github 仓库来开始写代码!

先决条件

为了搭建一个包含 Rust 和 WebAssembly 的高性能 Node.js 环境,你需要如下准备:

Docker

最简单的启动方式就是使用 Docker 来搭建开发环境。只需要克隆这个模板到你的电脑,然后运行如下 Docker 命令:

# 克隆代码到本地 $ git clone https://github.com/second-state/wasmedge-nodejs-starter $ cd wasmedge-nodejs-starter # 启动 Docker 容器 $ docker pull wasmedge/appdev_x86_64:0.8.2 $ docker run -p 3000:3000 --rm -it -v $(pwd):/app wasmedge/appdev_x86_64:0.8.2 (docker) $ cd /app

好了,你现在可以编译和运行代码了。

没有 Docker 的手动启动

命令如下。

# 安装 Rust $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh $ source $HOME/.cargo/env $ rustup override set 1.50.0 # 安装 Node.js 和 npm $ curl -sL https://deb.nodesource.com/setup_14.x | bash $ sudo apt-get install -y nodejs npm # 安装 rustwasmc 工具链 $ npm install -g rustwasmc # 如果权限有问题,加上 --unsafe-perm # WasmEdge 需要的系统依赖 $ sudo apt-get update $ sudo apt-get -y upgrade $ sudo apt install -y build-essential curl wget git vim libboost-all-dev llvm-dev liblld-10-dev # 安装 WasmEdge 需要的 nodejs addon $ npm install wasmedge-core $ npm install wasmedge-extensions

WasmEdge Runtime 需要最新版本的 libstdc++。 Ubuntu 20.04 LTS 已经有最新的库了。 如果你使用的是比较老的 Linux 发行版中,有一些选项需要升级,更详细的信息在这儿

然后,克隆示例源代码仓库。

git clone https://github.com/second-state/wasmedge-nodejs-starter cd wasmedge-nodejs-starter

Hello World

第一个示例是一个 hello world,向你展示应用程序的各个部分如何组合在一起。

Rust 写的 WebAssembly 程序

在这个例子中,Rust 程序将输入的字符串添加到 “hello” 后面。下面是 Rust 程序内容,位于 src/lib.rs。你可以在这个库文件中定义多个外部方法,所有的这些方法都可以在 host JavaScript 应用中通过 WebAssembly 调用。记得需要给每个函数添加 #[wasm_bindgen] 注解,这样 rustwasmc 就知道在构建时为这些函数生成正确的 JavaScript 到 Rust 接口。

  1. #![allow(unused)]
  2. fn main() {
  3. use wasm_bindgen::prelude::*;
  4. #[wasm_bindgen]
  5. pub fn say(s: String) -> String {
  6. let r = String::from("hello ");
  7. return r + &s;
  8. }
  9. }

然后你可以将 Rust 源代码编译成 WebAssembly 字节码,并且生成相应的 JavaScript 模块供 Node.js host 环境调用。

rustwasmc build

生成的文件在 pkg/ 目录下,.wasm 文件是 WebAssembly 字节码程序,.js 文件是 JavaScript 模块。

Node.js host 应用程序

然后进入 node 文件夹下,检查 JavaScript 程序 app.js。有了生成的 wasmedge_nodejs_starter_lib.js 模块,就很容易写出调用 WebAssembly 函数的 JavaScript 了。下面是 node 应用程序 app.js。简单的从生成的模块中引入 say() 函数。 node 应用程序从 HTTP GET 请求中拿到 name 参数后返回 “hello name”。

const { say } = require('../pkg/wasmedge_nodejs_starter_lib.js'); const http = require('http'); const url = require('url'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { const queryObject = url.parse(req.url,true).query; res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end(say(queryObject['name'])); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });

像下面一样启动 Node.js 应用程序。

$ node node/app.js Server running at http://127.0.0.1:3000/

然后,你可以在另外一个终端窗口中测试。

$ curl http://127.0.0.1:3000/?name=Wasm hello Wasm

完整的 web 应用程序

下面的例子展示了一个计算二次方程根的 web 应用程序,请在这里查看完整源代码.

用户在 web 表单中输入 a, b, c 三个值,web 应用程序调用 web 服务 /solve,计算出二次方程的根。

a*X^2 + b*X + c = 0

X 的根展示在输入表单下面。

getting-started-with-rust-function

HTML 文件 包含提交 web 表单到 /solve 的客户端 JavaScript,并且将结果放到页面的 #roots HTML 元素里。

$(function() { var options = { target: '#roots', url: "/solve", type: "post" }; $('#solve').ajaxForm(options); });

/solve URL 端点后的 Node.js 应用程序如下所示。他从输入表单中读取数据,将他们作为数组传递给 solve 函数,将返回结果放到 HTTP 返回内容中。

app.post('/solve', function (req, res) { var a = parseFloat(req.body.a); var b = parseFloat(req.body.b); var c = parseFloat(req.body.c); res.send(solve([a, b, c])) })

用 Rust 写的 solve 函数,运行在 WasmEdge Runtime。如果 JavaScript 端的调用参数是数组,Rust 函数接收到一个封装数组的 JSON 对象。在 Rust 代码中,我们首先解码 JSON,执行计算,然后返回一个 JSON 字符串的结果。

  1. #![allow(unused)]
  2. fn main() {
  3. #[wasm_bindgen]
  4. pub fn solve(params: &str) -> String {
  5. let ps: (f32, f32, f32) = serde_json::from_str(&params).unwrap();
  6. let discriminant: f32 = (ps.1 * ps.1) - (4. * ps.0 * ps.2);
  7. let mut solution: (f32, f32) = (0., 0.);
  8. if discriminant >= 0. {
  9. solution.0 = (((-1.) * ps.1) + discriminant.sqrt()) / (2. * ps.0);
  10. solution.1 = (((-1.) * ps.1) - discriminant.sqrt()) / (2. * ps.0);
  11. return serde_json::to_string(&solution).unwrap();
  12. } else {
  13. return String::from("not real numbers");
  14. }
  15. }
  16. }

让我们试试。

rustwasmc build npm install express # 这个应用程序需要 Node.js 的 express 框架 node node/server.js

在 web 浏览器中,输入 http://ip-addr:8080/ 来获取应用程序。注意:如果你使用的是 Docker,确保 Docker 容器中的 8080 端口映射到宿主的 8080 端口。

这就是二次方程的例子。

更多例子

在 Rust 和 JavaScript 之间除了可以传递字符串值外, rustwasmc 工具支持下面的数据类型。

  • Rust 调用参数可以是 i32String&strVec<u8>&[u8] 的组合。
  • 返回值可能是 i32 或者 String 或者 Vec<u8> 或者 void。
  • 对于复杂的数据结构,比如结构体,你可以使用 JSON 字符串来传递数据。

支持了 JSON,你可以用任意数量的输入参数调用 Rust 函数,并返回任意数量、任意类型的结果。

函数示例中的 Rust 程序 src/lib.rs 演示了如何传递多个不同类型的调用参数和返回结果。

  1. #![allow(unused)]
  2. fn main() {
  3. #[wasm_bindgen]
  4. pub fn obfusticate(s: String) -> String {
  5. (&s).chars().map(|c| {
  6. match c {
  7. 'A' ..= 'M' | 'a' ..= 'm' => ((c as u8) + 13) as char,
  8. 'N' ..= 'Z' | 'n' ..= 'z' => ((c as u8) - 13) as char,
  9. _ => c
  10. }
  11. }).collect()
  12. }
  13. #[wasm_bindgen]
  14. pub fn lowest_common_denominator(a: i32, b: i32) -> i32 {
  15. let r = lcm(a, b);
  16. return r;
  17. }
  18. #[wasm_bindgen]
  19. pub fn sha3_digest(v: Vec<u8>) -> Vec<u8> {
  20. return Sha3_256::digest(&v).as_slice().to_vec();
  21. }
  22. #[wasm_bindgen]
  23. pub fn keccak_digest(s: &[u8]) -> Vec<u8> {
  24. return Keccak256::digest(s).as_slice().to_vec();
  25. }
  26. }

最有意思的可能是 create_line() 函数。它需要两个 JSON 字符串,每一个都代表一个 Point 结构,返回一个 JSON 字符串代表 Line 结构。注意,PointLine 结构都使用了 SerializeDeserialize 注解,这样 Rust 编译器就会自动生成必要的代码来支持和 JSON 字符串之间的转换。

  1. #![allow(unused)]
  2. fn main() {
  3. use wasm_bindgen::prelude::*;
  4. use serde::{Serialize, Deserialize};
  5. #[derive(Serialize, Deserialize, Debug)]
  6. struct Point {
  7. x: f32,
  8. y: f32
  9. }
  10. #[derive(Serialize, Deserialize, Debug)]
  11. struct Line {
  12. points: Vec<Point>,
  13. valid: bool,
  14. length: f32,
  15. desc: String
  16. }
  17. #[wasm_bindgen]
  18. pub fn create_line (p1: &str, p2: &str, desc: &str) -> String {
  19. let point1: Point = serde_json::from_str(p1).unwrap();
  20. let point2: Point = serde_json::from_str(p2).unwrap();
  21. let length = ((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)).sqrt();
  22. let valid = if length == 0.0 { false } else { true };
  23. let line = Line { points: vec![point1, point2], valid: valid, length: length, desc: desc.to_string() };
  24. return serde_json::to_string(&line).unwrap();
  25. }
  26. #[wasm_bindgen]
  27. pub fn say(s: &str) -> String {
  28. let r = String::from("hello ");
  29. return r + s;
  30. }
  31. }

然后,让我们来检查下 JavaScript 程序 app.js,它展示了如何调用 Rust 函数。你可以看到,String&str 在 JavaScript 是简单的字符串,i32 是数字,Vec<u8> 或者 &[8] 是 JavaScript Uint8Array。JavaScript 对象在传入或者从 Rust 函数结果返回需要通过 JSON.stringify() 或者 JSON.parse() 转换。

const { say, obfusticate, lowest_common_denominator, sha3_digest, keccak_digest, create_line } = require('./functions_lib.js'); var util = require('util'); const encoder = new util.TextEncoder(); console.hex = (d) => console.log((Object(d).buffer instanceof ArrayBuffer ? new Uint8Array(d.buffer) : typeof d === 'string' ? (new util.TextEncoder('utf-8')).encode(d) : new Uint8ClampedArray(d)).reduce((p, c, i, a) => p + (i % 16 === 0 ? i.toString(16).padStart(6, 0) + ' ' : ' ') + c.toString(16).padStart(2, 0) + (i === a.length - 1 || i % 16 === 15 ? ' '.repeat((15 - i % 16) * 3) + Array.from(a).splice(i - i % 16, 16).reduce((r, v) => r + (v > 31 && v < 127 || v > 159 ? String.fromCharCode(v) : '.'), ' ') + '\n' : ''), '')); console.log( say("WasmEdge") ); console.log( obfusticate("A quick brown fox jumps over the lazy dog") ); console.log( lowest_common_denominator(123, 2) ); console.hex( sha3_digest(encoder.encode("This is an important message")) ); console.hex( 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 );

在运行 rustwasmc 来构建 Rust 库后,在 Node.js 环境中运行 app.js 会产生如下结果。

$ rustwasmc build ... Building the wasm file and JS shim file in pkg/ ... $ node node/app.js hello WasmEdge N dhvpx oebja sbk whzcf bire gur ynml qbt 246 000000 57 1b e7 d1 bd 69 fb 31 9f 0a d3 fa 0f 9f 9a b5 W.çѽiû1..Óú...µ 000010 2b da 1a 8d 38 c7 19 2d 3c 0a 14 a3 36 d3 c3 cb +Ú..8Ç.-<..£6ÓÃË 000000 7e c2 f1 c8 97 74 e3 21 d8 63 9f 16 6b 03 b1 a9 ~ÂñÈ.tã!Øc..k.±© 000010 d8 bf 72 9c ae c1 20 9f f6 e4 f5 85 34 4b 37 1b Ø¿r.®Á .öäõ.4K7. { points: [ { x: 1.5, y: 3.8 }, { x: 2.5, y: 5.8 } ], valid: true, length: 2.2360682, desc: 'A thin red line' }