🚀 入门文档:在 Node.js 应用中调用 Rust 函数

• 6 minutes to read

服务端的 WebAssembly 有很多非常好的用例,尤其是在AI,区块链和大数据应用方面。这个入门教程展示了如何将用 Rust 编写的 WebAssembly 函数合并到服务器上的 Node.js 应用程序中。这种方法结合了Rust 的性能,WebAssembly 的安全性和可移植性以及 JavaScript 的易用性。一个典型的应用程序是这样工作的。

  • 主机应用程序是用 JavaScript 编写的 Node.js Web 应用程序,调用 WebAssembly 函数。
  • sWebAssembly 字节码程序是用 Rust 编写的。它在 SSVM 中运行,并从 Node.js Web 应用程序中调用。

Fork 这个 GitHub repo ,开始编写你的 Rust 函数吧。

前提条件:

要使用 Rust 和 WebAssembly 设置高性能 Node.js 环境,需要安装以下内容:

Docker

最简单的入门方法是使用 Docker 构建开发环境。 clone 这个模板项目,然后运行以下 Docker 命令。

# Get the code
$ git clone https://github.com/second-state/ssvm-nodejs-starter
$ cd ssvm-nodejs-starter

# Run Docker container
$ docker build -t ssvm-nodejs:v1 .
$ docker run -p 3000:3000 --rm -it -v $(pwd):/app ssvm-nodejs:v1
(docker) $ cd /app

现在可以编译并运行代码了。

不使用 Docker,手工操作

了解在 Node.js 中调用 Rust 函数的完整设置指南

SSVM 依赖于最新版本的 libstdc ++。 Ubuntu 20.04 LTS已经拥有最新的库。如果您运行的 Linux 版本比较旧,可以选择这几种方式进行升级。

使用下面的命令来安装完整设置:

# Install Rust
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ source $HOME/.cargo/env

# Install Node.js
$ curl -sL https://deb.nodesource.com/setup_14.x |  bash
$ apt-get install -y nodejs

# Install ssvmup toolchain
$ npm install -g ssvmup # Append --unsafe-perm if permission denied

# OS dependencies for SSVM
$ sudo apt-get update
$ sudo apt-get -y upgrade
$ sudo apt install build-essential curl wget git vim libboost-all-dev

# Install the nodejs addon for SSVM
$ npm install ssvm

接下来,clone 模板的 GitHub repo

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

Hello World

第一个示例是 Hello World。这个简单的应用展示了应用程序的各个部分是如何组合在一起的。

Rust 中的 WebAssembly 函数

在这个示例中,Rust 程序将输入的字符串放到 hello 之后。下,下面是 Rust 程序的内容,放在 src/lib.rs 文件里。我们可以在这个文件中定义多个外部函数。通过 WebAssembly /SSVM,主机 JavaScript 应用可以调用所有 Rust 函数。只需记住用#[wasm_bindgen]注释每个函数。加过注释后, ssvmup 在构建 Rust 函数时,知道生成正确的 JavaScript 到 Rust 接口。

use wasm_bindgen::prelude::*;

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

接下来,将 Rust 源代码编译为 WebAssembly 字节码,并为 Node.js 主机环境生成随附的 JavaScript 模块。

$ ssvmup build

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

Node.js 主机应用程序

接下来,来到 node 文件夹并检查 JavaScript 程序 app.js。通过自动生成的 ssvm_nodejs_starter_lib.js 模块,编写 JavaScript 调用 WebAssembly 函数变得很容易。下面的代码是 node 应用程序 app.js。node 应用程序只是从生成的模块中导入 say() 函数。node 应用程序从传入的 HTTP GET 请求中获取 name 参数,并以“ hello name”进行响应。

const { say } = require('../pkg/ssvm_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 应用。获取完整的源代码请点击这里

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

用户在 Web 网页上输入 abc 的值,然后 Web 应用程序在 /solve 处调用 Web 服务以计算二次方程的根。

X 的根显示在表单下方的区域中。

HTML 文件包含客户端 JavaScript,用于将 Web 表单提交到 /solve 的,并将结果放入页面上的 #roots HTML元素中。

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

/solve URL 端点背后的 Node.js 应用程序 如下所示。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]))
})

Solve 函数是用 Rust 编写的,并在 SSVM 中运行。 JavaScript 端的调用参数是数组的值。 Rust 函数接收一个封装该数组的JSON 对象。在 Rust 代码中,我们首先解码 JSON,执行计算,然后以 JSON 字符串的形式返回结果值。

#[wasm_bindgen]
pub fn solve(params: &str) -> String {
  let ps: (f32, f32, f32) = serde_json::from_str(&params).unwrap();
  let discriminant: f32 = (ps.1 * ps.1) - (4. * ps.0 * ps.2);
  let mut solution: (f32, f32) = (0., 0.);
  if discriminant >= 0. {
    solution.0 = (((-1.) * ps.1) + discriminant.sqrt()) / (2. * ps.0);
    solution.1 = (((-1.) * ps.1) - discriminant.sqrt()) / (2. * ps.0);
    return serde_json::to_string(&solution).unwrap();
  } else {
    return String::from("not real numbers");
  }
}

测试一下。

`$ ssvmup build
$ npm install express # The application requires the Express framework in Node.js

$ node node/server.js`

在浏览器中转到 http://ip-addr:8080/ ,访问此应用程序。注意:如果你用的是 Docker,请确保 Docker 容器端口 8080 已映射到主机端口 8080。

一些复杂的例子

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

  • Rust 调用的参数可以是 i32String&strVec<u8>&[u8] 的任意组合
  • 返回值可以是 i32StringVec<u8>
  • 对于像结构这样的复杂数据类型,可以使用 JSON 字符串传递数据

通过 JSON 支持,你可以使用任意数量的输入参数调用 Rust 函数,并返回任意数量、任何类型的返回值。

GitHub 上的函数示例中的 Rust 程序位于 src/lib.rs,演示了如何传入各种得到支持的调用参数以及返回值。

#[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 结构都使用 SerializeDeserialize 进行了注释,以便 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 程序 app.js,这里显示了如何调用 Rust 函数。String&str 是 JavaScript 中的简单字符串,i32 是数字,而 Vec<u8>&[8] 是 JavaScript Uint8Array。 JavaScript 对象需要通过 JSON.stringify()JSON.parse() 才能传入 Rust 函数或从 Rust 函数返回。

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("SSVM") );
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 );

运行SSVM,构建 Rust 库,然后再运行 Node.js 环境里的 app.js,产生下面的输出。

$ ssvmup build
... Building the wasm file and JS shim file in pkg/ ...

$ node node/app.js
hello SSVM
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' } 

进阶学习

通过这篇入门文章,我们实现了一个非常简单的示例,掌握了如何从 Node.js 应用程序中的 JavaScript 调用 Rust 函数。如果你想要了解更多关于如何在 Node.js JavaScript 和 Rust 函数之间交换数据的资料,请查看下一篇文章:如何将任意参数从 JavaScript 程序传递给 Rust

RustWebAssemblyNode.jsgetting-startedrust-function-in-nodejs
Fast, safe, portable and serverless Rust functions as services